Skip to content

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:

bash
php artisan make:model Role -m
php
<?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');
    }
};
bash
php artisan make:model Permission -m
php
<?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');
    }
};
bash
php artisan make:model RolePermission -m
php
<?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');
    }
};
bash
php artisan make:model UserRole -m
php
<?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
<?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:

php
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
<?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
<?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
<?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
<?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
<?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
<?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:

php
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:

php
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:

php
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.