docs(adr-0025): promote to accepted + sync persona matrix with test tenant
CI / scan (pull_request) Failing after 2m31s
CI / commits (pull_request) Successful in 2m56s
CI / check (pull_request) Successful in 3m10s
CI / a11y (pull_request) Successful in 2m46s
Docs site / build (pull_request) Successful in 2m56s
CI / perf (pull_request) Successful in 7m6s
CI / scan (pull_request) Failing after 2m31s
CI / commits (pull_request) Successful in 2m56s
CI / check (pull_request) Successful in 3m10s
CI / a11y (pull_request) Successful in 2m46s
Docs site / build (pull_request) Successful in 2m56s
CI / perf (pull_request) Successful in 7m6s
The test tenant (apfrd.onmicrosoft.com) has been provisioned with the full role / user matrix. ADR-0025 was accepted as the implementation reference; this commit aligns the document with what is actually in place. Changes: - Frontmatter status: proposed -> accepted; date unchanged (decision was already final on 2026-05-20). - Privilege catalogue: expanded from 1 to 4 entries. Portal.Auditor, Portal.SecurityOfficer and Portal.DPO were anticipated future entries; they are now provisioned in the test tenant alongside Portal.Admin and ride the catalogue while their consuming surfaces land in subsequent PRs. - Test-tenant personas: rewritten from 10 to 19 entries so every one of the 24 functional-role groups carries at least one user (apf-role-partenaire remains intentionally empty as a placeholder per the existing rationale). The four privileges each have an assigned user. - New 'Provisioned in the test tenant' subsection records the Entra app-role GUIDs (kept in-repo because they are stable IDs the implementation will reference; group GUIDs stay gitignored). - README.md index: status row flipped to accepted. - CLAUDE.md: roll-up extended from '0001 -> 0024 accepted' to '0001 -> 0025 accepted', Architecture list grows an 'Authorization model' bullet.
This commit is contained in:
@@ -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).
|
- **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 `<title>`/`<desc>`, `<details>` 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).
|
- **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 `<title>`/`<desc>`, `<details>` 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).
|
- **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:<finess> / delegation:<dept> / region:<insee> / 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.
|
- **Runtime:** Node.js latest LTS major.
|
||||||
|
|
||||||
## Repository status
|
## 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.
|
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-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)).
|
- **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)).
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
---
|
---
|
||||||
status: proposed
|
status: accepted
|
||||||
date: 2026-05-20
|
date: 2026-05-20
|
||||||
decision-makers: R&D Lead
|
decision-makers: R&D Lead
|
||||||
tags: [security, backend, data]
|
tags: [security, backend, data]
|
||||||
@@ -48,15 +48,14 @@ Chosen: **three orthogonal axes** — `privileges`, `functional roles`, `scopes`
|
|||||||
|
|
||||||
**v1 catalogue.**
|
**v1 catalogue.**
|
||||||
|
|
||||||
| Privilege | Surface gated | Status |
|
| 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.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):
|
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.
|
||||||
|
|
||||||
- `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).
|
|
||||||
|
|
||||||
**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.
|
**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
|
## 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 |
|
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.
|
||||||
| ----------------------------- | -------------- | ------------------------------------------ | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------- |
|
|
||||||
| `admin@<tenant>` | `Portal.Admin` | `collaborateur`, `rh` | `unrestricted` | Admin app + cross-cutting reads. Goes through the `portal-admin` SPA. |
|
|
||||||
| `directeur-bordeaux@<tenant>` | — | `directeur-etablissement`, `collaborateur` | `etablissement:0330800013` | Single-site Directeur. Cannot read other établissements' data. |
|
|
||||||
| `directeur-complexe@<tenant>` | — | `directeur-etablissement`, `collaborateur` | `etablissement:0330800013`, `etablissement:0330800021` | Multi-site Directeur of a "complexe". |
|
|
||||||
| `rh-aquitaine@<tenant>` | — | `rh`, `collaborateur` | `delegation:33` | Cross-établissement HR scoped to one delegation. |
|
|
||||||
| `rh-siege@<tenant>` | — | `rh`, `responsable-paie`, `collaborateur` | `unrestricted` | National HR. |
|
|
||||||
| `collab-simple@<tenant>` | — | `collaborateur` | `self` | Baseline employee. Sees only their own data. |
|
|
||||||
| `tresorier-bordeaux@<tenant>` | — | `elu-cd-tresorier`, `elu-cd` | `delegation:33` | Departmental treasurer (governance, not workforce). |
|
|
||||||
| `dpo@<tenant>` | — | `dpo`, `collaborateur` | `unrestricted` | Cross-cutting compliance. v1 just `collaborateur` + `dpo` — `Portal.DPO` privilege when the surface exists. |
|
|
||||||
| `it@<tenant>` | — | `it`, `collaborateur` | `unrestricted` | Internal tech support. |
|
|
||||||
| `benevole-aquitaine@<tenant>` | — | `benevole`, `benevole-responsable` | `delegation:33` | Volunteer with leadership rights inside one delegation. |
|
|
||||||
|
|
||||||
**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`).
|
### Provisioned in the test tenant (2026-05-20)
|
||||||
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@<tenant>` 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).
|
|
||||||
|
|
||||||
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
|
### Consequences
|
||||||
|
|
||||||
|
|||||||
@@ -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 |
|
| [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 |
|
| [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 |
|
| [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 |
|
||||||
|
|||||||
Reference in New Issue
Block a user