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>
105 lines
5.0 KiB
Markdown
105 lines
5.0 KiB
Markdown
# 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.yaml`
|
|
- `backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs` and `OpenZaakDocumentSource.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
|
|
|
|
1. Grep `OpenZaakZaakSource.cs` and `OpenZaakDocumentSource.cs` for every ZGW
|
|
endpoint/verb called.
|
|
2. Replace `heeft_alle_autorisaties: true` with an explicit `autorisaties` list matching
|
|
exactly that set.
|
|
3. Re-run the full integration suite against the narrowed client; add any scope a 403
|
|
surfaces.
|
|
|
|
## Acceptance criteria
|
|
|
|
- [x] Client config has no wildcard/all-scopes grant.
|
|
- [x] `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).
|
|
Only `catalogi.schrijven` is 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's `AutorisatieValidator` for any `zaken.*` scope) — granted once
|
|
`zaaktype_url` is 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).
|