El Uso de los Gate, Permisos y Roles en Laravel: Una Guía para Principiantes

- Andrés Cruz

En Laravel, los roles y permisos han sido uno de los temas más confusos a lo largo de los años. Principalmente porque no hay documentación al respecto: las mismas cosas se "esconden" bajo otros términos en el marco, como "gates", "políticas", "guards", etc. 

Cuando se trata de controlar el acceso a diferentes partes de una aplicación web, los Gate en Laravel son una herramienta que podemos emplear de manera directa sin necesidad de instalar paquetes adicionales como Spatie. En este artículo, exploraremos qué son los Gate, cómo funcionan y cómo puedes aprovecharlos en tu proyecto.

¿Qué son los Gate?

Los Gate son una característica de autorización en Laravel que te permite definir reglas para determinar si un usuario tiene permiso para realizar una acción específica. Estas reglas se aplican a nivel de aplicación y pueden ser utilizadas para proteger rutas, acciones de controladores y cualquier otra parte de tu aplicación.

Los Gate en Laravel son una herramienta esencial para garantizar la seguridad y la autorización en tu aplicación. Aprender a utilizarlos correctamente te permitirá controlar quién puede acceder a qué partes de tu sistema.

Gate es lo mismo que Permiso

Una de las mayores confusiones, en mi opinión, es el término "gate". Creo que los desarrolladores habrían evitado mucha confusión si se les llamara como son.

Las puertas son permisos, llamados simplemente con otra palabra.

¿Cuáles son las acciones típicas que debemos realizar con permisos?

  • Define el permiso, ej. “administrar_usuarios”
  • Verifique el permiso en el front-end, ej. mostrar/ocultar el botón
  • Verifique el permiso en el back-end, ej. no puedo/no puedo actualizar los datos
    Así que sí, reemplaza la palabra "permiso" por "gate" y lo entenderás todo.

Un ejemplo sencillo de Laravel sería este:

 

app\Providers\AppServiceProvider.php

 

use App\Models\User;
use Illuminate\Support\Facades\Gate;

class AppServiceProvider extends ServiceProvider
{
   public function boot()
   {
       // Should return TRUE or FALSE
       Gate::define('manage_users', function(User $user) {
           return $user->is_admin == 1;
       });
   }
}

resources/views/navigation.blade.php:

<ul>
   <li>
       <a href="{{ route('projects.index') }}">Projects</a>
   </li>
   @can('manage_users')
   <li>
       <a href="{{ route('users.index') }}">Users</a>
   </li>
   @endcan
</ul>

routes/web.php:

Route::resource('users', UserController::class)->middleware('can:manage_users');

Gate puede significar más de un permiso. Entonces, en lugar de "manage_users", podrías definir algo como "admin_area". Pero en la mayoría de los ejemplos que he visto, Gate es sinónimo de Permiso.

Además, en algunos casos, los permisos se denominan "habilidades", como en el paquete Bouncer. También significa lo mismo: capacidad/permiso para alguna acción. Llegaremos a los paquetes más adelante en este artículo.

Varias formas de verificar el permiso

Otra fuente de confusión es cómo/dónde revisar el Gate. Es tan flexible que puedes encontrar ejemplos muy diferentes. Repasémoslos:

 

Opción 1. Rutas: middleware('can:xxxxxx')

Este es el ejemplo de arriba. Directamente en la ruta/grupo, podrá asignar el middleware:

Route::post('users', [UserController::class, 'store'])
   ->middleware('can:create_users');

Opción 2. Controlador: can () / cannot ()

En las primeras líneas del método Controller podemos ver algo como esto, con métodos can() o can(), idénticos a las directivas Blade:

public function store(Request $request)
{
   if (!$request->user()->can('create_users'))
       abort(403);
   }
}

El opuesto al anterior:

public function store(Request $request)
{
   if ($request->user()->cannot('create_users'))
       abort(403);
   }
}

O, si no tienes una variable $request, puedes usar el asistente auth():

public function create()
{
   if (!auth()->user()->can('create_users'))
       abort(403);
   }
}

Opción 3. Gate::allows() o Gate::denies()

Otra forma es utilizar una fachada de puerta:

public function store(Request $request)
{
   if (!Gate::allows('create_users')) {
       abort(403);
   }
}

O el negado:

public function store(Request $request)
{
   if (Gate::denies('create_users')) {
       abort(403);
   }
}

O, una forma más corta es usando helpers:

public function store(Request $request)
{
   abort_if(Gate::denies('create_users'), 403);
}

Opción 4. Controller: authorize()

Una opción aún más corta, y mi favorita, es usar Authorize() en Controladores. En caso de falla, devolvería una página 403 automáticamente.

