Files
ehoandClaude Opus 4.8 cf69d474cd
CI / frontend (push) Failing after 1m31s
CI / backend (push) Successful in 1m48s
CI / e2e (push) Successful in 4m19s
CI / storybook-a11y (push) Failing after 7m21s
CI / semgrep (push) Successful in 1m1s
CI / api-client-drift (push) Successful in 3m1s
docs: reference guides for stamdata, audit log, feature flags + document-feature skill
Three how-it-works/how-to-extend reference docs (docs/reference/), each with a
coupling section, indexed in docs/README.md. New document-feature skill so docs
ship in the same diff as the code.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-24 13:39:32 +02:00

3.3 KiB

Audit log (authz / PII-reveal trail) — how it's built & extended

A persisted, data-minimised trail of authorization decisions and sensitive reveals. By construction it never records a name, BSN, or the revealed value — only that a decision happened, on what kind of resource, and its outcome. Built in WP-41 (persisted backend) and surfaced by WP-42 (privacy/security showcase); it backs PRD-0002 — ABAC §8.

The one rule: one producer hub, not scattered logging

Every audited event flows through a single helper — AuditAuthz(…) in Program.cs. It writes the log line and persists the row. Endpoints don't hand-roll audit records; they call the hub. That's what keeps the "never log PII" guarantee enforceable in one place instead of trusting every call site.

Layered pipeline

Backend:

  • Data/AuthzAuditStore.csAuthzAuditEntry (At, Action, Resource, Decision, Role, CorrelationId) + Record(…) / List() (newest-first). No PII column — a reflection test asserts the schema stays that way.
  • Data/AppDbContext.csAuthzAudit DbSet + migration (persists to SQLite, WP-22).
  • Program.cs: AuditAuthz(…) (the hub, ~line 590) is called on every authz denial and on the BIG-nummer reveal/step-up; GET /admin/audit (read) is gated by CasesAdmin.

Frontend (src/app/beheer/):

  • domain/audit-entry.tsinfrastructure/audit.adapter.ts (list() + parseAuditEntries boundary) → application/audit.store.ts (root singleton, RemoteData, read-only) → ui/audit.page.ts (read-only table, gated on cases:manage, loads via a guarded effect once the capability resolves).

How to audit a new action

You do not touch the frontend. In the backend, at the decision point, call the hub:

AuditAuthz(ctx, action: "flags:manage", resource: key, decision: "deny");

Use a short stable action slug and a non-PII resource identifier (an id or table name, never a name/BSN). The admin table picks it up automatically.

Coupling

Deep-linked on the producer side by design, self-contained on the consumer side:

  • Consumer coupling: near zero. The audit store is read by exactly one page; no other component depends on it.
  • Producer coupling: centralized, not scattered. Many endpoints call AuditAuthz (stamdata/cases/flags denials, org-template edits, reveal-bignummer) — but all through the one hub, so it's a spoke-and-hub, not logic sprinkled across the codebase. Extend by calling the hub; never inline a new audit write.

Wire-up gotchas (both bit WP-41)

  • Add the read endpoint to the role.interceptor ROLE_AWARE list (/api/v1/admin/audit) or it 403s silently with no X-Role header.
  • The admin nav entry lives in ADMIN_LINKS, gated on cases:manage.

See also

  • PRD-0002 — ABAC §8 — the audit requirement.
  • WP-41 (persistence), WP-42 (showcase).
  • backend/src/BigRegister.Api/Data/AuthzAuditStore.cs — the no-PII schema.
  • src/app/beheer/ui/audit.page.ts — the read-only view.
  • Roles & accesscases:manage + admin gating.