feat(openzaak): least-privilege client scopes (WP-57)

setup_configuration has no YAML field for granular autorisaties, so
bigregister-test now starts at heeft_alle_autorisaties: false (dev + prod
template) and bootstrap-catalogus.sh grants exactly the ztc/zrc scopes the
harness needs via the Django ORM, sidestepping the zero-scope
chicken-and-egg with the JWT-authenticated Autorisaties REST API.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
eho
2026-07-30 14:05:54 +02:00
co-authored by Claude Sonnet 5
parent 89ad3490b0
commit 1e87997ea0
6 changed files with 112 additions and 13 deletions
+39
View File
@@ -16,6 +16,16 @@
# repeatedly against a long-lived instance, not just once per fresh volume. Prints the seeded
# zaak's `identificatie` + `url` on success; also writes them to seeded.env (repo-ignored) for
# OpenZaakIntegrationTests.cs to assert against.
#
# WP-57: `bigregister-test` starts with ZERO Autorisaties (data.yaml sets
# heeft_alle_autorisaties: false) — the setup_configuration YAML has no field for granular
# scopes at all (confirmed from vng_api_common's own ApplicatieConfigurationModel), so this
# script grants them itself via `manage.py shell` (Django ORM, inside the `web` container) at
# the two points they become grantable: ztc scopes up front (no zaaktype dependency), zrc
# scopes once `zaaktype_url` exists below. Going through the ORM instead of the
# JWT-authenticated Autorisaties REST API sidesteps a real chicken-and-egg: a client with zero
# scopes cannot grant itself any scope over that API. Re-running this script re-grants the same
# scopes (idempotent, like everything else here).
set -euo pipefail
cd "$(dirname "${BASH_SOURCE[0]}")"
@@ -61,6 +71,27 @@ oz() {
echo "$json"
}
# Grant (replace) an Autorisatie for $CLIENT_ID directly via the ORM (see the WP-57 note up
# top for why this bypasses the REST Autorisaties API). $1 = component, $2 = python list
# literal of scopes, $3.. = extra `Autorisatie(...)` kwargs as `name=value` (value already a
# valid Python literal, e.g. a quoted URL).
grant_scopes() {
local component="$1" scopes="$2"
shift 2
local extra="" kv
for kv in "$@"; do extra+=" $kv,"$'\n'; done
docker compose -f docker-compose.openzaak.yml exec -T --workdir /app/src web python manage.py shell <<PY
from vng_api_common.authorizations.models import Applicatie
app = Applicatie.objects.get(client_ids__contains=["$CLIENT_ID"])
app.autorisaties.filter(component="$component").delete()
app.autorisaties.create(
component="$component",
scopes=$scopes,
$extra)
PY
}
# $1 = list path+query (server-side-filtered to the natural key). Prints the first result's
# `url`, or nothing if the list is empty — the GET-before-POST idempotency check.
existing_url() {
@@ -86,6 +117,9 @@ until curl -sS -o /dev/null -w '%{http_code}' "$BASE/catalogi/api/v1/catalogusse
sleep 2
done
echo "Granting ztc scopes (catalogi.lezen, catalogi.schrijven — this script's own content-creation needs; the BFF only ever reads Catalogi)..."
grant_scopes ztc '["catalogi.lezen", "catalogi.schrijven"]'
echo "Catalogus..."
catalogus_url=$(existing_url "/catalogi/api/v1/catalogussen?domein=BIGR&rsin=$RSIN")
if [ -n "$catalogus_url" ]; then
@@ -134,6 +168,11 @@ print(json.dumps({
echo " created: $zaaktype_url"
fi
echo "Granting zrc scopes (zaken.aanmaken, zaken.bijwerken, zaken.lezen), scoped to $zaaktype_url — the one zaaktype this harness (and the BFF's Zgw:ZaaktypeUrls config) ever uses..."
grant_scopes zrc '["zaken.aanmaken", "zaken.bijwerken", "zaken.lezen"]' \
"zaaktype=\"$zaaktype_url\"" \
'max_vertrouwelijkheidaanduiding="openbaar"'
echo "Statustypen (publish needs a begin AND an end status)..."
statustype_url=$(existing_statustype_url "$zaaktype_url" 1)
if [ -n "$statustype_url" ]; then