public function store(Request $request)
{
   $this->authorize('create_users');
}

Opción 5. Clase Form Request 

He notado que muchos desarrolladores generan clases de Solicitud de formulario solo para definir las reglas de validación, ignorando por completo el primer método de esa clase, que es autorizar().

También puedes usarlo para revisar los Gates. De esta manera, logra una separación de preocupaciones, lo cual es una buena práctica para código sólido, de modo que el Controlador no se encarga de la validación, porque se realiza en su clase dedicada de Solicitud de formulario.

 

public function store(StoreUserRequest $request)
{
   // No check is needed in the Controller method
}

class StoreProjectRequest extends FormRequest
{
   public function authorize()
   {
       return Gate::allows('create_users');
   }

   public function rules()
   {
       return [
           // ...
       ];
   }
}

Política: conjunto de permisos basado en modelos

Si sus permisos se pueden asignar a un modelo Eloquent, en un controlador CRUD típico, puede crear una clase de Política alrededor de ellos.

Si ejecutamos este comando:

php artisan make:policy ProductPolicy --model=Product

Generará el archivo app/Policies/UserPolicy.php, con los métodos predeterminados que tienen un comentario para explicar su propósito:

use App\Models\Product;
use App\Models\User;

class ProductPolicy
{
   use HandlesAuthorization;

   /**
    * Determine whether the user can view any models.
    */
   public function viewAny(User $user)
   {
       //
   }

   /**
    * Determine whether the user can view the model.
    */
   public function view(User $user, Product $product)
   {
       //
   }

   /**
    * Determine whether the user can create models.
    */
   public function create(User $user)
   {
       //
   }

   /**
    * Determine whether the user can update the model.
    */
   public function update(User $user, Product $product)
   {
       //
   }

   /**
    * Determine whether the user can delete the model.
    */
   public function delete(User $user, Product $product)
   {
       //
   }

   /**
    * Determine whether the user can restore the model.
    */
   public function restore(User $user, Product $product)
   {
       //
   }

   /**
    * Determine whether the user can permanently delete the model.
    */
   public function forceDelete(User $user, Product $product)
   {
       //
   }
}

En cada uno de esos métodos, se define la condición para la devolución de true/false. Entonces, si seguimos los mismos ejemplos que Gates antes, podemos hacer esto:

class ProductPolicy
{
   public function create(User $user)
   {
       return $user->is_admin == 1;
   }
}

Luego, puede verificar la Política de una manera muy similar a la de Gates:

public function store(Request $request)
{
   $this->authorize('create', Product::class);
}

Entonces, especifica el nombre del método y el nombre de clase de la Política.

En otras palabras, las Políticas son simplemente otra forma de agrupar los permisos, en lugar de Puertas. Si sus acciones giran principalmente en torno a CRUD de modelos, entonces las políticas probablemente sean una opción más conveniente y mejor estructurada que Gates.

Rol: Conjunto universal de permisos

Analicemos otra confusión: en la documentación de Laravel, no encontrará ninguna sección sobre Roles de usuario. La razón es simple: el término "roles" está inventado artificialmente para agrupar el permiso bajo algún tipo de nombre, como "administrador" o "editor".

Desde el punto de vista del marco, no hay "roles", sólo gates/políticas que puede agrupar de la forma que desee.

En otras palabras, un rol es una entidad FUERA del marco de Laravel, por lo que debemos construir la estructura de roles nosotros mismos. Puede ser parte de la confusión general de autenticación, pero tiene mucho sentido porque debemos controlar cómo se definen los roles:

  • ¿Es un rol o múltiples roles?
  • ¿Puede un usuario tener un rol o múltiples roles?
  • ¿Quién puede gestionar los roles en el sistema?
    etc.


Entonces, la funcionalidad Role es otra capa de su aplicación Laravel. Aquí es donde llegamos a los paquetes de Laravel que pueden ayudar. Pero también podemos crear los roles sin ningún paquete:

  • Cree una tabla de base de datos de "roles" y un modelo de roles
  • Agregue una relación de Usuario a Rol: uno a muchos o muchos a muchos
  • Defina los roles predeterminados y asignarlos a los usuarios existentes
  • Asignar un rol predeterminado en el registro
  • Cambie Gate/políticas para verificar el rol en su lugar

Entonces, en lugar de:

class ProductPolicy
{
   public function create(User $user)
   {
       return $user->is_admin == 1;
   }
}

Hacemos:

class ProductPolicy
{
   public function create(User $user)
   {
       return $user->role_id == Role::ADMIN;
   }
}

