docs(adr-0025): authorization model — privileges × roles × scopes #201
Reference in New Issue
Block a user
Delete Branch "docs/adr-0025-authorization-model"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Proposes ADR-0025 — the portal's general-purpose authorization model. Status:
proposed. No code in this PR; the goal is to lock the model and the v1 catalogues before the implementation chantier opens.The model rejects stargate's linear hierarchy (
Admin ⊃ Directeur ⊃ RH ⊃ Collaborateur) and adopts three orthogonal axes:Portal.Admin, futurePortal.Auditor, …)rolesclaimrh,directeur-etablissement,elu-cd-tresorier, …)groupsclaim → curated slug catalogueetablissement:0330800013,delegation:33,unrestricted, …)user_scopestable (v1) ; future Pléiades feedThe three axes compose at sign-in into a session-resident
Principal. The portal's guards consume the structured shape; a deterministic projector flattens it to theroles[]list thatapf-ai-serviceexpects per ADR-0024.What lands
docs/decisions/0025-authorization-model-privileges-roles-scopes.md— full MADR with context, decision drivers, four considered options, exhaustive v1 catalogues, Entra-side configuration,Principalshape, AI-service projection, guard surface, ten test-tenant personas, consequences, confirmation criteria, pros/cons per option, ABAC migration path, related ADRs, and proposed follow-up ADRs.docs/decisions/README.md— index row for ADR-0025 (proposed, tagssecurity, backend, data, 2026-05-20).No
CLAUDE.mdupdate — ADR stays inproposeduntil reviewed; the accepted-ADRs roll-up at the top ofCLAUDE.mdstays at 0001 → 0024. Promotion toacceptedlands in the same PR that ships the implementation skeleton (libs/feature/authextension withPrincipalbuilder + catalogues).Highlights worth review focus
Portal.Adminin v1. Anticipated future entries (Portal.Auditor,Portal.SecurityOfficer,Portal.DPO) are mentioned in the ADR but not formalised — each one rides an amendment ADR.@RequireRole('x')literal in code.self,etablissement:<finess>,delegation:<dept>,region:<insee>,siege,unrestricted. Thevaluecarriers are documented (FINESS code rather than internaletablissement.idbecause FINESS is stable across reorgs).Principalshape is the contract for everything downstream. Documented field-by-field. Built once at sign-in, persisted in the Redis session, refreshed on every authenticated request.PrincipalProjectorfor the AI service is mechanical: union of privileges + roles + scope-strings, no inclusive expansion. The projector is the only seam that knows about the flat shape; the rest of the portal never touches it.Test-tenant personas
The ADR proposes ten test users covering every interesting combination of the three axes. Table in §"Test-tenant personas" of the ADR; here's the summary the user can act on:
admin@<tenant>Portal.Admincollaborateur,rhunrestricteddirecteur-bordeaux@<tenant>directeur-etablissement,collaborateuretablissement:0330800013directeur-complexe@<tenant>directeur-etablissement,collaborateuretablissement:*scopesrh-aquitaine@<tenant>rh,collaborateurdelegation:33rh-siege@<tenant>rh,responsable-paie,collaborateurunrestrictedcollab-simple@<tenant>collaborateurselftresorier-bordeaux@<tenant>elu-cd-tresorier,elu-cddelegation:33dpo@<tenant>dpo,collaborateurunrestrictedit@<tenant>it,collaborateurunrestrictedbenevole-aquitaine@<tenant>benevole,benevole-responsabledelegation:33Entra test-tenant setup the user needs to provision after acceptance:
Portal.Admin(likely already there from the existing dev tenant; document the GUID inapps/portal-bff/.env.test).apf-role-collaborateur,apf-role-chef-equipe,apf-role-chef-service,apf-role-directeur-etablissement,apf-role-directeur-territorial,apf-role-rh,apf-role-responsable-paie,apf-role-comptable,apf-role-juriste,apf-role-dpo,apf-role-rssi,apf-role-it,apf-role-formation,apf-role-qualite,apf-role-communication,apf-role-elu-ca,apf-role-elu-cd,apf-role-elu-cd-president,apf-role-elu-cd-tresorier,apf-role-elu-cd-secretaire,apf-role-delegue,apf-role-benevole,apf-role-benevole-responsable,apf-role-partenaire.groupMembershipClaims: 'SecurityGroup'+optionalClaims.idToken: [{ name: 'groups' }]so the BFF sees the memberships in the ID token.GUIDs and credentials stay in the operator's hands (out of git). When the user has provisioned the tenant, drop the GUIDs into
infra/test-tenant.entra.json(gitignored) and the implementation PR wireslibs/feature/auth/src/lib/entra-group-to-role.tsagainst them.Notes for the reviewer
RoleMapper. The mapper's inclusive expansion (Admin → [admin, directeur, rh, collaborateur]) bakes in the wrong assumption — that roles form a chain. Reusing it would force every new role into the chain too. The three-axis model has no such forcing function.Personis conspicuously absent here. Authorization is keyed on the portal-sideUseraccount (Entra OID, session). The proposedPerson+Usersplit lands in a sibling ADR (proposed: ADR-0026) because the two decisions have different audiences — auth model is a backend/security concern; golden record is a data/domain concern. They will land in coordinated PRs.etablissement:*scopes. FINESS codes are the canonical APF identifier for an établissement, stable across the internal-database churn (etablissement merges, reorgs, system migrations). Using the FINESS as the scope value means scope strings stay readable, debuggable, and stable when an établissement gets a new internalidafter a Prisma migration.user_scopestable already hasexpiresAtto lay the groundwork; extending the role axis with time bounds is a future ADR amendment when a concrete use case lands.apf-ai-service's RBAC matrix tests expect (each chunk's ACL is a string-match againstPrincipal.roles[]). The ADR explicitly notes that the projector is the only place that knows about the flat shape — keeping the AI-side contract honoured without polluting the portal-side guards.Test plan
prettier --check docs/decisions/0025-authorization-model-privileges-roles-scopes.md— passes (hook ran on commit).0008,0009,0010,0011,0013,0020,0021,0024, plus the proposed ADR-0026 / ADR-0027 placeholders).docs/decisions/README.mdfollows the table's existing format.security,backend,dataare all in the existing vocab.Principalshape, the projection contract for the AI service, and the ten test personas. Catalogue closures are deliberate; raising the lid requires an amendment so the v1 list deserves a careful pass.What's next (once accepted)
The implementation phasing recorded in the ADR's §"More Information":
libs/feature/auth/src/lib/authorization.types.ts(catalogue constants),entra-group-to-role.ts(slug map), and the OIDC callback hook that extendsreq.session.userwithprivileges/roles/scopes. No new guards yet.@RequireRole+@RequireScopedecorators + guard tests. Stub principal in unit tests; real session in e2e.pnpm runscript: every@RequireRole('...')/@RequirePrivilege('...')/ scope literal must exist in the catalogue constants.prisma/seed.tspopulating the ten personas'user_scopesrows. Depends on thePerson+Userschema PR landing first.In parallel, the user provisions the test tenant per the §"Test-tenant personas" instructions above.