Files
atomic-design-poc/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-03.md
T
ehoandClaude Opus 5 0298ecc506 fix(uploads): delete the dead POST /registrations (RB-06)
POST /registrations passed its Documents list straight to Submit, which calls
DocumentStore.Link on every digital documentId in it — and linking a document
blocks its owner from ever deleting it (DeleteOwned returns 409 Linked). That
path had no ForeignIds ownership check, so any authenticated citizen could
post another citizen's document id and permanently block them from deleting
their own diploma scan. POST /applications/{id}/submit, the endpoint actually
in use, has had that guard since it was written.

Deleted rather than guarded: the endpoint is dead. No frontend caller, and
the whole registratie flow goes through /applications/{id}/submit.
RegistratieRequest went with it, and so did SubmissionRules.RejectRegistratie
— reachable only from here, and contradicted by the live path, which treats a
handmatig diploma as "does not auto-approve" rather than a 422 rejection. Its
own message said as much while being returned as a rejection. That last part
is a judgement call beyond the ticket's wording; reverting the two
SubmissionRules hunks restores it in isolation.

Coverage moved rather than vanished: the problem+json shape assertion is now
on /change-requests (the other endpoint on the same Submit helper), and the
linked-delete 409 test goes through the real submit path.

swagger.json, the generated client and the behaviour spec regenerated.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 11:04:03 +02:00

2.4 KiB

RB-03 — mask the owner BSN on the cross-owner case lists

Status: implemented · 2026-08-27 · Source findings: 07-bio2-compliance.md BIO-003 · 99-backlog.md RB-03

What was wrong

Mappers.ToAdminSummaryDto set Owner = a.Owner — the raw BSN. Two endpoints consume it, both cross-owner lists read by someone who is not the subject:

  • GET /admin/cases (cases:manage)
  • GET /werkvoorraad (aanvraag:beoordelen)

GET /beoordeling/{id} — the detail view of the same data — already masked. So the detail screen showed ******782 while the list one click earlier showed the whole BSN.

What changed

File Change
Domain/People/Pii.cs new — Pii.MaskTail, moved out of Program.cs
Contracts/Mappers.cs Owner = Pii.MaskTail(a.Owner, 3)
Program.cs local MaskTail deleted; two call sites point at Pii.MaskTail
AdminCasesTests.cs asserts the masked value and that DemoOwner does not appear
WerkvoorraadTests.cs same assertion, replacing the IsNullOrEmpty one

Masked in the mapper, not at the endpoints. The point of the ticket is that both lists inherit it, so a third cross-owner list cannot be added that forgets to mask.

MaskTail moved to Domain/People/Pii.cs because it now has three callers across three folders (Contracts, Program.cs, and Data once RB-04 lands), and a second hand-rolled copy is how one of them drifts into leaking. It is documented as idempotent, which is what lets /beoordeling/{id} keep its own call: IZaakSource has a second implementation (OpenZaakZaakSource → ZgwZaakMapper, which maps Owner from the zaak identificatie), so that endpoint's guarantee should not depend on which source answered.

Blast radius on the frontend — none

Both consumers use the value for display only (admin-cases.page.ts:101, beoordeling-view.ts:40, werkvoorraad-item-view.ts:28); the parse* boundaries require a non-empty string, which a masked BSN still is. Nothing keys, filters or looks up by owner.

Verification

dotnet format --verify-no-changes clean. dotnet test: 251 passed, 1 failed — the pre-existing OpenZaakIntegrationTests.Admin_cases_…, which needs a live container.