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>
6.0 KiB
RB-08 — route DELETE /admin/uploads/{documentId} through CasesAdmin; delete the orphaned IsAdmin gate
Status: implemented · 2026-08-27 · Source findings: 07-bio2-compliance.md BIO-003 · 99-backlog.md RB-08
What was wrong
Program.cs:790 (pre-change) had static bool IsAdmin(HttpContext ctx) => ctx.Request.Headers["X-Admin"] == "true";
and DELETE /admin/uploads/{documentId} (:274) was gated by IsAdmin alone — outside
Authz, outside every wrapper the four sibling admin surfaces use, and writing no
AuthzAuditStore row at all. DocumentStore.AdminDelete bypasses ownership and deletes the
row and its bytes; the only record left behind was a DocumentStore.Audit("delete-admin", …)
metadata row, which never surfaces on /beheer/audit.
grep -rn "X-Admin" over apps, libs, backend, e2e (re-verified before deleting the
gate, as the ticket asked) confirmed the finding: the only sender was
backend/tests/BigRegister.Tests/EndpointTests.cs:231. No frontend or e2e path uses this
header — it was an orphaned gate, not a live seam.
What changed
| File | Change |
|---|---|
Program.cs DELETE /admin/uploads/{id} |
now CasesAdmin(ctx, () => DocumentStore.AdminDelete(...) ? NoContent : NotFound) — same wrapper the other four admin-cases endpoints use; response doc changed Produces(403) → ProducesProblem(403) to match CasesAdmin's Results.Problem |
Program.cs IsAdmin |
deleted |
Program.cs two stale comments |
the endpoint's own comment rewritten to describe the new gate; the brief-section banner comment ("mirrors the X-Admin seam") no longer references a gate that doesn't exist |
tests/BigRegister.Tests/EndpointTests.cs |
Admin_delete_requires_admin_role sends X-Role: admin instead of X-Admin: true |
tests/BigRegister.Tests/AuthzAuditTests.cs |
new An_admin_upload_delete_is_recorded — asserts the cases:manage/allow/Admin row count increases by exactly one after the delete |
No new Authz capability was added — CasesAdmin/Authz.CanManageCases is the wrapper the
ticket named as the expected outcome, and nothing about this endpoint needed a narrower
capability than "manage cases" already provides.
RB-07 already moved AuditAuthz onto the allow path for every *Admin wrapper, so
routing through CasesAdmin gives BIO-003's missing audit row for free. No second
AuditAuthz call was added — confirmed by reading CasesAdmin's body (Program.cs): it
calls AuditAuthz(ctx, "cases:manage", "cases", ok, principal) unconditionally before
branching on ok.
Judgement calls
- Test asserts a count delta, not mere presence.
CasesAdminaudits under a fixed"cases"resource literal shared by everycases:managecall (GET /admin/cases,DELETE /admin/cases/{id},GET /admin/audititself, and now this endpoint), soAssert.Contains(rows, cases:manage/allow/Admin)would already be satisfied by this test class's other tests even without the fix. The new test counts matching rows before and after the delete and asserts the count grew by exactly one. It readsAuthzAuditStore.List()in-process rather than throughGET /admin/audit— that endpoint is itself aCasesAdminread, so calling it to take the "before" measurement would have written its owncases:manage/allowrow and silently inflated the count by one every time it was called (caught this by running the test once against the fix with an HTTP-based baseline: it failed with an off-by-one before switching to the in-process read). - Two comments referencing the old gate were also updated, not just the endpoint mapping itself — one directly above the endpoint, one in the brief-section banner comment ("dev-only stand-in via X-Role, mirrors the X-Admin seam") that would otherwise describe a gate that no longer exists.
Known residual
None new. RB-01's implementation note already records that GET /uploads/{id}/content is
reached with no identity header via plain browser navigation — that residual is RB-09's
territory, not this ticket's, and is untouched here.
Verification
- Reverted
Program.cs's endpoint change only (git stash pushon that one file, tests left in place) and re-randotnet test --filter "AuthzAuditTests|EndpointTests": bothEndpointTests.Admin_delete_requires_admin_roleandAuthzAuditTests.An_admin_upload_delete_is_recordedfailed red (403 Forbidden — the old gate rejectsX-Role: admin, and the count-delta test throws onEnsureSuccessStatusCodebefore it can assert). Restored the fix (git stash pop) and re-ran: both green. dotnet build: clean.dotnet test(full suite): 253 passed, 1 failed — the pre-existingOpenZaakIntegrationTests.Admin_cases_returns_the_seeded_zaak_mapped_through_real_HTTP_and_JWT, which needs a live OpenZaak container and fails identically on a clean tree; not touched by this ticket.