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

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. CasesAdmin audits under a fixed "cases" resource literal shared by every cases:manage call (GET /admin/cases, DELETE /admin/cases/{id}, GET /admin/audit itself, and now this endpoint), so Assert.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 reads AuthzAuditStore.List() in-process rather than through GET /admin/audit — that endpoint is itself a CasesAdmin read, so calling it to take the "before" measurement would have written its own cases:manage/allow row 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 push on that one file, tests left in place) and re-ran dotnet test --filter "AuthzAuditTests|EndpointTests": both EndpointTests.Admin_delete_requires_admin_role and AuthzAuditTests.An_admin_upload_delete_is_recorded failed red (403 Forbidden — the old gate rejects X-Role: admin, and the count-delta test throws on EnsureSuccessStatusCode before 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-existing OpenZaakIntegrationTests.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.