Nuevamente, aquí tienes algunas opciones para verificar los roles. En el ejemplo anterior, asumimos que hay una relación de pertenencia entre Usuario y Rol, y también hay constantes en el modelo de rol como ADMIN = 1, como EDITOR = 2, solo para evitar consultar demasiado la base de datos.

Pero si prefieres ser flexible, puedes consultar la base de datos cada vez:

class ProductPolicy
{
   public function create(User $user)
   {
       return $user->role->name == 'Administrator';
   }

Haciéndolo flexible: permisos guardados en la base de datos

En mi experiencia personal, el modelo habitual para construirlo todo en conjunto es este:

  • Todos los permisos y roles se guardan en la base de datos y se administran con algún panel de administración.
  • Relaciones: permisos de roles de muchos a muchos, el usuario pertenece al rol (o roles de muchos a muchos).
    Luego, en AppServiceProvider, realiza un bucle foreach a partir de todos los permisos de la base de datos y ejecuta una declaración Gate::define() para cada uno de ellos, devolviendo verdadero/falso según el rol;
    Y finalmente, verifica los permisos con @can('permission_name') y $this->authorize('permission_name'), como en los ejemplos anteriores.
$roles = Role::with('permissions')->get();
$permissionsArray = [];
foreach ($roles as $role) {
   foreach ($role->permissions as $permissions) {
       $permissionsArray[$permissions->title][] = $role->id;
   }
}

// Every permission may have multiple roles assigned
foreach ($permissionsArray as $title => $roles) {
   Gate::define($title, function ($user) use ($roles) {
       // We check if we have the needed roles among current user's roles
       return count(array_intersect($user->roles->pluck('id')->toArray(), $roles)) > 0;
   });
}

En otras palabras, no verificamos ningún acceso por roles. El rol es solo una capa "artificial", un conjunto de permisos que se transforma en Gates durante el ciclo de vida de la aplicación.

¿Parece complicado? No te preocupes, aquí es donde llegamos a los paquetes que pueden ayudarte.

Paquetes para administrar roles/permisos

Los paquetes más populares para esto son Spatie Laravel Permission y Bouncer, tengo un artículo largo aparte sobre ellos. El artículo es muy antiguo, pero los líderes del mercado siguen siendo los mismos debido a su estabilidad.

Lo que hacen esos paquetes es ayudarle a abstraer la gestión de permisos en un lenguaje amigable para los humanos, con métodos que pueda recordar y usar fácilmente.

Mire esta hermosa sintaxis del permiso Spatie:

$user->givePermissionTo('edit articles');
$user->assignRole('writer');
$role->givePermissionTo('edit articles');
$user->can('edit articles');

Bouncer es quizás un poco menos intuitivo pero sigue siendo muy bueno:

Bouncer::allow($user)->to('create', Post::class);
Bouncer::allow('admin')->to('ban-users');
Bouncer::assign('admin')->to($user);

Entonces, estos paquetes son la "capa" final de autenticación/autorización que cubrimos aquí en este artículo. Espero que ahora tengas una idea completa y puedas elegir qué estrategia usar.

¿Qué pasa con los Guards?

Ah, esos. Causan tanta confusión a lo largo de los años. Muchos desarrolladores pensaron que los guardias son roles y comenzaron a crear tablas de base de datos separadas como "administradores" y luego asignándolas como Guards. En parte, porque en la documentación puede encontrar fragmentos de código como Auth::guard('admin')->attempt($credentials))

Incluso envié una solicitud de extracción a los documentos con una advertencia para evitar este malentendido.

En la documentación oficial, puede encontrar este párrafo:

En esencia, las instalaciones de autenticación de Laravel están compuestas por "Guards" y "proveedores". Los guardias definen cómo se autentican los usuarios para cada solicitud. Por ejemplo, Laravel viene con un protector de sesión que mantiene el estado mediante el almacenamiento de sesión y las cookies.

Entonces, los Guards son un concepto más global que los roles. Un ejemplo de guardia es "sesión"; más adelante en la documentación, podrá ver un ejemplo de guardia JWT. En otras palabras, una guardia es un mecanismo de autenticación completo y, para la mayoría de los proyectos de Laravel, nunca necesitarás cambiar el Guard ni siquiera saber cómo funcionan. Los Guards están fuera de este tema de roles/permisos.

 

Artículo original:

https://laravel-news.com/laravel-gates-policies-guards-explained

Andrés Cruz

Desarrollo con Laravel, Django, Flask, CodeIgniter, HTML5, CSS3, MySQL, JavaScript, Vue, Android, iOS, Flutter

Andrés Cruz en Udemy

Acepto recibir anuncios de interes sobre este Blog.