The backend half of the sweep RD-18 did for the front end. git blame holds the provenance and stays correct when the code moves; the comment names a closed ticket and tells the reader nothing the sentence around it does not. public/letter.css and LetterHtml.golden.html change together, because the renderer inlines the CSS and the golden file snapshots the result. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
278 lines
13 KiB
Bash
Executable File
278 lines
13 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Seeds business content
|
|
# (catalogus/zaaktype/statustype/roltype/zaak/status/rol) into the OpenZaak harness started
|
|
# by docker-compose.openzaak.yml. `setup_configuration/data.yaml` only covers infra config
|
|
# (JWTSecret + Applicatie) — confirmed by reading the installed `django_setup_configuration`
|
|
# steps inside the `openzaak/open-zaak` image itself: the only app-registered step besides the
|
|
# generic sites/credentials/applicaties ones is Selectielijst API config. There is NO
|
|
# declarative-YAML equivalent upstream for Catalogi/Zaken content, so this script does it the
|
|
# same way the BFF itself does at runtime — plain REST calls with a hand-rolled HS256 JWT (see
|
|
# ZgwTokenProvider.cs, mirrored here in bash+openssl so this script has no extra dependency
|
|
# beyond curl/openssl/python3, python3 already required by the JSON bodies below).
|
|
#
|
|
# Idempotent: every resource is looked up by its natural key (GET with the same filter OpenZaak
|
|
# enforces uniqueness/identity on) before creating it, so re-running against an
|
|
# already-seeded instance reuses what's there instead of erroring or duplicating. Safe to run
|
|
# 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.
|
|
#
|
|
# `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]}")"
|
|
|
|
BASE="http://localhost:8000"
|
|
CLIENT_ID="bigregister-test"
|
|
SECRET="bigregister-test-secret"
|
|
RSIN="123443210" # elfproef-valid RSIN, already used as the fixture Bronorganisatie
|
|
# in OpenZaakZaakSourceTests.cs — reused here for consistency.
|
|
BSN="111222333" # elfproef-valid BSN, already used as the fixture caller BSN.
|
|
ZAAK_REF="BIG-2026-000123"
|
|
|
|
b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
|
|
|
|
jwt() {
|
|
local header='{"alg":"HS256","typ":"JWT"}'
|
|
local payload
|
|
payload=$(printf '{"iss":"%s","iat":%d,"client_id":"%s","user_id":"%s","user_representation":"%s"}' \
|
|
"$CLIENT_ID" "$(date +%s)" "$CLIENT_ID" "$CLIENT_ID" "bootstrap")
|
|
local h p signing_input sig
|
|
h=$(printf '%s' "$header" | b64url)
|
|
p=$(printf '%s' "$payload" | b64url)
|
|
signing_input="$h.$p"
|
|
sig=$(printf '%s' "$signing_input" | openssl dgst -sha256 -hmac "$SECRET" -binary | b64url)
|
|
printf '%s.%s' "$signing_input" "$sig"
|
|
}
|
|
|
|
# $1 = method, $2 = path, $3 = JSON body (optional)
|
|
oz() {
|
|
local method="$1" path="$2" body="${3:-}"
|
|
# Content-Crs/Accept-Crs: every ZGW write must declare a coordinate reference system even
|
|
# when no geometry is involved — OpenZaak 412s without it.
|
|
local args=(-sS -X "$method" -H "Authorization: Bearer $(jwt)" -H "Content-Type: application/json" \
|
|
-H "Content-Crs: EPSG:4326" -H "Accept-Crs: EPSG:4326")
|
|
[ -n "$body" ] && args+=(-d "$body")
|
|
local response
|
|
response=$(curl "${args[@]}" -w $'\n%{http_code}' "$BASE$path")
|
|
local http_code="${response##*$'\n'}"
|
|
local json="${response%$'\n'*}"
|
|
if [[ ! "$http_code" =~ ^2 ]]; then
|
|
echo "FAILED $method $path -> $http_code: $json" >&2
|
|
exit 1
|
|
fi
|
|
echo "$json"
|
|
}
|
|
|
|
# Grant (replace) an Autorisatie for $CLIENT_ID directly via the ORM (see the 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() {
|
|
oz GET "$1" | python3 -c 'import json,sys; r=json.load(sys.stdin)["results"]; print(r[0]["url"] if r else "")'
|
|
}
|
|
|
|
# $1 = zaaktype URL, $2 = volgnummer. Statustype has no server-side volgnummer filter, so this
|
|
# lists by zaaktype (server-filtered) and matches volgnummer client-side.
|
|
existing_statustype_url() {
|
|
oz GET "/catalogi/api/v1/statustypen?zaaktype=$1" | python3 -c '
|
|
import json, sys
|
|
data = json.load(sys.stdin)
|
|
vol = int(sys.argv[1])
|
|
for r in data["results"]:
|
|
if r["volgnummer"] == vol:
|
|
print(r["url"])
|
|
break
|
|
' "$2"
|
|
}
|
|
|
|
echo "Waiting for OpenZaak..."
|
|
until curl -sS -o /dev/null -w '%{http_code}' "$BASE/catalogi/api/v1/catalogussen" | grep -q '^2\|^401\|^403'; do
|
|
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
|
|
echo " exists: $catalogus_url"
|
|
else
|
|
catalogus=$(oz POST /catalogi/api/v1/catalogussen "$(printf '{"domein":"BIGR","rsin":"%s","contactpersoonBeheerNaam":"BIG Register"}' "$RSIN")")
|
|
catalogus_url=$(echo "$catalogus" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
|
echo " created: $catalogus_url"
|
|
fi
|
|
|
|
echo "Zaaktype (concept)..."
|
|
zaaktype_url=$(existing_url "/catalogi/api/v1/zaaktypen?catalogus=$catalogus_url&identificatie=ZT-HERREG")
|
|
if [ -n "$zaaktype_url" ]; then
|
|
echo " exists: $zaaktype_url"
|
|
else
|
|
zaaktype=$(oz POST /catalogi/api/v1/zaaktypen "$(python3 -c '
|
|
import json, sys
|
|
print(json.dumps({
|
|
"identificatie": "ZT-HERREG",
|
|
"omschrijving": "Herregistratie arts",
|
|
"vertrouwelijkheidaanduiding": "openbaar",
|
|
"doel": "Herregistratie in het BIG-register",
|
|
"aanleiding": "Aanvraag door de zorgverlener",
|
|
"indicatieInternOfExtern": "extern",
|
|
"handelingInitiator": "indienen",
|
|
"onderwerp": "Herregistratie",
|
|
"handelingBehandelaar": "behandelen",
|
|
"doorlooptijd": "P30D",
|
|
"opschortingEnAanhoudingMogelijk": False,
|
|
"verlengingMogelijk": False,
|
|
"publicatieIndicatie": False,
|
|
"productenOfDiensten": ["https://example.com/producten/herregistratie"],
|
|
"referentieproces": {"naam": "Herregistratie"},
|
|
"verantwoordelijke": "CIBG",
|
|
"catalogus": sys.argv[1],
|
|
"beginGeldigheid": "2026-01-01",
|
|
"versiedatum": "2026-01-01",
|
|
"besluittypen": [],
|
|
"gerelateerdeZaaktypen": [],
|
|
# Must belong to the same procestype as the resultaattype selectielijstklasse below
|
|
# (OpenZaak cross-checks this against the public VNG selectielijst API).
|
|
"selectielijstProcestype": "https://selectielijst.openzaak.nl/api/v1/procestypen/e1b73b12-b2f6-4c4e-8929-94f84dd2a57d",
|
|
}))
|
|
' "$catalogus_url")")
|
|
zaaktype_url=$(echo "$zaaktype" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
|
echo " created: $zaaktype_url"
|
|
fi
|
|
|
|
echo "Granting zrc scopes (zaken.aanmaken, zaken.bijwerken, zaken.lezen, zaken.statussen.toevoegen), scoped to $zaaktype_url — the one zaaktype this harness (and the BFF's Zgw:ZaaktypeUrls config) ever uses. zaken.statussen.toevoegen is needed for the besluit write: zaken.aanmaken only covers the ONE status set at zaak creation, a later status (the besluit's eindstatus) needs this scope or OpenZaak 403s ('mag je slechts 1 status zetten')..."
|
|
grant_scopes zrc '["zaken.aanmaken", "zaken.bijwerken", "zaken.lezen", "zaken.statussen.toevoegen"]' \
|
|
"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
|
|
echo " exists (Ontvangen): $statustype_url"
|
|
else
|
|
statustype_url=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Ontvangen","volgnummer":1}' "$zaaktype_url")" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
|
echo " created (Ontvangen): $statustype_url"
|
|
fi
|
|
statustype_eind_url=$(existing_statustype_url "$zaaktype_url" 2)
|
|
if [ -n "$statustype_eind_url" ]; then
|
|
echo " exists (Afgehandeld): $statustype_eind_url"
|
|
else
|
|
statustype_eind_url=$(oz POST /catalogi/api/v1/statustypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","volgnummer":2}' "$zaaktype_url")" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
|
echo " created (Afgehandeld): $statustype_eind_url"
|
|
fi
|
|
|
|
echo "Resultaattype (publish needs at least one; existence-only requirement)..."
|
|
if [ "$(oz GET "/catalogi/api/v1/resultaattypen?zaaktype=$zaaktype_url" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["results"]))')" != "0" ]; then
|
|
echo " exists"
|
|
else
|
|
# The two URLs below are real reference-list entries on the public VNG selectielijst API
|
|
# (selectielijst.openzaak.nl) — OpenZaak validates both by fetching them, same as it does
|
|
# for a zaaktype URL, so a made-up URL 404s here.
|
|
oz POST /catalogi/api/v1/resultaattypen "$(printf '{"zaaktype":"%s","omschrijving":"Afgehandeld","resultaattypeomschrijving":"https://selectielijst.openzaak.nl/api/v1/resultaattypeomschrijvingen/7cb315fb-4f7b-4a43-aca1-e4522e4c73b3","selectielijstklasse":"https://selectielijst.openzaak.nl/api/v1/resultaten/cc5ae4e3-a9e6-4386-bcee-46be4986a829","archiefnominatie":"blijvend_bewaren"}' "$zaaktype_url")" >/dev/null
|
|
echo " created"
|
|
fi
|
|
|
|
echo "Roltype (initiator)..."
|
|
roltype_url=$(existing_url "/catalogi/api/v1/roltypen?zaaktype=$zaaktype_url&omschrijvingGeneriek=initiator")
|
|
if [ -n "$roltype_url" ]; then
|
|
echo " exists: $roltype_url"
|
|
else
|
|
roltype_url=$(oz POST /catalogi/api/v1/roltypen "$(printf '{"zaaktype":"%s","omschrijving":"Initiator","omschrijvingGeneriek":"initiator"}' "$zaaktype_url")" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
|
echo " created: $roltype_url"
|
|
fi
|
|
|
|
echo "Publishing zaaktype..."
|
|
zaaktype_uuid=$(echo "$zaaktype_url" | sed 's#.*/##')
|
|
zaaktype_concept=$(oz GET "/catalogi/api/v1/zaaktypen/$zaaktype_uuid" | python3 -c 'import json,sys; print(json.load(sys.stdin)["concept"])')
|
|
if [ "$zaaktype_concept" = "False" ]; then
|
|
echo " already published"
|
|
else
|
|
oz POST "/catalogi/api/v1/zaaktypen/$zaaktype_uuid/publish" >/dev/null
|
|
echo " published"
|
|
fi
|
|
|
|
echo "Zaak..."
|
|
zaak_url=$(existing_url "/zaken/api/v1/zaken?identificatie=$ZAAK_REF")
|
|
if [ -n "$zaak_url" ]; then
|
|
echo " exists: $zaak_url"
|
|
else
|
|
zaak=$(oz POST /zaken/api/v1/zaken "$(python3 -c '
|
|
import json, sys
|
|
print(json.dumps({
|
|
"zaaktype": sys.argv[1],
|
|
"bronorganisatie": sys.argv[2],
|
|
"verantwoordelijkeOrganisatie": sys.argv[2],
|
|
"startdatum": "2026-07-28",
|
|
"identificatie": sys.argv[3],
|
|
}))
|
|
' "$zaaktype_url" "$RSIN" "$ZAAK_REF")")
|
|
zaak_url=$(echo "$zaak" | python3 -c 'import json,sys; print(json.load(sys.stdin)["url"])')
|
|
echo " created: $zaak_url"
|
|
fi
|
|
|
|
echo "Status..."
|
|
if [ "$(oz GET "/zaken/api/v1/statussen?zaak=$zaak_url" | python3 -c 'import json,sys; print(len(json.load(sys.stdin)["results"]))')" != "0" ]; then
|
|
echo " exists"
|
|
else
|
|
oz POST /zaken/api/v1/statussen "$(python3 -c '
|
|
import json, sys
|
|
print(json.dumps({"zaak": sys.argv[1], "statustype": sys.argv[2], "datumStatusGezet": "2026-07-28T12:00:00Z"}))
|
|
' "$zaak_url" "$statustype_url")" >/dev/null
|
|
echo " created"
|
|
fi
|
|
|
|
echo "Rol (initiator, seeded BSN)..."
|
|
if [ -n "$(existing_url "/zaken/api/v1/rollen?zaak=$zaak_url&omschrijvingGeneriek=initiator")" ]; then
|
|
echo " exists"
|
|
else
|
|
oz POST /zaken/api/v1/rollen "$(python3 -c '
|
|
import json, sys
|
|
print(json.dumps({
|
|
"zaak": sys.argv[1],
|
|
"betrokkeneType": "natuurlijk_persoon",
|
|
"roltype": sys.argv[2],
|
|
"roltoelichting": "Initiator",
|
|
"betrokkeneIdentificatie": {"inpBsn": sys.argv[3]},
|
|
}))
|
|
' "$zaak_url" "$roltype_url" "$BSN")" >/dev/null
|
|
echo " created"
|
|
fi
|
|
|
|
cat > seeded.env <<EOF
|
|
ZAAK_REFERENTIE=$ZAAK_REF
|
|
ZAAK_URL=$zaak_url
|
|
ZAAKTYPE_LABEL=Herregistratie arts
|
|
CALLER_BSN=$BSN
|
|
EOF
|
|
|
|
echo
|
|
echo "Seed complete. $ZAAK_REF ($zaak_url) — see seeded.env"
|