Dans presque tous les panneaux d'administration, il existe une propriété qui signifie à la fois utiliser le produit, enregistrer des personnes, accorder des privilèges et traverser des frontières. En général, elle s'appelle admin. C'est pratique jusqu'au jour où quelqu'un demande : un administrateur du client peut-il créer un autre administrateur identique à lui ?
La séparation des tâches part de l'idée que certaines autorités ne devraient pas aller ensemble juste par commodité. Dans le catalogue, ça devient des permissions distinctes :
users.write -- enregistrer des personnes
authz.roles.assign_tenant -- attribuer des rôles du tenant
authz.grants.direct -- accorder des exceptions temporaires
platform.tenant.bypass -- traverser les tenantsPourquoi « n'accorder que ce qu'on possède » échoue
La règle la plus courante est : un utilisateur ne peut accorder que des permissions qu'il a lui-même. Elle a l'air sûre et elle a deux défauts.
Le premier, c'est l'auto-réplication. Si l'administrateur possède toutes les permissions de admin, le subset check lui permet aussi d'accorder admin à n'importe qui. La politique « l'admin du tenant enregistre des personnes et attribue des rôles opérationnels, mais ne crée pas d'autre admin » n'est tout simplement pas exprimable.
Le second, c'est l'inverse : la règle oblige celui qui a seulement besoin d'accorder une capacité à la posséder. Un responsable qui distribue l'accès aux microdonnées se met à lire des microdonnées, sans aucune raison de le faire.
La matrice d'autorité
La solution, c'est de modéliser directement l'autorité de concession :
role_grantable(grantor_role, target_role)
tenant_admin -> viewer
tenant_admin -> analyst
platform_admin -> tenant_admin
platform_admin -> viewer
platform_admin -> analystRemarquez ce qui n'y est pas : tenant_admin -> tenant_admin. La règle métier cesse d'être un effet de bord d'une comparaison d'ensembles et devient une ligne inspectable. Pour les grants directs, le même raisonnement s'applique, avec une autre matrice :
permission_grantable(grantor_role, permission_code)
tenant_admin -> analysis.run
tenant_admin -> data.query
tenant_admin -> content.shareEt au-dessus des deux matrices, il y a un hard deny : aucune autorité de tenant n'accorde de permission de plateforme, quoi que dise la matrice. Si quelqu'un se trompe dans la configuration, la frontière tient toujours.
Le prédicat, avec les parenthèses au bon endroit
can_assign(actor, target, role) :=
(
tenant_branch(actor, target)
OR platform_branch(actor)
)
AND role_is_grantable(actor, role)
AND target_role_content_is_allowed(actor, role)Ça ressemble à de la pédanterie, mais ici la précédence, c'est de la sécurité. Sans les parenthèses, A OR B AND C AND D devient A OR (B AND C AND D), et tout acteur qui satisfait la branche tenant saute les vérifications de matrice. C'est un bypass complet caché dans un opérateur. L'implémentation doit refléter la formule, ou utiliser des early returns équivalents qui ne laissent aucun doute.
Le dernier terme, target_role_content_is_allowed, compte quand le tenant peut créer ses propres rôles : la validation regarde le contenu du rôle, pas son nom. Un rôle custom appelé « Lecteur » qui contient authz.roles.assign_tenant n'est pas un rôle de lecteur.
Enregistrer n'est pas accorder
POST /users -- crée l'identité
POST /users/{id}/roles -- accorde l'autoritéDeux endpoints, deux services, deux traces. Réunir les deux dans un formulaire « nouvel utilisateur » avec un dropdown de rôle fait de chaque enregistrement une concession, et l'audit doit alors séparer ce que l'architecture a mélangé.
Bloquer l'auto-attribution, actor == target, est un garde-fou raisonnable. Mais ce n'est que ça : le vrai contrôle, c'est la matrice. Si la matrice permet l'escalade, empêcher quelqu'un de se l'appliquer à lui-même l'oblige seulement à demander à un collègue.
Des grants directs avec échéance
Les exceptions temporaires existent, et mieux vaut les modéliser que faire semblant du contraire. Mais le grant direct ne peut pas devenir une deuxième implémentation incontrôlée du RBAC. Les règles que je suis :
expires_atobligatoire. Il n'existe pas de grant éternel.- Justification obligatoire, enregistrée avec.
- Concession auditée dans la même transaction.
- L'expiration entre dans la fraîcheur de la session.
- Récurrence surveillée : le même grant renouvelé tous les mois, c'est un rôle que personne n'a voulu créer.
- Permissions de plateforme non délégables par aucun mécanisme de tenant.
Provenance
Pour chaque permission effective, le système doit pouvoir dire d'où elle vient : rôle système, rôle custom ou grant direct, qui l'a accordée, quand et jusqu'à quand. Ça transforme « pourquoi cette personne peut-elle faire ça ? » en une requête, et non en une enquête manuelle dans les logs.
Ce texte fait partie d'une série qui commence par L'autorisation en quatre questions.