Compare commits
1 Commits
7e152e4432
...
feat/2-cat
| Author | SHA1 | Date | |
|---|---|---|---|
| 5966571920 |
9
Makefile
9
Makefile
@@ -21,7 +21,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-down help
|
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down help
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
|
||||||
ci: lint build unit smoke
|
ci: lint build unit smoke
|
||||||
@@ -71,6 +71,13 @@ openzaak-smoke:
|
|||||||
echo "GET /zaken/api/v1/ -> $$root (expect 200)"; test "$$root" = "200"; \
|
echo "GET /zaken/api/v1/ -> $$root (expect 200)"; test "$$root" = "200"; \
|
||||||
echo "OpenZaak smoke OK"'
|
echo "OpenZaak smoke OK"'
|
||||||
|
|
||||||
|
## openzaak-seed: bring OpenZaak up and seed the BIG catalogus (idempotent)
|
||||||
|
openzaak-seed: openzaak-up
|
||||||
|
@bash -c 'for i in $$(seq 1 50); do \
|
||||||
|
c=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/catalogi/api/v1/ || true); \
|
||||||
|
[ "$$c" = "200" ] && break; sleep 3; done; echo "OpenZaak ready ($$c)"'
|
||||||
|
python3 infra/openzaak/seed_catalogus.py
|
||||||
|
|
||||||
## openzaak-down: stop and remove the OpenZaak stack (wipes data)
|
## openzaak-down: stop and remove the OpenZaak stack (wipes data)
|
||||||
openzaak-down:
|
openzaak-down:
|
||||||
docker compose -f $(OZ_COMPOSE) down --volumes
|
docker compose -f $(OZ_COMPOSE) down --volumes
|
||||||
|
|||||||
53
docs/architecture/adr-0002-catalogus-design.md
Normal file
53
docs/architecture/adr-0002-catalogus-design.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
# ADR-0002: BIG catalogus design and OpenZaak seeding
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-06-03
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Relates to:** S-01 (#2)
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
S-01 needs a reproducible `BIG` catalogus in OpenZaak with a **lean** `BIG-registratie`
|
||||||
|
zaaktype (only schema-mandatory fields) plus a `bsn` eigenschap, and a JWT client that
|
||||||
|
can list zaaktypen. We had to decide *how* to provision this idempotently at startup.
|
||||||
|
|
||||||
|
Findings from the OpenZaak image (`openzaak/open-zaak:latest`):
|
||||||
|
- `setup_configuration` (run by the init container) is declarative and idempotent, with
|
||||||
|
steps for **JWT secrets** and **applicaties** (`vng_api_common_credentials`,
|
||||||
|
`vng_api_common_applicaties`) — but **no step for catalogi/zaaktypen**.
|
||||||
|
- Catalogus/zaaktype/eigenschap can only be created through the **ZTC REST API**.
|
||||||
|
- Publishing a zaaktype requires ≥1 roltype, ≥1 resultaattype and ≥2 statustypen.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
1. **Provision the JWT client declaratively** via `infra/openzaak/setup_configuration/data.yaml`:
|
||||||
|
a `JWTSecret` (`big-reference-seed` / dev secret) and an `Applicatie` with
|
||||||
|
`heeft_alle_autorisaties: true`. Idempotent, runs in the init container.
|
||||||
|
2. **Seed the catalogus/zaaktype/eigenschap via the ZTC API** with an idempotent,
|
||||||
|
stdlib-only script (`infra/openzaak/seed_catalogus.py`, `make openzaak-seed`). It mints
|
||||||
|
a ZGW JWT from the provisioned client and matches existing objects (by `domein` /
|
||||||
|
`identificatie` / `naam`, querying `status=alles` so concepts are seen) before creating.
|
||||||
|
3. **Keep the zaaktype a CONCEPT (not published).** Publishing pulls in roltypen,
|
||||||
|
statustypen and resultaattypen, which go beyond "schema-mandatory"; those arrive with
|
||||||
|
the workflow/zaak slices that actually need a published type. Listing uses `status=alles`.
|
||||||
|
4. **Disable outbound notifications** (`NOTIFICATIONS_DISABLED=true`) until Open Notificaties
|
||||||
|
(NRC) lands in S-01-c — otherwise every ZTC write 500s trying to notify.
|
||||||
|
5. **Fixed dev values:** RSIN `517439943` (elfproef-valid test value); the JWT secret is
|
||||||
|
dev-only and documented as such.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- **Reproducible & version-robust:** the API-driven seed doesn't depend on fixture PKs or
|
||||||
|
a catalogi `setup_configuration` step that may change between versions.
|
||||||
|
- **Teaches the pattern:** the seed talks to OpenZaak exactly the way the ACL will later —
|
||||||
|
through the documented ZGW API, with a JWT (ADR-0001).
|
||||||
|
- The seed is a script, but a **data loader is explicitly anticipated** (PRD §8); it lives
|
||||||
|
under `infra/openzaak/`, not as ad-hoc tooling.
|
||||||
|
- **Follow-ups:** re-enable notifications when NRC is up (S-01-c); publish the zaaktype (add
|
||||||
|
the related types) when a slice needs to create real zaken; pin the OpenZaak image tag.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
- **Fully declarative in `data.yaml`** — rejected: no catalogi/zaaktype step exists.
|
||||||
|
- **Django `loaddata` fixture** — rejected: brittle, tied to model PKs and the exact image
|
||||||
|
version; bypasses the API the rest of the system uses.
|
||||||
@@ -9,9 +9,32 @@ migrations, the OpenZaak API, and a celery worker.
|
|||||||
```bash
|
```bash
|
||||||
make openzaak-up # start the stack (first run pulls images + migrates: 1-3 min)
|
make openzaak-up # start the stack (first run pulls images + migrates: 1-3 min)
|
||||||
make openzaak-smoke # start + assert it's up with auth enforced (403/302/200)
|
make openzaak-smoke # start + assert it's up with auth enforced (403/302/200)
|
||||||
|
make openzaak-seed # start + seed the BIG catalogus (idempotent)
|
||||||
make openzaak-down # stop and wipe data
|
make openzaak-down # stop and wipe data
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Seed the BIG catalogus
|
||||||
|
|
||||||
|
`make openzaak-seed` brings the stack up and runs `infra/openzaak/seed_catalogus.py`,
|
||||||
|
which creates (idempotently, via the ZTC API):
|
||||||
|
|
||||||
|
- catalogus **BIG**
|
||||||
|
- a lean **BIG-REGISTRATIE** zaaktype (concept; only schema-mandatory fields)
|
||||||
|
- a **bsn** eigenschap on it
|
||||||
|
|
||||||
|
then confirms the JWT client can list it. See **ADR-0002** for the design (why the
|
||||||
|
zaaktype stays a concept, why notifications are disabled, why the API not a fixture).
|
||||||
|
|
||||||
|
**JWT client** (provisioned declaratively by `setup_configuration/data.yaml`, **dev only**):
|
||||||
|
|
||||||
|
| | |
|
||||||
|
|---|---|
|
||||||
|
| client_id | `big-reference-seed` |
|
||||||
|
| secret | `insecure-dev-secret-change-me` |
|
||||||
|
| authorizations | `heeft_alle_autorisaties` (all) |
|
||||||
|
|
||||||
|
The seed mints a ZGW JWT (HS256) from these and calls `/catalogi/api/v1/...`.
|
||||||
|
|
||||||
`make openzaak-smoke` polls until the API responds, then asserts:
|
`make openzaak-smoke` polls until the API responds, then asserts:
|
||||||
|
|
||||||
| Check | Expected |
|
| Check | Expected |
|
||||||
@@ -43,8 +66,9 @@ The Makefile auto-points `DOCKER_HOST` at the Podman socket when it exists, so t
|
|||||||
- **Not in `make ci`.** The OpenZaak smoke is a separate, heavier check (large image
|
- **Not in `make ci`.** The OpenZaak smoke is a separate, heavier check (large image
|
||||||
pull + migrations); it is intentionally kept out of `make ci` so the core gate
|
pull + migrations); it is intentionally kept out of `make ci` so the core gate
|
||||||
stays fast. Run `make openzaak-smoke` when you touch the OpenZaak stack.
|
stays fast. Run `make openzaak-smoke` when you touch the OpenZaak stack.
|
||||||
- **No catalogus/zaaktypen yet.** Seeding the `BIG` catalogus + `BIG-registratie`
|
- **Notifications disabled.** `NOTIFICATIONS_DISABLED=true` until Open Notificaties
|
||||||
zaaktype and a JWT client is the next slice (**S-01-b**); authenticated zaaktype
|
(NRC) lands in S-01-c — otherwise ZTC writes 500 trying to notify. Re-enable then.
|
||||||
listing isn't testable until then.
|
- **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/
|
||||||
|
resultaattypen — beyond the lean seed). List with `?status=alles`.
|
||||||
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
|
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
|
||||||
a known-good tag as part of the catalogus-design ADR.
|
a known-good tag (ADR-0002 follow-up).
|
||||||
|
|||||||
@@ -44,11 +44,18 @@ services:
|
|||||||
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
||||||
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
||||||
DISABLE_2FA: "true"
|
DISABLE_2FA: "true"
|
||||||
|
# Notifications go to Open Notificaties (NRC), which arrives in S-01-c.
|
||||||
|
# Until then, disable outbound notifications so writes don't 500.
|
||||||
|
NOTIFICATIONS_DISABLED: "true"
|
||||||
OPENZAAK_SUPERUSER_USERNAME: admin
|
OPENZAAK_SUPERUSER_USERNAME: admin
|
||||||
DJANGO_SUPERUSER_PASSWORD: admin
|
DJANGO_SUPERUSER_PASSWORD: admin
|
||||||
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
||||||
RUN_SETUP_CONFIG: "false"
|
RUN_SETUP_CONFIG: "true"
|
||||||
command: /setup_configuration.sh
|
command: /setup_configuration.sh
|
||||||
|
volumes:
|
||||||
|
# :z relabels for SELinux; the dir/file must be world-readable for the
|
||||||
|
# container user (rootless Podman uid mapping). See docs/runbooks/openzaak.md.
|
||||||
|
- ./setup_configuration:/app/setup_configuration:ro,z
|
||||||
depends_on:
|
depends_on:
|
||||||
oz-db:
|
oz-db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
137
infra/openzaak/seed_catalogus.py
Normal file
137
infra/openzaak/seed_catalogus.py
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Idempotent seed of the BIG catalogus into OpenZaak via the ZTC API.
|
||||||
|
|
||||||
|
Creates (if absent):
|
||||||
|
- catalogus "BIG"
|
||||||
|
- a lean "BIG-registratie" zaaktype (only schema-mandatory fields)
|
||||||
|
- a "bsn" eigenschap on that zaaktype
|
||||||
|
- then publishes the zaaktype.
|
||||||
|
|
||||||
|
Auth uses the JWT client provisioned by setup_configuration (see ADR-0002).
|
||||||
|
Stdlib only — no pip deps. Re-running is safe (matches existing by identifier).
|
||||||
|
"""
|
||||||
|
import base64, hashlib, hmac, json, os, sys, time, urllib.error, urllib.request
|
||||||
|
|
||||||
|
BASE = os.environ.get("OZ_BASE", "http://localhost:8000")
|
||||||
|
CLIENT_ID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed")
|
||||||
|
SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
|
||||||
|
ZTC = f"{BASE}/catalogi/api/v1"
|
||||||
|
RSIN = "517439943" # elfproef-valid test RSIN
|
||||||
|
|
||||||
|
|
||||||
|
def token():
|
||||||
|
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
|
||||||
|
hdr = {"alg": "HS256", "typ": "JWT"}
|
||||||
|
pl = {"iss": CLIENT_ID, "iat": int(time.time()), "client_id": CLIENT_ID,
|
||||||
|
"user_id": "seed", "user_representation": "seed"}
|
||||||
|
seg = b64(json.dumps(hdr, separators=(",", ":")).encode()) + b"." + \
|
||||||
|
b64(json.dumps(pl, separators=(",", ":")).encode())
|
||||||
|
sig = b64(hmac.new(SECRET.encode(), seg, hashlib.sha256).digest())
|
||||||
|
return (seg + b"." + sig).decode()
|
||||||
|
|
||||||
|
|
||||||
|
def api(method, path, body=None):
|
||||||
|
url = path if path.startswith("http") else f"{ZTC}{path}"
|
||||||
|
data = json.dumps(body).encode() if body is not None else None
|
||||||
|
req = urllib.request.Request(url, data=data, method=method, headers={
|
||||||
|
"Authorization": "Bearer " + token(),
|
||||||
|
"Content-Type": "application/json",
|
||||||
|
"Accept": "application/json",
|
||||||
|
})
|
||||||
|
try:
|
||||||
|
with urllib.request.urlopen(req, timeout=30) as r:
|
||||||
|
return r.status, json.loads(r.read() or "null")
|
||||||
|
except urllib.error.HTTPError as e:
|
||||||
|
return e.code, json.loads(e.read() or "null")
|
||||||
|
|
||||||
|
|
||||||
|
def find(path):
|
||||||
|
status, body = api("GET", path)
|
||||||
|
if status != 200:
|
||||||
|
sys.exit(f"GET {path} -> {status}: {body}")
|
||||||
|
return body.get("results", [])
|
||||||
|
|
||||||
|
|
||||||
|
def main():
|
||||||
|
# 1. Catalogus
|
||||||
|
existing = [c for c in find(f"/catalogussen?domein=BIG") if c.get("domein") == "BIG"]
|
||||||
|
if existing:
|
||||||
|
cat = existing[0]
|
||||||
|
print(f"skip catalogus BIG ({cat['url']})")
|
||||||
|
else:
|
||||||
|
st, cat = api("POST", "/catalogussen", {
|
||||||
|
"domein": "BIG", "rsin": RSIN,
|
||||||
|
"contactpersoonBeheerNaam": "BIG Beheer",
|
||||||
|
})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create catalogus -> {st}: {cat}")
|
||||||
|
print(f"create catalogus BIG ({cat['url']})")
|
||||||
|
|
||||||
|
# 2. Zaaktype (concept)
|
||||||
|
# status=alles so concept zaaktypen are matched too (else we'd duplicate).
|
||||||
|
zts = [z for z in find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
||||||
|
if z.get("identificatie") == "BIG-REGISTRATIE"]
|
||||||
|
if zts:
|
||||||
|
zt = zts[0]
|
||||||
|
print(f"skip zaaktype BIG-REGISTRATIE ({zt['url']}) concept={zt.get('concept')}")
|
||||||
|
else:
|
||||||
|
st, zt = api("POST", "/zaaktypen", {
|
||||||
|
"identificatie": "BIG-REGISTRATIE",
|
||||||
|
"omschrijving": "BIG-registratie",
|
||||||
|
"vertrouwelijkheidaanduiding": "openbaar",
|
||||||
|
"doel": "Registratie van een zorgprofessional in het BIG-register",
|
||||||
|
"aanleiding": "Aanvraag tot registratie",
|
||||||
|
"indicatieInternOfExtern": "extern",
|
||||||
|
"handelingInitiator": "indienen",
|
||||||
|
"onderwerp": "BIG-registratie",
|
||||||
|
"handelingBehandelaar": "behandelen",
|
||||||
|
"doorlooptijd": "P30D",
|
||||||
|
"opschortingEnAanhoudingMogelijk": False,
|
||||||
|
"verlengingMogelijk": False,
|
||||||
|
"publicatieIndicatie": False,
|
||||||
|
"productenOfDiensten": [],
|
||||||
|
"referentieproces": {"naam": "BIG-registratie"},
|
||||||
|
"catalogus": cat["url"],
|
||||||
|
"besluittypen": [],
|
||||||
|
"gerelateerdeZaaktypen": [],
|
||||||
|
"beginGeldigheid": "2026-01-01",
|
||||||
|
"versiedatum": "2026-01-01",
|
||||||
|
"verantwoordelijke": RSIN,
|
||||||
|
})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create zaaktype -> {st}: {json.dumps(zt, indent=2)}")
|
||||||
|
print(f"create zaaktype BIG-REGISTRATIE ({zt['url']})")
|
||||||
|
|
||||||
|
# 3. bsn eigenschap (only addable while concept)
|
||||||
|
eigs = [e for e in find(f"/eigenschappen?zaaktype={zt['url']}&status=alles")
|
||||||
|
if e.get("naam") == "bsn"]
|
||||||
|
if eigs:
|
||||||
|
print("skip eigenschap bsn")
|
||||||
|
elif zt.get("concept", True):
|
||||||
|
st, eig = api("POST", "/eigenschappen", {
|
||||||
|
"naam": "bsn",
|
||||||
|
"definitie": "Burgerservicenummer van de zorgprofessional",
|
||||||
|
"zaaktype": zt["url"],
|
||||||
|
"specificatie": {"groep": "aanvrager", "formaat": "tekst",
|
||||||
|
"lengte": "9", "kardinaliteit": "1", "waardenverzameling": []},
|
||||||
|
})
|
||||||
|
if st != 201:
|
||||||
|
sys.exit(f"create eigenschap -> {st}: {json.dumps(eig, indent=2)}")
|
||||||
|
print("create eigenschap bsn")
|
||||||
|
else:
|
||||||
|
print("warn zaaktype already published; cannot add bsn eigenschap")
|
||||||
|
|
||||||
|
# Intentionally NOT published. Publishing requires roltypen, resultaattypen
|
||||||
|
# and statustypen, which go beyond the "lean / schema-mandatory" zaaktype this
|
||||||
|
# slice asks for; they arrive with the workflow/zaak slices. See ADR-0002.
|
||||||
|
|
||||||
|
# 4. Verify the JWT client can list the zaaktype (concepts included).
|
||||||
|
zaaktypen = find(f"/zaaktypen?catalogus={cat['url']}&status=alles")
|
||||||
|
names = [z.get("identificatie") for z in zaaktypen]
|
||||||
|
print(f"zaaktypen in BIG: {names}")
|
||||||
|
assert "BIG-REGISTRATIE" in names, "BIG-REGISTRATIE not listed"
|
||||||
|
print("OK — BIG catalogus seeded (BIG-REGISTRATIE concept + bsn eigenschap)")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
22
infra/openzaak/setup_configuration/data.yaml
Normal file
22
infra/openzaak/setup_configuration/data.yaml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
# OpenZaak setup_configuration (idempotent, declarative).
|
||||||
|
# Provisions the JWT client the seed + ACL use to call OpenZaak's APIs.
|
||||||
|
# Dev-only credentials — not for production.
|
||||||
|
#
|
||||||
|
# Steps come from vng_api_common.contrib.setup_configuration (see ADR-0002).
|
||||||
|
|
||||||
|
vng_api_common_credentials_config_enable: true
|
||||||
|
vng_api_common_credentials:
|
||||||
|
items:
|
||||||
|
- identifier: big-reference-seed
|
||||||
|
secret: insecure-dev-secret-change-me
|
||||||
|
|
||||||
|
vng_api_common_applicaties_config_enable: true
|
||||||
|
vng_api_common_applicaties:
|
||||||
|
items:
|
||||||
|
# uuid must be given explicitly as a string (the step's auto-default is a
|
||||||
|
# UUID object that fails its own validation).
|
||||||
|
- uuid: "11111111-1111-4111-8111-111111111111"
|
||||||
|
client_ids:
|
||||||
|
- big-reference-seed
|
||||||
|
label: BIG reference seed client
|
||||||
|
heeft_alle_autorisaties: true
|
||||||
Reference in New Issue
Block a user