feat(behandelportal): WP-62 medewerker caller identity + authz seam
Splits backend CallerIdentity into the two ADR-0002 §3 actor kinds (ZorgverlenerCaller/MedewerkerCaller), a stub X-Medewerker/X-Rollen header path mirroring WP-53's citizen stub, and Authz.CanBeoordelen as the first medewerker capability — backend-only, no consumer until WP-64. Also fixes the backlog README's stale WP-61 status (done, but table said todo). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -111,8 +111,8 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | done |
|
||||
| [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | done |
|
||||
| [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | done |
|
||||
| [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | todo |
|
||||
| [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | todo |
|
||||
| [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | done |
|
||||
| [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | done |
|
||||
| [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | todo |
|
||||
| [WP-64](WP-64-behandelportal-werkvoorraad.md) | Behandelportal: werkvoorraad (queue) screen | 11 · Behandelportal | todo |
|
||||
| [WP-65](WP-65-behandelportal-beoordeling.md) | Behandelportal: zaak detail + beoordeling (decision) screen | 11 · Behandelportal | todo |
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WP-62 — Backend: medewerker caller identity + authz seam
|
||||
|
||||
Status: todo
|
||||
Status: done
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
@@ -50,17 +50,17 @@ SSO — out of scope per CLAUDE.md, same as DigiD).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `CallerIdentity` represents both actor kinds without breaking any existing
|
||||
- [x] `CallerIdentity` represents both actor kinds without breaking any existing
|
||||
zorgverlener call site (WP-53's tests still green).
|
||||
- [ ] A stub medewerker identity resolves from a request header, mirroring the existing
|
||||
- [x] A stub medewerker identity resolves from a request header, mirroring the existing
|
||||
citizen stub.
|
||||
- [ ] At least one capability flag (`canBeoordelen`) computable for a medewerker
|
||||
- [x] At least one capability flag (`canBeoordelen`) computable for a medewerker
|
||||
identity, unit-tested.
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test` (existing WP-53 tests unaffected + new medewerker tests
|
||||
green).
|
||||
green) — 182/182 (168 baseline + 14 new). `dotnet format --verify-no-changes` clean.
|
||||
|
||||
## Out of scope
|
||||
|
||||
@@ -71,3 +71,26 @@ Any actual backoffice endpoint using this (WP-64+); real employee SSO/eHerkennin
|
||||
If the union is modeled as a bolt-on rather than replacing the flat type, existing
|
||||
zorgverlener call sites could break — mitigated by keeping WP-53's existing tests as a
|
||||
regression gate.
|
||||
|
||||
## Outcome notes
|
||||
|
||||
- **The `Files` list undersold the blast radius.** `CallerIdentity` became `abstract`
|
||||
with two derived records (`ZorgverlenerCaller`, `MedewerkerCaller`), which is a hard
|
||||
compile error at every `new CallerIdentity(...)` and every `.Bsn` read outside
|
||||
`Domain/Authorization/` — 17 `ctx.Caller().Bsn` reads in `Program.cs` alone, plus 6
|
||||
seam signatures (`IDocumentSource.Upload`, `IZaakSource.ListMyCases` and their
|
||||
Local/OpenZaak implementations) narrowed to `ZorgverlenerCaller` where `.Bsn` is used
|
||||
as an ownership key, plus test fixtures in 4 test files.
|
||||
`CallerIdentity.SubjectId` (BSN or medewerkerId) is the trick that kept the
|
||||
token-mint-only call sites (`ZgwTokenProvider.Mint`, `ZgwHttpClient`, `IZaakSource
|
||||
.CreateZaak`, `IDocumentSource.LinkToZaak`) compiling with zero signature changes —
|
||||
they never needed the BSN specifically, just _an_ id for the ZGW audit trail.
|
||||
- **`Role` (`PrincipalRole`, the existing dev-role stand-in) stays on the base record**,
|
||||
not per-variant — it's an orthogonal axis (both actor kinds can be any dev role),
|
||||
which is why `Authz.ResolvePrincipal(ctx) => new(ctx.Caller().Role)` and its ~15
|
||||
call sites needed no changes at all.
|
||||
- **A new extension, `ctx.Zorgverlener()`**, narrows `CallerIdentity` to
|
||||
`ZorgverlenerCaller` or throws — deliberately a 500, not a 403, since no medewerker
|
||||
reaches any SSP endpoint today (nothing sends `X-Medewerker` yet). WP-64 should
|
||||
map this to a 403 once real backoffice traffic exists; flagging it now so it isn't
|
||||
mistaken for an oversight.
|
||||
|
||||
@@ -229,6 +229,22 @@ CallerIdentity.cs`):
|
||||
instead of calling `ApplicationStore` directly — the last "reads a static store directly" gap
|
||||
the ACL caveat below used to flag for a citizen-facing endpoint.
|
||||
|
||||
**WP-62 split `CallerIdentity` into the two actor kinds ADR-0002 §3 requires** — a
|
||||
`ZorgverlenerCaller` (citizen, the WP-53 shape above) or a `MedewerkerCaller` (backoffice
|
||||
employee: `MedewerkerId` + `Rollen`, no BSN), backend-only, unused by any frontend until WP-64.
|
||||
`StubIdentityProvider` selects the medewerker kind when `X-Medewerker` is present (its value is
|
||||
the medewerkerId; `X-Rollen` is a comma-separated rollen list, defaulting to `Behandelaar`) —
|
||||
takes precedence over `X-Subject`; absent, every request today, falls through to the
|
||||
zorgverlener path unchanged. `CallerIdentity.SubjectId` (BSN or medewerkerId) is what
|
||||
`ZgwTokenProvider.Mint` now reads instead of `.Bsn` directly, so the ZGW JWT's `user_id` is
|
||||
correct for either kind with no further change (WP-66's besluit write mints this for free). The
|
||||
ownership-scoping seams (`ctx.Zorgverlener()`, `IDocumentSource.Upload`, `IZaakSource
|
||||
.ListMyCases`) are narrowed to `ZorgverlenerCaller` — a medewerker hitting a citizen-scoped SSP
|
||||
endpoint is a 500 today (unreachable, since no consumer sends `X-Medewerker` yet; WP-64 upgrades
|
||||
it to a 403 once real backoffice traffic exists). `Authz.CanBeoordelen(CallerIdentity)` is the
|
||||
first medewerker capability (rol-based, `MedewerkerRol.Behandelaar`), shipped only as a decision
|
||||
flag, never a rollen matrix.
|
||||
|
||||
## The five ZGW APIs (context for later slices)
|
||||
|
||||
| API | Component | Used by |
|
||||
|
||||
@@ -32,6 +32,18 @@ Both are wired only under `isDevMode()` — they do not exist in a production bu
|
||||
Mechanism: `src/app/shared/infrastructure/role.ts` reads the role and the HTTP interceptor stamps it
|
||||
as an `X-Role` header on role-aware requests; the backend resolves it into a `Principal`.
|
||||
|
||||
## Actor kinds (backend, WP-62)
|
||||
|
||||
`X-Role`/`Principal` above is a coarse role that applies to **either** of two actor kinds the
|
||||
backend now models (ADR-0002 §3): a **zorgverlener** (this SSP's citizen — has a BSN) or a
|
||||
**medewerker** (backoffice employee — no BSN, has `Rollen`). `StubIdentityProvider` picks the
|
||||
medewerker kind from a dev header, `X-Medewerker` (+ `X-Rollen`), mirroring `X-Role`/`X-Subject`
|
||||
above — **the SSP's FE never sends either header**; they exist only for the backend's own tests
|
||||
and for the behandelportal (WP-64+) to use later. `Authz.CanBeoordelen(caller)` is the first
|
||||
medewerker capability — a rol-based decision flag (`MedewerkerRol.Behandelaar`), not a role
|
||||
entry on `/me`, since `/me`'s `RoleCapabilities` is keyed on `Principal` and can't see the actor
|
||||
kind.
|
||||
|
||||
## What each role unlocks
|
||||
|
||||
Capabilities are resolved server-side (`backend/src/BigRegister.Api/Domain/Authorization/Authz.cs`,
|
||||
|
||||
Reference in New Issue
Block a user