fix(brief): keep the dev hatches out of production builds (RB-11)
BIO-012: roleInterceptor/subjectInterceptor are correctly registered only under isDevMode(), but three hand-written fetch adapters (reveal-bignummer, letter-preview, org-template's proefbrief) bypass HttpClient and set X-Role/X-Subject themselves with no guard. The readers underneath, role.ts and subject.ts, were ungated too: they read ?role=/?subject= and wrote it into sessionStorage on any navigation, in any build -- for ?subject= that value is a BSN, which is exactly what SessionStore's G1 comment promises never happens. Gate both layers: currentRole()/currentSubject() return their safe default immediately outside isDevMode() (no query-param read, no sessionStorage write), and the three adapters additionally wrap their headers in isDevMode() so a production request carries neither header at all, matching what an HttpClient request already does once the interceptors aren't registered. TE-002: reveal-bignummer's response-shape validation was a "Trust boundary" a spec could only reach by stubbing globalThis.fetch. Exported it as parseRevealed(body), matching the other 30 parse* boundaries in the repo. Same treatment for letter-preview's errorMessage and org-template's proefbrief error mapping (extracted from an inline try/catch into a named, exported function first, since it wasn't already separate). BIO-006(a): reveal-bignummer sent X-Step-Up: 'true' unconditionally, so the backend's step-up precondition constrained nothing. reveal() now takes a stepUp flag; BriefStore.revealBigNummer() -- reachable only after the UI's confirm() gesture -- is the one that supplies it, so the literal no longer lives in the transport adapter. BIO-006(b): documented in roles-and-access.md that drafter is also the backend's fallback identity (StubIdentityProvider's catch-all arm), not just the dev switcher's initial choice -- so the least-privilege consequence of it also being the only role that may reveal a BSN is visible. Doc correction, same diff: roles-and-access.md's "wired only under isDevMode()" claim was false for the three hand-written fetch paths; it now says where the gate lives (interceptor registration and the reader functions) so it doesn't go stale the same way again. CLAUDE.md's dev-only claims needed no correction -- they already noted these three calls bypass the interceptor. Every fix has a test confirmed red by temporarily reverting the source change and rerunning the suite before restoring it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -20,7 +20,15 @@ acting role to exercise the drafter/approver/admin flows.
|
||||
|
||||
## How to switch role (dev only)
|
||||
|
||||
Both are wired only under `isDevMode()` — they do not exist in a production build.
|
||||
Both are wired only under `isDevMode()` — they do not exist in a production build. That
|
||||
gate lives in two places: the `roleInterceptor` registration (`app.config.ts`) for every
|
||||
`HttpClient` request, **and** inside `role.ts`'s `currentRole()` itself, because three
|
||||
hand-written `fetch` calls (`reveal-bignummer.adapter.ts`, `letter-preview.adapter.ts`,
|
||||
`org-template.adapter.ts`'s proefbrief) read the role directly and bypass the
|
||||
interceptor entirely (RB-11/BIO-012). Before RB-11, `currentRole()` had no such gate, so
|
||||
`?role=` kept working through those three calls in a production build even though this
|
||||
page said otherwise; the same defect applied to `?subject=` and `subject.ts`, which is
|
||||
how a BSN reached `sessionStorage` in any build.
|
||||
|
||||
- **Dev switcher (easiest):** open the `⚙ state` panel (bottom-right in a dev build) and pick a
|
||||
role from the **role** dropdown. The page reloads with the new role.
|
||||
@@ -66,6 +74,16 @@ The admin pages appear in the header nav and in the dashboard **"Beheer"** secti
|
||||
matching capability is present — otherwise they are reachable only by URL (and the route guard
|
||||
redirects a user who lacks the capability back to `/dashboard`).
|
||||
|
||||
**`drafter` is also the backend's fallback identity (BIO-006).** It is not only the dev
|
||||
switcher's initial selection — `StubIdentityProvider`'s role switch resolves **any**
|
||||
request with no `X-Role` header at all (or an unrecognised one) to `drafter` too. Because
|
||||
`drafter` is also the _only_ role that may reveal a BIG-nummer, the least-privilege
|
||||
consequence is real: an unauthenticated or misconfigured caller inherits the PII-reveal
|
||||
capability by default, rather than the weakest one. This is acceptable only because the
|
||||
POC has no real identity or step-up yet (see the pre-production compliance checklist —
|
||||
binding the reveal to an app-overlay attribute instead of the coarse role is a named,
|
||||
not-yet-built item); it must not survive real identity and step-up.
|
||||
|
||||
## The one principle
|
||||
|
||||
Identity (AD/OIDC, faked here) supplies **coarse roles**; the app owns a **fine-grained capability**
|
||||
|
||||
Reference in New Issue
Block a user