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>
4.7 KiB
WP-62 — Backend: medewerker caller identity + authz seam
Status: done Phase: 11 — Behandelportal
Why
The backend's only identity today is CallerIdentity (BSN + display name, from WP-53)
modeling a single zorgverlener actor. ADR-0002 requires a second actor kind
(medewerker/employee) that authenticates differently (no BSN, has rollen) and needs its
own capability checks for backoffice calls. This slice adds that identity + authz surface
on the backend only — unused by any frontend until WP-64 calls it, matching the same
"seam, not provider" discipline WP-53 used for citizen identity (stub, no real employee
SSO — out of scope per CLAUDE.md, same as DigiD).
Read first
- ADR-0002 §3 — Principal union
backend/src/BigRegister.Api/Domain/Authorization/CallerIdentity.cs,IIdentityProvider.cs,StubIdentityProvider.cs(WP-53's pattern to extend/mirror)- WP-53
Decisions (pre-made, don't relitigate)
- Model the two actor kinds as a discriminated union (mirroring ADR-0002 §3:
{ kind: 'zorgverlener'; bsn }|{ kind: 'medewerker'; medewerkerId; rollen }), backend-side, extendingCallerIdentityrather than introducing a parallel type. - Stub the medewerker identity the same way WP-53 stubbed citizen identity (a
header-driven
StubIdentityProvidervariant) — no real employee SSO/eHerkenning. - New capability checks (e.g.
canBeoordelen) are computed backend-side and exposed only as decision flags, never a permission matrix shipped to a frontend (ADR-0001 discipline, reaffirmed by ADR-0002 §3).
Files
Domain/Authorization/CallerIdentity.cs(extend to the union)Domain/Authorization/StubIdentityProvider.cs(medewerker variant)Domain/Authorization/Authz.cs(medewerker capability checks)- Tests
Steps
- Extend
CallerIdentityto the two-actor-kind union. - Extend the stub identity provider to produce a
medewerkeridentity from a header/config, alongside the existing zorgverlener stub. - Add capability checks a backoffice caller needs (start with
canBeoordelen; extend as WP-65 needs more). - Unit tests for both identity kinds and the new capability checks — no consumer exists yet (WP-64+ will call this).
Acceptance criteria
CallerIdentityrepresents 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 citizen stub.
- 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) — 182/182 (168 baseline + 14 new). dotnet format --verify-no-changes clean.
Out of scope
Any actual backoffice endpoint using this (WP-64+); real employee SSO/eHerkenning.
Risks
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
Fileslist undersold the blast radius.CallerIdentitybecameabstractwith two derived records (ZorgverlenerCaller,MedewerkerCaller), which is a hard compile error at everynew CallerIdentity(...)and every.Bsnread outsideDomain/Authorization/— 17ctx.Caller().Bsnreads inProgram.csalone, plus 6 seam signatures (IDocumentSource.Upload,IZaakSource.ListMyCasesand their Local/OpenZaak implementations) narrowed toZorgverlenerCallerwhere.Bsnis 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 whyAuthz.ResolvePrincipal(ctx) => new(ctx.Caller().Role)and its ~15 call sites needed no changes at all.- A new extension,
ctx.Zorgverlener(), narrowsCallerIdentitytoZorgverlenerCalleror throws — deliberately a 500, not a 403, since no medewerker reaches any SSP endpoint today (nothing sendsX-Medewerkeryet). WP-64 should map this to a 403 once real backoffice traffic exists; flagging it now so it isn't mistaken for an oversight.