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>
5.0 KiB
WP-57 — Least-privilege client scopes
Status: done Phase: 10 — OpenZaak production hardening
Why
The harness's OpenZaak client is granted heeft_alle_autorisaties: true in
setup_configuration/data.yaml — acceptable for a disposable test rig, wrong for anything
closer to production, where the BFF's client should hold only the Autorisaties it actually
exercises.
Read first
backend/openzaak/setup_configuration/data.yamlbackend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.csandOpenZaakDocumentSource.cs(the actual ZGW endpoints/verbs called)
Decisions (pre-made, don't relitigate)
- Scope precisely to what the BFF calls today: zaken (aanmaken, bijwerken, lezen), statussen (aanmaken), rollen (aanmaken), documenten/zaakinformatieobjecten (aanmaken, lezen) — enumerate exactly at kickoff from the client code, don't guess broader.
- No wildcard/all-scope grant in any environment beyond the pre-WP-56 disposable dev rig.
Files
backend/openzaak/setup_configuration/data.yaml(Autorisaties block)backend/openzaak/README.md
Steps
- Grep
OpenZaakZaakSource.csandOpenZaakDocumentSource.csfor every ZGW endpoint/verb called. - Replace
heeft_alle_autorisaties: truewith an explicitautorisatieslist matching exactly that set. - Re-run the full integration suite against the narrowed client; add any scope a 403 surfaces.
Acceptance criteria
- Client config has no wildcard/all-scopes grant.
OpenZaakIntegrationTests(WP-54) pass unchanged against the narrowed client.
What actually happened
vng_api_common's ApplicatieConfigurationModel (the class backing
setup_configuration's vng_api_common_applicaties step, read from the installed package
inside the openzaak/open-zaak:1.29.1 image) only has fields for
uuid/client_ids/label/heeft_alle_autorisaties — there is no YAML field for granular
autorisaties at all. So data.yaml now sets heeft_alle_autorisaties: false (both the dev
harness and the prod template), which leaves bigregister-test with zero Autorisaties
until something else grants them.
That "something else" can't be the JWT-authenticated Autorisaties REST API — a zero-scope
client can't grant itself scope over an API gated by scope (confirmed from
ApplicatieViewSet.required_scopes: update/partial_update need
autorisaties.bijwerken). bootstrap-catalogus.sh grants the scopes directly via the ORM
instead (docker compose exec web python manage.py shell, workdir /app/src) — no
JWT/REST layer involved, so no circularity. Two grants, both idempotent (delete-then-create):
ztc:catalogi.lezen+catalogi.schrijven— granted up front (no zaaktype dependency). Onlycatalogi.schrijvenis provisioning-only; the BFF itself only ever reads Catalogi.zrc:zaken.aanmaken+zaken.bijwerken+zaken.lezen, scoped to the one zaaktype (zaaktype=<ZT-HERREG url>,max_vertrouwelijkheidaanduiding=openbaar— both fields are required by OpenZaak'sAutorisatieValidatorfor anyzaken.*scope) — granted oncezaaktype_urlis known, right after the zaaktype is created/resolved.
Reading the actual RolViewSet/StatusViewSet/ZaakInformatieObjectViewSet
required_scopes (not just the scope docstrings, which are aspirational/descriptive) showed
the decision text's "statussen (aanmaken), rollen (aanmaken)" don't map to separate OpenZaak
scopes — zaken.aanmaken alone (OR'd against alternatives) already covers the first status
and the initiator rol; there is no rollen.aanmaken scope. documenten/zaakinformatieobjecten
scope was not granted: Zgw:InformatieobjecttypeUrls is empty in appsettings.json, so
OpenZaakDocumentSource.Upload can't function in this harness regardless of scope (throws
before any HTTP call) — nothing to scope precisely to yet. Left as a documented follow-up
(the script would also need to seed an informatieobjecttype to have something concrete to
scope documenten.aanmaken to).
Verified for real: down -v fresh volume → up -d → bootstrap-catalogus.sh (all
"created", scopes granted, heeft_alle_autorisaties: False confirmed via manage.py shell)
→ dotnet test --filter Category=Integration green → reran bootstrap-catalogus.sh again
under the now-narrowed client (all "exists", scopes re-granted idempotently, no 403s) →
confirmed the narrowing is real, not just untested, by DELETEing the seeded zaak with a
hand-rolled JWT for this client: 403 permission_denied (zaak deletion needs
zaken.verwijderen/zaken.geforceerd-bijwerken, neither granted).
Verification
cd backend && dotnet test --filter Category=Integration against the harness with the
narrowed client.
Out of scope
Rotating/expiring the client credential itself — defer until multi-tenant/production ops actually need it.
Risks
An overlooked scope only surfaces as a runtime 403 against a real instance — mitigated by running the full integration suite, which already exercises every current call path (WP-54).
Depends on: WP-56 (provisioning mechanism this scopes down).