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