diff --git a/docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md b/docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md new file mode 100644 index 0000000..15a296f --- /dev/null +++ b/docs/architecture/adr-0021-acl-resolves-zaaktype-by-identificatie.md @@ -0,0 +1,67 @@ +# ADR-0021: The ACL resolves its zaaktype by identificatie, not a pinned URL + +- **Status:** Accepted +- **Date:** 2026-07-22 +- **Deciders:** Respellion engineering +- **Relates to:** S-27 (#113), proposed in #117. The cleaner design deliberately split out of S-B04 + (#110, ADR-0020), which fixed the local stack with an infra-only bootstrap. + +## Context + +The ACL was handed a **pinned zaaktype URL** (`Acl__Defaults__ZaaktypeUrl`) and diploma +informatieobjecttype URL. OpenZaak assigns those UUIDs at creation, so the URL is not knowable when +the compose file is written — every stack had to seed the catalogus and then capture + inject the +resulting URLs out of band: `run-domain-check.sh` for CI, and the `local-seed` → `acl.env` bootstrap +(ADR-0020) for `make local`. Brittle, and a stale/placeholder URL failed opaquely (OpenZaak 400). + +## Decision + +**The ACL resolves its zaaktype (by `identificatie`) and diploma informatieobjecttype (by +`omschrijving`) from OpenZaak's Catalogi API, instead of being handed the URLs.** + +- **Config:** `AclDefaults.ZaaktypeUrl`/`InformatieobjecttypeUrl` → `ZaaktypeIdentificatie` + (`BIG-REGISTRATIE`) / `InformatieobjecttypeOmschrijving` (`Diploma`). +- **Lookup (gateway, §8.1):** `GET /catalogi/api/v1/zaaktypen?status=definitief&identificatie=…` → + the published zaaktype URL; `GET /catalogi/api/v1/informatieobjecttypen?status=definitief` matched + on `omschrijving`. Reuses the gateway's existing catalogus-query machinery. +- **Timing = lazy + cached (`CachedZaaktypeCatalog`).** Resolve on first use (first zaak open / + document store) and cache for the process lifetime. Lazy avoids a startup ordering coupling — the + ACL never crash-loops when it boots before the catalogus is published. A **failed** resolution is + not cached, so it is retried on the next call (e.g. once the zaaktype is published); a restart + re-resolves. +- **Failure mode:** no published match → a clear "No published zaaktype with identificatie '…' found + in OpenZaak — is the BIG catalogus seeded and published?" error, replacing the opaque placeholder + 400. + +## Consequences + +**Positive** + +- No stack captures or injects a server-assigned URL any more: `run-domain-check.sh` drops the + `ACL_ZAAKTYPE_URL`/`ACL_INFORMATIEOBJECTTYPE_URL` capture+inject, `docker-compose.yml`/`.local.yml` + drop the placeholder URL env, and `local-seed`/`acl.env` shrink to a single line. The ACL + self-configures from the catalogus it already talks to. +- The failure mode is legible (a named error instead of a 400 on a zeros-UUID). + +**Negative / costs** + +- The ACL still needs its OpenZaak **BaseUrl** pointed at a **URL-valid host (a container IP)**, so + the base-URL injection from ADR-0020 stays (the local `acl.env` now carries only that; CI keeps + `ACL_OPENZAAK_BASEURL`). This is **not** something S-27 can remove: OpenZaak validates the + `zaaktype` field on zaak-create with Django's URLValidator and **rejects a single-label host** + (`http://openzaak:8000/…` → `zaaktype: bad-url, "Voer een geldige URL in."`, confirmed empirically). + So ADR-0020's `seed-env` volume + ACL entrypoint shim are **simplified, not deleted**. +- New branching in the gateway/resolver → unit + integration test surface; the mutation ratchet + covers it (§5). +- A seed step still **creates + publishes** the zaaktype (this ADR changes only discovery). Reaching + OpenZaak's Catalogi API to *seed* likewise needs the IP host (its query params hit the same + URLValidator) — unchanged from before. + +## Alternatives considered + +- **Resolve at startup** (eager). Simpler cache, but reintroduces the ordering coupling (crash-loop + if the catalogus isn't published yet). Rejected in favour of lazy. +- **Per-request resolution** (no cache). No stale-cache risk, but a Catalogi lookup on every ACL + operation. Rejected; a process-lifetime cache with restart-to-refresh is enough here. +- **Keep the pinned URL** (status quo / ADR-0020 only). Rejected — the brittleness this ADR removes is + exactly what S-27 was carved out to fix. diff --git a/docs/demo-script.md b/docs/demo-script.md index 9191f05..c94a4e0 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -26,9 +26,31 @@ make verify-local # → "OK — a fresh local stack completed the flow with # test123); it shows as INGESCHREVEN in the openbaar register at http://localhost:8141. ``` -> The zaaktype UUID is server-assigned, so `local-seed` writes the real URL into a shared volume as -> `acl.env` and the ACL sources it on startup (ADR-0020). The cleaner long-term fix — the ACL -> resolving its zaaktype by `identificatie` — is tracked separately as S-27 (#113). +> The zaaktype is discovered by the ACL itself since S-27 (below); `local-seed`'s `acl.env` now +> carries only OpenZaak's IP base URL, which the ACL still needs because OpenZaak rejects a +> single-label host on zaak-create (ADR-0020 + ADR-0021). + +--- + +## S-27 — ACL resolves its zaaktype by identificatie, not a pinned URL (#113, ADR-0021) + +**Outcome:** the ACL discovers its BIG zaaktype (by `identificatie`) and diploma informatieobjecttype +(by `omschrijving`) from OpenZaak's Catalogi API, instead of being handed the server-assigned URLs. +No user-visible behaviour change — the flow runs exactly as before — but no stack captures/injects a +zaaktype URL any more, and a missing catalogus now fails with a clear message instead of an opaque 400. + +```bash +# The live ACL↔OpenZaak integration test proves resolution against a real seeded OpenZaak: +make verify-acl # → "resolves the published BIG-REGISTRATIE zaaktype + Diploma informatieobjecttype by business key" + +# End-to-end unchanged (the ACL self-discovers the zaaktype during the flow): +make verify-local # local stack — still green, now with no zaaktype-URL injection +make verify-domain # CI stack — recreates the ACL pointed only at OpenZaak's IP (no URL to inject) +``` + +> The ACL still needs its OpenZaak base URL at a URL-valid host (a container IP): OpenZaak's +> URLValidator rejects a single-label host like `openzaak:8000` on zaak-create. So ADR-0020's base-URL +> injection stays; only the zaaktype/informatieobjecttype **URL** injection is gone (ADR-0021). --- diff --git a/infra/docker-compose.local.yml b/infra/docker-compose.local.yml index beafbb8..0bb7aad 100644 --- a/infra/docker-compose.local.yml +++ b/infra/docker-compose.local.yml @@ -315,21 +315,22 @@ services: context: ../services/acl dockerfile: Dockerfile image: register-referentie/acl:dev - # The base/zaaktype/informatieobjecttype below are PLACEHOLDERS. The real, server-assigned - # values are written by the local-seed one-shot into seed-env:/seed/acl.env, which the entrypoint - # sources (set -a) so they override these before the app starts (S-B04, #110, ADR-0020). Sourcing - # a runtime-generated env file is why we override the entrypoint here rather than use `env_file:` - # (which compose reads at parse time, before the seed has run). + # The ACL discovers its zaaktype + informatieobjecttype URLs from the Catalogi API by the business + # keys below (S-27, ADR-0021), so no URL is injected. It still needs its OpenZaak BaseUrl pointed at + # a URL-valid host (OpenZaak rejects a single-label host like `openzaak` on zaak-create), so the + # local-seed one-shot writes that IP base into seed-env:/seed/acl.env, which the entrypoint sources + # (set -a) before the app starts. A runtime-generated env file is why we override the entrypoint here + # rather than use `env_file:` (which compose reads at parse time, before the seed has run). entrypoint: ["/bin/sh", "-c", "set -a; . /seed/acl.env; set +a; exec dotnet Acl.Api.dll"] environment: - Acl__OpenZaak__BaseUrl: http://openzaak:8000/ + Acl__OpenZaak__BaseUrl: http://openzaak:8000/ # placeholder; seed-env/acl.env supplies the IP base Acl__OpenZaak__ClientId: big-reference-seed Acl__OpenZaak__Secret: insecure-dev-secret-change-me Acl__Defaults__Bronorganisatie: "517439943" Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943" Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar - Acl__Defaults__ZaaktypeUrl: http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000 - Acl__Defaults__InformatieobjecttypeUrl: http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000 + Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE + Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma ports: - "8100:8080" volumes: diff --git a/infra/docker-compose.yml b/infra/docker-compose.yml index ce76cdf..99f18fe 100644 --- a/infra/docker-compose.yml +++ b/infra/docker-compose.yml @@ -15,12 +15,12 @@ # # docker compose -f infra/docker-compose.yml up -d --build --wait # -# After first boot, seed the BIG catalogus and note the zaaktype URL: -# python infra/openzaak/seed_catalogus.py -# Then set ACL_ZAAKTYPE_URL in a .env file or your shell and re-up the acl -# service: -# export ACL_ZAAKTYPE_URL=http://openzaak:8000/catalogi/api/v1/zaaktypen/ -# docker compose -f infra/docker-compose.yml up -d acl +# After first boot, seed + publish the BIG catalogus: +# OZ_PUBLISH=1 python infra/openzaak/seed_catalogus.py +# The ACL discovers the zaaktype by identificatie (S-27, ADR-0021), so there is no URL to inject — +# just point its BaseUrl at an OpenZaak host OpenZaak accepts on zaak-create (a container IP; a +# single-label host is rejected): +# ACL_OPENZAAK_BASEURL=http://:8000/ docker compose -f infra/docker-compose.yml up -d acl services: @@ -304,11 +304,12 @@ services: Acl__Defaults__Bronorganisatie: "517439943" Acl__Defaults__VerantwoordelijkeOrganisatie: "517439943" Acl__Defaults__Vertrouwelijkheidaanduiding: openbaar - # Override with the real zaaktype URL after running seed_catalogus.py. - Acl__Defaults__ZaaktypeUrl: ${ACL_ZAAKTYPE_URL:-http://openzaak:8000/catalogi/api/v1/zaaktypen/00000000-0000-0000-0000-000000000000} - # The informatieobjecttype a diploma is filed under (S-10b). Placeholder until seed_catalogus.py - # (OZ_PUBLISH=1) reports the real URL, which verify-domain injects like the zaaktype URL. - Acl__Defaults__InformatieobjecttypeUrl: ${ACL_INFORMATIEOBJECTTYPE_URL:-http://openzaak:8000/catalogi/api/v1/informatieobjecttypen/00000000-0000-0000-0000-000000000000} + # The ACL resolves the (server-assigned) zaaktype + diploma informatieobjecttype URLs from the + # Catalogi API by these stable business keys (S-27, ADR-0021) — no URL to capture and inject. + # BaseUrl above stays overridable because OpenZaak rejects a single-label host on zaak creation, + # so verify-domain still points the ACL at OpenZaak's container IP. + Acl__Defaults__ZaaktypeIdentificatie: BIG-REGISTRATIE + Acl__Defaults__InformatieobjecttypeOmschrijving: Diploma ports: - "8100:8080" healthcheck: diff --git a/infra/local/seed-zaaktype.sh b/infra/local/seed-zaaktype.sh index 5773ad4..80906ed 100755 --- a/infra/local/seed-zaaktype.sh +++ b/infra/local/seed-zaaktype.sh @@ -21,15 +21,13 @@ echo ">> seeding + publishing the BIG zaaktype at ${OZ_BASE} (idempotent)" out="$(python3 /work/seed_catalogus.py)" echo "$out" -zt="$(printf '%s\n' "$out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)" -iot="$(printf '%s\n' "$out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)" -[ -n "$zt" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; } -[ -n "$iot" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; } +# Sanity-check that the zaaktype was actually published (the ACL discovers it by identificatie, S-27). +printf '%s\n' "$out" | grep -q '^ZAAKTYPE_URL ' || { echo "ERROR: seed did not publish the zaaktype" >&2; exit 1; } -# The ACL entrypoint sources this; these keys override the placeholder defaults in the compose file. +# The ACL resolves the zaaktype/informatieobjecttype URLs itself (S-27, ADR-0021); the only value it +# still needs injected is the OpenZaak base URL at a URL-valid host (the container IP), because OpenZaak +# rejects a single-label host on zaak-create. The ACL entrypoint sources this. cat > /out/acl.env <> wrote /out/acl.env (base=${OZ_BASE}/ zaaktype=${zt})" +echo ">> wrote /out/acl.env (base=${OZ_BASE}/)" diff --git a/infra/openzaak/__pycache__/seed_catalogus.cpython-314.pyc b/infra/openzaak/__pycache__/seed_catalogus.cpython-314.pyc new file mode 100644 index 0000000..8f2c9ea Binary files /dev/null and b/infra/openzaak/__pycache__/seed_catalogus.cpython-314.pyc differ diff --git a/infra/run-domain-check.sh b/infra/run-domain-check.sh index 3c360d6..833032a 100755 --- a/infra/run-domain-check.sh +++ b/infra/run-domain-check.sh @@ -30,21 +30,18 @@ oz_ip="$(ip "$oz")"; dom_ip="$(ip "$dom")" oz_base="http://$oz_ip:8000" echo ">> openzaak=$oz_ip domain=$dom_ip network=$net" -echo ">> seeding a published BIG zaaktype (idempotent) and capturing its URL" +echo ">> seeding + publishing a BIG zaaktype (idempotent)" sid="$(docker create --network "$net" -e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 python:3-slim python /seed.py)" docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null seed_out="$(docker start -a "$sid")" -zt_url="$(printf '%s\n' "$seed_out" | sed -n 's/^ZAAKTYPE_URL //p' | head -1)" -iot_url="$(printf '%s\n' "$seed_out" | sed -n 's/^INFORMATIEOBJECTTYPE_URL //p' | head -1)" docker rm -f "$sid" >/dev/null -[ -n "$zt_url" ] || { echo "ERROR: seed did not report a ZAAKTYPE_URL" >&2; exit 1; } -[ -n "$iot_url" ] || { echo "ERROR: seed did not report an INFORMATIEOBJECTTYPE_URL" >&2; exit 1; } -echo ">> zaaktype: $zt_url" -echo ">> informatieobjecttype: $iot_url" +printf '%s\n' "$seed_out" | grep -q '^ZAAKTYPE_URL ' || { echo "ERROR: seed did not publish the zaaktype" >&2; exit 1; } -echo ">> recreating the acl service pointed at the seeded zaaktype + informatieobjecttype (host-consistent)" -ACL_ZAAKTYPE_URL="$zt_url" ACL_INFORMATIEOBJECTTYPE_URL="$iot_url" ACL_OPENZAAK_BASEURL="$oz_base/" \ - docker compose -f "$compose" up -d acl +# The ACL resolves the zaaktype + informatieobjecttype by identificatie/omschrijving (S-27, ADR-0021), +# so there is no URL to inject — only the OpenZaak base URL, pointed at the same host's container IP +# (OpenZaak rejects a single-label host on zaak-create). +echo ">> recreating the acl service pointed at OpenZaak's IP (it resolves the zaaktype itself, S-27)" +ACL_OPENZAAK_BASEURL="$oz_base/" docker compose -f "$compose" up -d acl WAIT_TIMEOUT="${WAIT_TIMEOUT:-120}" bash "$here/wait-healthy.sh" acl echo ">> submitting a registration to the domain"