ADR-0033 — Capacités contextualisées : RBAC comme source, la capacité comme unité d'autorisation¶
Date : 2026-08-29
Catégorie : A (modèle d'autorisation transverse)
Décideur : Lead architecte DYNORS
Statut : Acceptée — validée architecte le 2026-08-29 (deux invariants de revue intégrés :
§2 catalogue ouvert ≠ capacité exécutable, §5 octroi direct strictement additif) ; validation
CTO à programmer
Amende : [[0003-entitlements-source-agnostique-events]] §1, 1ʳᵉ phrase
Amendée par : [[0034-ownership-iam-et-policy-decision-point]] (2026-08-30) — §0 ci-dessous
tranchait implicitement la propriété du modèle IAM ; ADR-0034 la tranche explicitement et
distingue gouvernance et exécution. §5 corrigé le 2026-08-30 (échéance d'octroi direct).
S'appuie sur : [[0003-entitlements-source-agnostique-events]] (amendée 2026-08-29),
[[0031-habilitation-projet-affectation-projection-locale]], [[0032-resolution-habilitation-user-group]]
Complète (série stratégique « Groupe technologique DYNORS », adoptée en lot le 2026-06-21) :
ADR-2026-05-15-iam-rbac-abac-trust-layer (contribution partielle à la réserve « spec policy point
ABAC »), ADR-2026-05-15-bounded-contexts-ecosysteme-dynors, ADR-2026-05-15-pattern-delegation,
ADR-2026-05-15-moteur-workflow-approbation-transverse, ADR-2026-05-15-operating-model-modele-economique
Contexte¶
ADR-0031 et ADR-0032 ont posé la chaîne « affectation → droits → projection → enforcement » et
introduit deux premières capacités SIRRAT, COMPOSE et DEPLOY. L'étape suivante (E-3) doit
câbler les gardes de SIRRAT sur ces capacités.
Le risque est de figer le moteur autour de ces deux-là. Les besoins déjà identifiables ne s'y réduisent pas :
FORGE_READ FORGE_CONFIGURE TEMPLATE_CREATE CONSTANT_MANAGE
SECRET_RESOLVE DEPLOY_DEV DEPLOY_INT DEPLOY_RMOA
ROLLBACK AUDIT_READ VIEW_PROJECT VIEW_DEPLOYMENTS
et les qualités métier réelles non plus :
TECH_LEAD CHEF_PROJET RESPONSABLE_PRODUIT DEVELOPER OPS MANAGER
Le produit cartésien de ces deux listes est déjà de l'ordre de la centaine. Si chaque combinaison
devait donner un rôle technique, on obtiendrait TECH_LEAD_WITH_FORGE_CONFIGURE,
OPS_SANS_DEPLOY_RMOA, et ainsi de suite — puis, six mois plus tard, vingt énumérations et des
if (role == …) dispersés dans chaque consommateur. C'est le résultat par défaut si l'on ne
tranche pas maintenant, parce que chaque ajout pris isolément paraîtra raisonnable.
Décision¶
0. Amendement d'ADR-0003 §1 — « ne décide rien » → « aucune décision discrétionnaire »¶
ADR-0003 §1 dit de dynors-entitlements : « Il ne décide rien par lui-même : il consomme des
events émis par les producteurs légitimes ». La formulation n'excluait que l'arbitraire, mais
elle a été lue comme « le producteur envoie des permissions déjà calculées ». Appliquée
telle quelle, elle oblige chaque producteur à connaître la signification de ses propres rôles
pour chaque consommateur : TAKKU, source du fait de staffing, devrait savoir ce qu'OPS vaut
dans SIRRAT, puis dans GitLab, puis ailleurs. C'est une politique d'autorisation placée dans un
contexte Delivery & Staffing, et dupliquée producteur par producteur.
Nouvelle rédaction, qui remplace cette phrase :
dynors-entitlementsne prend aucune décision discrétionnaire. Il consomme les faits émis par les producteurs légitimes, leur applique une politique d'habilitation déclarée, versionnée et auditée, puis projette et expose les droits effectifs.
La nuance porte tout le poids : il n'invente pas le droit, il applique une politique
pré-définie. Ce n'est pas la même chose qu'un if (role == TECH_LEAD) enfoui dans un service.
Le reste d'ADR-0003 — projection source-agnostique, multi-producteurs, UPSERT idempotent,
fraîcheur, audit — est inchangé.
TAKKU Alice → DAWALALE → TECH_LEAD (le fait)
↓
ENTITLEMENTS politique v12, déclarée : (la règle)
TECH_LEAD × consumer=sirrat → COMPOSE, DEPLOY, FORGE_READ
TECH_LEAD × consumer=gitlab → MAINTAINER
↓
PROJECTION capacités effectives par projet (le résultat)
↓
SIRRAT / GITLAB enforcement (l'application)
Frontière de gouvernance — à ne pas laisser glisser¶
Entitlements applique la politique ; il ne produit pas la politique.
Précision apportée par ADR-0034 (2026-08-30). Cette section supposait résolue une question
qu'elle n'avait pas posée : qui possède le modèle IAM ? Un audit de l'existant a montré que la
table des bounded contexts (adoptée 2026-06-21) attribue « rôle, permission » au contexte
Identité & Accès, tandis que dynors-auth.md dit que ces mêmes concepts sont « résolus par
dynors-entitlements » — une contradiction antérieure à cette ADR, qu'elle a rendue visible sans
la nommer. La formulation de référence est désormais :
Le contexte Identité & Accès gouverne le modèle d'autorisation ;
dynors-entitlementsen constitue le moteur d'évaluation et de projection des droits effectifs.
Autrement dit : propriétaire du concept ≠ service qui exécute la décision. Tout ce qui suit dans cette ADR reste valable — c'est la justification de la localisation qui manquait, pas la localisation elle-même.
La politique est définie et gouvernée par l'architecture / la sécurité / l'administration
habilitée. dynors-entitlements en est le runtime d'application, pas l'auteur. Toute
modification doit être versionnée, datée, attribuée, auditée et réversible, et
policyVersion doit figurer dans le journal de résolution ainsi que dans chaque projection
produite — sans quoi on ne peut plus répondre à « sous quelle politique ce droit a-t-il été
calculé », et un droit qu'on ne sait pas rattacher à sa règle ne se retire jamais.
Forme minimale du catalogue de politique :
businessRole · consumer · capability · version · validFrom · validTo?
et, au premier besoin réel seulement : scope, circle, resourceType, conditions (§4).
Transition assumée jusqu'à E-6¶
D'ici l'étape E-6, TAKKU exécute cette politique sans la posséder : le fichier
entitlement-policy.yaml est étiqueté propriété de dynors-entitlements, TAKKU ne contient
aucun mapping en dur — un test scanne son code pour le vérifier — et E-6 sera un déplacement
de l'exécution vers le propriétaire, pas une réécriture. L'invariant à tenir pendant toute la
période :
TAKKU peut exécuter temporairement une politique qu'il ne possède pas ; il ne doit plus la définir.
1. Trois niveaux distincts, jamais confondus¶
RÔLE MÉTIER TECH_LEAD · CHEF_PROJET · RESPONSABLE_PRODUIT · DEVELOPER · OPS · MANAGER
→ décrit la POSITION de la personne
→ propriété de TAKKU (contexte Delivery & Staffing)
CAPACITÉ COMPOSE · DEPLOY · FORGE_CONFIGURE · SECRET_RESOLVE · AUDIT_READ · …
→ décrit ce qu'elle PEUT FAIRE dans un produit
→ unité finale d'autorisation
CONTEXTE projet · cercle · ressource
→ décrit OÙ cette capacité s'applique
Le rôle métier n'est jamais une autorisation. C'est une source pratique de capacités, rien de plus. La décision d'autorisation s'écrit :
can( subject, capability, project?, circle?, resource? )
et jamais :
if (role == TECH_LEAD) ← interdit dans tout consommateur
hasRole(SIRRAT_OPS) && hasProject(p) ← deux moteurs concurrents, interdit aussi
Un consommateur qui connaît un nom de rôle métier a déjà franchi la frontière.
2. Catalogue de capacités ouvert¶
Une capacité est un code, pas une valeur d'énumération fermée. Ajouter FORGE_CONFIGURE,
ROLLBACK ou AUDIT_READ se fait :
- dans le catalogue de la politique (déclaratif, versionné) ;
- et, côté consommateur, uniquement là où un acte l'exige.
Aucun producteur n'est recompilé. Aucun nouveau rôle technique n'est créé.
Catalogue ouvert ≠ capacité inconnue exécutable¶
C'est l'invariant à ne pas rater, parce que l'erreur symétrique est tentante : « le catalogue est ouvert, donc toute chaîne reçue devient utilisable ». Non.
capacité reçue mais inconnue du moteur SIRRAT
→ CONSERVÉE dans la projection
→ visible, auditable, affichable
→ N'AUTORISE AUCUNE ACTION
Les deux moitiés comptent, et elles ne se déduisent pas l'une de l'autre :
- Conserver, parce que l'écarter perdrait en silence une décision qui a bien été prise par entitlements, et rendrait incompréhensible un écran de revue après une mise à jour de politique.
- N'autoriser rien, parce qu'un moteur qui accorderait un privilège sur la foi d'une chaîne qu'il ne sait pas interpréter transformerait le catalogue ouvert en trou d'autorisation : il suffirait qu'une politique nomme une capacité pour qu'elle devienne exécutable partout.
Exemple :
FORGE_CONFIGURE reçu
mais SIRRAT ne sait pas encore quel acte cette capacité garde
→ stocké
→ visible dans « Habilitations reçues »
→ aucun privilège supplémentaire
C'est du fail-closed : le moteur n'exécute que les capacités qu'il sait explicitement associer à une action.
Conséquence technique : l'unité persistée est une clé, pas une valeur d'énumération¶
Si SirratProjectCapability reste une énumération Java fermée en persistance, le catalogue
n'est pas réellement ouvert — il l'est sur le papier et fermé dans la base, et une capacité
inconnue serait rejetée à la lecture, pas conservée.
PROJECTION capabilityKey : String ← stable, normalisé, ouvert
MOTEUR D'ENFORCEMENT catalogue des capacités RÉELLEMENT supportées, avec l'acte qu'elles gardent
Le moteur d'enforcement garde son propre catalogue, fermé et petit : ce sont les clés que les gardes savent écrire. Les deux ne se confondent pas, et c'est ce décalage qui permet à la politique d'avancer avant le code sans jamais accorder au-delà.
Cette section remplace R-104 (« capacité inconnue ignorée ») — corrigée en E-3.
3. Aucune implication entre capacités¶
{DEPLOY} ne devient PAS {COMPOSE, DEPLOY}
Un rôle qui doit avoir les deux les porte tous les deux, écrits dans la politique. Un consommateur qui compléterait l'ensemble reçu déciderait à la place de la politique — et cette décision-là n'apparaîtrait dans aucun journal.
SIRRAT applique la décision reçue ; il ne la complète pas.
L'ordre DEVELOPER < OPS d'ADR-0032 reste vrai, mais comme inclusion d'ensembles produite par
la politique, pas comme hiérarchie codée : OPS contient les capacités de DEVELOPER parce que
la table le dit, et c'est vérifiable par un test.
4. Dimensions de contexte optionnelles¶
Toutes les capacités n'ont pas la même portée, et exiger toutes les dimensions produirait des valeurs de remplissage — c'est-à-dire des mensonges.
| Portée | Exemple | Écriture |
|---|---|---|
| Projet | FORGE_CONFIGURE sur DAWALALE |
project |
| Projet × cercle | DEPLOY sur DAWALALE / RMOA |
project + circle |
| Transverse | SIRRAT_PLATFORM_ENGINEER |
ni l'un ni l'autre |
Les capacités transverses restent explicitement nommées et exceptionnelles. Une capacité sans contexte est une capacité qui s'applique partout : elle doit se voir dans la revue, pas se déduire d'un champ resté vide.
Les dimensions non utilisées ne sont pas introduites. resourceType et conditions sont
prévues par ce modèle et ne seront ajoutées qu'au premier besoin réel. Une dimension créée avant
son usage se remplit toute seule de mauvaises valeurs, et l'on ne peut plus la retirer.
L'étage ABAC organisationnel est reporté, pas oublié¶
L'ADR IAM prévoit des attributs d'organisation (entité, BU, division, centre de coût,
équipe) résolus depuis le contexte Organisation (ADR-05) — qui n'existe pas encore.
project × circle en est une instance restreinte et suffisante pour P1 (ADR-0034 §7). Les
attributs d'organisation seront ajoutés quand un cas d'usage réel les demandera ; attendre ce
contexte pour terminer l'autorisation de SIRRAT laisserait des trous ouverts derrière un programme
de plusieurs mois.
Portée d'E-3 : le modèle est conceptuel, le moteur ne l'est pas¶
can(subject, capability, project?, circle?, resource?) est le modèle. E-3 n'implémente
pas un moteur ABAC universel par anticipation — il évalue ce que SIRRAT utilise réellement :
subject × project × capability × circle lorsque la capacité l'exige
resource, resourceType et conditions restent réservés. Un moteur générique construit
avant ses besoins coûte deux fois : à l'écrire, puis à le contraindre quand les vrais besoins
arrivent et ne rentrent pas dans la généralité qu'on avait imaginée.
5. Octroi direct de capacité, sans inventer de rôle¶
Une capacité doit pouvoir être accordée hors de tout rôle :
Alice · TECH_LEAD sur DAWALALE
capacités calculées : COMPOSE · DEPLOY · FORGE_READ
+ FORGE_CONFIGURE
project = dawalale
expiresAt = 2026-09-30
reason = migration des templates
sans créer TECH_LEAD_WITH_FORGE_CONFIGURE. Même principe pour un GROUP
(GROUP equipe-plateforme → FORGE_READ) et pour une portée particulière
(TECH_LEAD → DAWALALE → FORGE_CONFIGURE, mais pas sur DOSALI).
Ce qu'un octroi direct doit porter — corrigé le 2026-08-30 :
reason obligatoire pourquoi ce droit existe
grantedBy obligatoire qui l'a accordé
grantedAt obligatoire quand
source obligatoire d'où vient la décision
audit obligatoire trace de l'octroi ET de la révocation
additif uniquement jamais de DENY / REMOVE (voir plus bas)
expiresAt OPTIONNEL absent ⇒ actif jusqu'à révocation explicite
révocation explicite toujours possible, même sans échéance
expiresAt est optionnel, et aucune durée minimale ou maximale n'est imposée ici. Un octroi
direct peut être une habilitation assumée et durable :
Alice · AUDIT_READ · DAWALALE · expiresAt = 2026-12-31 ← borné
Alice · AUDIT_READ · DAWALALE · expiresAt = NULL ← jusqu'à révocation
Alice · AUDIT_READ · DAWALALE · 4 heures (incident) ← court, et toujours un octroi direct
Le mécanisme reste générique. Si certaines capacités sensibles — SIRRAT_BREAK_GLASS,
SECRET_RESOLVE — doivent exiger une échéance, c'est une règle de politique propre à ces
capacités, pas une propriété universelle de l'octroi direct. Inscrire ici une durée plancher
handicaperait tous les autres octrois pour un besoin qui n'en concerne que quelques-uns.
Ce qui reste inconditionnel, c'est reason et grantedBy : une exception sans motif est
irrévocable de fait — personne n'ose retirer ce que personne ne sait justifier — et une exception
sans auteur n'a pas d'autorité à interroger le jour où on la conteste.
Ces octrois vivent dans les entitlements, pas dans la table de traduction. La table dit ce que vaut un rôle ; l'octroi direct est une décision individuelle.
L'octroi direct est strictement additif — et l'exception restrictive passe ailleurs¶
droits effectifs = capacités dérivées du rôle ∪ octrois directs
L'union, et rien d'autre. On ne crée pas à ce stade :
DENY DEPLOY ← non
REMOVE COMPOSE ← non
Non par oubli, mais parce que dès qu'on ajoute le retrait par capacité, il faut définir une algèbre de précédence complète :
droits de GROUP + surcharge USER de rôle + octrois de capacité + retraits de capacité
↓
qui l'emporte sur qui, dans quel ordre ?
Cette algèbre demande sa propre ADR et son propre jeu de cas limites. E-3 n'en a pas besoin, et l'improviser en chemin produirait exactement le genre de règle implicite que tout ce chantier retire.
L'exception restrictive reste donc portée par la surcharge USER d'ADR-0032 : elle remplace
le résultat GROUP/rôle pour un projet, dans les deux sens. C'est ce qui permet d'exprimer
« Alice est TECH_LEAD mais, sur DOSALI, seulement DEVELOPER » sans la sortir de son équipe —
donc sans fausser le staffing pour régler un problème d'habilitation.
| Besoin | Mécanisme | ADR |
|---|---|---|
| élargir ponctuellement | octroi direct de capacité, additif et motivé (échéance facultative) | 0033 §5 |
| restreindre sur un projet | surcharge USER du rôle sur ce projet |
0032 §3 |
| restreindre une capacité isolée | non couvert — demande une ADR d'algèbre de précédence | — |
| agir au nom d'un autre | Delegation + token d'élévation — mécanisme distinct |
ADR-2026-05-15-pattern-delegation |
Octroi direct ≠ délégation — le discriminant n'est pas la durée¶
La confusion est facile parce que les deux peuvent être temporaires. Ce n'est pas là que passe la frontière : c'est l'identité de l'acteur.
OCTROI DIRECT Alice reçoit la capacité. Alice agit comme Alice.
Un seul acteur dans la trace.
Entre dans la projection.
DÉLÉGATION Alice utilise un pouvoir de Bob. actor_delegated_from = Bob.
Deux acteurs dans la trace.
N'entre PAS dans la projection — token distinct __Host-elv,
lu par JwtAuthFilter (ADR pattern-delegation, amendement B).
Un octroi direct de quatre heures pendant un incident reste un octroi direct si Alice agit en
son propre nom. Une délégation de trois mois resterait une délégation. Fusionner les deux
détruirait actor_delegated_from, exigence de l'audit trail probant — un audit doit pouvoir
répondre « qui a agi, et pour le compte de qui », et seule la délégation a une réponse à la
seconde moitié de la question.
6. Où vit quoi — récapitulatif¶
| Propriétaire | Contenu | |
|---|---|---|
| Fait métier | TAKKU | personne / groupe × projet × rôle métier, avec période |
| Politique de traduction | dynors-entitlements | businessRole × consumer → capabilities, versionnée |
| Octroi direct | dynors-entitlements | capacité, sujet, portée, échéance, motif |
| Droits effectifs | dynors-entitlements | projection aplatie, avec policyVersion |
| Enforcement | SIRRAT / GitLab / autres | can(subject, capability, context) |
Aucune de ces cases ne connaît le contenu des autres. TAKKU ignore ce que vaut OPS ; SIRRAT
ignore qu'OPS existe.
Conséquences¶
Positif. Ajouter une capacité, un rôle métier ou un consommateur ne touche aucun code de
producteur. La même qualité métier produit des décisions différentes selon le consommateur —
TECH_LEAD donnant COMPOSE · DEPLOY · FORGE_READ · FORGE_CONFIGURE dans SIRRAT et MAINTAINER
dans GitLab — sans qu'aucune des deux ne connaisse l'autre. Une exception temporaire s'exprime
sans polluer le référentiel de rôles.
Coût. Une politique à gouverner explicitement, et une indiscipline qui coûte cher : le jour où un consommateur ajoute « juste ce cas-là » en dur, la frontière retombe. Des tests doivent la tenir, pas seulement la documentation.
Ce qui devient une dette à solder. SIRRAT_OPS est aujourd'hui une enveloppe technique de
session : dès qu'un projet confère une capacité opérationnelle, la session reçoit les scopes de
lecture et d'écriture. Elle n'accorde rien par elle-même, mais tant que les actes ne sont pas
gardés par capacité, l'enforcement reste incomplet.
Critère de sortie d'E-3, non négociable : à la fin de l'étape, SIRRAT_OPS ne constitue
plus une source d'autorisation métier. Toute décision se prend depuis la projection :
require(project, circle, capability) ← la seule forme admise
hasRole(SIRRAT_OPS) && hasProject(project) ← proscrite : deux moteurs concurrents
Elle ne peut subsister, transitoirement, que comme enveloppe d'ouverture de session — et cette enveloppe elle-même est réévaluée après E-3, une fois qu'on voit ce qu'il en reste réellement.
Composants impactés : dynors-entitlements (catalogue de politique, octrois directs),
dynors-internal/applications/takku (exécution transitoire de la politique, à retirer en E-6),
dynors-internal/applications/sirrat (ouverture du catalogue de capacités, gardes E-3).
Alternatives écartées¶
- RBAC pur — un rôle technique par combinaison de droits. Produit le cartésien
rôle × capacité, donc
TECH_LEAD_WITH_FORGE_CONFIGUREet ses cousins. Écartée : c'est exactement le résultat qu'on veut éviter, et il arrive par accumulation de décisions individuellement raisonnables. - Capacités en énumération fermée par consommateur. Simple et typée, mais chaque nouvelle capacité devient un déploiement coordonné de tous les producteurs. Écartée.
- Moteur de règles à conditions dynamiques dans entitlements. Couvrirait tout, y compris ce dont on n'a pas besoin ; un service critique difficile à gouverner. Écartée au profit d'une table déclarative (ADR-0003 amendée).
- Introduire
circle,resourceTypeetconditionsdès maintenant. Écartée : dimensions sans usage, donc valeurs de remplissage. Elles sont prévues, pas créées.
Références¶
- ADR-0003 (amendement 2026-08-29 — politique déclarative, frontière de gouvernance)
- ADR-0031 (affectation → droits → projection locale)
- ADR-0032 (résolution
USER/GROUP, ordre, révision monotone) dynors-docs/docs/05-architecture-si/dynors-entitlements.md§3.0dynors-internal/applications/sirrat/docs/JOURNAL_ANOMALIES_SIRRAT.md(registre d'exécution)