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>
This commit is contained in:
eho
2026-09-08 23:00:38 +02:00
co-authored by Claude Opus 5
parent 097e8468e0
commit 12f17d9d73
161 changed files with 154 additions and 24 deletions
@@ -0,0 +1,63 @@
# 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.