Roles y permisos
Cómo se nombran, siembran y conceden los permisos, y cómo proteger tus pantallas.
El acceso dentro de la aplicación lo deciden permisos agrupados en roles, sobre el paquete laravel-permission de Spatie. Esta guía explica la convención de nombres, lo que el seeder crea por sí solo, las claves que puedes cambiar en config/app.php, cómo se administran los roles desde el panel y cómo proteger el código que escribas.
Las piezas
Los modelos son App\Models\Role y App\Models\Permission, sustituidos a través del bloque models de config/permission.php. Ambos extienden los modelos de Spatie, ambos hacen borrado suave, y ambos registran sus cambios en el registro de actividad. Las tablas son las del paquete por omisión (roles, permissions, model_has_roles, model_has_permissions, role_has_permissions), configuradas en config/permission.php. Todo permiso pertenece al guard web. El name de un rol se guarda como slug — Support Team se vuelve support-team — y el panel lo muestra como título, Support Team, pasado por el traductor.
Las revisiones de permisos se guardan en caché 24 horas. El paquete vacía la caché cuando un rol o permiso se guarda por sus propios métodos, y el panel la vacía otra vez después de editar o clonar un rol.
La convención de nombres
Cada permiso son dos palabras: {acción} {recurso}. La acción es una de create, retrieve, update, delete; el recurso es el singular de la tabla del modelo. Los permisos que trae una instalación nueva:
| Recurso | Permisos |
|---|---|
user |
create user, retrieve user, update user, delete user |
role |
create role, retrieve role, update role, delete role |
permission |
los cuatro, aunque la política sólo permite leer |
announcement |
los cuatro |
communication_log |
retrieve communication_log, delete communication_log y el par crear/actualizar, que la política nunca concede |
activity |
retrieve activity |
tracking_event_type |
los cuatro |
tracking_session |
retrieve tracking_session |
tracking_event |
los cuatro |
El panel parte un permiso por su espacio: la primera palabra elige la columna de la matriz de la pantalla de roles, la segunda la fila.
Lo que crea el seeder
php artisan db:seed corre, en orden, PermissionSeeder, RoleSeeder, AdminSeeder (las cuentas sudo), UserSeeder (las cuentas default_users) y AddOnSeeder (cobros). Puedes correr cada uno por separado:
php artisan db:seed --class=PermissionSeeder
php artisan db:seed --class=RoleSeeder
PermissionSeeder arma su lista desde dos fuentes, ambas configuradas en el bloque Permissions de config/app.php:
| Clave | Por omisión | Qué hace |
|---|---|---|
discover_front_permissions |
true |
Lee cada clase de recurso en app/Front/Resources/ y deriva sus permisos de las $actions que el recurso declara: create cuando la lista tiene create o store, retrieve cuando tiene show o index, update cuando tiene edit o update, delete cuando tiene destroy. Un recurso de sólo lectura con $actions = ['index', 'show'] recibe sólo retrieve. |
permissions |
[] |
Permisos extra tuyos, como 'nombre del permiso' => 'guard'. Cualquier nombre sirve; la forma de dos palabras es una convención, no una regla. |
allow_permisisons_deletion |
true |
Sí, con la falta de ortografía: esa es la clave real. Cuando es verdadera, un permiso en la base de datos que no está en ninguna de las dos fuentes se borra en suave en la siguiente siembra. Ponla en false si insertas permisos fuera del seeder y quieres que sobrevivan. |
Los permisos se insertan o actualizan por nombre y guard, y un permiso borrado en suave que reaparece en las fuentes se restaura. El descubrimiento lee el mapa de clases de Composer, así que corre composer dump-autoload después de agregar un recurso y antes de sembrar.
// config/app.php
'permissions' => [
'export reports' => 'web',
'dashboard user' => 'web',
],
RoleSeeder luego se asegura de que existan tres roles:
| Clave | Por omisión | Qué hace |
|---|---|---|
admin_role |
'admin' |
El rol superior. Se crea si falta y, en cada corrida, recibe todos los permisos de la tabla. El middleware de /admin revisa este rol por su nombre literal admin, así que cambiar sólo la clave deja el panel cerrado. |
default_role |
null |
Cuando está definido, el rol se crea si falta y, sólo al crearse, recibe los permisos listados en default_role_permissions. Nada en el kit lo asigna a cuentas nuevas: existe para que tengas listo un rol inicial que repartir, y RolePolicy se niega a eliminarlo. |
default_role_permissions |
[] |
Nombres de permisos concedidos a default_role la primera vez que se crea. Editar esta lista después no toca un rol que ya existe; concédelos desde el panel. |
| — | 'client' |
Siempre se crea un rol client. El registro se lo asigna a cada cuenta nueva. No trae permisos salvo que le concedas alguno. |
AdminSeeder asigna admin_role a cada correo de config('app.sudo'). Vuelve a correr los seeders cada vez que agregues un recurso o un permiso propio; son idempotentes.
Administrar roles desde el panel
/admin/roles lista los roles. Create pide un nombre y, ya que el rol existe, Edit muestra el selector de permisos:
- Una matriz con una fila por recurso y las columnas Create, See, Update, Delete. Clic en el encabezado de una columna para alternar la columna entera, clic en el nombre de una fila para alternar la fila entera. Cada casilla muestra el nombre exacto del permiso al pasar el cursor.
- Una lista Permissions debajo, con cada permiso que no sigue la forma de dos palabras, como los que agregaste en
config/app.php. - Los recursos cuya política declara un
$requiredConfigque está apagado quedan fuera de la matriz, para que a un rol no se le pueda conceder un módulo que no existe.
La acción Clone de un rol lo copia con todos sus permisos bajo el nombre {name}-copia (luego -copia-2, y así) y abre la copia para editarla.
Candados integrados en RolePolicy: el rol de administrador no se puede editar ni eliminar, y el default_role no se puede eliminar. Todo lo demás sigue los permisos de role.
Los roles se asignan a las cuentas en /admin/users: el campo Role del formulario de usuario es un select múltiple, visible sólo para editores que tienen a su vez el rol de administrador. Al guardar, la cuenta recibe una Role Update Notification que nombra los roles agregados y quitados. UserPolicy prohíbe eliminar una cuenta sudo.
Políticas
Cada recurso está protegido por una política en app/Policies/ que extiende App\Policies\BasePolicy. La clase base necesita una propiedad y mapea los métodos estándar de política a permisos:
class PlanPolicy extends BasePolicy
{
protected string $name = 'plan';
protected $requiredConfig = 'features.plans_enabled';
}
| Método de la política | Permiso revisado |
|---|---|
viewAny, view |
retrieve {name} |
create |
create {name} |
update |
update {name} |
delete, restore, forceDelete, viewDeleted |
delete {name} |
Dos ganchos afinan eso:
$requiredConfignombra una clave de configuración. Cuando es falsa,before()devuelvefalsey toda revisión falla, también para el rol de administrador.AnnouncementPolicyusafeatures.announcements; las políticas de seguimiento usanfeatures.tracking_enabled.extraValidation()se combina con AND en cada revisión. Sobrescríbelo para agregar una condición que aplique a todo el recurso.
Laravel resuelve la política por convención: App\Models\Plan queda protegido por App\Policies\PlanPolicy. Un modelo que vive en otro lado se registra a mano en AppServiceProvider::boot() con Gate::policy(Model::class, ModelPolicy::class), como el modelo del registro de actividad.
Las políticas incluidas y en qué se apartan de la tabla:
| Política | Diferencia |
|---|---|
UserPolicy |
delete exige además que el objetivo no sea sudo. |
RolePolicy |
update niega el rol de administrador; delete niega el rol de administrador y el rol por omisión. |
PermissionPolicy |
viewAny y view siempre son verdaderos; toda escritura es falsa. |
CommunicationLogPolicy |
create y update son falsos. |
AnnouncementPolicy |
$requiredConfig = 'features.announcements'. |
TrackingEventTypePolicy, TrackingSessionPolicy |
$requiredConfig = 'features.tracking_enabled'. |
TrackingEventPolicy |
Misma bandera; viewAny, view y create exigen el rol de administrador en vez de un permiso; update y delete son falsos. |
ActivityPolicy |
No extiende BasePolicy. viewAny exige el rol de administrador y un correo sudo; view el rol de administrador; toda escritura es falsa. |
Proteger tu propio código
Todo lo que construyas fuera de Laravel Front usa los mismos permisos. Elige la capa que encaje:
Una pantalla Livewire. Autoriza en mount(); el fallo se vuelve un 403.
public function mount(?Plan $plan)
{
$this->authorize($plan?->exists ? 'update' : 'create', $plan ?? Plan::class);
}
Una ruta. bootstrap/app.php registra los middleware de Spatie bajo los alias role, permission y role_or_permission, junto al can propio de Laravel:
Route::middleware('permission:export reports')->group(function () {
Route::livewire('/reports', App\Reports::class)->name('reports');
});
Route::middleware('role_or_permission:admin|retrieve plan')->group(function () {
// ...
});
Una vista. Oculta lo que la persona no puede hacer en vez de dejar que se tope con un 403.
@can('create plan')
<x-button href="{{ route('admin.front.plans.create') }}">
{{ __('New plan') }}
</x-button>
@endcan
@can('update', $plan)
<x-button href="{{ route('admin.front.plans.edit', $plan) }}">
{{ __('Edit') }}
</x-button>
@endcan
PHP plano. $user->can('delete plan'), Gate::allows('update', $plan), o $user->hasPermissionTo('export reports') para un permiso sin política detrás. Reserva $user->hasRole(config('app.admin_role')) para decisiones de presentación, como si dibujar el enlace al panel; la autorización pasa por permisos para que un rol que crees después pueda recibir la misma capacidad.
Dónde entra sudo
sudo no es un rol y no tiene permisos. Es un atributo calculado del usuario, verdadero cuando el correo está en config('app.sudo'). El seeder le da a esas cuentas el rol de administrador, que es lo que de verdad concede el acceso; el atributo en sí sólo se consulta donde el código lo nombra: la Dev Zone, el registro de actividad, la protección contra eliminar un usuario sudo y la entrada de la barra lateral hacia la Dev Zone. Quitar un correo de la lista quita esos extras pero deja el rol en su lugar. Mira /help/sudo-and-super-administrators.