feat(auth): authorization catalogues + Principal builder skeleton (ADR-0025)
CI / check (pull_request) Failing after 28s
CI / commits (pull_request) Failing after 25s
CI / scan (pull_request) Failing after 56s
CI / a11y (pull_request) Failing after 33s
CI / perf (pull_request) Failing after 47s
Docs site / build (pull_request) Failing after 26s

Per ADR-0025's implementation phasing, lands the closed-set
catalogues + the OIDC-callback hook that composes the
session-resident Principal. No new guards yet.

- libs/shared/auth: framework-agnostic lib hosting
  authorization.types.ts (4 privileges + 24 functional roles + 6
  scope kinds + Principal type) and entra-group-to-role.ts
  (validated GUID -> slug resolver). 17 unit tests against the
  catalogues + resolver contracts.
- BFF: PrincipalBuilder composes the three axes once at sign-in;
  ScopeResolver seam (StubScopeResolver returns unrestricted in
  v1, replaced by a Prisma-backed implementation when ADR-0026's
  user_scopes table lands). 19 persona-driven tests covering the
  test tenant's full provisioning + edge cases (unknown
  privilege drift, unknown group GUID, empty permissions).
- AuthService extracts the Entra "groups" claim; AuthenticatedUser
  grows the groups: readonly string[] field.
- SessionEstablisher builds + persists the principal alongside
  the legacy user shape (additive — guards consuming user.oid
  unchanged).
- ENTRA_GROUP_MAP_PATH env var + load-entra-group-map.ts read
  the tenant-private map from infra/<env>-tenant.entra.json
  (gitignored; .example.json committed with the schema).
- ADR-0025 path references updated from libs/feature/auth
  (Angular-scoped, scope:portal-shell) to libs/shared/auth
  (consumable by both BFF and SPA).
- notes/entra-groups-claim-activation.md: operator runbook for
  the "Token configuration -> Add groups claim" step in the
  Entra admin centre.

Test plan:
- pnpm nx affected -t lint test build : 13 projects green
  (497 BFF tests + 17 shared-auth tests + 1 shared-util test +
  full lint + full build).
