Implementación de Permisos en Laravel
Introducción
Laravel proporciona políticas para definir la lógica de autorización para su aplicación. Estas políticas establecen las reglas que rigen qué usuarios están autorizados para realizar acciones específicas en los modelos de su aplicación. En esta guía, discutiremos los pasos necesarios para implementar permisos detallados en una aplicación Laravel utilizando un esquema de base de datos, un trait y una política.
Prerrequisitos
Antes de continuar, asegúrese de tener una aplicación Laravel configurada y en funcionamiento. También debe estar familiarizado con las migraciones, modelos y políticas de Laravel.
Esquema de Base de Datos
Para implementar permisos en una aplicación Laravel, cree un esquema de base de datos que incluya tablas para roles, permisos y sus relaciones. El siguiente ejemplo demuestra cómo crear estas tablas utilizando migraciones de Laravel:
php artisan make:model Role -m
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Ejecutar las migraciones.
*/
public function up(): void
{
Schema::create('roles', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Revertir las migraciones.
*/
public function down(): void
{
Schema::dropIfExists('roles');
}
};
php artisan make:model Permission -m
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Ejecutar las migraciones.
*/
public function up(): void
{
Schema::create('permissions', function (Blueprint $table) {
$table->id();
$table->string('name');
$table->text('description')->nullable();
$table->timestamps();
$table->softDeletes();
});
}
/**
* Revertir las migraciones.
*/
public function down(): void
{
Schema::dropIfExists('permissions');
}
};
php artisan make:model RolePermission -m
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Ejecutar las migraciones.
*/
public function up(): void
{
Schema::create('role_permissions', function (Blueprint $table) {
$table->id();
$table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
$table->foreignId('permission_id')->constrained('permissions')->onDelete('cascade');
$table->softDeletes();
$table->timestamps();
});
}
/**
* Revertir las migraciones.
*/
public function down(): void
{
Schema::dropIfExists('role_permissions');
}
};
php artisan make:model UserRole -m
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Ejecutar las migraciones.
*/
public function up(): void
{
Schema::create('user_roles', function (Blueprint $table) {
$table->id();
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
$table->foreignId('role_id')->constrained('roles')->onDelete('cascade');
$table->timestamps();
$table->softDeletes();
});
}
/**
* Revertir las migraciones.
*/
public function down(): void
{
Schema::dropIfExists('user_roles');
}
};
En este ejemplo, creamos tablas para roles, permisos y sus relaciones. La tabla roles
incluye un ID, un nombre, una descripción y marcas de tiempo. La tabla permissions
consta de un ID, un nombre, una descripción y marcas de tiempo. La tabla role_permissions
contiene un ID, un role_id
, un permission_id
y marcas de tiempo. Las columnas role_id
y permission_id
son claves foráneas que hacen referencia a las columnas id
en las tablas roles
y permissions
, respectivamente.
Esquemas de Modelos
Para proporcionar sugerencias de tipo y mejorar la legibilidad del código al interactuar con los modelos Role
, Permission
y User
, creamos las clases <Model>Schema
correspondientes. Estas clases definen constantes que representan los nombres de las columnas en las tablas de la base de datos correspondientes.
Por ejemplo, aquí está la clase PermissionSchema
:
<?php
namespace App\Schemas;
class PermissionSchema
{
const id = 'id';
const name = 'name';
const description = 'description';
const created_at = 'created_at';
const updated_at = 'updated_at';
const deleted_at = 'deleted_at';
}
En el ejemplo anterior, estamos definiendo constantes para cada una de las columnas en la tabla de permisos. Luego podemos usar estas constantes para proporcionar sugerencias de tipo y mejorar la legibilidad del código al interactuar con el modelo Permission
y sus datos asociados.
Más adelante usaremos estos esquemas para definir el nombre de las columnas al interactuar con los modelos. Por ejemplo, en el trait HasPermissions
que crearemos más adelante, usamos la constante PermissionSchema::name
para hacer referencia a la columna name
en la tabla de permisos:
use App\Schemas\PermissionSchema;
// ...
public function hasPermission(string $permission): bool
{
return $this->roles->some(function ($role) use ($permission) {
return $role->permissions->contains(PermissionSchema::name, $permission);
});
}
Al usar PermissionSchema::name
en lugar de la cadena cruda "name", estamos haciendo que nuestro código sea más legible y menos propenso a errores. Además, si alguna vez cambiáramos el nombre de la columna name
en la tabla de permisos, simplemente podríamos actualizar la constante PermissionSchema::name
para reflejar el nuevo nombre, y todas las referencias a esa constante en nuestro código se actualizarían automáticamente.
Al usar esquemas de modelos de esta manera, podemos hacer que nuestro código sea más mantenible y más fácil de trabajar con el tiempo.
Modelos
Para implementar permisos detallados en una aplicación Laravel, cree cuatro modelos: Permission
, Role
, RolePermission
y UserRole
. Estos modelos interactúan con las tablas de la base de datos que almacenan información sobre roles, permisos y relaciones de usuario/rol.
Aquí está el modelo Role
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $name
* @property string|null $description
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
*/
class Role extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
public function users(): BelongsToMany
{
return $this->belongsToMany(User::class, 'user_roles')->withTimestamps();
}
public function permissions(): BelongsToMany
{
return $this->belongsToMany(Permission::class, 'role_permissions')->withTimestamps();
}
}
En el ejemplo del modelo Role
anterior, usamos la relación BelongsToMany
para definir la relación entre los modelos Role
y User
, así como la relación entre los modelos Role
y Permission
.
Aquí está el modelo Permission
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property string $name
* @property string|null $description
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
*/
class Permission extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'role_permissions')->withTimestamps();
}
}
En el ejemplo del modelo Permission
anterior, usamos la relación BelongsToMany
para definir la relación entre los modelos Permission
y Role
.
Aquí está el modelo RolePermission
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property int $role_id
* @property int $permission_id
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* @property \Illuminate\Support\Carbon|null $deleted_at
*/
class RolePermission extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
public function permission(): BelongsTo
{
return $this->belongsTo(Permission::class);
}
}
En el ejemplo del modelo RolePermission
anterior, usamos la relación BelongsTo
para definir la relación entre los modelos RolePermission
y Role
, así como la relación entre los modelos RolePermission
y Permission
.
Aquí está el modelo UserRole
:
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\SoftDeletes;
/**
* @property int $id
* @property int $user_id
* @property int $role_id
* @property \Illuminate\Support\Carbon $created_at
* @property \Illuminate\Support\Carbon $updated_at
* @property \Illuminate\Support\Carbon $deleted_at
*/
class UserRole extends Model
{
use HasFactory, SoftDeletes;
protected $guarded = ['id'];
public function user(): BelongsTo
{
return $this->belongsTo(User::class);
}
public function role(): BelongsTo
{
return $this->belongsTo(Role::class);
}
}
En el ejemplo del modelo UserRole
anterior, usamos la relación BelongsTo
para definir la relación entre los modelos UserRole
y User
, así como la relación entre los modelos UserRole
y Role
.
Finalmente, aquí está el modelo User
:
<?php
namespace App\Models;
use App\Traits\HasPermissions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
/**
* Modelo de Usuario que representa a un usuario en el sistema.
*
* @property int $id
* @property string $name
* @property string $email
* @property \DateTime|null $email_verified_at
* @property string $password
* @property string|null $remember_token
* @property int|null $contact_id
* @property \DateTime $created_at
* @property \DateTime $updated_at
*/
class User extends Authenticatable
{
use HasFactory, HasPermissions, Notifiable;
/**
* Los atributos que son asignables en masa.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* Los atributos que deben ocultarse para la serialización.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Los atributos que deben ser convertidos.
*
* @var array<string, string>
*/
protected $casts = [
'email_verified_at' => 'datetime',
];
public function roles(): BelongsToMany
{
return $this->belongsToMany(Role::class, 'user_roles')->withTimestamps();
}
}
En el ejemplo del modelo User
anterior, usamos la relación BelongsToMany
para definir la relación entre los modelos User
y Role
. Además, utilizamos varios traits y propiedades para manejar la autenticación, las notificaciones y los permisos.
Trait de Verificación de Permisos
Para simplificar las verificaciones de permisos en una política, cree un trait que agregue un método hasPermission()
a la clase que lo utiliza. El siguiente ejemplo demuestra cómo crear este trait:
<?php
namespace App\Traits;
use App\Schemas\PermissionSchema;
trait HasPermissions
{
public function hasPermission(string $permission): bool
{
return $this->roles->some(function ($role) use ($permission) {
return $role->permissions->contains(PermissionSchema::name, $permission);
});
}
}
Este método simplifica el proceso de verificar si un usuario tiene un permiso específico.
El método hasPermission
toma un parámetro de cadena, $permission
, y devuelve un valor booleano. Verifica si el usuario actual tiene el permiso especificado examinando todos los roles asociados con el usuario.
Si ninguno de los roles posee el $permission
requerido, la función some
devuelve false
, indicando que el usuario no tiene el permiso necesario.
Al incorporar este trait en su modelo User
, puede verificar fácilmente si un usuario tiene un permiso específico.
Implementación de Permisos Detallados con Políticas
Para aplicar permisos detallados utilizando el modelo User
, cree una clase de política para cada modelo al que desee controlar el acceso. En este ejemplo, crearemos una PostPolicy
para gestionar el acceso al modelo Post
.
Primero, cree la clase PostPolicy
:
namespace App\Policies;
use App\Models\User;
use App\Models\Post;
use Illuminate\Auth\Access\HandlesAuthorization;
class PostPolicy
{
use HandlesAuthorization;
public function viewAny(User $user)
{
return $user->hasPermission('viewAny-posts');
}
public function view(User $user, Post $post)
{
return $user->hasPermission('view-posts') && $post->user_id === $user->id;
}
public function create(User $user)
{
return $user->hasPermission('create-posts');
}
public function update(User $user, Post $post)
{
return $user->hasPermission('update-posts') && $post->user_id === $user->id;
}
public function delete(User $user, Post $post)
{
return $user->hasPermission('delete-posts') && $post->user_id === $user->id;
}
}
Tenga en cuenta que utilizamos el trait hasPermission
agregado al modelo User
.
A continuación, registre la política en el AuthServiceProvider
. Aquí hay un ejemplo de cómo registrar la PostPolicy
:
namespace App\Providers;
use App\Models\Post;
use App\Policies\PostPolicy;
use Illuminate\Foundation\Support\Providers\AuthServiceProvider as ServiceProvider;
class AuthServiceProvider extends ServiceProvider
{
protected $policies = [
Post::class => PostPolicy::class,
];
public function boot()
{
$this->registerPolicies();
}
}
En este ejemplo, registramos la PostPolicy
para el modelo Post
. Esto instruye a Laravel a usar la PostPolicy
para controlar el acceso a las instancias del modelo Post
.
Ahora, puede usar esta política en sus controladores para autorizar acciones. Aquí hay un ejemplo de cómo usar la PostPolicy
en un controlador:
namespace App\Http\Controllers;
use App\Models\Post;
use Illuminate\Http\Request;
class PostController extends Controller
{
public function edit(Post $post)
{
$this->authorize('update', $post);
return view('posts.edit', compact('post'));
}
public function update(Request $request, Post $post)
{
$this->authorize('update', $post);
// Actualizar el post...
}
}
En este ejemplo, usamos el método authorize()
para verificar si el usuario autenticado tiene permiso para actualizar el post dado, empleando el método update
de la PostPolicy
. Si el usuario está autorizado, le permitimos editar o actualizar el post. Si no, devolvemos un error HTTP 403 Forbidden.
Al usar políticas para controlar el acceso a sus modelos, se asegura de que solo los usuarios autorizados puedan realizar acciones sensibles en su aplicación.