Presque tous les projets traitent les permissions de la même façon, en stockant chaque permission comme une ligne, et à juste titre : c'est ce que font les paquets standards et cela fonctionne. Le problème apparaît quand un même utilisateur a besoin de permissions différentes selon le bureau, le projet ou le client. Vous verrez ici ce qui casse exactement à ce moment-là et quand il vaut la peine de déplacer le catalogue de permissions dans le code. Chez AndorraDev nous travaillons surtout avec Laravel, donc les exemples et les comparaisons s'appuient sur son écosystème, mais la décision de fond se pose de la même manière avec n'importe quel framework.
Une nouvelle permission ne devrait pas être une ligne à semer
Dans l'approche habituelle, une permission est un enregistrement dans une table. Pour ajouter « gérer les factures » vous écrivez un seeder, vous l'exécutez en local, puis en préproduction, et vous pensez à l'exécuter en production. Si vous oubliez, la fonctionnalité est déployée mais personne ne peut s'en servir.
Et à l'inverse, quand vous supprimez une fonctionnalité la permission reste là, parce que l'effacer fait peur. Au bout de deux ans vous avez une table pleine de permissions qui ne veulent plus rien dire et que personne n'ose nettoyer.
Il y a une autre façon de voir les choses : le catalogue de permissions est du code. Il vit dans un fichier, il est versionné avec git, il se déploie avec le reste et il disparaît quand vous effacez la ligne.
Ce qui change quand la permission est du code
Le changement n'est pas cosmétique. Déplacer le catalogue de la base vers le code change les outils que vous pouvez lui appliquer, et ce sont soudain les mêmes que pour tout le reste : le contrôle de version, la recherche de l'éditeur et le compilateur.
Le déploiement. Une nouvelle permission arrive avec le même commit que la fonctionnalité qu'elle protège. Aucune étape manuelle à oublier, aucune désynchronisation entre environnements.
Le refactoring. Vous cherchez où une permission est utilisée avec le même outil que pour n'importe quoi d'autre. La renommer est un renommage normal, pas une migration.
Les types. Si vous déclarez les permissions dans une énumération, l'éditeur complète et une faute de frappe devient une erreur de compilation, pas une permission jamais satisfaite :
enum UserPermission: string
{
case ManageClients = 'manage_clients';
case HandleClients = 'handle_clients';
case ManageProperties = 'manage_properties';
case ManageAppointments = 'manage_appointments';
case ManageUsers = 'manage_users';
}
Cette énumération est réelle et compte treize cas. C'est tout le catalogue de permissions d'une plateforme immobilière en production, et cela tient sur un écran.
Le cas qui casse la plupart des systèmes de permissions
Jusqu'ici, ce ne sont que des avantages de maintenance, et aucun ne justifie à lui seul de changer de paquet. Ce qui le justifie, c'est une exigence qui apparaît dès que l'application a plus d'un bureau, et que les systèmes fondés sur des lignes ne règlent qu'à moitié.
Un utilisateur n'a pas des permissions, il a des permissions quelque part. La même personne peut gérer les clients de l'agence d'Andorre-la-Vieille et seulement consulter ceux d'Escaldes. Elle peut administrer un projet et n'être que lectrice sur un autre.
La plupart des systèmes résolvent cela à moitié, avec un identifiant unique d'équipe ou d'organisation accroché à l'affectation. Cela marche si votre hiérarchie n'a qu'un niveau. Dès qu'il faut délimiter par types de choses différents, cela ne suffit plus.
L'approche alternative est que la portée soit polymorphe : n'importe quel modèle peut être la portée d'une affectation.
$user->permissions('manage_users')->on($office)->check();
$user->permissions('manage_users')->on($organization)->check();
Même question, même utilisateur, deux réponses légitimement différentes.
Déclarer les permissions comme des routes
La déclaration ressemble délibérément à un fichier de routes : elle dit quelles permissions existent, qui peut les avoir et sur quoi.
Permission::create([
UserPermission::ManageClients->value => text('permissions.manage_clients', 'Gérer les clients'),
])->for(User::class)
->on([Office::class, Organization::class])
->implies(UserPermission::HandleClients->value);
Cela se lit d'une traite : la permission de gérer les clients existe, des utilisateurs peuvent l'avoir, elle s'accorde sur une agence ou sur une organisation, et qui l'a possède aussi celle de suivre les clients.
Ce for() n'est pas décoratif. Il empêche d'accorder à un modèle une permission qui n'était pas prévue pour lui, une classe d'erreur qu'un système à base de lignes ne détecte pas avant que quelqu'un n'essaie.
Décidez tôt si une permission globale a du sens dans votre domaine. Restreindre avec on(Office::class) déclare quelles portées sont valides, et accorder sans portée accorde globalement, ce qui est parfois exactement ce que vous voulez pour un super-administrateur. Si chez vous toute permission doit être liée à une ressource, rendez-le explicite là où vous accordez : ce sera une seule ligne et un seul endroit à auditer.
Une permission qui en implique une autre
La hiérarchie évite l'erreur la plus courante de ces systèmes : devoir penser à accorder les cinq permissions qui n'en sont en réalité qu'une.
En déclarant que gérer les clients implique les suivre, toute vérification de la seconde passe aussi pour qui détient la première. Sur la plateforme citée il y a deux hiérarchies de ce type, et elles éliminent toute une famille d'incidents du genre « il a les droits d'administrateur mais ça ne le laisse pas voir la fiche ».
Demander qui peut faire quelque chose, et pas seulement s'il peut
Il y a une requête dont on a toujours besoin et que l'on prévoit presque jamais à la conception : donne-moi les utilisateurs qui peuvent faire ceci ici. Il vous la faut pour la liste « affecter à », pour répartir les alertes et pour savoir vers qui escalader.
Si les permissions sont des lignes, cette requête est une jointure de trois tables que vous finissez par réécrire à la main chaque fois. Avec des permissions déclarées, elle se résout comme un filtre Eloquent de plus, et en respectant la hiérarchie :
$advisors = User::where('active', true)
->withPermission(UserPermission::HandleClients, $office)
->get();
Sur Abodara cette requête apparaît six fois, toujours pour la même chose : décider à qui l'on peut affecter un client ou un bien dans cette agence précise.
Ce qui existe sur le marché et en quoi cela diffère
La référence sur Laravel est spatie/laravel-permission, un paquet excellent et très éprouvé. La comparaison honnête est celle-ci.
Spatie stocke les permissions dans une table, avec de vraies clés étrangères et une couche de cache qui évite d'aller en base à chaque vérification. Il apporte des guards, des middlewares et des commandes. Si votre modèle de permissions est global ou à un seul niveau d'équipe, c'est un choix difficile à battre.
Ce qu'il ne fait pas, c'est le cas ci-dessus. Sa fonctionnalité d'équipes est un identifiant scalaire, un team_id, pas une portée polymorphe. Vous ne pouvez pas dire « cette permission sur cette agence et cette autre sur cette organisation » sans le construire par-dessus.
Larallow, que nous maintenons, est né exactement de ce besoin : des permissions déclarées dans le code, avec une portée polymorphe sur n'importe quel modèle et le support de plusieurs types d'acteur. Si votre application a des agences, des organisations, des projets ou des clients et que les droits changent selon lequel, il convient mieux. Si vos permissions sont globales, l'autre option vous donnera plus d'outils autour.
Trois choses à bien mettre en place
Déclarer le catalogue dans le code entraîne trois conséquences directes, et toutes se règlent à la mise en place.
C'est l'application qui valide le catalogue au moment d'accorder. Les permissions étant des déclarations et non des lignes, celui qui vérifie qu'une permission existe est la couche applicative, pas une clé étrangère. Accordez toujours via l'API du paquet et le catalogue se respecte tout seul. C'est en échange de cela que renommer une permission est un renommage d'éditeur et non une migration.
Tranchez d'abord les cas évidents. Une vérification avec portée atteint la base, donc sur des écrans à nombreuses vérifications il vaut la peine de couper d'abord par l'évident, du type administrateur général, et de mémoriser le résultat le temps de la requête. Deux lignes, et l'essentiel du travail disparaît.
Déclarez les permissions dans un provider dédié. Le catalogue vit en mémoire parce qu'un service provider le charge : mieux vaut que ce soit un provider à lui, plutôt que de l'accrocher au provider général de l'application. Il est alors disponible dans n'importe quel contexte d'exécution.
Et si vous ne travaillez pas avec Laravel
Le critère se transpose entièrement, et c'est lui qui compte au moment de choisir.
Demandez-vous si vos permissions changent selon la ressource. Si la réponse est non, stockez-les en base et utilisez ce qui existe déjà dans votre framework : vous gagnez intégrité et cache sans rien écrire.
Si la réponse est oui, ce qu'il vous faut c'est une affectation à trois pattes, qui, quoi et sur quoi, cette troisième patte pouvant être n'importe quoi. Cela se monte sur n'importe quelle stack, et c'est la conception qui compte, pas le paquet.
Où est le code
Le paquet est publié sur Packagist, le code sur GitHub et la documentation du paquet couvre toutes les options.
Si vous montez une application où les droits dépendent de l'agence, du projet ou du client, c'est un problème que nous avons résolu plusieurs fois. Nous le traitons en applications web sur mesure et développement Laravel, et si vous nous en parlez nous vous dirons si cela vaut le coup.