Skip to content

Implementación de Permisos en Laravel

Introducción

Laravel proporciona políticas para definir la lógica de autorización en tu aplicación. Estas políticas establecen las reglas que determinan qué usuarios están autorizados para realizar acciones específicas en los modelos de tu aplicación. En esta guía, discutiremos los pasos necesarios para implementar permisos detallados en una aplicación de Laravel utilizando un esquema de base de datos, un trait y una política.

Prerrequisitos

Antes de continuar, asegúrate de tener una aplicación Laravel configurada y en funcionamiento. También debes estar familiarizado con las migraciones, modelos y políticas de Laravel.

Esquema de Base de Datos

Para implementar permisos en una aplicación Laravel, crea un esquema de base de datos que incluya tablas para roles, permisos y sus relaciones. El siguiente ejemplo muestra cómo crear estas tablas utilizando migraciones de Laravel:

php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

class CreateRolesTable extends Migration
{
    public function up()
    {
        Schema::create('roles', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('roles');
    }
}

class CreatePermissionsTable extends Migration
{
    public function up()
    {
        Schema::create('permissions', function (Blueprint $table) {
            $table->id();
            $table->string('name');
            $table->text('description')->nullable();
            $table->timestamps();
        });
    }

    public function down()
    {
        Schema::dropIfExists('permissions');
    }
}

class CreateRolePermissionsTable extends Migration
{
    public function up()
    {
        Schema::create('role_permissions', function (Blueprint $table) {
            $table->id();
            $table->unsignedBigInteger('role_id');
            $table->unsignedBigInteger('permission_id');
            $table->timestamps();

            $table->foreign('role_id')->references('id')->on('roles')->onDelete('cascade');
            $table->foreign('permission_id')->references('id')->on('permissions')->onDelete('cascade');
        });
    }

    public function down()
    {
        Schema::dropIfExists('role_permissions');
    }
}

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 externas 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 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 utilizaremos 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 en bruto "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 también.

Al utilizar esquemas de modelos de esta manera, podemos hacer que nuestro código sea más fácil de mantener y trabajar con el tiempo.

Modelos

Para implementar permisos detallados en una aplicación Laravel, crea tres modelos: Role, Permission y User. Estos modelos interactúan con las tablas de la base de datos que almacenan información sobre roles, permisos y relaciones usuario/rol.

Ejemplo del modelo Role:

php
namespace App\Models;

use App\Schemas\RolePermissionSchema;
use App\Schemas\UserRoleSchema;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Role extends Model
{
    use HasFactory;

    public function users(): BelongsToMany
    {
        return $this->belongsToMany(User::class, UserRoleSchema::table)->withTimestamps();
    }

    public function permissions(): BelongsToMany
    {
        return $this->belongsToMany(Permission::class, RolePermissionSchema::table)->withTimestamps();
    }
}

En el ejemplo del modelo Role anterior, utilizamos 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.

Ejemplo del modelo Permission:

php
namespace App\Models;

use App\Schemas\RolePermissionSchema;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;

class Permission extends Model
{
    use HasFactory;

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, RolePermissionSchema::table)->withTimestamps();
    }
}

En el ejemplo del modelo Permission anterior, utilizamos la relación BelongsToMany para definir la relación entre los modelos Permission y Role.

Finalmente, ejemplo del modelo User:

php
namespace App\Models;

use App\Schemas\UserRoleSchema;
use App\Traits\HasPermissions;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Relations\BelongsToMany;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
use Laravel\Sanctum\HasApiTokens;

class User extends Authenticatable
{
    use HasApiTokens, HasFactory, Notifiable, HasPermissions;

    protected $fillable = [
        'name',
        'email',
        'password',
    ];

    protected $hidden = [
        'password',
        'remember_token',
    ];

    protected $casts = [
        'email_verified_at' => 'datetime',
    ];

    public function roles(): BelongsToMany
    {
        return $this->belongsToMany(Role::class, UserRoleSchema::table)->withTimestamps();
    }
}

En el ejemplo del modelo User anterior, utilizamos 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 la verificación de permisos en una política, crea un trait que agregue un método hasPermission() a la clase que lo utiliza. El siguiente ejemplo muestra 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. Comprueba 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, lo que indica que el usuario no tiene el permiso necesario.

Al incorporar este trait en tu modelo User, puedes verificar fácilmente si un usuario tiene un permiso específico.

Implementación de Permisos Detallados con Políticas

Para aplicar permisos detallados usando el modelo User, crea una clase de política para cada modelo al que deseas controlar el acceso. En este ejemplo, crearemos una PostPolicy para gestionar el acceso al modelo Post.

Primero, crea 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;
    }
}

Ten en cuenta que utilizamos el trait hasPermission agregado al modelo User.

A continuación, registra la política en el AuthServiceProvider. Aquí hay un ejemplo de registro de 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 indica a Laravel que utilice la PostPolicy para controlar el acceso a las instancias del modelo Post.

Ahora, puedes usar esta política en tus controladores para autorizar acciones. Aquí hay un ejemplo de uso de 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);

        // Update the post...
    }
}

En este ejemplo, utilizamos el método authorize() para verificar si el usuario autenticado tiene permiso para actualizar la publicación dada, empleando el método update de la PostPolicy. Si el usuario está autorizado, le permitimos editar o actualizar la publicación. Si no, devolvemos un error HTTP 403 Forbidden.

Al utilizar políticas para controlar el acceso a tus modelos, te aseguras de que solo los usuarios autorizados puedan realizar acciones sensibles en tu aplicación.