Desarrollamos tu web presencial SPA DESDE 300€. Si, es una locura. Web SPA DESDE 300€ — Si, es una locura. Hablemos →

Cuándo conviene definir los roles y permisos en código en vez de en la base de datos

Cuándo conviene definir los roles y permisos en código en vez de en la base de datos

Casi todos los proyectos resuelven los permisos igual, guardando cada permiso como una fila, y con razón: es lo que hacen los paquetes estándar y funciona. El problema aparece cuando un mismo usuario necesita permisos distintos según la oficina, el proyecto o el cliente. Aquí verás qué se rompe exactamente en ese punto y cuándo compensa mover el catálogo de permisos al código. En AndorraDev trabajamos sobre todo con Laravel, así que los ejemplos y las comparaciones van con su ecosistema, pero la decisión de fondo se plantea igual en cualquier framework.

Un permiso nuevo no debería ser una fila que sembrar

En el enfoque habitual, un permiso es un registro en una tabla. Para añadir "gestionar facturas" escribes un seeder, lo ejecutas en local, lo ejecutas en preproducción y te acuerdas de ejecutarlo en producción. Si se te olvida, la funcionalidad está desplegada pero nadie puede usarla.

Y al revés, cuando eliminas una funcionalidad el permiso se queda ahí, porque borrarlo da miedo. A los dos años tienes una tabla con permisos que ya no significan nada y nadie se atreve a limpiar.

Hay otra forma de verlo: el catálogo de permisos es código. Vive en un archivo, se versiona con git, se despliega con el resto y desaparece cuando borras la línea.

Lo que cambia cuando el permiso es código

El cambio no es de estilo. Mover el catálogo de la base de datos al código altera qué herramientas puedes usar sobre él, y de golpe pasan a servir las mismas que usas para todo lo demás: el control de versiones, el buscador del editor y el compilador.

El despliegue. Un permiso nuevo llega con el mismo commit que la funcionalidad que protege. No hay paso manual que olvidar ni desincronización entre entornos.

El refactor. Puedes buscar dónde se usa un permiso con la misma herramienta con la que buscas cualquier otra cosa. Renombrarlo es un renombrado normal, no una migración.

Los tipos. Si declaras los permisos en un enum, el editor te autocompleta y un error tipográfico es un error de compilación, no un permiso que nunca se cumple:

enum UserPermission: string
{
    case ManageClients = 'manage_clients';
    case HandleClients = 'handle_clients';
    case ManageProperties = 'manage_properties';
    case ManageAppointments = 'manage_appointments';
    case ManageUsers = 'manage_users';
}

Ese enum es real y tiene trece casos. Es todo el catálogo de permisos de una plataforma inmobiliaria en producción, y cabe en una pantalla.

El caso que rompe la mayoría de sistemas de permisos

Hasta aquí todo son ventajas de mantenimiento, y ninguna justifica por sí sola cambiar de paquete. Lo que sí lo justifica es un requisito que aparece en cuanto la aplicación tiene más de una oficina, y que los sistemas basados en filas resuelven a medias.

Un usuario no tiene permisos, tiene permisos en un sitio. La misma persona puede gestionar clientes en la oficina de Andorra la Vella y solo consultar los de Escaldes. Puede administrar un proyecto y ser un simple lector en otro.

La mayoría de sistemas resuelve esto a medias, con un único identificador de equipo o de organización colgando de la asignación. Funciona si tu jerarquía tiene un solo nivel. En cuanto necesitas acotar por tipos distintos de cosa, deja de servir.

El planteamiento alternativo es que el ámbito sea polimórfico: cualquier modelo puede ser el ámbito de una asignación.

$user->permissions('manage_users')->on($office)->check();
$user->permissions('manage_users')->on($organization)->check();

Misma pregunta, mismo usuario, dos respuestas legítimamente distintas.

Declarar los permisos como si fueran rutas

La declaración se parece deliberadamente a un archivo de rutas: dice qué permisos existen, quién puede tenerlos y sobre qué.

Permission::create([
    UserPermission::ManageClients->value => text('permissions.manage_clients', 'Gestionar clientes'),
])->for(User::class)
  ->on([Office::class, Organization::class])
  ->implies(UserPermission::HandleClients->value);

Se lee de corrido: existe el permiso de gestionar clientes, lo pueden tener usuarios, se concede sobre una oficina o sobre una organización, y quien lo tiene tiene también el de atender clientes.

Ese for() no es decorativo. Impide conceder a un modelo un permiso que no estaba pensado para él, que es una clase de error que en un sistema basado en filas no se detecta hasta que alguien lo prueba.

Andie recomienda

Decide pronto si en tu dominio tiene sentido un permiso global. Restringir con on(Office::class) declara qué ámbitos son válidos, y conceder sin ámbito concede globalmente, que a veces es justo lo que quieres para un superadministrador. Si en tu caso todo permiso debe ir siempre atado a un recurso, hazlo explícito en el punto donde concedes: será una sola línea y tendrás un único sitio que auditar.

