Gairebé tots els projectes resolen els permisos igual, desant cada permís com una fila, i amb raó: és el que fan els paquets estàndard i funciona. El problema apareix quan un mateix usuari necessita permisos diferents segons l'oficina, el projecte o el client. Aquí veuràs què es trenca exactament en aquest punt i quan compensa moure el catàleg de permisos al codi. A AndorraDev treballem sobretot amb Laravel, així que els exemples i les comparacions van amb el seu ecosistema, però la decisió de fons es planteja igual en qualsevol framework.
Un permís nou no hauria de ser una fila que cal sembrar
En l'enfocament habitual, un permís és un registre en una taula. Per afegir "gestionar factures" escrius un seeder, l'executes en local, l'executes en preproducció i te'n recordes d'executar-lo en producció. Si t'oblides, la funcionalitat està desplegada però ningú la pot fer servir.
I a l'inrevés, quan elimines una funcionalitat el permís es queda allà, perquè esborrar-lo fa por. Al cap de dos anys tens una taula amb permisos que ja no signifiquen res i ningú gosa netejar.
Hi ha una altra manera de veure-ho: el catàleg de permisos és codi. Viu en un fitxer, es versiona amb git, es desplega amb la resta i desapareix quan esborres la línia.
Què canvia quan el permís és codi
El canvi no és d'estil. Moure el catàleg de la base de dades al codi altera quines eines hi pots fer servir, i de cop passen a servir les mateixes que uses per a tota la resta: el control de versions, el cercador de l'editor i el compilador.
El desplegament. Un permís nou arriba amb el mateix commit que la funcionalitat que protegeix. No hi ha pas manual que oblidar ni desincronització entre entorns.
El refactor. Pots buscar on es fa servir un permís amb la mateixa eina amb què busques qualsevol altra cosa. Reanomenar-lo és un reanomenament normal, no una migració.
Els tipus. Si declares els permisos en un enum, l'editor t'autocompleta i un error tipogràfic és un error de compilació, no un permís que no es compleix mai:
enum UserPermission: string
{
case ManageClients = 'manage_clients';
case HandleClients = 'handle_clients';
case ManageProperties = 'manage_properties';
case ManageAppointments = 'manage_appointments';
case ManageUsers = 'manage_users';
}
Aquest enum és real i té tretze casos. És tot el catàleg de permisos d'una plataforma immobiliària en producció, i cap en una pantalla.
El cas que trenca la majoria de sistemes de permisos
Fins aquí tot són avantatges de manteniment, i cap no justifica per si sola canviar de paquet. El que sí que ho justifica és un requisit que apareix així que l'aplicació té més d'una oficina, i que els sistemes basats en files resolen a mitges.
Un usuari no té permisos, té permisos en un lloc. La mateixa persona pot gestionar clients a l'oficina d'Andorra la Vella i només consultar els d'Escaldes. Pot administrar un projecte i ser un simple lector en un altre.
La majoria de sistemes ho resol a mitges, amb un únic identificador d'equip o d'organització penjant de l'assignació. Funciona si la teva jerarquia té un sol nivell. Així que necessites acotar per tipus diferents de cosa, deixa de servir.
El plantejament alternatiu és que l'àmbit sigui polimòrfic: qualsevol model pot ser l'àmbit d'una assignació.
$user->permissions('manage_users')->on($office)->check();
$user->permissions('manage_users')->on($organization)->check();
Mateixa pregunta, mateix usuari, dues respostes legítimament diferents.
Declarar els permisos com si fossin rutes
La declaració s'assembla deliberadament a un fitxer de rutes: diu quins permisos existeixen, qui els pot tenir i sobre què.
Permission::create([
UserPermission::ManageClients->value => text('permissions.manage_clients', 'Gestionar clients'),
])->for(User::class)
->on([Office::class, Organization::class])
->implies(UserPermission::HandleClients->value);
Es llegeix de corrandes: existeix el permís de gestionar clients, el poden tenir usuaris, es concedeix sobre una oficina o sobre una organització, i qui el té també té el d'atendre clients.
Aquest for() no és decoratiu. Impedeix concedir a un model un permís que no estava pensat per a ell, que és una mena d'error que en un sistema basat en files no es detecta fins que algú ho prova.
Decideix aviat si al teu domini té sentit un permís global. Restringir amb on(Office::class) declara quins àmbits són vàlids, i concedir sense àmbit concedeix globalment, que de vegades és justament el que vols per a un superadministrador. Si al teu cas tot permís ha d'anar sempre lligat a un recurs, fes-ho explícit al punt on concedeixes: serà una sola línia i tindràs un únic lloc a auditar.
Un permís que n'implica un altre
La jerarquia evita l'error més comú d'aquests sistemes, que és haver de recordar concedir els cinc permisos que en realitat són un.
Declarant que gestionar clients implica atendre'ls, qualsevol comprovació del segon passa també per a qui tingui el primer. A la plataforma que esmento hi ha dues jerarquies així, i eliminen tota una mena d'incidències del tipus "té permís d'administrador però no el deixa veure la fitxa".
Preguntar qui pot fer una cosa, i no només si pot
Hi ha una consulta que sempre es necessita i que gairebé mai es contempla en dissenyar: dona'm els usuaris que poden fer això aquí. La necessites per al desplegable d'"assignar a", per repartir avisos i per saber a qui escalar.
Si els permisos són files, aquesta consulta és un join de tres taules que acabes escrivint a mà cada vegada. Amb els permisos declarats es resol com un filtre més d'Eloquent, i respectant la jerarquia:
$advisors = User::where('active', true)
->withPermission(UserPermission::HandleClients, $office)
->get();
A Abodara aquesta consulta apareix sis vegades, sempre per al mateix: decidir a qui es pot assignar un client o una propietat en aquella oficina concreta.
Què hi ha al mercat i en què es diferencia
La referència a Laravel és spatie/laravel-permission, i és un paquet excel·lent i molt provat. La comparació honesta és aquesta.
Spatie desa els permisos en una taula, amb claus foranes de debò i una capa de memòria cau que evita anar a base de dades en cada comprovació. Porta guards, middleware i comandes de consola. Si el teu model de permisos és global o d'un sol nivell d'equip, és una tria difícil de millorar.
El que no fa és el cas de dalt. La seva funció d'equips és un identificador escalar, un team_id, no un àmbit polimòrfic. No pots dir "aquest permís sobre aquesta oficina i aquest altre sobre aquesta organització" sense construir-t'ho tu per sobre.
Larallow, que és el que mantenim, va néixer exactament d'aquesta necessitat: permisos declarats en codi, amb àmbit polimòrfic sobre qualsevol model i amb suport per a diversos tipus d'actor. Si la teva aplicació té oficines, organitzacions, projectes o clients i els permisos canvien segons quin, encaixa millor. Si els teus permisos són globals, l'altra opció et donarà més eines al voltant.
Tres coses que cal muntar bé
Declarar el catàleg en codi porta tres conseqüències directes, i totes tres es resolen a la posada en marxa.
El catàleg el valida l'aplicació en concedir. Com que els permisos són declaracions i no files, qui comprova que un permís existeix és la capa d'aplicació, no una clau forana. Concedeix sempre a través de l'API del paquet i el catàleg es respecta sol. A canvi d'això és pel que reanomenar un permís és un reanomenament de l'editor i no una migració.
Resol abans els casos evidents. Una comprovació amb àmbit arriba a la base de dades, així que en pantalles amb moltes comprovacions compensa tallar primer pel que és obvi, del tipus administrador general, i memoritzar el resultat durant la petició. Són dues línies i treuen la major part de la feina.
Declara els permisos en un proveïdor dedicat. El catàleg viu en memòria perquè el carrega un proveïdor de serveis, així que convé que sigui un de propi i només per a això, en comptes de penjar-lo del proveïdor general de l'aplicació. Així està disponible en qualsevol context d'execució.
I si no treballes amb Laravel
El criteri es trasllada sencer, i és el que de debò importa en triar.
Pregunta't si els teus permisos canvien segons el recurs. Si la resposta és no, desa'ls a base de dades i fes servir el que ja existeix al teu framework: guanyes integritat i memòria cau sense escriure res.
Si la resposta és sí, el que necessites és que l'assignació tingui tres potes, qui, què i sobre què, i que aquesta tercera pota pugui ser qualsevol cosa. Això ho pots muntar sobre qualsevol stack, i és el disseny el que importa, no el paquet.
On és el codi
El paquet està publicat a Packagist, el codi a GitHub i la documentació del paquet recull totes les opcions.
Si estàs muntant una aplicació on els permisos depenen de l'oficina, del projecte o del client, és un problema que hem resolt diverses vegades. El treballem a aplicacions web a mida i desenvolupament Laravel, i si ens ho expliques et diem si et compensa.