# 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](../reference/architecture/0002-user-groups-and-bounded-contexts.md) - `backend/src/BigRegister.Api/Domain/Authorization/CallerIdentity.cs`, `IIdentityProvider.cs`, `StubIdentityProvider.cs` (WP-53's pattern to extend/mirror) - [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) ## 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, extending `CallerIdentity` rather than introducing a parallel type. - Stub the medewerker identity the same way WP-53 stubbed citizen identity (a header-driven `StubIdentityProvider` variant) — 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 1. Extend `CallerIdentity` to the two-actor-kind union. 2. Extend the stub identity provider to produce a `medewerker` identity from a header/config, alongside the existing zorgverlener stub. 3. Add capability checks a backoffice caller needs (start with `canBeoordelen`; extend as WP-65 needs more). 4. Unit tests for both identity kinds and the new capability checks — no consumer exists yet (WP-64+ will call this). ## Acceptance criteria - [x] `CallerIdentity` represents both actor kinds without breaking any existing zorgverlener call site (WP-53's tests still green). - [x] A stub medewerker identity resolves from a request header, mirroring the existing citizen stub. - [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) — 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 `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.