Implementación de un Sistema basado en Roles y permisos con laravel-permission

Author
Por Darío Rivera
Publicado el en Laravel

Uno de los aspectos esenciales en la mayoría de aplicaciones es la implementación de un sistema basado en roles. Laravel provee de manera nativa dos elementos llamados Gates y Policies para manejar este problema. Sin embargo, un enfoque más profesional delega cierta responsabilidad a la base de datos agregando roles, permisos, permisos sobre roles  y permisos sobre usuarios.

Prerrequisitos

Para poder seguir este tutorial debes tener en cuenta los siguientes prerrequisitos.

- Laravel 5.8 o superior
- La clase User debe implementar la interfaz Illuminate\Contracts\Auth\Access\Authorizable
- La clase User no debe tener una propiedad role o roles (o campo en la base de datos)
- La clase User no debe tener el método permissions()

Instalación

El primer consiste en instalar la librería de laravel/permission de Spatie desde composer.

composer require spatie/laravel-permission

Una vez hecho esto se debe publicar las migraciones el las configuraciones del paquete con el siguiente comando.

php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"

Finalmente debes correr las migraciones.

php artisan migrate

La configuración por defecto del paquete quedará en el archivo config/permission.php.

Uso

Antes que nada, debes asegurarte de agregar el trait HasRoles en el modelo User.

...
use Spatie\Permission\Traits\HasRoles;

class User extends Authenticatable
{
    use Notifiable;
    use HasRoles;

    ...
}

Creación de roles y permisos

Una vez hecho esto podrás crear un rol y un permiso de la siguiente manera:

use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;

$role = Role::create(['name' => 'writer']);
$permission = Permission::create(['name' => 'edit articles']);

Asignación de roles y permisos

Para asignar un permiso a un rol puedes hacer bien sea con la instancia de un rol o con la instancia de un permiso de la siguiente manera:

$role->syncPermissions($permissions);
$permission->syncRoles($roles);

Revocar permisos

De la misma forma se puede revocar un permiso con cualquiera de las instancias disponibles, bien sea permiso o rol.

$role->revokePermissionTo($permission);
$permission->removeRole($role);

Autorización

Para correr la autorización sobre un recurso, por ejemplo en un controlador, basta utilizar el método authorize pasándole como parámetro el slug creado para el permiso.

$this->authorize('product.index');

Este método verificará si el usuario en sesión tiene la autorización al permiso product.index. En caso de no tenerlo se arrojará una excepción AuthorizationException que laravel manejará y mostrará al usuario como un HTTP 403 - Forbidden.

Creación dinámica de permisos

Una buena práctica en la gestión de permisos es la creación de seeders para poblar la base de datos con los permisos de tu aplicación, los roles, algunas asignaciones, etc. Para esto, vamos crear una constante llamada Resource en donde tendremos los slugs de los permisos, y vamos a asignar un par de slugs dependiendo de tus necesidades.

namespace App\Constants;

use App\Constants\Concerns\HasEnumValues;
use MyCLabs\Enum\Enum;

class Resource extends Enum
{
    public const USER_INDEX = 'user.index';
    public const PRODUCT_INDEX = 'product.index';
    public const PRODUCT_CREATE = 'product.create';

    public static function supported(): array
    {
        return collect(static::toArray())->values()->toArray();
    }
}

Nota que hemos decidido usar también la librería myclabs/php-enum para manejar esta constante manera más dinámica.

En la clase Resource hemos definido tres permisos, una para el listado de usuarios, otro para el listado de productos y otro para la creación de productos. Una vez hecho esto, nuestro seeder quedaría de la siguiente manera.

use App\Constants\Resource;
use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Permission;

class PermissionSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        foreach (Resource::supported() as $permission) {
            Permission::create(['name' => $permission]);
        }
    }
}

Como puedes ver, no hace falta especificar cada uno de los valores constantes de la clase Resource, con el método supported() obtenemos todos estos valores y los insertamos en la base de datos con la ayuda de Permission.

Creación dinámica de roles

Para la creación de roles no hace falta crear una clase de constantes ya que los roles pueden crearse de manera dinámica en la base de datos. Lo que se puede hacer es crear un seeder con los roles que estarán disponibles después de la instalación de la aplicación.

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;

class RoleSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $roles = ['Admin', 'Moderator', 'Guest'];

        foreach ($roles as $role) {
            Role::create(['name' => $role]);
        }
    }
}

Asignación de permisos

Una vez creados los roles y los permisos en la base de datos, y suponiendo que vas a utilizar permisos basados en roles y no directamente asignados a un usuario, solo resta realizar las asignaciones pertienentes a cada rol. Para esto, puedes crear un seeder en donde separes un método para cada asignación.

use Illuminate\Database\Seeder;
use Spatie\Permission\Models\Role;
use Spatie\Permission\Models\Permission;
use App\Constants\Resource;

class RoleHasPermissionSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $this->createAdminRolePermissions();
        $this->createUserRolePermissions();
    }

    /**
     * Creates the permissions for the role Admin
     */
    private function createAdminRolePermissions()
    {
        $role = Role::findByName('Admin');
        $role->syncPermissions(Permission::all());
    }

    /**
     * Creates the permissions for the role Moderator
     */
    private function createModeratorRolePermissions()
    {
        $role = Role::findByName('User');
        $role->syncPermissions(Permission::where('name', Resource::PRODUCT_INDEX)->first());
    }

    ...
}

Finalmente no olvides agregar estos seeders a la clase DatabaseSeeder.

$this->call(RoleSeeder::class);
$this->call(PermissionSeeder::class);
$this->call(RoleHasPermissionSeeder::class);

Como ayuda adicional te dejo este seeder para la creación del usuario Administrador con todos los permisos.

use App\User;
use Illuminate\Database\Seeder;

class AdminUserSeeder extends Seeder
{
    /**
     * Run the database seeds.
     *
     * @return void
     */
    public function run()
    {
        $user = User::create([
            'name' => 'Admin',
            'email' => 'admin@admin.com',
            'password' => bcrypt('password'),
        ]);

        $user->assignRole('Admin');
    }
}

Acerca de Darío Rivera

Author

Application Architect at Elentra Corp . Quality developer and passionate learner with 10+ years of experience in web technologies. Creator of EasyHttp , an standard way to consume HTTP Clients.

LinkedIn Twitter Instagram

Sólo aquellos que han alcanzado el éxito saben que siempre estuvo a un paso del momento en que pensaron renunciar.