docs(backlog): add WP-53 (identity seam + citizen-scoping) and WP-54 (OpenZaak harness)
The two highest-value OpenZaak roadmap gaps, each written self-contained (a "current state" handoff section) so a fresh session can execute from the file + repo alone: - WP-53: replace the stubbed owner/BSN with a real per-request CallerIdentity (pluggable stub, not DigiD), threading it into Authz, the ZGW JWT user claims, and a citizen-scoped read (rol__…__inpBsn). Production-blocking for a real deployment. - WP-54: a separate docker-compose OpenZaak + scripted bootstrap + opt-in Category=Integration test — makes 50/51/52 developable against a live instance instead of only fixtures; kept out of the default gate. Indexed in the backlog README (rows + phase-9 ordering note) and cited from openzaak-integration.md. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -43,60 +43,62 @@ WP-19's own file), so it's a separate manual/CI step, not chained into the other
|
||||
Gates land before the work they cover; each lint rule lands in the same WP as the fixes
|
||||
for its existing violations, so every WP ends green.
|
||||
|
||||
| WP | Title | Phase | Status |
|
||||
| ---------------------------------------------- | ---------------------------------------------------------------------------------- | --------------------------- | ------ |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
|
||||
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
|
||||
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
|
||||
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
|
||||
| WP-32 | Undo/redo in the stamdata editor (folded into WP-31 — no separate file) | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
|
||||
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
|
||||
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
|
||||
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
|
||||
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
|
||||
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | done |
|
||||
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | done |
|
||||
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine (plop; ui-component/bff = skills) | 8 · platform/DX/showcase | done |
|
||||
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | todo |
|
||||
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
| [WP-47](WP-47-feature-flags.md) | Runtime feature flags (catalog-in-code, admin toggle, FE+backend) | 8 · platform/DX/showcase | done |
|
||||
| [WP-48](WP-48-stamdata-deletion-protection.md) | Stamdata deletion protection (CI referential gate + editor expire/warn) | 8 · platform/DX/showcase | done |
|
||||
| [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | todo |
|
||||
| WP | Title | Phase | Status |
|
||||
| ------------------------------------------------------ | ---------------------------------------------------------------------------------- | --------------------------- | ------ |
|
||||
| [WP-01](WP-01-axe-ci-gate.md) | Axe-on-every-story CI gate | 0 · gates | done |
|
||||
| [WP-02](WP-02-check-tokens.md) | Harden `check:tokens` + fix what it catches | 0 · gates | done |
|
||||
| [WP-03](WP-03-contracts-purity.md) | Boundaries I: contracts purity + ApiClient confinement | 0 · gates | done |
|
||||
| [WP-04](WP-04-ui-not-infrastructure.md) | Boundaries II: `ui ↛ infrastructure` + showcase sanction | 0 · gates | done |
|
||||
| [WP-05](WP-05-parse-boundaries.md) | Parse-don't-validate closure + MDX | 1 · FP/DDD | done |
|
||||
| [WP-06](WP-06-typed-async.md) | Generic async template contexts — kill `$any()` | 1 · FP/DDD | done |
|
||||
| [WP-07](WP-07-brief-idioms.md) | Brief on the shared idioms + RemoteData MDX | 1 · FP/DDD | done |
|
||||
| [WP-08](WP-08-store-idiom.md) | One store idiom + machine naming + TEA MDX | 1 · FP/DDD | done |
|
||||
| [WP-09](WP-09-pure-logic.md) | Pure-logic closure: dates + missing command specs | 1 · FP/DDD | done |
|
||||
| [WP-10](WP-10-button-fidelity.md) | CIBG button fidelity | 2 · CIBG | done |
|
||||
| [WP-11](WP-11-markup-fidelity.md) | CIBG markup fidelity: application-link + absent-class triage | 2 · CIBG | done |
|
||||
| [WP-12](WP-12-datablock.md) | CIBG Datablock for application data | 2 · CIBG | done |
|
||||
| [WP-13](WP-13-cibg-gap-register.md) | CIBG-gap register + hygiene + MDX | 2 · CIBG | done |
|
||||
| [WP-14](WP-14-storybook-taxonomy.md) | Storybook taxonomy reorg + Layers MDX | 3 · Storybook | done |
|
||||
| [WP-15](WP-15-missing-stories.md) | Missing stories: shell + brief components | 3 · Storybook | done |
|
||||
| [WP-16](WP-16-component-a11y.md) | Component a11y: description wiring + alert role | 4 · a11y | done |
|
||||
| [WP-17](WP-17-app-a11y.md) | App-level a11y: route focus, template lint, WCAG checklist | 4 · a11y | done |
|
||||
| [WP-18](WP-18-abac-capability-spine.md) | ABAC capability spine (Principal + capabilities, phase P1) | 5 · productie-volwassenheid | done |
|
||||
| [WP-19](WP-19-e2e-smoke.md) | Playwright e2e smoke | 5 · productie-volwassenheid | done |
|
||||
| [WP-20](WP-20-second-locale.md) | Second locale proof | 5 · productie-volwassenheid | done |
|
||||
| [WP-21](WP-21-resilience-seams.md) | Resilience seams (correlation-id, idempotency, retry) | 5 · productie-volwassenheid | done |
|
||||
| [WP-22](WP-22-durable-persistence.md) | Durable persistence (optional tier) | 5 · productie-volwassenheid | done |
|
||||
| [WP-23](WP-23-org-template-backend.md) | Org-template backend + admin role | 6 · Brief v2 | done |
|
||||
| [WP-24](WP-24-letter-canvas.md) | Letter canvas (edit on the letter) | 6 · Brief v2 | done |
|
||||
| [WP-25](WP-25-letter-preview-html.md) | Server-rendered letter preview (HTML; PDF deferred) | 6 · Brief v2 | done |
|
||||
| [WP-26](WP-26-org-template-editor.md) | Admin org-template editor | 6 · Brief v2 | done |
|
||||
| [WP-27](WP-27-brief-ux-layer.md) | Brief UX layer (undo/redo, standaardbrief, diff) | 6 · Brief v2 | done |
|
||||
| [WP-28](WP-28-brief-v2-demo-polish.md) | Brief v2 demo polish (scenarios, e2e, docs) | 6 · Brief v2 | todo |
|
||||
| [WP-29](WP-29-stamdata-beheer-editor.md) | Stamdata beheer editor (low-code, PR-emitting) | follow-on · ADR-0004 | done |
|
||||
| [WP-30](WP-30-ci-perf-followups.md) | CI performance follow-ups (node_modules cache, runner image, path filters) | follow-on · CI/infra | todo |
|
||||
| [WP-31](WP-31-shared-store-helpers.md) | Shared store helpers (ActionState/SaveState, history, debounced-save, RemoteData) | 7 · refinements | done |
|
||||
| WP-32 | Undo/redo in the stamdata editor (folded into WP-31 — no separate file) | 7 · refinements | done |
|
||||
| [WP-33](WP-33-dev-switchers.md) | In-app dev switchers (scenario + role) | 7 · refinements | done |
|
||||
| [WP-34](WP-34-adres-phone-brp-readonly.md) | Adres: phone field + BRP address read-only | 7 · refinements | done |
|
||||
| [WP-35](WP-35-one-concept-per-type.md) | One Concept per case type (server-enforced) | 7 · refinements | done |
|
||||
| [WP-36](WP-36-admin-cases.md) | Admin cases page + admin delete | 7 · refinements | done |
|
||||
| [WP-37](WP-37-dev-switcher-reset.md) | Dev-switcher reset fix (scenario/role URL param) | 8 · platform/DX/showcase | done |
|
||||
| [WP-38](WP-38-dependency-graph-boundaries.md) | Dependency graph + declarative boundaries (visualize + enforce) | 8 · platform/DX/showcase | done |
|
||||
| [WP-39](WP-39-showcase-snippets-animations.md) | Showcase: linked code snippets + teaching animations | 8 · platform/DX/showcase | done |
|
||||
| [WP-40](WP-40-pii-kernel.md) | PII kernel: branded `Bsn` VO (elfproef) + masked-value atom | 8 · platform/DX/showcase | done |
|
||||
| [WP-41](WP-41-persisted-authz-audit.md) | Persisted, queryable authz/PII-reveal audit (no PII) | 8 · platform/DX/showcase | done |
|
||||
| [WP-42](WP-42-privacy-security-showcase.md) | Privacy & security showcase page (mask + no-PII log) | 8 · platform/DX/showcase | done |
|
||||
| [WP-43](WP-43-scaffold-generators.md) | Runnable generators: value-object / form-machine (plop; ui-component/bff = skills) | 8 · platform/DX/showcase | done |
|
||||
| [WP-44](WP-44-context-generator.md) | Runnable generator: `gen:context` | 8 · platform/DX/showcase | todo |
|
||||
| [WP-45](WP-45-create-ssp-generator.md) | `create-ssp` bootstrap generator (mechanise new-ssp) | 8 · platform/DX/showcase | todo |
|
||||
| [WP-46](WP-46-vitest-coverage.md) | Vitest coverage (report + report-only thresholds) | 8 · platform/DX/showcase | done |
|
||||
| [WP-47](WP-47-feature-flags.md) | Runtime feature flags (catalog-in-code, admin toggle, FE+backend) | 8 · platform/DX/showcase | done |
|
||||
| [WP-48](WP-48-stamdata-deletion-protection.md) | Stamdata deletion protection (CI referential gate + editor expire/warn) | 8 · platform/DX/showcase | done |
|
||||
| [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | todo |
|
||||
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | todo |
|
||||
|
||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
||||
@@ -122,6 +124,13 @@ Phase 9 (OpenZaak/ZGW integration, WP-49..52) is strictly ordered 49 → 50 →
|
||||
the source seam + ZGW client the rest reuse; 50 (create) needs a zaak to exist; 51 (documents)
|
||||
links to 50's zaak; 52 (notificaties) reacts to changes on the zaken 49–51 manage. All slices
|
||||
stay behind `Zgw:Enabled` (default off) so the POC keeps running offline.
|
||||
Two cross-cutting WPs sit alongside that read→write arc: **53** (a real per-request identity
|
||||
seam + citizen-scoping — replaces the stubbed owner/BSN; production-blocking for a real
|
||||
deployment of 49–52) and **54** (a docker OpenZaak harness + opt-in integration test — makes
|
||||
50/51/52 developable against a live instance instead of only fixtures). 53 is independent of the
|
||||
CRUD arc and can land any time; 54 depends on 49 (something to read) and unlocks realistic
|
||||
testing for the rest. Both are self-contained (each WP file carries its own current-state
|
||||
handoff) and sized for a fresh Sonnet session.
|
||||
|
||||
## WP template
|
||||
|
||||
|
||||
@@ -0,0 +1,117 @@
|
||||
# WP-53 — Inbound identity + citizen-scoping (the ZGW auth seam)
|
||||
|
||||
Status: todo
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
|
||||
WP-49 made the cases read path swappable, but everything runs as a **stubbed identity**: the
|
||||
principal comes from an `X-Role` header and the "owner" is a single hardcoded BSN. A real
|
||||
OpenZaak integration needs a genuine per-request user in order to (a) fill the ZGW JWT
|
||||
`user_id`/`user_representation` audit claims, and (b) **scope zaken to the logged-in citizen**
|
||||
(you must never return another citizen's cases). This WP threads a real identity through the
|
||||
system **without** building DigiD/OIDC itself — CLAUDE.md keeps real auth out of scope, so the
|
||||
deliverable is the _seam_: a per-request `CallerIdentity` (subject BSN + display name) produced
|
||||
by a **pluggable, stubbed** provider, consumed everywhere the hardcoded owner is used today.
|
||||
Production later swaps the stub for OIDC/DigiD without touching any consumer.
|
||||
|
||||
## Context — current state (read before designing; this is the handoff, no prior chat needed)
|
||||
|
||||
Identity is faked in these exact places — this WP replaces the fakes with one identity flow:
|
||||
|
||||
- **Backend principal**: `backend/src/BigRegister.Api/Domain/Authorization/Authz.cs` —
|
||||
`ResolvePrincipal(ctx)` reads the `X-Role` header (drafter/approver/admin). Its own doc
|
||||
comment says _"A real system builds this from verified AD/OIDC claims … everything else in
|
||||
this file carries over unchanged once that swap happens."_ That is the seam to formalize.
|
||||
- **Hardcoded owner/BSN**: `backend/src/BigRegister.Api/Data/DocumentStore.cs` —
|
||||
`public const string DemoOwner = "19012345601";` (the single seeded citizen's BIG-nummer).
|
||||
Grep `DemoOwner` across `Program.cs` + stores — every "whose data is this" decision uses it.
|
||||
- **Owner-scoped stores** already take an `owner` string:
|
||||
`Data/ApplicationStore.cs` (`List(owner)`, `Get(id, owner)`, `CreateConcept(type, owner)`,
|
||||
`Submit(id, owner, …)`) and `Data/DocumentStore.cs`. They are ready to receive a real BSN —
|
||||
today the endpoints pass `DocumentStore.DemoOwner`.
|
||||
- **ZGW JWT user claims** are static: `Zgw/ZgwTokenProvider.cs` `Mint()` reads
|
||||
`ZgwOptions.UserId` / `ZgwOptions.UserRepresentation` (constant strings). These must become
|
||||
**per-request** (the acting citizen), or the ZGW audit trail is wrong.
|
||||
- **The cases read interface** `Data/IZaakSource.cs` has one method, `ListCases(now)`, with **no
|
||||
caller** — it returns the admin cross-owner list. There is no citizen-scoped "my cases" read
|
||||
yet, and `OpenZaakZaakSource` lists ALL zaken (`{ZrcBaseUrl}/zaken`, no filter).
|
||||
- **Correlation middleware** (`Program.cs`, the `app.Use(...)` block setting
|
||||
`X-Correlation-Id`) is the pattern/location to add an identity-resolution middleware next to.
|
||||
- **Frontend** identity is the dev role switch: `?role=drafter|approver|admin` + the `⚙ state`
|
||||
panel + `SessionStore` (`src/app/auth/`), documented in `docs/reference/roles-and-access.md`.
|
||||
The FE already persists a session (localStorage). No FE change is required for the backend
|
||||
seam, but the citizen's BSN must originate from the session, not a constant — note where.
|
||||
|
||||
ZGW detail that drives the scoping query: OpenZaak filters a citizen's zaken via the query
|
||||
param `rol__betrokkeneIdentificatie__natuurlijkPersoon__inpBsn=<bsn>` on `GET {ZRC}/zaken`.
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md) — the seam + the "two nested
|
||||
ACLs" section (this WP is about the identity that flows through both).
|
||||
- [ADR-0005 — OpenZaak behind the BFF](../reference/architecture/0005-openzaak-behind-bff.md)
|
||||
("Deferred: real inbound OIDC/JWT auth" — this WP formalizes the seam, not the provider).
|
||||
- [ADR-0002 — user groups & bounded contexts](../reference/architecture/0002-user-groups-and-bounded-contexts.md),
|
||||
[roles-and-access.md](../reference/roles-and-access.md).
|
||||
- `CLAUDE.md` → "Out of scope: Real auth/DigiD" — respect it: build the seam + a stub, not DigiD.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **Seam, not provider.** Introduce a `CallerIdentity` (subject BSN + display name + role) and an
|
||||
`IIdentityProvider` with a **`StubIdentityProvider`** (reads the existing `X-Role` + a
|
||||
configurable/`X-Subject` BSN, defaulting to the seeded citizen). Production swaps the provider;
|
||||
no consumer changes. Do **not** add DigiD/OIDC.
|
||||
- **One source of "who".** Resolve `CallerIdentity` once per request (middleware, beside the
|
||||
correlation block) and flow it to: `Authz.ResolvePrincipal`, the store `owner` arguments
|
||||
(replace `DocumentStore.DemoOwner` call sites), and `ZgwTokenProvider.Mint(caller)`.
|
||||
- **Citizen-scoped reads are separate from admin reads.** Keep the admin cross-owner list
|
||||
(`cases:manage`) as-is; add a citizen-scoped "my zaken" path that filters by the caller's BSN
|
||||
(ZGW `rol__…__inpBsn`; local store: `List(owner)`).
|
||||
- **Ownership stays server-authoritative.** The BSN comes from the resolved identity, never from
|
||||
a client-supplied body field.
|
||||
|
||||
## Files
|
||||
|
||||
- New: `Domain/Authorization/CallerIdentity.cs`, `Domain/Authorization/IIdentityProvider.cs` +
|
||||
`StubIdentityProvider.cs`; an identity-resolution middleware in `Program.cs`.
|
||||
- Edit: `Domain/Authorization/Authz.cs` (build the principal from `CallerIdentity`),
|
||||
`Zgw/ZgwTokenProvider.cs` (`Mint(CallerIdentity)`), `Zgw/OpenZaakZaakSource.cs` (BSN filter on
|
||||
the citizen read), `Data/IZaakSource.cs` (+ a caller-scoped read), `Program.cs` (replace
|
||||
`DemoOwner` call sites with the resolved BSN; DI-register the provider).
|
||||
- Tests: identity resolution (stub), token carries the per-request user, citizen read filters by
|
||||
BSN (stub handler asserts the query param), admin read still cross-owner.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add `CallerIdentity` + `IIdentityProvider` + `StubIdentityProvider` (X-Role + X-Subject BSN,
|
||||
default = seeded citizen); DI-register; resolve once in middleware into `HttpContext.Items`.
|
||||
2. Route `Authz.ResolvePrincipal` and every `DemoOwner` call site through the resolved identity.
|
||||
3. `ZgwTokenProvider.Mint(caller)` — per-request `user_id`/`user_representation`.
|
||||
4. Add a caller-scoped cases read to `IZaakSource` (+ both impls); `OpenZaakZaakSource` adds the
|
||||
`rol__…__inpBsn` filter; local uses `ApplicationStore.List(owner)`.
|
||||
5. Tests as above; keep the admin list unchanged.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] No `DocumentStore.DemoOwner` reference remains in request handling (grep clean); ownership
|
||||
comes from the resolved identity.
|
||||
- [ ] ZGW JWT carries the acting citizen's `user_id`/`user_representation` (test-verified).
|
||||
- [ ] A citizen read returns only that BSN's zaken (local + ZGW-stub tests); admin read unchanged.
|
||||
- [ ] `dotnet test` green; `npm run ci` green with **no api-client drift** (FE contract intact).
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; manual: `X-Role`/`X-Subject` (or `?role=`) still switches identity
|
||||
offline; with `Zgw:Enabled=true` (WP-54 harness) a citizen sees only their zaken.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Real DigiD/OIDC/JWT validation (this is the seam + stub only), FE login redesign, multi-tab
|
||||
session sync (CLAUDE.md out-of-scope list).
|
||||
|
||||
## Risks
|
||||
|
||||
- Missing a `DemoOwner` call site → a citizen sees another's data. Mitigate: grep gate in the
|
||||
acceptance criteria + a test that two identities don't see each other's cases.
|
||||
- ZGW rol filter param name is exact and version-sensitive; assert it in the stub-handler test.
|
||||
@@ -0,0 +1,114 @@
|
||||
# WP-54 — Docker OpenZaak integration-test harness
|
||||
|
||||
Status: todo
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
|
||||
Everything ZGW so far is verified against **fixtures + a stub `HttpMessageHandler`** — nothing
|
||||
exercises a real OpenZaak. That is fine for unit-testing the mapper/JWT/pagination, but it does
|
||||
not prove the BFF actually talks to OpenZaak (auth accepted, real response shapes, real
|
||||
pagination, zaaktype resolution). This WP stands up a **local OpenZaak via docker**, seeds a
|
||||
minimal catalog + a client credential matching `ZgwOptions`, and adds an **opt-in** integration
|
||||
test that points the BFF at it with `Zgw:Enabled=true`. It closes the "no live instance" gap
|
||||
called out in the roadmap and makes WP-50/51/52 developable against something real.
|
||||
|
||||
## Context — current state (read before designing; self-contained handoff)
|
||||
|
||||
- **What exists to test**: `backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs` (lists zaken,
|
||||
follows `{count,next,previous,results}`, resolves zaaktype labels from Catalogi, Bearer auth
|
||||
via `ZgwTokenProvider`), selected when `Zgw:Enabled=true`. Config is the `Zgw` section of
|
||||
`appsettings.json` (`ZrcBaseUrl`, `ZtcBaseUrl`, `ClientId`, `Secret`, `UserId`,
|
||||
`UserRepresentation`) → `Zgw/ZgwOptions.cs`.
|
||||
- **Current tests** (the pattern to extend, not replace): `ZgwZaakMapperTests` (inline JSON),
|
||||
`OpenZaakZaakSourceTests` (stub handler). These stay as fast unit tests; the new integration
|
||||
test is a **separate, opt-in** category so the default `dotnet test` and CI stay fast/offline.
|
||||
- **Existing compose**: repo root `docker-compose.yml` runs FE + BFF only (Swagger at :5000).
|
||||
Do **not** bolt OpenZaak onto it — OpenZaak is heavy (postgres + redis + celery). Use a
|
||||
**separate** compose file so `docker compose up` stays light for everyone else.
|
||||
- **Test project**: `backend/tests/BigRegister.Tests/` (xunit). It uses
|
||||
`Microsoft.AspNetCore.Mvc.Testing` (`TestWebApplicationFactory.cs`) — the factory can be
|
||||
configured to override the `Zgw` config section to point at the compose instance.
|
||||
- **CLAUDE.md / backlog GREEN**: the default local gate and CI must remain runnable without
|
||||
docker/OpenZaak. Anything requiring the harness is explicitly separate (like e2e is today —
|
||||
a distinct job, not chained into `npm run ci`).
|
||||
|
||||
OpenZaak facts that shape the harness (from the ZGW research):
|
||||
|
||||
- OpenZaak is the reference impl of the 5 ZGW APIs; the published
|
||||
`docker-compose.yml` (github.com/open-zaak/open-zaak) brings up **openzaak (web + celery),
|
||||
postgres, redis** and (optionally) Open Notificaties. A one-time **bootstrap** is required:
|
||||
create a superuser, register an **Application** in the Autorisaties API with a **Client
|
||||
ID/Secret + scopes** (`zaken.lezen`, `catalogi.lezen`, …), and import/create a **Catalogus
|
||||
with a zaaktype** (published) so there is something to read.
|
||||
- Auth = the HS256 JWT the BFF already mints; the harness's client credentials must **match**
|
||||
`ZgwOptions.ClientId`/`Secret`.
|
||||
- Reading a zaak needs read scope on **both** Zaken and Catalogi (zaaktype resolution).
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md) (the seam + config keys).
|
||||
- [ADR-0005](../reference/architecture/0005-openzaak-behind-bff.md).
|
||||
- WP-19 (`WP-19-e2e-smoke.md`) — the precedent for a heavy, separate, opt-in test job (mirror
|
||||
its "not chained into the default gate" structure).
|
||||
- Upstream: OpenZaak docker docs (https://open-zaak.readthedocs.io/) + the repo's
|
||||
`docker-compose.yml` and its `import` fixtures for a demo catalogus.
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- **Separate compose file** (e.g. `backend/openzaak/docker-compose.openzaak.yml`), never merged
|
||||
into the root compose. `docker compose up` stays FE+BFF only.
|
||||
- **Opt-in test category.** Tag the integration test `[Trait("Category", "Integration")]`;
|
||||
exclude it from the default run (`dotnet test --filter Category!=Integration`) and from the
|
||||
standard CI jobs. Provide a documented command / optional manual CI job to run it.
|
||||
- **Bootstrap is scripted, not manual.** A checked-in setup (compose `import` fixture or a small
|
||||
bootstrap script) creates the client credentials (matching `ZgwOptions`), a catalogus, and one
|
||||
published zaaktype + one zaak — so the test is deterministic and repeatable.
|
||||
- **Provider stays stubbed** (WP-53) — this WP is about the ZGW round-trip, not real user auth.
|
||||
|
||||
## Files
|
||||
|
||||
- New: `backend/openzaak/docker-compose.openzaak.yml` + bootstrap fixture/script + a short
|
||||
`backend/openzaak/README.md` (how to bring it up, credentials, teardown).
|
||||
- New: `backend/tests/BigRegister.Tests/OpenZaakIntegrationTests.cs` (Category=Integration):
|
||||
configure the BFF (`WebApplicationFactory`) with `Zgw:Enabled=true` + the compose URLs/creds,
|
||||
hit `/admin/cases`, assert the seeded zaak comes back mapped.
|
||||
- Edit: `docs/reference/openzaak-integration.md` (add a "Run against real OpenZaak" section);
|
||||
optionally a manual/gated CI job in `.github/workflows/ci.yml` mirroring the e2e job's shape.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add the separate compose file bringing up OpenZaak + postgres + redis; pin image versions.
|
||||
2. Script the bootstrap: superuser, Autorisaties Application (Client ID/Secret = `ZgwOptions`),
|
||||
a catalogus + one published zaaktype + one zaak with a natuurlijk-persoon rol (the seeded BSN).
|
||||
3. Write the opt-in integration test: point the BFF at the compose instance, assert the mapped
|
||||
zaak (id = uuid, type = zaaktype label, status) via `/admin/cases`.
|
||||
4. Document `docker compose -f … up` + the run command; optionally add a manual CI job.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `docker compose -f backend/openzaak/docker-compose.openzaak.yml up` yields a reachable
|
||||
OpenZaak with the seeded catalogus + zaak, and credentials matching `ZgwOptions`.
|
||||
- [ ] The Category=Integration test passes against it; the BFF returns the seeded zaak mapped to
|
||||
`ApplicationSummaryDto` through the real HTTP + JWT path.
|
||||
- [ ] Default `dotnet test` and `npm run ci` are unaffected (integration test excluded, no docker
|
||||
needed); `docker compose up` (root) is unchanged.
|
||||
|
||||
## Verification
|
||||
|
||||
`docker compose -f backend/openzaak/docker-compose.openzaak.yml up -d` →
|
||||
`dotnet test --filter Category=Integration` → green; then teardown.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Documenten/DRC + Notificaties services in the harness (add when WP-51/52 land), a permanent
|
||||
always-on CI job (keep it opt-in/manual — OpenZaak startup is slow), performance testing.
|
||||
|
||||
## Risks
|
||||
|
||||
- OpenZaak startup is slow + resource-heavy → keep it opt-in; a permanent CI job would blow the
|
||||
same runner budget WP's storybook cap just fixed.
|
||||
- ZGW API/version drift vs the pinned image can change response shapes → pin image versions and
|
||||
keep the fixture in the repo.
|
||||
- Bootstrap client scopes must include `catalogi.lezen` or zaaktype resolution 403s — cover in
|
||||
the setup script.
|
||||
@@ -113,12 +113,15 @@ Principles this demonstrates:
|
||||
|
||||
Caveat: today only the cases **read** path has a source interface (`IZaakSource`). Other BFF
|
||||
endpoints still read `SeedData`/static stores directly — ACL-ready (the DTO seam exists) but not
|
||||
yet swappable. That is the WP-50/51/52 roadmap.
|
||||
yet swappable. That is the WP-50/51/52 roadmap, plus the two cross-cutting WPs the arc needs for
|
||||
production: **WP-53** (a real per-request identity seam + citizen-scoping — today the owner/BSN
|
||||
is stubbed) and **WP-54** (a docker OpenZaak harness + opt-in integration test — today everything
|
||||
is fixture/mock-tested against no live instance).
|
||||
|
||||
## See also
|
||||
|
||||
- [ADR-0005 — OpenZaak behind the BFF](architecture/0005-openzaak-behind-bff.md) — the decision.
|
||||
- [ADR-0001 — BFF-lite + decision DTOs](architecture/0001-bff-lite-decision-dtos.md) — why the FE doesn't change.
|
||||
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51/52 (later slices).
|
||||
- [WP-49](../project/backlog/WP-49-openzaak-zaken-read-seam.md) (this), WP-50/51/52 (CRUD arc), WP-53/54 (identity seam + integration harness).
|
||||
- `backend/src/BigRegister.Api/Zgw/` — the client; `Data/IZaakSource.cs` — the seam.
|
||||
- [ZGW standard (VNG)](https://vng-realisatie.github.io/gemma-zaken/) · [OpenZaak auth docs](https://open-zaak.readthedocs.io/en/stable/client-development/authentication.html).
|
||||
|
||||
Reference in New Issue
Block a user