9695c9080b90d4dbd82617eb81c286a81c9dc6ef
2 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
9695c9080b |
feat(structures): add Region/Delegation/Structure schema + seed (ADR-0027 PR 1)
Schema (Prisma): three new public-schema models per ADR-0027 - Region (INSEE code PK), Delegation (dept code PK + regionCode FK), Structure (portal-internal code PK + kind discriminator + nullable unique finess/siret/codePaie + nullable delegationCode FK). Migration: hand-written, matches the style of init_audit_schema and users_directory. DDL + CHECK constraint on Structure.kind mirroring STRUCTURE_KINDS + indexes + inline seed (Region 75 Nouvelle-Aquitaine, Delegation 33 Gironde, structures 0330800013 + 0330800021 medico-social Bordeaux/Merignac + siege APF national). Seed is the minimum the 19 test-tenant personas reference; ADR-0029's cascade sync supersedes it when it ships. Catalogue (TypeScript): apps/portal-bff/src/structures/structure-kind.ts exports STRUCTURE_KINDS as const + StructureKind type union + isStructureKind type guard. Jest spec covers catalogue content, type guard, narrowing. Drift gate: extended to scan property-literal "kind: 'X'" expressions in files that import from structure-kind.ts. Three-layer defense in depth for Structure.kind = TS type union + Postgres CHECK + CI gate. 22 spec tests passing. No consumer code yet - PrismaScopeResolver that dereferences Structure.code from UserScope.value lives in ADR-0026 PR 2, which depends on this PR landing first. Reviewer heads-up: schema.prisma already has a User model (ADR-0020 user-directory cache); ADR-0026 PR 1 introduces a different User (UUID PK + Person FK). Naming collision flagged for that PR to resolve - not touched here. |
||
|
|
2eb01f59b3 |
feat(ci): catalogue-drift gate for @RequirePrivilege/@RequireRole literals (ADR-0025) (#211)
## Summary
Phase 3 (and last) of [ADR-0025](docs/decisions/0025-authorization-model-privileges-roles-scopes.md)'s implementation phasing (§"More Information" / §347): a small CI gate that asserts every string literal passed to the authorization decorators belongs to the closed catalogue declared in `libs/shared/auth/src/lib/authorization.types.ts`.
The TypeScript decorator signatures (`(...privileges: [Privilege, ...Privilege[]])`) already enforce this at compile time. The gate is **defence-in-depth** against the escape hatches the type system cannot catch:
- explicit `as Privilege` / `as FunctionalRole` casts (`'Portal.Foo' as Privilege`),
- the rare case where a developer hand-edits the catalogue type union without updating the runtime constant.
Runs in `<1s`, so it rides the existing `check` CI job rather than spinning up its own.
## What lands
| File | Role |
| --- | --- |
| `scripts/check-catalogue-drift.mjs` | The gate. TypeScript compiler API. Parses `authorization.types.ts` to extract the catalogues, walks every `.ts` file under `apps/` and `libs/`, finds each `CallExpression` whose callee identifier matches `RequirePrivilege` / `RequireRole`, validates every string-literal argument. Non-literal args are skipped (the TypeScript signature catches them already). Reports grouped by file with `line:column` per violation, exits 1 on drift. |
| `scripts/check-catalogue-drift.spec.mjs` | 13 tests via `node --test` (built-in runner, no Vitest dep). Covers catalogue parsing, file-level violation detection, workspace-wide aggregation, skipped folders, gen-stub exclusion, line/column reporting. |
| `package.json` | `ci:catalogue-drift` + `ci:catalogue-drift:test` scripts. |
| `.gitea/workflows/ci.yml` | The existing `check` job now runs the gate's unit tests first (fail-fast if the gate itself is broken), then the gate against the live workspace. |
## Notes for the reviewer
- **Why not an ESLint custom rule.** ADR-0025 §347 floats either option. The ESLint route would give editor-time feedback (red squiggles) but means standing up a local ESLint plugin lib — net ~300 lines of plumbing for a gate the TypeScript signature already enforces in real time via the literal-union types. The pnpm-script route mirrors the existing `ci:gzip-budgets` pattern; ~250 lines including tests; runs in the same `check` CI job that already installs deps. Editor feedback is **already provided by `tsc`**, so the gate's job reduces to "catch escape hatches in CI", which the script does fine.
- **Why `node --test` rather than Vitest / Jest.** The script lives in `scripts/`, outside any Nx project. Wiring Vitest for one spec file would mean a vitest.config + tsconfig.spec + Nx project just to host it. Node's built-in test runner ships with the runtime we already pin (Node 24 in `.nvmrc`), no config, ~250ms wall clock for 13 tests.
- **Generated gRPC stubs are skipped.** `apps/portal-bff/src/grpc/gen/apf-ai/common.ts` defines a `roles: string[]` proto field used by the AI-bridge — entirely unrelated to the ADR-0025 functional-role catalogue. The skip list is explicit (`SKIPPED_SUBPATHS`); adding a new codegen output later is one line.
- **Spec files are NOT skipped.** `auth-guards.persona-matrix.spec.ts` references catalogue values via the decorators in its test fixtures. Those are deliberate references and must stay in sync — if a future ADR amendment removes a role, the spec catches it on the same run as the production code. Explicitly tested in `does NOT skip spec files`.
- **Non-literal arguments (variable indirection) are skipped.** A pattern like `const slug = getRole(); @RequireRole(slug)` would not be caught by string-literal inspection. This is intentional: the TypeScript decorator signature already requires `slug` to be typed `FunctionalRole`, so the type system handles it; the gate's value-add is on literal misspellings the type system cannot see past an `as Privilege` cast.
- **Self-test confirmed the gate works.** Injected `RequirePrivilege('Portal.RogueDrift')` and `RequireRole('rogue-role-x')` into an existing spec, ran `pnpm ci:catalogue-drift`, observed:
```
catalogue-drift: violations found
apps/portal-bff/src/auth/require-privilege.guard.spec.ts
135:18 @RequirePrivilege('Portal.RogueDrift') — not in PRIVILEGES
136:13 @RequireRole('rogue-role-x') — not in FUNCTIONAL_ROLES
```
Exit code 1. Reverted the injection; gate green again.
- **Adding a third decorator** (per ADR-0025's anticipated growth) is one line in `DECORATOR_TO_CATALOGUE` plus the matching test fixture. The script does not hardcode the decorator name list beyond that map.
- **Why no scope-kind check.** `@RequireScope` takes an `(req) => ScopableResource` extractor, not literals. The scope `kind` values are constrained by the `ScopeKind` discriminated union, which TypeScript enforces at every `{ kind: ... }` construction site. No literal escape hatch worth gating in v1.
## Test plan
- [x] `pnpm ci:catalogue-drift:test` — 13/13 green via `node --test`.
- [x] `pnpm ci:catalogue-drift` — clean against the live workspace: `catalogue-drift: clean (catalogues: 4 privileges, 24 roles).`
- [x] Self-test by injection — script catches `'Portal.RogueDrift'` and `'rogue-role-x'` with file:line:column, exits 1.
- [x] `pnpm exec prettier --check` on the new files — clean.
- [ ] CI run on this PR exercises the new step in the `check` job.
## What's next
ADR-0025's phasing closes with this PR. Remaining authorization work waits on [ADR-0026](docs/decisions/) (proposed) — the `Person` + `User` schema brings the Prisma-backed `user_scopes` table; that PR replaces `StubScopeResolver` with a `PrismaScopeResolver` and unlocks the first concrete `@RequireScope` consumer surfaces.
---------
Co-authored-by: Julien Gautier <julien.gautier@apf.asso.fr>
Reviewed-on: #211
|