Un permiso que implica otro

La jerarquía evita el error más común de estos sistemas, que es tener que acordarse de conceder los cinco permisos que en realidad son uno.

Declarando que gestionar clientes implica atenderlos, cualquier comprobación del segundo pasa también para quien tenga el primero. En la plataforma que menciono hay dos jerarquías así, y eliminan toda una clase de incidencias del tipo "tiene permiso de administrador pero no le deja ver la ficha".

Preguntar quién puede hacer algo, y no solo si puede

Hay una consulta que se necesita siempre y que casi nunca se contempla al diseñar: dame los usuarios que pueden hacer esto aquí. La necesitas para el desplegable de "asignar a", para repartir avisos y para saber a quién escalar.

Si los permisos son filas, esa consulta es un join de tres tablas que acabas escribiendo a mano cada vez. Con los permisos declarados se resuelve como un filtro más de Eloquent, y respetando la jerarquía:

$advisors = User::where('active', true)
    ->withPermission(UserPermission::HandleClients, $office)
    ->get();

En Abodara esa consulta aparece seis veces, siempre para lo mismo: decidir a quién se le puede asignar un cliente o una propiedad en esa oficina concreta.

Qué hay en el mercado y en qué se diferencia

La referencia en Laravel es spatie/laravel-permission, y es un paquete excelente y muy probado. La comparación honesta es esta.

Spatie guarda los permisos en una tabla, con claves foráneas de verdad y una capa de caché que evita ir a base de datos en cada comprobación. Trae guards, middleware y comandos de consola. Si tu modelo de permisos es global o de un solo nivel de equipo, es una elección difícil de mejorar.

Lo que no hace es el caso de arriba. Su función de equipos es un identificador escalar, un team_id, no un ámbito polimórfico. No puedes decir "este permiso sobre esta oficina y este otro sobre esta organización" sin construírtelo tú por encima.

Larallow, que es el que mantenemos, nació exactamente de esa necesidad: permisos declarados en código, con ámbito polimórfico sobre cualquier modelo y con soporte para varios tipos de actor. Si tu aplicación tiene oficinas, organizaciones, proyectos o clientes y los permisos cambian según cuál, encaja mejor. Si tus permisos son globales, la otra opción te va a dar más herramientas alrededor.

Tres cosas que hay que montar bien

Declarar el catálogo en código trae tres consecuencias directas, y las tres se resuelven en la puesta en marcha.

El catálogo lo valida la aplicación al conceder. Como los permisos son declaraciones y no filas, quien comprueba que un permiso existe es la capa de aplicación, no una clave foránea. Concede siempre a través de la API del paquete y el catálogo se respeta solo. A cambio de eso es por lo que renombrar un permiso es un renombrado del editor y no una migración.

Resuelve antes los casos evidentes. Una comprobación con ámbito llega a la base de datos, así que en pantallas con muchas comprobaciones compensa cortar primero por lo obvio, del tipo administrador general, y memorizar el resultado durante la petición. Son dos líneas y quitan la mayor parte del trabajo.

Declara los permisos en un proveedor dedicado. El catálogo vive en memoria porque lo carga un proveedor de servicios, así que conviene que sea uno propio y solo para esto, en vez de colgarlo del proveedor general de la aplicación. Así está disponible en cualquier contexto de ejecución.

Y si no trabajas con Laravel

El criterio se traslada entero, y es el que de verdad importa al elegir.

Pregúntate si tus permisos cambian según el recurso. Si la respuesta es no, guárdalos en base de datos y usa lo que ya existe en tu framework: ganas integridad y caché sin escribir nada.

Si la respuesta es sí, lo que necesitas es que la asignación tenga tres patas, quién, qué y sobre qué, y que esa tercera pata pueda ser cualquier cosa. Eso lo puedes montar sobre cualquier stack, y es el diseño lo que importa, no el paquete.

Dónde está el código

El paquete está publicado en Packagist, el código en GitHub y la documentación del paquete recoge todas las opciones.

Si estás montando una aplicación donde los permisos dependen de la oficina, del proyecto o del cliente, es un problema que hemos resuelto varias veces. Lo trabajamos en aplicaciones web a medida y desarrollo Laravel, y si nos lo cuentas te decimos si te compensa.

Escrito por
Edu Lazaro
Edu Lazaro
Founder & Lead Developer en AndorraDev

Desarrollador full-stack con más de 15 años de experiencia en Laravel, React, Node.js y arquitecturas cloud. Ayudo a empresas en Andorra a construir su presencia digital.

Partner de diseño · ionospace.
Necesitas ayuda? ×
Andie by AndorraDev
Asistente IA + equipo humano
Asistente IA de AndorraDev
Andie
Hola! Soy Andie, el asistente IA de AndorraDev. ¿En qué puedo ayudarte? Si necesitas hablar con Edu, solo pídelo.
17:27