# 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/=`. 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//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.