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>
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.