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