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>
This commit is contained in:
eho
2026-08-27 13:55:41 +02:00
co-authored by Claude Opus 5
parent e89525eef6
commit 494cee9d08
4 changed files with 126 additions and 11 deletions
+9 -10
View File
@@ -269,13 +269,14 @@ api.MapDelete("/uploads/{documentId}", (string documentId, HttpContext ctx) =>
.ProducesProblem(StatusCodes.Status409Conflict)
.Produces(StatusCodes.Status404NotFound);
// Admin delete (seam): a real system requires an admin role; here an X-Admin header
// stands in. Bypasses ownership, unlinks, and flags the submission for review.
api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx) =>
!IsAdmin(ctx) ? Results.StatusCode(StatusCodes.Status403Forbidden)
: DocumentStore.AdminDelete(documentId, "admin") ? Results.NoContent() : Results.NotFound())
// Admin delete: bypasses ownership, unlinks, and flags the submission for review. Gated
// by the same CasesAdmin wrapper (cases:manage) the other admin-cases endpoints use
// (RB-08/BIO-003) — it used to be gated by a standalone X-Admin header, outside Authz and
// unaudited; CasesAdmin gives it the missing AuthzAuditStore row for free (RB-07).
api.MapDelete("/admin/uploads/{documentId}", (string documentId, HttpContext ctx) => CasesAdmin(ctx, () =>
DocumentStore.AdminDelete(documentId, "admin") ? Results.NoContent() : Results.NotFound()))
.Produces(StatusCodes.Status204NoContent)
.Produces(StatusCodes.Status403Forbidden)
.ProducesProblem(StatusCodes.Status403Forbidden)
.Produces(StatusCodes.Status404NotFound);
// --- Applications (aanvragen): the system of record the dashboard reads. ---
@@ -611,8 +612,8 @@ api.MapPut("/admin/flags/{key}", (string key, SetFeatureFlagRequest req, HttpCon
// --- Brief (letter composition). One demo brief per owner; the server owns the
// status machine + authorization (Authz, PRD-0002 phase P1). Principal is a
// dev-only stand-in via X-Role (mirrors the X-Admin seam and the FE ?role=
// toggle) — no real identities in this POC. ---
// dev-only stand-in via X-Role (mirrors the FE ?role= toggle) — no real
// identities in this POC. ---
api.MapGet("/brief", (HttpContext ctx) =>
{
@@ -787,8 +788,6 @@ api.MapPost("/admin/org-template/{subOrgId}/rollback/{version:int}", (string sub
app.Run();
static bool IsAdmin(HttpContext ctx) => ctx.Request.Headers["X-Admin"] == "true";
// One gate for every org-template endpoint — the enforce twin of the
// `orgtemplate:edit` capability RoleCapabilities emits (single Authz source).
//