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>
4.0 KiB
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.
StamdataAdmingates 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; ifAuthzAuditStoreever 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.