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>
This commit is contained in:
eho
2026-09-08 23:00:38 +02:00
co-authored by Claude Opus 5
parent 097e8468e0
commit 12f17d9d73
161 changed files with 154 additions and 24 deletions
@@ -0,0 +1,57 @@
# 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:
```csharp
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.