This commit is contained in:
Julien Gautier
2026-05-23 19:48:39 +02:00
parent c9a1e195fe
commit 540d9dc28a
35 changed files with 1709 additions and 29 deletions
+41 -8
View File
@@ -2,14 +2,15 @@
Infrastructure-as-code artefacts for the project. Separate from application code and from documentation: this folder contains the recipes and configs that the team and ops use to stand up running infrastructure (CI runners, future local-dev databases, future on-prem deploy assets).
| Subject | File / Folder | ADR / Reference |
| -------------------------------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Self-hosted CI runners (Gitea Actions) | [`ci-runners.compose.yml`](ci-runners.compose.yml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) |
| Shared `act_runner` configuration | [`runner-config.yaml`](runner-config.yaml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) |
| CI runners convenience script | [`ci-runners.sh`](ci-runners.sh) | See "Convenience script" below |
| Runtime state of the runners | `data/` (git-ignored after `.gitignore`) | — |
| Env-vars template for the runners | `.env.example` (`.env` is git-ignored) | — |
| Local-dev runtime stack | [`local/`](local/) | [ADR-0006](../docs/decisions/0006-persistence-postgresql-prisma.md), [ADR-0010](../docs/decisions/0010-session-management-redis.md), [ADR-0012](../docs/decisions/0012-observability-pino-opentelemetry.md), [ADR-0013](../docs/decisions/0013-audit-trail-separated-postgres-append-only.md) |
| Subject | File / Folder | ADR / Reference |
| -------------------------------------- | --------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Self-hosted CI runners (Gitea Actions) | [`ci-runners.compose.yml`](ci-runners.compose.yml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) |
| Shared `act_runner` configuration | [`runner-config.yaml`](runner-config.yaml) | [ADR-0015 §"Runners"](../docs/decisions/0015-cicd-gitea-actions.md) |
| CI runners convenience script | [`ci-runners.sh`](ci-runners.sh) | See "Convenience script" below |
| Runtime state of the runners | `data/` (git-ignored after `.gitignore`) | — |
| Env-vars template for the runners | `.env.example` (`.env` is git-ignored) | — |
| Local-dev runtime stack | [`local/`](local/) | [ADR-0006](../docs/decisions/0006-persistence-postgresql-prisma.md), [ADR-0010](../docs/decisions/0010-session-management-redis.md), [ADR-0012](../docs/decisions/0012-observability-pino-opentelemetry.md), [ADR-0013](../docs/decisions/0013-audit-trail-separated-postgres-append-only.md) |
| Entra group GUID → role slug map | [`test-tenant.entra.example.json`](test-tenant.entra.example.json) (`*-tenant.entra.json` is git-ignored) | [ADR-0025 §"Sources of truth — Entra-side configuration"](../docs/decisions/0025-authorization-model-privileges-roles-scopes.md) |
Future folders / files that will land here as the corresponding ADRs ship:
@@ -230,6 +231,38 @@ This stack is **dev-only**. The corresponding production layout (HA Postgres, Re
---
## Entra group map — `test-tenant.entra.example.json`
Pure JSON object keyed on Entra security-group GUID (lower-case), valued by an `apf-role-<slug>` slug from the ADR-0025 functional-role catalogue. The BFF loads it at boot through `EntraGroupToRoleResolver` (from `shared-auth`) and uses it on every sign-in to translate the `groups` claim into the 24-entry catalogue's role slugs.
The 24 entries below cover the entire v1 catalogue — including `partenaire`, which ships empty in the test tenant by design but is kept in the schema so a typo or omission fails the parser at boot rather than silently dropping the role.
### Provisioning a real file
```bash
cp infra/test-tenant.entra.example.json infra/test-tenant.entra.json
# Then for each role replace the placeholder GUID with the real one
# from Entra:
# Microsoft Entra admin centre → Groups → <apf-role-*> → Object ID.
# Point the BFF at the file via apps/portal-bff/.env:
# ENTRA_GROUP_MAP_PATH=infra/test-tenant.entra.json
```
The real file (`infra/<env>-tenant.entra.json`) is git-ignored because the group GUIDs are tenant-private — leaking them does not authorize anything by itself, but it does reveal the tenant's internal authorization topology. Each environment (test / preprod / prod) carries its own file; the slugs are stable across environments, the GUIDs are not.
If `ENTRA_GROUP_MAP_PATH` is unset, the resolver runs with an empty map: every user signs in successfully but receives an empty `roles[]` (and consequently no `apf-role-*` UI). The BFF logs a WARN at boot so an operator can spot the missing config; this is a deliberate fail-soft posture so a fresh dev environment is not blocked by an Entra-side dependency.
Validation rules enforced at boot by `parseEntraGroupMap` (in `libs/shared/auth/`):
- keys must look like a GUID (`8-4-4-4-12` hex);
- values must be members of `FUNCTIONAL_ROLES`;
- the same GUID cannot map to two different slugs (case-insensitive).
A malformed file crashes the BFF at startup. The error message names the offending key / value.
---
## Future infra concerns — placeholders
These are listed here so a contributor knows where to expect related files; they don't exist yet.
+26
View File
@@ -0,0 +1,26 @@
{
"00000000-0000-0000-0000-000000000001": "collaborateur",
"00000000-0000-0000-0000-000000000002": "chef-equipe",
"00000000-0000-0000-0000-000000000003": "chef-service",
"00000000-0000-0000-0000-000000000004": "directeur-etablissement",
"00000000-0000-0000-0000-000000000005": "directeur-territorial",
"00000000-0000-0000-0000-000000000006": "rh",
"00000000-0000-0000-0000-000000000007": "responsable-paie",
"00000000-0000-0000-0000-000000000008": "comptable",
"00000000-0000-0000-0000-000000000009": "juriste",
"00000000-0000-0000-0000-00000000000a": "dpo",
"00000000-0000-0000-0000-00000000000b": "rssi",
"00000000-0000-0000-0000-00000000000c": "it",
"00000000-0000-0000-0000-00000000000d": "formation",
"00000000-0000-0000-0000-00000000000e": "qualite",
"00000000-0000-0000-0000-00000000000f": "communication",
"00000000-0000-0000-0000-000000000010": "elu-ca",
"00000000-0000-0000-0000-000000000011": "elu-cd",
"00000000-0000-0000-0000-000000000012": "elu-cd-president",
"00000000-0000-0000-0000-000000000013": "elu-cd-tresorier",
"00000000-0000-0000-0000-000000000014": "elu-cd-secretaire",
"00000000-0000-0000-0000-000000000015": "delegue",
"00000000-0000-0000-0000-000000000016": "benevole",
"00000000-0000-0000-0000-000000000017": "benevole-responsable",
"00000000-0000-0000-0000-000000000018": "partenaire"
}