Files
atomic-design-poc/docs/project/archive/refactor-backlog-setup/refactor-backlog/implementation/rb-07.md
T
ehoandClaude Opus 5 12f17d9d73 docs: archive the finished backlogs (RD-30)
Two backlog trees are complete: `docs/project/backlog/` (75 files, every
WP done) and `docs/project/refactor-backlog-setup/` (the arc before it).
Move both under `docs/project/archive/` with `git mv`, so history stays
intact through `git log --follow`. `SHOWCASE-ROADMAP.md` moves with them,
because it points at the now-archived backlog README.

Add `docs/project/archive/README.md`. It states that these trees are
historical and names the two directories that are still live.

Repoint every inbound reference named in RD-30's Files table: CLAUDE.md,
the root README, both backend READMEs, `LetterHtml.cs`, `a11y.mdx`, the
`document-feature` and `new-ssp` skills, and the readable-codebase PLAN,
README, and RD-19 ticket. Fix two upward-relative links inside the moved
WP files (WP-68, WP-69) that gained a directory level and would otherwise
break. Repoint `.prettierignore`'s two agent-prompt exclusions to their
new path, so prettier keeps leaving those files' exact wording alone.

Mark RD-30 done and check off its acceptance criteria; flip its README
row to done.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-08 23:00:38 +02:00

4.0 KiB
Raw Blame History

RB-07 — audit the allow path, not just the denial

Status: implemented · 2026-08-27 · Source findings: 07-bio2-compliance.md BIO-007 (+ the outstanding half of CQ-004) · 99-backlog.md RB-07

What was wrong

All five authorization gates called AuditAuthz(..., allowed: false, ...) only on the deny branch; the allow branch called action() and returned. So /beheer/audit — the queryable trail the product ships as its audit surface — could answer "who was turned away" but never "who changed this".

Nothing recorded: PUT /admin/flags/{key}, PUT /admin/org-template/{subOrgId}, POST /admin/org-template/{subOrgId}/rollback/{version}, DELETE /admin/cases/{id}, DELETE /admin/uploads/{documentId}, POST /brief/approve|reject|send, and POST /beoordeling/{id}/besluit. The comment above OrgAdmin claimed the endpoints logged their own effect instead; publish and admin case delete do, the other six did not log at all.

What changed

File Change
Program.cs × 5 gates var ok = Authz.CanX(p); AuditAuthz(ctx, …, ok, p); if (ok) return action();
Program.cs FlagsAdmin takes a per-call resource (see below)
Program.cs LogBrief takes HttpContext, writes the audit row alongside the log line
Program.cs besluit one aanvraag:besluit row recording what was decided
AuthzAuditTests.cs allow-path row; the flag key + value; a refused brief transition
BriefEndpointTests.cs the allow side of brief:submit
BeoordelingTests.cs the aanvraag:besluit row

The row is written by the gate, not the endpoint. That is the point: a new admin endpoint cannot be added that forgets to audit itself. Same reasoning for the brief — every transition already funnelled through LogBrief for its log line, so the audit call went there too, which covers submit/approve/reject/send in one place and any fifth transition automatically. The decision recorded is the transition's own outcome, so a 403 or a 409 is as visible as a success.

FlagsAdmin gained a resource parameter — the one deviation from BIO-007's minimal remediation, and the reason is in the finding itself: the toggle endpoint writes no log line of its own, so a constant "feature-flags" row would record that a flag changed without recording which. It now writes feature-flags/<key>=<value>. One call site. OrgAdmin/CasesAdmin keep their coarse refs because those endpoints do log the specific object; that asymmetry is deliberate, not an oversight.

The besluit gets a second row. The Beoordelen gate records that a behandelaar was allowed to act; aanvraag:besluit records what they decided (aanvraag/<id>/Goedkeuren). Only the first would leave "who rejected this aanvraag" unanswerable, which is the question the trail exists for.

Consequences worth knowing

  • Row volume goes up. StamdataAdmin gates read endpoints, so every admin page load now writes rows. That is what "audit the allow path" means and BIO-007 asks for it explicitly; if AuthzAuditStore ever needs retention or sampling, this is the change that made it necessary.
  • This unblocks ADR-C-009. Clause (4) of agent 06's four-part test is "writes are admin-capability-gated and audited". Both surfaces now are, so the amendment can be signed without ratifying a control the code does not implement.
  • CQ-004's outstanding half is closed. PUT /admin/flags/{key} writes an audit row.

Verification

dotnet test: 252 passed, 1 failed — the pre-existing OpenZaakIntegrationTests.Admin_cases_…, which needs a live container.