Files
atomic-design-poc/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-01.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

3.5 KiB

RB-01 — authorize GET /uploads/{id}/content and /uploads/status

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

What was wrong

GET /uploads/{documentId}/content took (string documentId) — no HttpContext, so no authorization was possible at all. It streams diploma and identity scans; the only protection was the unguessability of the document GUID. DELETE on the same resource has been owner-scoped (DocumentStore.DeleteOwned) since it was written.

GET /uploads/status?localIds= had the same shape, and leaks less but still confirms whether a given client-chosen localId exists anywhere in the store, plus its documentId.

What changed

File Change
Program.cs /uploads/{documentId}/content takes HttpContext; allowed for the owning ZorgverlenerCaller or a caller passing Authz.CanBeoordelen; else 404
Program.cs /uploads/status takes HttpContext; scoped to ctx.Zorgverlener().Bsn
Data/DocumentStore.cs ByLocalIds second parameter owner; filters on it (the only call site is the endpoint above)
tests/BigRegister.Tests/UploadAccessTests.cs new — 5 cases

The two actor kinds are matched, not branched on a boolean, because ctx.Zorgverlener() throws for a MedewerkerCaller — a behandelaar reading an aanvraag's linked documents (beoordeling-documenten.component.ts) is a legitimate caller here:

var allowed = ctx.Caller() switch
{
  ZorgverlenerCaller z => doc?.Owner == z.Bsn,
  var caller => Authz.CanBeoordelen(caller),
};

404, not 403, per the ticket: a foreign document id must not be distinguishable from one that never existed. doc is null || !allowed collapses both to the same answer, and /uploads/status reports a foreign localId as "unknown" — the same word an id that never existed gets.

Known residual — this endpoint is reached without identity headers

Both callers link to the URL directly (<a href> in beoordeling-documenten.component.ts, previewUrl in libs/shared/src/upload/upload.adapter.ts), so the request is a plain browser navigation that carries no X-Medewerker / X-Subject header and never passes through an Angular interceptor. StubIdentityProvider therefore resolves it to the seeded citizen, which owns every document in the POC, so both links keep working — by coincidence, not by authorization. That coincidence is BIO-002, and it is fixed by RB-09 (making IIdentityProvider able to express "no identity"), not here. RB-09 will need this endpoint to receive a real credential — a signed URL or a cookie — rather than the ambient default.

Verification

dotnet build clean. dotnet test: 250 passed, 1 failed — the failure is OpenZaakIntegrationTests.Admin_cases_returns_the_seeded_zaak_mapped_through_real_HTTP_and_JWT, which needs a live OpenZaak container and fails identically on a stashed tree, i.e. it pre-dates this change.