Aller au contenu

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-entitlements ne 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-entitlements en 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_CONFIGURE et 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, resourceType et conditions dè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.0
  • dynors-internal/applications/sirrat/docs/JOURNAL_ANOMALIES_SIRRAT.md (registre d'exécution)