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>
2.1 KiB
RB-04 — mask the BSN recorded as AuditEntry.Actor
Status: implemented · 2026-08-27 · Source findings: 07-bio2-compliance.md BIO-005 · 99-backlog.md RB-04
What was wrong
DocumentStore writes one audit row per upload and per user delete, with the acting
citizen's raw BSN as AuditEntry.Actor, persisted to SQLite. The class's own doc comment
says "The audit log holds metadata only (never file content or other PII)" — a BSN in
every row is precisely other PII. Same failure shape as RB-02, in a second store.
What changed
| File | Change |
|---|---|
Data/DocumentStore.cs Add |
Audit("upload", …, Pii.MaskTail(owner, 3)) |
Data/DocumentStore.cs DeleteOwned |
Audit("delete-user", …, Pii.MaskTail(owner, 3)) |
Data/DocumentStore.cs Audit |
doc comment: actors arrive already redacted |
UploadAccessTests.cs |
new The_document_audit_trail_records_a_masked_actor |
Masked at the two call sites, not inside Audit — unlike RB-03, where masking in the
mapper was the point. Audit's third actor is the literal "admin" (from AdminDelete),
and MaskTail("admin", 3) is "**min": masking centrally would mean guessing which
actors are BSNs and which are role names. The contract is stated on Audit instead.
StoredDocument.Owner is untouched, per the ticket. It is the authorization key —
DeleteOwned, ForeignIds and now the RB-01 content check all compare against it — so it
has to stay whole. The BSN remains where it is load-bearing and leaves the trail where it
was only decoration.
Nothing reads DocumentStore.AuditLog today (no endpoint exposes it), so this is a
data-at-rest fix with no response-shape change.
Verification
dotnet format --verify-no-changes clean. dotnet test: 252 passed, 1 failed — the
pre-existing OpenZaakIntegrationTests.Admin_cases_…, which needs a live container.