feat(privacy): WP-41 — persisted, queryable authz/PII-reveal audit

Persist the security-relevant events (authz denials + BIG-nummer reveal/step-up) into a
data-minimised EF table (AuthzAuditEntry: At/Action/Resource/Decision/Role/CorrelationId —
never a name/BSN/value), extending the DocumentStore AuditEntry pattern (migration AuthzAudit).
AuditAuthz now persists via AuthzAuditStore.Record alongside its log line. GET /admin/audit
(admin-gated by the existing CasesAdmin) returns the trail newest-first. +3 backend tests incl.
a schema-carries-no-PII reflection test. Typed client regenerated (audit() + AuthzAuditDto);
no FE consumer yet (a future audit view must add the ROLE_AWARE prefix). Finishes WP-42's audit half.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-23 15:53:33 +02:00
co-authored by Claude Opus 4.8
parent 8c54ede6eb
commit 0f30143c5d
12 changed files with 590 additions and 5 deletions
+11
View File
@@ -330,6 +330,15 @@ api.MapDelete("/admin/cases/{id}", (string id, HttpContext ctx) => CasesAdmin(ct
.Produces(StatusCodes.Status404NotFound)
.ProducesProblem(StatusCodes.Status403Forbidden);
// Queryable authz/PII-reveal audit trail (WP-41) — data-minimised, no PII. Admin-gated
// via the existing CasesAdmin (cases:manage); a dedicated audit:read cap is a later refinement.
api.MapGet("/admin/audit", (HttpContext ctx) => CasesAdmin(ctx, () =>
Results.Ok(AuthzAuditStore.List()
.Select(a => new AuthzAuditDto(a.At.ToString("o"), a.Action, a.Resource, a.Decision, a.Role, a.CorrelationId))
.ToList())))
.Produces<List<AuthzAuditDto>>()
.ProducesProblem(StatusCodes.Status403Forbidden);
// PRD-0002 §6: coarse, role-derived capabilities for nav/menu-level checks (NOT
// tied to a specific brief's live status — see BriefDecisionsDto for that).
api.MapGet("/me", (HttpContext ctx) => new MeDto(Authz.RoleCapabilities(Authz.ResolvePrincipal(ctx))))
@@ -558,6 +567,8 @@ void AuditAuthz(HttpContext ctx, string action, string resource, bool allowed, P
app.Logger.LogInformation(
"authz action={Action} resource={Resource} decision={Decision} role={Role} correlationId={Cid}",
action, resource, allowed ? "allow" : "deny", principal.Role, cid);
// Persist the queryable, data-minimised trail (WP-41) alongside the log line.
AuthzAuditStore.Record(action, resource, allowed, principal.Role.ToString(), cid);
}
// Keep the last `keep` characters, mask the rest — mirrors the FE maskTail