diff --git a/CLAUDE.md b/CLAUDE.md
index 7d47913..e068212 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -53,13 +53,14 @@ The structural, security, observability, and quality choices are recorded as ADR
- **Documentation site:** `docs/**/*.md` rendered as a separate static site via **VitePress** (Vite-based, Node-only toolchain, Markdown-first). Mermaid diagrams via `vitepress-plugin-mermaid`. Deployed on its own hostname behind the shared reverse-proxy; CI hook on `docs/` changes rebuilds + publishes. Decoupled from the apps — content lives in `docs/`, no in-app Markdown viewer — see [ADR-0022](docs/decisions/0022-docs-site-vitepress.md).
- **Charts + dashboards:** `D3 + Observable Plot` wrapped in `libs/shared/charts/`, one Angular component per chart type (bar, donut, line, stacked-bar, …). A11y baked in by the lib (SVG `
`/``, `` tabular fallback, colour-blind-safe palettes, AA-contrast text, `prefers-reduced-motion` gate). Bundle stays under [ADR-0017](docs/decisions/0017-performance-budgets-lighthouse-ci.md)'s lazy-chunk cap via per-`d3-*` module tree-shaking. Future bespoke visualisations land in raw D3 inside the same lib — see [ADR-0023](docs/decisions/0023-charts-d3-observable-plot.md).
- **AI service relay:** dedicated `apf-ai-service` repo (ASP.NET Core, Microsoft Agent Framework) consumed via native gRPC HTTP/2 only — proto contract vendored under `apps/portal-bff/src/grpc/proto/apf-ai/` with `ts-proto` codegen committed alongside. BFF dials with `@grpc/grpc-js` (h2c in dev, h2 + TLS in prod), bridges `ChatService.Chat` to `text/event-stream` for the SPA, exposes `RagService.Search` and `ModelsService.ListModels` as plain JSON endpoints. Identity travels as an unsigned `Principal` (subject, roles, attributes) in the proto body for the POC, hashed via the audit module's `HashUserIdService` so portal and AI service audit trails join on the same `actor_id_hash`. Production hardening (signed envelope vs mTLS) deferred — see [ADR-0024](docs/decisions/0024-ai-service-relay-grpc-sse-bridge.md).
+- **Authorization model:** three orthogonal axes — **privileges** (Entra app roles, `Portal.*`), **functional roles** (Entra security groups → curated `apf-role-*` slug catalogue, 24 entries v1), **scopes** (portal-side `user_scopes` table, future Pléiades feed; kinds = `self / etablissement: / delegation: / region: / siege / unrestricted`). Composed at sign-in into a session-resident `Principal`; portal guards consume the structured shape, a deterministic `PrincipalProjector` flattens it to the AI-service `roles[]` contract. Replaces stargate's linear hierarchy. Catalogues are closed-set, drift gated by CI — see [ADR-0025](docs/decisions/0025-authorization-model-privileges-roles-scopes.md).
- **Runtime:** Node.js latest LTS major.
## Repository status
The Nx workspace is **scaffolded and operational**. The three apps (`portal-shell`, `portal-admin`, `portal-bff`) and the four lib roots (`libs/feature/`, `libs/shared/state`, `libs/shared/tokens`, `libs/shared/ui`, `libs/shared/util`) are in place; CI runs `format:check / lint / test / build` on every PR.
-ADRs 0001 → 0024 are accepted and cover the structural, security, observability, quality, i18n, admin-app, docs-site, charts, and AI-relay choices. **Shipped on `main`:**
+ADRs 0001 → 0025 are accepted and cover the structural, security, observability, quality, i18n, admin-app, docs-site, charts, AI-relay, and authorization choices. **Shipped on `main`:**
- **Phase-1 foundation** — Nx workspace, Angular `portal-shell`, NestJS `portal-bff`, Prisma + Postgres, Pino + OpenTelemetry, Husky/lint-staged/commitlint, Gitea Actions CI.
- **Phase-2 auth + audit + security** — OIDC Auth Code + PKCE via MSAL Node, Redis sessions with AES-256-GCM at rest, idle 30 min sliding + absolute 12 h hard ceiling, RP-initiated logout, double-submit CSRF, `audit.events` append-only schema with role-based grants, helmet + env-driven CORS allowlist + rate limiting + structured error envelope (see [ADR-0021](docs/decisions/0021-phase-2-security-baseline.md)).
diff --git a/docs/decisions/0025-authorization-model-privileges-roles-scopes.md b/docs/decisions/0025-authorization-model-privileges-roles-scopes.md
index a592a92..e48c9af 100644
--- a/docs/decisions/0025-authorization-model-privileges-roles-scopes.md
+++ b/docs/decisions/0025-authorization-model-privileges-roles-scopes.md
@@ -1,5 +1,5 @@
---
-status: proposed
+status: accepted
date: 2026-05-20
decision-makers: R&D Lead
tags: [security, backend, data]
@@ -48,15 +48,14 @@ Chosen: **three orthogonal axes** — `privileges`, `functional roles`, `scopes`
**v1 catalogue.**
-| Privilege | Surface gated | Status |
-| -------------- | -------------------------------------------------------- | ------------------------------------------------------- |
-| `Portal.Admin` | `portal-admin` app (CMS, menus, user list, audit viewer) | already in place — [ADR-0020](0020-portal-admin-app.md) |
+| Privilege | Surface gated | Status |
+| ------------------------ | ----------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- |
+| `Portal.Admin` | `portal-admin` app (CMS, menus, user list, audit viewer) | already in place — [ADR-0020](0020-portal-admin-app.md) |
+| `Portal.Auditor` | Read-only access to the audit viewer for compliance staff who should not write to anything (surface lands later). | provisioned in test tenant; consumer surface deferred |
+| `Portal.SecurityOfficer` | RSSI-specific dashboards (incident timeline, vuln scanner output, key rotation status — surface lands later). | provisioned in test tenant; consumer surface deferred |
+| `Portal.DPO` | DPO-specific surface (data subject requests, retention exceptions — surface lands later). | provisioned in test tenant; consumer surface deferred |
-That is the entire v1 catalogue. New privileges are added by ADR amendment + a one-line Entra manifest change. Anticipated near-future entries (recorded for context, not yet defined):
-
-- `Portal.Auditor` — read-only access to the audit viewer for compliance staff who should not write to anything.
-- `Portal.SecurityOfficer` — RSSI-specific dashboards (incident timeline, vuln scanner output, key rotation status).
-- `Portal.DPO` — DPO-specific surface (data subject requests, retention exceptions).
+The four privileges above are the entire v1 catalogue. The last three were provisioned in the test tenant ahead of their consuming surfaces — they ride in the catalogue now so the surfaces can be built against a stable contract. Adding a fifth privilege is an ADR amendment + a one-line Entra manifest change.
**Why these are privileges, not functional roles.** They gate **what part of the portal you can see**, not **what kind of APF work you do**. A DPO is a functional role (the person's job); `Portal.DPO` would be the portal slice they consume in that capacity. The two often align but are distinct — an `it` functional role might hold `Portal.Admin` privilege; a `dpo` functional role might hold `Portal.DPO` privilege.
@@ -290,29 +289,46 @@ Three composable decorators on NestJS controllers, layered on top of the existin
## Test-tenant personas
-The user has a fresh test tenant they can configure freely. Below is the minimal set of test users that exercises every interesting combination of the three axes. The user can extend the list later — these ten cover the v1 guard surface.
+The 19 personas below exercise every interesting combination of the three axes; together they cover 23 of the 24 functional-role groups and all four privileges. The only intentional gap is `apf-role-partenaire` — placeholder, no consuming surface yet, assignment deferred until the first partner-facing feature lands.
-| Login | Privileges | Functional roles | Scopes | Purpose |
-| ----------------------------- | -------------- | ------------------------------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
-| `admin@` | `Portal.Admin` | `collaborateur`, `rh` | `unrestricted` | Admin app + cross-cutting reads. Goes through the `portal-admin` SPA. |
-| `directeur-bordeaux@` | — | `directeur-etablissement`, `collaborateur` | `etablissement:0330800013` | Single-site Directeur. Cannot read other établissements' data. |
-| `directeur-complexe@` | — | `directeur-etablissement`, `collaborateur` | `etablissement:0330800013`, `etablissement:0330800021` | Multi-site Directeur of a "complexe". |
-| `rh-aquitaine@` | — | `rh`, `collaborateur` | `delegation:33` | Cross-établissement HR scoped to one delegation. |
-| `rh-siege@` | — | `rh`, `responsable-paie`, `collaborateur` | `unrestricted` | National HR. |
-| `collab-simple@` | — | `collaborateur` | `self` | Baseline employee. Sees only their own data. |
-| `tresorier-bordeaux@` | — | `elu-cd-tresorier`, `elu-cd` | `delegation:33` | Departmental treasurer (governance, not workforce). |
-| `dpo@` | — | `dpo`, `collaborateur` | `unrestricted` | Cross-cutting compliance. v1 just `collaborateur` + `dpo` — `Portal.DPO` privilege when the surface exists. |
-| `it@` | — | `it`, `collaborateur` | `unrestricted` | Internal tech support. |
-| `benevole-aquitaine@` | — | `benevole`, `benevole-responsable` | `delegation:33` | Volunteer with leadership rights inside one delegation. |
+The matrix was provisioned in the `apfrd.onmicrosoft.com` test tenant on 2026-05-20. The operator-facing checklist (group → users, copy-paste-friendly for Entra UI) lives at `notes/entra-group-members.md`; this table is the canonical source.
-**Entra setup the user needs to run** (test tenant only):
+| Login (`@apfrd.onmicrosoft.com`) | Privileges | Functional roles | Scopes | Purpose |
+| --------------------------------- | ------------------------------ | ------------------------------------------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------- |
+| `admin` | `Portal.Admin` | `collaborateur`, `rh` | `unrestricted` | Admin app + cross-cutting reads. Goes through the `portal-admin` SPA. |
+| `directeur-bordeaux` | — | `collaborateur`, `directeur-etablissement` | `etablissement:0330800013` | Single-site Directeur. Cannot read other établissements' data. |
+| `directeur-complexe` | — | `collaborateur`, `directeur-etablissement` | `etablissement:0330800013`, `etablissement:0330800021` | Multi-site Directeur of a "complexe". |
+| `rh-aquitaine` | — | `collaborateur`, `rh`, `formation` | `delegation:33` | Cross-établissement HR + training scoped to one delegation. |
+| `rh-siege` | — | `collaborateur`, `rh`, `responsable-paie`, `comptable` | `unrestricted` | National HR + payroll + accounting (siège-level admin staff). |
+| `collaborateur-simple` | — | `collaborateur` | `self` | Baseline employee. Sees only their own data. |
+| `tresorier-bordeaux` | — | `elu-cd`, `elu-cd-tresorier` | `delegation:33` | Departmental treasurer (governance, not workforce — no `collaborateur`). |
+| `dpo` | `Portal.DPO`, `Portal.Auditor` | `collaborateur`, `dpo`, `qualite` | `unrestricted` | Cross-cutting compliance, quality, and read-only audit access. |
+| `it` | — | `collaborateur`, `it` | `unrestricted` | Internal tech support. |
+| `benevole-aquitaine` | — | `benevole`, `benevole-responsable`, `delegue` | `delegation:33` | Volunteer with leadership rights + local delegate role. |
+| `chef-equipe-bordeaux` | — | `collaborateur`, `chef-equipe` | `etablissement:0330800013` | Team-lead granularity within an établissement. |
+| `chef-service-bordeaux` | — | `collaborateur`, `chef-service` | `etablissement:0330800013` | Service-head granularity within an établissement. |
+| `directeur-territorial-aquitaine` | — | `collaborateur`, `directeur-territorial` | `delegation:33` | Multi-établissement regional director, scope-by-delegation. |
+| `juriste-siege` | — | `collaborateur`, `juriste` | `unrestricted` | Siège-level legal counsel. |
+| `rssi` | `Portal.SecurityOfficer` | `collaborateur`, `rssi` | `unrestricted` | Security officer with the future RSSI-specific dashboard privilege. |
+| `communication-siege` | — | `collaborateur`, `communication` | `unrestricted` | Siège-level communications staff. |
+| `elu-ca-national` | — | `elu-ca` | `siege` | National-board member (governance, no employment). |
+| `president-cd-aquitaine` | — | `elu-cd`, `elu-cd-president` | `delegation:33` | Departmental-council president (top of governance hierarchy in a délégation). |
+| `secretaire-cd-aquitaine` | — | `elu-cd`, `elu-cd-secretaire` | `delegation:33` | Departmental-council secretary (governance counterpart of the treasurer). |
-1. Create one app role: `Portal.Admin` (already in place; document the GUID in `apps/portal-bff/.env.test`).
-2. Create one Entra security group per functional role in the catalogue above (`apf-role-collaborateur`, `apf-role-rh`, …). Capture the GUIDs in a fresh file `infra/test-tenant.entra.json` (gitignored — sensitive) and map them in `libs/feature/auth/src/lib/entra-group-to-role.ts`.
-3. Create the 10 test users above; assign group memberships per the matrix; assign `Portal.Admin` app role to the `admin@` user only.
-4. Seed `user_scopes` rows for each test user via a future `prisma/seed.ts` (lands with the `Person` + `User` schema PR; not in this ADR).
+### Provisioned in the test tenant (2026-05-20)
-The Entra group GUIDs and the user passwords stay in the operator's hands (out of git). The mapping file references them by name only.
+The four privileges live in the `apfrd.onmicrosoft.com` app registration with the following GUIDs:
+
+| Privilege | Entra app role GUID |
+| ------------------------ | -------------------------------------- |
+| `Portal.Admin` | `3e45c572-71f7-4823-92be-43d2a98b5c84` |
+| `Portal.Auditor` | `753815ec-9469-4edd-ae7e-3f29da8a3f75` |
+| `Portal.SecurityOfficer` | `6f50ce58-a1e1-496b-a3c0-655559c66a28` |
+| `Portal.DPO` | `39b8815f-b3fd-4597-9679-77dcbf788a07` |
+
+The 24 `apf-role-*` security groups were provisioned with the membership matrix above. Their GUIDs are tenant-specific and stay out of the repo; the implementation PR captures them in a gitignored `infra/test-tenant.entra.json` and references them by name in `libs/feature/auth/src/lib/entra-group-to-role.ts`.
+
+Scopes are **not** carried by Entra. They live in the portal-side `user_scopes` table, populated by `prisma/seed.ts` once the `Person` + `User` schema (proposed ADR-0026) lands. Until then the implementation skeleton honours the `unrestricted` default for testing.
### Consequences
diff --git a/docs/decisions/README.md b/docs/decisions/README.md
index 4197b39..145c533 100644
--- a/docs/decisions/README.md
+++ b/docs/decisions/README.md
@@ -68,4 +68,4 @@ ADRs are listed in numerical order. To slice by topic, filter on the `Tags` colu
| [0022](0022-docs-site-vitepress.md) | Documentation site — VitePress + Mermaid plugin, separate static deployment | accepted | `process`, `infrastructure` | 2026-05-15 |
| [0023](0023-charts-d3-observable-plot.md) | Charts + dashboards — D3 + Observable Plot wrapped in `libs/shared/charts` | accepted | `frontend`, `accessibility`, `performance` | 2026-05-16 |
| [0024](0024-ai-service-relay-grpc-sse-bridge.md) | AI service relay — vendored gRPC protos, NestJS gRPC client, SSE bridge to the SPA, POC unsigned principal | accepted | `backend`, `security`, `observability` | 2026-05-19 |
-| [0025](0025-authorization-model-privileges-roles-scopes.md) | Authorization model — three orthogonal axes (privileges × functional roles × scopes), Entra-backed | proposed | `security`, `backend`, `data` | 2026-05-20 |
+| [0025](0025-authorization-model-privileges-roles-scopes.md) | Authorization model — three orthogonal axes (privileges × functional roles × scopes), Entra-backed | accepted | `security`, `backend`, `data` | 2026-05-20 |