Files
atomic-design-poc/docs/project/refactor-backlog-setup/refactor-backlog/implementation/rb-08.md
T
ehoandClaude Opus 5 494cee9d08 fix(uploads): route the admin delete through CasesAdmin (RB-08)
DELETE /admin/uploads/{documentId} was gated by a standalone
`X-Admin: true` header check (`IsAdmin`), outside the `Authz` module
entirely and outside the `CasesAdmin`/`StamdataAdmin`/`OrgAdmin`/
`FlagsAdmin` wrappers the four sibling admin surfaces use. It wrote no
AuthzAuditStore row, so a destructive cross-owner document delete never
appeared on /beheer/audit. A repo-wide grep confirmed the only sender of
X-Admin was the backend test itself — no frontend or e2e path depends on
it — so the gate was safe to delete outright.

Routed the endpoint through CasesAdmin (Authz.CanManageCases), the same
wrapper the other admin-cases endpoints use. RB-07 already moved
AuditAuthz onto every *Admin wrapper's allow path, so this gets the
missing audit row for free with no second AuditAuthz call. Deleted the
now-unused IsAdmin function and updated the two comments that referenced
the old X-Admin seam.

Updated EndpointTests.cs's Admin_delete_requires_admin_role to send
X-Role: admin instead of X-Admin: true, and added
AuthzAuditTests.An_admin_upload_delete_is_recorded, which asserts the
cases:manage/allow row count increases by exactly one (a plain
Contains would already be satisfied by this test class's other
cases:manage calls). Verified both tests fail red against the
pre-fix gate.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-27 13:55:41 +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.