Files
atomic-design-poc/docs/project/archive/refactor-backlog-setup/refactor-backlog/implementation/rb-01.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +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.