Merge pull request 'feat(infra): wire OpenZaak → Open Notificaties notifications (closes #56)' (#57) from feat/56-nrc-notification-wiring into main
All checks were successful
CI / lint (push) Successful in 52s
CI / build (push) Successful in 40s
CI / unit (push) Successful in 48s
CI / mutation (push) Successful in 1m35s
CI / verify-stack (push) Successful in 4m44s

Reviewed-on: #57
This commit was merged in pull request #57.
This commit is contained in:
2026-06-30 12:29:43 +00:00
20 changed files with 643 additions and 130 deletions

View File

@@ -63,31 +63,27 @@ jobs:
path: services/acl/StrykerOutput/**/reports/mutation-report.html path: services/acl/StrykerOutput/**/reports/mutation-report.html
if-no-files-found: warn if-no-files-found: warn
integration: # One stage for every check that needs the live stack. On the single self-hosted
# runner jobs run sequentially, so booting OpenZaak once (instead of once per job)
# is the cheapest layout (issue #58). No setup-dotnet: the ACL test runs in a built
# image and everything reaches services by container IP. Needs Docker + egress
# (base images, nuget, selectielijst.openzaak.nl).
verify-stack:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: https://github.com/actions/checkout@v4 - uses: https://github.com/actions/checkout@v4
# No setup-dotnet: `make integration` runs the seed and the test as containers # Bring the full stack up + wait for health — this also is the DoD "compose up
# *inside* the OpenZaak compose network (reaching it by container IP), so dotnet # reaches green health" smoke (it replaces the old compose-smoke job).
# lives in the test image and the runner needs only Docker. This sidesteps the - name: Bring up the full stack & wait for health
# runner being unable to reach published ports (gitea-actions-gotchas.md §5). run: make verify-up
# Needs egress to pull base images + nuget + selectielijst.openzaak.nl. - name: ACL ↔ OpenZaak integration tests
- run: make integration run: make verify-acl
- name: dump OpenZaak logs on failure - name: OpenZaak → NRC notification delivery
run: make verify-nrc
# Log dump must precede teardown (which removes the containers).
- name: Dump container logs on failure
if: failure() if: failure()
run: docker compose -f infra/openzaak/docker-compose.yml logs --no-color --tail=80 oz-init openzaak 2>&1 || true run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-init keycloak acl bff 2>&1 || true
- name: tear down on failure - name: Tear down
if: failure() if: always()
run: docker compose -f infra/openzaak/docker-compose.yml down --volumes 2>&1 || true run: make down
compose-smoke:
runs-on: ubuntu-latest
steps:
- uses: https://github.com/actions/checkout@v4
- run: make smoke
- name: dump container logs on failure
if: failure()
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=80 oz-init openzaak nrc-init nrc-web flowable-init keycloak acl bff 2>&1 || true
- name: tear down on failure
if: failure()
run: docker compose -f infra/docker-compose.yml down --volumes 2>&1 || true

View File

@@ -18,7 +18,7 @@ WAIT_SVCS := openzaak nrc-web acl bff
# volumes are `external`, so compose won't remove them — CFG_VOLS lists them for # volumes are `external`, so compose won't remove them — CFG_VOLS lists them for
# explicit teardown. See docs/runbooks/gitea-actions-gotchas.md. # explicit teardown. See docs/runbooks/gitea-actions-gotchas.md.
SEED := bash infra/seed-config.sh SEED := bash infra/seed-config.sh
CFG_VOLS := rr-oz-config rr-kc-realms rr-fl-bpmn CFG_VOLS := rr-oz-config rr-nrc-config rr-kc-realms rr-fl-bpmn
# Local-only stack: same services but config is bind-mounted (no seed step), so a # Local-only stack: same services but config is bind-mounted (no seed step), so a
# plain `docker compose -f infra/docker-compose.local.yml up` works on any local # plain `docker compose -f infra/docker-compose.local.yml up` works on any local
# engine. This is the no-make / Windows-friendly path. See that file's header. # engine. This is the no-make / Windows-friendly path. See that file's header.
@@ -43,10 +43,11 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
endif endif
endif endif
.PHONY: ci lint build unit mutation integration smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help .PHONY: ci lint build unit mutation integration verify verify-up verify-acl verify-nrc verify-notifications smoke up down local local-down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
## ci: run the full pipeline — lint, build, unit, mutation, smoke (mirrors Gitea Actions) ## ci: run the full pipeline — lint, build, unit, mutation, verify (mirrors Gitea Actions)
ci: lint build unit mutation smoke ## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
ci: lint build unit mutation verify
## lint: verify formatting (no changes) ## lint: verify formatting (no changes)
lint: lint:
@@ -77,14 +78,14 @@ mutation:
# podman-compose, and needing no `--wait` flag or host port access. The one-shots # podman-compose, and needing no `--wait` flag or host port access. The one-shots
# (oz-init, flowable-init) aren't polled; they just need to have run. # (oz-init, flowable-init) aren't polled; they just need to have run.
smoke: smoke:
$(SEED) oz kc fl $(SEED) oz nrc kc fl
docker compose -f $(COMPOSE) up -d --build docker compose -f $(COMPOSE) up -d --build
bash -c 'WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS); rc=$$?; docker compose -f $(COMPOSE) down --volumes; docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; exit $$rc' bash -c 'WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS); rc=$$?; docker compose -f $(COMPOSE) down --volumes; docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; exit $$rc'
## up: seed config volumes and start the full stack (use instead of bare ## up: seed config volumes and start the full stack (use instead of bare
## `docker compose up`, which can't self-seed the external config volumes) ## `docker compose up`, which can't self-seed the external config volumes)
up: up:
$(SEED) oz kc fl $(SEED) oz nrc kc fl
docker compose -f $(COMPOSE) up -d --build docker compose -f $(COMPOSE) up -d --build
## down: stop and remove the local stack (incl. the external config volumes) ## down: stop and remove the local stack (incl. the external config volumes)
@@ -106,12 +107,45 @@ local-down:
changelog: changelog:
git-cliff --output CHANGELOG.md git-cliff --output CHANGELOG.md
## integration: ACL integration tests against a real OpenZaak (S-04a, #46). The # ── ZGW verification ───────────────────────────────────────────────────────
## seed and the test run inside the compose network (reaching http://openzaak:8000), # On the single runner CI jobs run sequentially, so the OpenZaak-dependent checks
## so this works on the hosted CI runner where a runner process can't reach the # share ONE full-stack bring-up: the `verify-stack` CI job runs `verify-up` then
## stack's published ports. Brings the stack up, seeds a PUBLISHED BIG zaaktype, # `verify-acl` + `verify-nrc` as steps against the same stack (issue #58). The
## runs the Integration-category tests, then always tears down. Kept out of # check logic lives in stack-agnostic runners that reach services by container IP
## `unit`/`mutation` because it needs the live stack. See infra/run-integration.sh + ADR-0006. # (gitea-actions-gotchas.md §5/§6); `integration` / `verify-notifications` are local
# convenience wrappers that bring up a lighter stack and call the same runners.
## verify-up: bring the FULL stack up and wait for health (CI verify-stack step 1;
## subsumes the old compose-smoke health gate — the DoD "up reaches green" check).
verify-up:
$(SEED) oz nrc kc fl
docker compose -f $(COMPOSE) up -d --build
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS)
## verify-acl: ACL ↔ OpenZaak integration tests against the already-running stack.
verify-acl:
bash infra/run-acl-integration.sh
## verify-nrc: OpenZaak → NRC notification delivery against the already-running stack.
verify-nrc:
bash infra/run-notification-check.sh
## verify: local mirror of the CI verify-stack job — full stack up once, both checks,
## tear down (always). For fast single-concern local iteration use `integration`
## (oz-only) or `verify-notifications` (oz+nrc) instead.
verify:
$(SEED) oz nrc kc fl
docker compose -f $(COMPOSE) up -d --build
@bash -c 'set -e; rc=0; \
WAIT_TIMEOUT=420 bash infra/wait-healthy.sh $(WAIT_SVCS) \
&& bash infra/run-acl-integration.sh \
&& bash infra/run-notification-check.sh || rc=$$?; \
docker compose -f $(COMPOSE) down --volumes >/dev/null 2>&1; \
docker volume rm -f $(CFG_VOLS) >/dev/null 2>&1; \
exit $$rc'
## integration: local convenience — ACL integration test against a throwaway
## OpenZaak-only stack (fast iteration). CI uses verify-acl on the shared stack.
integration: integration:
bash infra/run-integration.sh bash infra/run-integration.sh
@@ -147,10 +181,16 @@ openzaak-down:
docker compose -f $(OZ_COMPOSE) down --volumes docker compose -f $(OZ_COMPOSE) down --volumes
-docker volume rm -f rr-oz-config -docker volume rm -f rr-oz-config
## stack-up: start OpenZaak + Open Notificaties together (shared network) ## verify-notifications: local convenience — OpenZaak → NRC notification delivery
## against a throwaway oz+nrc stack (S-01-c). CI uses verify-nrc on the shared stack.
verify-notifications:
bash infra/verify-notifications.sh
## stack-up: start OpenZaak + Open Notificaties together (shared network), with
## OpenZaak publishing notifications to NRC (S-01-c).
stack-up: stack-up:
$(SEED) oz $(SEED) oz nrc
docker compose $(STACK_FILES) up -d OZ_NOTIFICATIONS_DISABLED=false docker compose $(STACK_FILES) up -d
## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable ## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable
stack-smoke: stack-up stack-smoke: stack-up
@@ -169,7 +209,7 @@ stack-smoke: stack-up
## stack-down: stop and remove both stacks (wipes data) ## stack-down: stop and remove both stacks (wipes data)
stack-down: stack-down:
docker compose $(STACK_FILES) down --volumes docker compose $(STACK_FILES) down --volumes
-docker volume rm -f rr-oz-config -docker volume rm -f rr-oz-config rr-nrc-config
## keycloak-up: start Keycloak with the four imported realms ## keycloak-up: start Keycloak with the four imported realms
keycloak-up: keycloak-up:

View File

@@ -46,10 +46,11 @@ itself. No new test dependency is added.**
`OpenZaakGateway` against it. `OpenZaakGateway` against it.
- **The lane is kept out of the fast checks.** `make unit` runs with - **The lane is kept out of the fast checks.** `make unit` runs with
`--filter "Category!=Integration"`; Stryker is pinned to `Acl.Tests` (`test-projects`), so `--filter "Category!=Integration"`; Stryker is pinned to `Acl.Tests` (`test-projects`), so
neither the unit nor the mutation lane needs a live stack. A new `make integration` target neither the unit nor the mutation lane needs a live stack. A `make integration` target
(`infra/run-integration.sh`) brings the stack up, seeds, runs the lane, and always tears down (`infra/run-integration.sh`) brings up a throwaway OpenZaak and runs the lane locally.
— mirrored by a Gitea Actions `integration` job. This matches `make` being the single source In CI the check runs as the `verify-acl` step of the consolidated `verify-stack` job
of truth (ADR-0005). (issue #58) — one shared full-stack bring-up. This matches `make` being the single
source of truth (ADR-0005).
- **Publishing is opt-in in the seed.** `infra/openzaak/seed_catalogus.py` gains an - **Publishing is opt-in in the seed.** `infra/openzaak/seed_catalogus.py` gains an
`OZ_PUBLISH=1` path that adds the relations OpenZaak's publish requires — two statustypen `OZ_PUBLISH=1` path that adds the relations OpenZaak's publish requires — two statustypen
(begin/eind), a roltype, and a resultaattype whose Selectielijst procestype is matched onto (begin/eind), a roltype, and a resultaattype whose Selectielijst procestype is matched onto

View File

@@ -0,0 +1,77 @@
# ADR-0007: Wiring OpenZaak → Open Notificaties (NRC) for notifications
- **Status:** Accepted
- **Date:** 2026-06-29
- **Deciders:** Respellion engineering
- **Relates to:** S-01-c (#56); completes S-01 (#2); unblocks the Event Subscriber (#7); builds on ADR-0002 (catalogus/seed) and ADR-0006 (runner-safe container harnesses)
## Context
S-01 brought OpenZaak + Open Notificaties (NRC) up in compose but **deferred the
notification wiring**: OpenZaak ran with `NOTIFICATIONS_DISABLED=true` and NRC's
`setup_configuration` was empty. The walking skeleton (PRD §12) needs the upstream
event path — a zaak created in OpenZaak must publish a notification NRC fans out to
subscribers — before the Event Subscriber (#7) can consume it.
The OpenZaak↔NRC handshake is intricate and several details are non-obvious; they
were nailed down by iterating `setup_configuration` against the running stack.
## Decision
**Provision both sides declaratively via `setup_configuration`, authenticate with the
existing `big-reference-seed` client, and run NRC's celery-beat so deliveries happen.**
- **OpenZaak** (`infra/openzaak/setup_configuration/data.yaml`): a `zgw_consumers`
service `nrc` (api_type `nrc`, the NRC API root) plus `notifications_config` naming
it. `NOTIFICATIONS_DISABLED` is flipped to `false` **only when NRC is present**
the full stack and the local twin set it; OpenZaak-only bring-ups (`openzaak-up`,
the ACL integration test) default it back to `true` via `OZ_NOTIFICATIONS_DISABLED`
so they don't 500 publishing to an absent NRC.
- **NRC** (`infra/opennotificaties/setup_configuration/data.yaml`): the
`big-reference-seed` JWT credential (to verify OpenZaak's token), a `zgw_consumers`
`ac` service pointing at **OpenZaak's Autorisaties API**, the `autorisaties_api`
step delegating authorization to that AC, and the `zaken` kanaal. NRC's init
container switches from `migrate` to `/setup_configuration.sh`; its data.yaml is
delivered through the `rr-nrc-config` external volume by `infra/seed-config.sh`
(the same `docker cp` pattern as OpenZaak — bind mounts don't reach the CI runner's
daemon).
- **celery-beat is required.** NRC accepts a notification and writes a
`ScheduledNotification`; a periodic `execute_notifications` task (celery-beat,
every `NOTIFICATION_SEC_INTERVAL`s) drains it to the worker for delivery. The lean
S-01 stack dropped beat — so notifications were accepted but never delivered. An
`nrc-beat` service is added to every compose; the interval is lowered to 5s.
Verification is a runner-safe smoke (`infra/run-notification-check.sh`): it seeds a
published BIG zaaktype, registers an abonnement to a webhook sink, creates a zaak, and
asserts the sink receives the `zaken`/`create` notification — all from containers
**inside** the compose network (ADR-0006). Locally it runs via `make verify-notifications`
(a throwaway oz+nrc stack); in CI it runs as the `verify-nrc` step of the consolidated
`verify-stack` job (one shared full-stack bring-up — issue #58).
## Consequences
- **Positive:** the walking-skeleton event path works end to end; #7 can consume real
notifications; the wiring is declarative and reproducible from a fresh `make`.
- **Gotchas captured (see gitea-actions-gotchas.md):**
- **Single-label hosts aren't URL-valid.** OpenZaak/NRC reject `http://openzaak…`
/`http://nrc-web…` in URLs they validate (Django `URLValidator`); the verify
harness reaches services and registers the sink callback **by container IP**.
- **Abonnement callbacks must enforce auth.** NRC probes the callback during
registration and refuses it (`no-auth-on-callback-url`) unless it returns 401
without the configured `Authorization`; the sink enforces a bearer token.
- **Cost:** an extra long-running service (`nrc-beat`) per stack, and the verify job
needs egress (base images + `selectielijst.openzaak.nl`, since the published
zaaktype the check creates a zaak against depends on it — ADR-0006).
- **Dev-only credentials** reused (`big-reference-seed` / its secret) across publish,
AC lookup, and seeding — acceptable for the reference app, not production.
## Alternatives considered
- **NRC with its own (non-AC) authorization** — rejected: delegating to OpenZaak's
Autorisaties API is the upstream-intended model and reuses the applicatie that
already grants `heeft_alle_autorisaties`.
- **Keep beat out, deliver synchronously** — not an option: Open Notificaties 1.16
delivers via scheduled notifications drained by beat; there is no sync path.
- **A persistent abonnement in `setup_configuration`** instead of registering one in
the verify harness — deferred: the real subscriber is #7; the harness's sink
abonnement is throwaway and IP-specific.

View File

@@ -17,19 +17,21 @@ and CI cannot drift:
| `build` | `make build``dotnet build … -c Release` | .NET 10 SDK | | `build` | `make build``dotnet build … -c Release` | .NET 10 SDK |
| `unit` | `make unit``dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK | | `unit` | `make unit``dotnet test … -c Release --filter "Category!=Integration"` | .NET 10 SDK |
| `mutation` | `make mutation``dotnet tool restore``dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK | | `mutation` | `make mutation``dotnet tool restore``dotnet stryker` (ACL); uploads the HTML report as an artifact | .NET 10 SDK |
| `integration` | `make integration``infra/run-integration.sh`: OpenZaak up → seed a **published** BIG zaaktype + run `Acl.IntegrationTests` **as containers inside the compose network** → tear down | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) | | `verify-stack` | the single live-stack stage — steps: `make verify-up` (full stack up + health, the DoD smoke) → `make verify-acl` (ACL ↔ OpenZaak) → `make verify-nrc` (OpenZaak → NRC delivery) → `make down` | container engine + egress (base images, nuget, `selectielijst.openzaak.nl`) |
| `compose-smoke` | `make smoke` → seed config volumes → `up -d` (full stack) → `up --wait` durable services → `down` | container engine + compose v2 |
> **The `integration` job needs no `setup-dotnet`.** dotnet runs inside the test > **Why one `verify-stack` job, not three.** The single self-hosted runner runs jobs
> image, and both the seed and the test join the OpenZaak network and reach it by > **sequentially**, so booting OpenZaak once (instead of once per check) is the
> container IP — so the runner never has to reach a published port > cheapest layout (issue #58). It subsumes the old `integration`, `notifications`, and
> (see [gitea-actions-gotchas.md §5](gitea-actions-gotchas.md)). > `compose-smoke` jobs — the bring-up step *is* the "compose up reaches green health"
> gate. No `setup-dotnet`: the ACL test runs in a built image and every check reaches
> services by **container IP** (the runner can't reach published ports — see
> [gitea-actions-gotchas.md §5/§6](gitea-actions-gotchas.md)).
All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`, All `uses:` references are absolute, tag-pinned URLs (`https://github.com/actions/checkout@v4`,
`https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea `https://github.com/actions/setup-dotnet@v4`) per CLAUDE.md §8.7 and §15 — Gitea
Actions resolves them from GitHub. Actions resolves them from GitHub.
> **`compose-smoke` runs on a containerized runner.** Workspace bind mounts do > **`verify-stack` runs on a containerized runner.** Workspace bind mounts do
> **not** reach the sibling containers Compose starts, so config/assets are > **not** reach the sibling containers Compose starts, so config/assets are
> streamed into external named volumes via `docker cp` (`infra/seed-config.sh`), > streamed into external named volumes via `docker cp` (`infra/seed-config.sh`),
> and the upstream images are used verbatim (no build). If you add a service that > and the upstream images are used verbatim (no build). If you add a service that
@@ -91,19 +93,23 @@ the containerized CI runner). Keep the two files in sync.
## Running CI locally (`make ci`) ## Running CI locally (`make ci`)
Until the runner exists, run the full pipeline yourself before pushing: `make ci` runs the exact same checks as the pipeline — handy to run before pushing:
```bash ```bash
make ci # lint + build + unit + mutation + smoke — the fast pipeline lanes make ci # lint + build + unit + mutation + verify — mirrors the pipeline
make lint # or a single stage make lint # or a single stage
make mutation # Stryker.NET ratchet on the ACL make mutation # Stryker.NET ratchet on the ACL
make smoke # compose up --wait, curl /health, tear down make verify # the live-stack stage: full stack up once → ACL + NRC checks → down
make integration # ACL ↔ real OpenZaak (its own CI job; not part of `make ci`)
``` ```
> `make integration` is a separate, heavier lane (it stands the OpenZaak stack up and > **`make verify`** mirrors the CI `verify-stack` job: it boots the full stack once and
> seeds a published zaaktype), so it is **not** folded into `make ci`. Run it before > runs both the ACL ↔ OpenZaak and OpenZaak → NRC checks against it. For fast,
> pushing changes that touch the ACL gateway or the OpenZaak seed. See ADR-0006. > single-concern local iteration use a lighter throwaway stack instead:
>
> ```bash
> make integration # ACL ↔ OpenZaak only (no NRC)
> make verify-notifications # OpenZaak → NRC delivery only
> ```
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`. **Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.

View File

@@ -152,8 +152,30 @@ service by name (`http://openzaak:8000`). For a test/seed that needs the repo's
code, deliver it via a **built image** (not a bind mount — §1), then code, deliver it via a **built image** (not a bind mount — §1), then
`docker run --network <stack>_cg …`. `docker run --network <stack>_cg …`.
**Applied**`make integration` (the ACL ↔ real-OpenZaak test, ADR-0006) does **Applied**`make integration` (ADR-0006) and `make verify-notifications` (ADR-0007)
exactly this: `infra/run-integration.sh` runs the seed and the test as containers on do exactly this: they run the seed/test/driver as containers on the stack network and
the OpenZaak network and reaches it by **container IP** (a single-label service name reach services by **container IP** (see §6).
like `openzaak` isn't URL-valid — OpenZaak echoes the request host into the URLs it
returns and then rejects them with Django's `URLValidator`; an IPv4 literal passes). ---
## 6. OpenZaak / NRC reject single-label hosts in URLs
**Symptom** — talking to OpenZaak or NRC by compose **service name** fails where a URL
is validated: catalogus/zaaktype filters, the zaak `zaaktype` URL, and abonnement
`callbackUrl` come back `400 "Voer een geldige URL in."` — even though the host
resolves and is reachable.
**Why** — these apps validate URLs with Django's `URLValidator`, which rejects a
**single-label** host like `openzaak` or `nrc-web` (no dot, and not `localhost`).
`localhost` passes (so it's invisible in host-port-based local runs); in-network the
reality is a service name or an IPv4 literal — and only the IP passes.
**Fix** — in-network tooling reaches OpenZaak/NRC by **container IP**
(`docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}'`), not
service name; the notif verify harness also registers the sink callback by IP.
(`infra/run-acl-integration.sh`, `infra/run-notification-check.sh`.)
**Related — abonnement callbacks must enforce auth.** NRC probes a callback when an
abonnement is registered and refuses it (`no-auth-on-callback-url`) unless it returns
**401** without the configured `Authorization`. The verify sink
(`infra/notification-sink.py`) enforces a bearer token for exactly this reason.

View File

@@ -59,7 +59,8 @@ 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_DISABLED: "true" # Publish notifications to NRC (always present in this twin). See ADR-0007.
NOTIFICATIONS_DISABLED: "false"
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
@@ -122,7 +123,9 @@ services:
networks: [cg] networks: [cg]
nrc-init: nrc-init:
# Migrations only (see the canonical compose / ADR-0002); no config needed. # Migrations + setup_configuration (S-01-c): the JWT credential, Autorisaties-API
# delegation, and the `zaken` kanaal that let OpenZaak publish. Config is
# bind-mounted here (this twin is the local/no-make path). See ADR-0007.
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1} image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
environment: &nrc-env environment: &nrc-env
DJANGO_SETTINGS_MODULE: nrc.conf.docker DJANGO_SETTINGS_MODULE: nrc.conf.docker
@@ -141,7 +144,11 @@ services:
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_PASSWORD: admin DJANGO_SUPERUSER_PASSWORD: admin
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
command: ["sh", "-c", "/wait_for_db.sh && OTEL_SDK_DISABLED=True python src/manage.py migrate"] RUN_SETUP_CONFIG: "true"
NOTIFICATION_SEC_INTERVAL: "5"
command: /setup_configuration.sh
volumes:
- ./opennotificaties/setup_configuration:/app/setup_configuration:ro,z
depends_on: depends_on:
nrc-db: nrc-db:
condition: service_healthy condition: service_healthy
@@ -176,6 +183,17 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
networks: [cg] networks: [cg]
# Celery beat drains scheduled notifications to subscribers — required for
# delivery, not optional. See ADR-0007.
nrc-beat:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
environment: *nrc-env
command: /celery_beat.sh
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
# ── Keycloak (S-02) ────────────────────────────────────────────────────── # ── Keycloak (S-02) ──────────────────────────────────────────────────────
keycloak: keycloak:
image: quay.io/keycloak/keycloak:26.1 image: quay.io/keycloak/keycloak:26.1

View File

@@ -62,7 +62,10 @@ 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_DISABLED: "true" # Publish notifications to NRC (always present in this full stack). The NRC
# service + notifications_config are provisioned by setup_configuration
# (infra/openzaak/setup_configuration/data.yaml). See ADR-0007 / S-01-c.
NOTIFICATIONS_DISABLED: "false"
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
@@ -146,11 +149,17 @@ services:
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_PASSWORD: admin DJANGO_SUPERUSER_PASSWORD: admin
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
# Migrations only for now. No setup_configuration steps are enabled yet (the RUN_SETUP_CONFIG: "true"
# OZ<->NRC notification wiring lands in S-06), and NRC's `setup_configuration` # nrc-beat fires `execute_notifications` this often to drain scheduled
# aborts with "No steps enabled" on the empty data.yaml — so we run `migrate` # notifications to subscribers (upstream default 20s). See ADR-0007.
# directly instead of /setup_configuration.sh. See data.yaml and ADR-0002. NOTIFICATION_SEC_INTERVAL: "5"
command: ["sh", "-c", "/wait_for_db.sh && OTEL_SDK_DISABLED=True python src/manage.py migrate"] # Runs migrations + setup_configuration (S-01-c): the JWT credential, the
# Autorisaties-API delegation, and the `zaken` kanaal that let OpenZaak publish.
# data.yaml is streamed into rr-nrc-config by infra/seed-config.sh (bind mounts
# don't reach sibling containers on the CI runner). See data.yaml + ADR-0007.
command: /setup_configuration.sh
volumes:
- nrc-config:/app/setup_configuration:ro
depends_on: depends_on:
nrc-db: nrc-db:
condition: service_healthy condition: service_healthy
@@ -185,6 +194,18 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
networks: [cg] networks: [cg]
# Celery beat drains the ScheduledNotification rows the API creates on publish
# and hands them to the worker. Without it, notifications are accepted but never
# delivered to subscribers — required, not optional. See ADR-0007.
nrc-beat:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
environment: *nrc-env
command: /celery_beat.sh
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
# ── Keycloak (S-02) ────────────────────────────────────────────────────── # ── Keycloak (S-02) ──────────────────────────────────────────────────────
keycloak: keycloak:
image: quay.io/keycloak/keycloak:26.1 image: quay.io/keycloak/keycloak:26.1
@@ -309,6 +330,9 @@ volumes:
oz-config: oz-config:
external: true external: true
name: rr-oz-config name: rr-oz-config
nrc-config:
external: true
name: rr-nrc-config
kc-realms: kc-realms:
external: true external: true
name: rr-kc-realms name: rr-kc-realms

39
infra/notification-sink.py Executable file
View File

@@ -0,0 +1,39 @@
#!/usr/bin/env python3
"""A throwaway webhook sink for verifying the OpenZaak → NRC notification path.
NRC delivers abonnement callbacks here as POSTs; each body is printed to stdout
(prefixed `NOTIFICATION `) so the verify harness can assert on `docker logs`.
NRC refuses to register an abonnement whose callback is unauthenticated
(`no-auth-on-callback`): when validating it sends a probe and expects the callback
to reject a request without the configured `Authorization` value. So this sink
enforces that header (EXPECTED_AUTH env) — 401 without it, 204 with it.
Stdlib only. Listens on :9000. See infra/verify-notifications.sh / S-01-c (#56).
"""
import http.server
import os
import sys
EXPECTED_AUTH = os.environ.get("EXPECTED_AUTH", "Bearer notification-sink-token")
class Handler(http.server.BaseHTTPRequestHandler):
def do_POST(self):
length = int(self.headers.get("content-length", 0))
body = self.rfile.read(length).decode("utf-8", "replace")
if self.headers.get("Authorization") != EXPECTED_AUTH:
self.send_response(401)
self.end_headers()
return
print("NOTIFICATION " + body, flush=True)
self.send_response(204)
self.end_headers()
def log_message(self, *args): # silence default request logging
pass
if __name__ == "__main__":
http.server.HTTPServer(("0.0.0.0", 9000), Handler).serve_forever()
sys.exit(0)

View File

@@ -52,11 +52,18 @@ services:
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_PASSWORD: admin DJANGO_SUPERUSER_PASSWORD: admin
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
# Migrations only for now. No setup_configuration steps are enabled yet (the RUN_SETUP_CONFIG: "true"
# OZ<->NRC notification wiring lands in S-06), and NRC's `setup_configuration` # Delivery cadence: nrc-beat fires `execute_notifications` this often to drain
# aborts with "No steps enabled" on the empty data.yaml — so we run `migrate` # scheduled notifications to subscribers. Upstream default is 20s; 5s keeps the
# directly instead of /setup_configuration.sh. See data.yaml and ADR-0002. # walking-skeleton + the verify smoke responsive.
command: ["sh", "-c", "/wait_for_db.sh && OTEL_SDK_DISABLED=True python src/manage.py migrate"] NOTIFICATION_SEC_INTERVAL: "5"
# Runs migrations + setup_configuration (S-01-c): the JWT credential, the
# Autorisaties-API delegation, and the `zaken` kanaal that let OpenZaak publish
# notifications. data.yaml is streamed into this external volume by
# infra/seed-config.sh (same pattern as oz-init). See data.yaml + ADR-0006.
command: /setup_configuration.sh
volumes:
- nrc-config:/app/setup_configuration:ro
depends_on: depends_on:
nrc-db: nrc-db:
condition: service_healthy condition: service_healthy
@@ -89,8 +96,25 @@ services:
condition: service_completed_successfully condition: service_completed_successfully
networks: [cg] networks: [cg]
# Celery beat: periodically fires `execute_notifications`, which drains the
# ScheduledNotification rows the API creates on publish and hands them to the
# worker for delivery. Without beat, notifications are accepted but never
# delivered to subscribers — so it is required, not optional. See ADR-0007.
nrc-beat:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-1.16.1}
environment: *nrc-env
command: /celery_beat.sh
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
volumes: volumes:
nrc-db: nrc-db:
# populated out-of-band by infra/seed-config.sh (docker cp) — see that script.
nrc-config:
external: true
name: rr-nrc-config
networks: networks:
cg: cg:

View File

@@ -1,5 +1,41 @@
# Open Notificaties setup_configuration. # Open Notificaties (NRC) setup_configuration (S-01-c, #56).
# Stage 1 (this commit): intentionally minimal — the init container runs # Wires NRC so OpenZaak can publish notifications:
# migrations; no steps enabled yet. The OpenZaak<->NRC notification wiring # - the JWT credential OpenZaak authenticates with,
# (Services, Authorization, JWT, Kanalen) is added next. See ADR-0002 / S-01-c. # - delegation of authorization checks to OpenZaak's Autorisaties API (AC),
{} # - the `zaken` kanaal OpenZaak publishes zaak events on.
# Dev-only credentials — not for production. Steps from nrc.setup_configuration.
# 1. JWT credential NRC uses to verify the token OpenZaak presents.
vng_api_common_credentials_config_enable: true
vng_api_common_credentials:
items:
- identifier: big-reference-seed
secret: insecure-dev-secret-change-me
# 2. The Autorisaties API (OpenZaak's AC) NRC consults to authorize publishers.
zgw_consumers_config_enable: true
zgw_consumers:
services:
- identifier: openzaak-ac
label: OpenZaak Autorisaties API
api_type: ac
api_root: http://openzaak:8000/autorisaties/api/v1/
auth_type: zgw
client_id: big-reference-seed
secret: insecure-dev-secret-change-me
# 3. Delegate authorization to that AC.
autorisaties_api_config_enable: true
autorisaties_api:
authorizations_api_service_identifier: openzaak-ac
# 4. The kanaal OpenZaak publishes zaak events on.
notifications_kanalen_config_enable: true
notifications_kanalen_config:
items:
- naam: zaken
documentatie_link: https://github.com/VNG-Realisatie/gemma-zaken
filters:
- bronorganisatie
- zaaktype
- vertrouwelijkheidaanduiding

View File

@@ -47,9 +47,12 @@ 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. # Notifications are OFF by default so OpenZaak-only bring-ups (openzaak-up,
# Until then, disable outbound notifications so writes don't 500. # the ACL integration test) don't 500 trying to reach an absent NRC. When
NOTIFICATIONS_DISABLED: "true" # OpenZaak runs together with the NRC stack, set OZ_NOTIFICATIONS_DISABLED=false
# (make stack-up does) to publish; the NRC service + notifications_config that
# name it are provisioned by setup_configuration (data.yaml, S-01-c).
NOTIFICATIONS_DISABLED: "${OZ_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

View File

@@ -20,3 +20,22 @@ vng_api_common_applicaties:
- big-reference-seed - big-reference-seed
label: BIG reference seed client label: BIG reference seed client
heeft_alle_autorisaties: true heeft_alle_autorisaties: true
# ── OpenZaak → Open Notificaties (NRC) publishing (S-01-c, #56) ─────────────
# The NRC service OpenZaak posts notifications to, authenticating with the same
# big-reference-seed client (NRC verifies the JWT and authorizes it via the AC).
zgw_consumers_config_enable: true
zgw_consumers:
services:
- identifier: nrc
label: Open Notificaties
api_type: nrc
api_root: http://nrc-web:8000/api/v1/
auth_type: zgw
client_id: big-reference-seed
secret: insecure-dev-secret-change-me
# Point OpenZaak's notifications at that service. Requires NOTIFICATIONS_DISABLED=false.
notifications_config_enable: true
notifications_config:
notifications_api_service_identifier: nrc

37
infra/run-acl-integration.sh Executable file
View File

@@ -0,0 +1,37 @@
#!/usr/bin/env bash
#
# Run the ACL integration tests (Category=Integration) against the OpenZaak that is
# ALREADY running — works for any stack: oz-only (`make integration`), the standalone
# oz+nrc stack, or the full compose stack (the CI `verify-stack` job). Seeds a
# published BIG zaaktype (idempotent), then builds + runs the test image on the stack
# network, reaching OpenZaak by container IP (a single-label host isn't URL-valid;
# the runner can't reach published ports — see gitea-actions-gotchas.md §5/§6).
#
# Does NOT manage the stack lifecycle: the caller owns bring-up + teardown. Plain
# docker primitives only (docker/podman-portable). See ADR-0006.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/.." && pwd)"
# The OpenZaak API container, matched across compose projects + docker/podman naming
# (`<project>[-_]openzaak[-_]<n>`); the delimiters exclude oz-db / oz-redis / oz-init.
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
[ -n "$oz" ] || { echo "ERROR: no running OpenZaak container found — bring the stack up first" >&2; exit 1; }
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
oz_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$oz")"
oz_base="http://$oz_ip:8000"
echo ">> OpenZaak at $oz_base on network $net"
echo ">> seeding a published 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
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> building the integration test image"
docker build -f "$root/services/acl/Dockerfile.integration" -t rr-acl-integration "$root/services/acl"
echo ">> running the ACL integration tests (inside the network)"
docker run --rm --network "$net" -e "OZ_BASE=$oz_base" rr-acl-integration

View File

@@ -1,21 +1,14 @@
#!/usr/bin/env bash #!/usr/bin/env bash
# #
# Run the ACL ↔ real-OpenZaak integration test (S-04a / #46) end to end. # Local convenience: run the ACL integration test against a throwaway OpenZaak-only
# stack (fast iteration on the ACL gateway). Brings OpenZaak up, runs the shared
# stack-agnostic check (infra/run-acl-integration.sh), then always tears down.
# #
# Everything that talks to OpenZaak runs *inside* the compose network and reaches # CI does not use this — there the full stack is brought up once and the same runner
# it by service name (http://openzaak:8000) — the hosted CI runner can't reach the # is invoked as a step (see the `verify-stack` job / Makefile `verify-*`). See ADR-0006.
# stack's published ports (sibling containers) and bind mounts don't reach the
# daemon either (gitea-actions-gotchas.md §1/§5). So we use only plain docker
# primitives (run / create / cp / build) — portable across docker compose (CI) and
# podman-compose (local), exactly like infra/seed-config.sh. See ADR-0006.
#
# Steps: bring OpenZaak up → wait for it healthy → seed a PUBLISHED BIG zaaktype
# (a seed container on the network) → build + run the test container on the network
# → always tear down.
set -euo pipefail set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
root="$(cd "$here/.." && pwd)"
OZ_COMPOSE="$here/openzaak/docker-compose.yml" OZ_COMPOSE="$here/openzaak/docker-compose.yml"
cleanup() { cleanup() {
@@ -29,40 +22,13 @@ bash "$here/seed-config.sh" oz
docker compose -f "$OZ_COMPOSE" up -d docker compose -f "$OZ_COMPOSE" up -d
echo ">> waiting for the OpenZaak API container to be healthy" echo ">> waiting for the OpenZaak API container to be healthy"
# Match the API container under both docker compose (openzaak-openzaak-1) and
# podman-compose (openzaak_openzaak_1) naming; the regex excludes oz-db / oz-redis.
api=""
for _ in $(seq 1 140); do for _ in $(seq 1 140); do
api="$(docker ps -q --filter 'name=openzaak[-_]openzaak' | head -1)" oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
if [ -n "$api" ]; then if [ -n "$oz" ] && [ "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$oz" 2>/dev/null || true)" = healthy ]; then
status="$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$api" 2>/dev/null || true)" break
[ "$status" = "healthy" ] && break
fi fi
sleep 3 sleep 3
done done
[ -n "$api" ] || { echo "ERROR: OpenZaak API container never appeared" >&2; exit 1; } [ -n "${oz:-}" ] || { echo "ERROR: OpenZaak never came up" >&2; exit 1; }
[ "${status:-}" = "healthy" ] || { echo "ERROR: OpenZaak not healthy (status=${status:-none})" >&2; exit 1; }
# The network the API container is attached to — joined by the seed + test below. bash "$here/run-acl-integration.sh"
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$api" | head -1)"
# Reach OpenZaak by container IP, not by the service name. OpenZaak echoes its
# request Host into the self-referential URLs it returns, then validates those URLs
# with Django's URLValidator — which rejects a single-label host like `openzaak`
# ("Voer een geldige URL in") while accepting an IPv4 literal (and `localhost`).
oz_ip="$(docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$api")"
oz_base="http://${oz_ip}:8000"
echo ">> OpenZaak healthy on network $net at $oz_base"
echo ">> seeding a published BIG zaaktype (OZ_PUBLISH=1, inside the network)"
sid="$(docker create --network "$net" \
-e "OZ_BASE=$oz_base" -e OZ_PUBLISH=1 \
python:3-slim python /seed_catalogus.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed_catalogus.py"
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> building the integration test image"
docker build -f "$root/services/acl/Dockerfile.integration" -t rr-acl-integration "$root/services/acl"
echo ">> running the integration tests (inside the network)"
docker run --rm --network "$net" -e "OZ_BASE=$oz_base" rr-acl-integration

72
infra/run-notification-check.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
#
# Verify the OpenZaak → NRC notification path against an ALREADY-RUNNING oz+nrc stack
# (the standalone stack via `make verify-notifications`, or the full compose stack in
# the CI `verify-stack` job). Seeds a published BIG zaaktype (idempotent), registers
# an abonnement to a webhook sink, creates a zaak, and asserts the sink receives the
# `zaken`/`create` notification. All in-network, reaching services by container IP
# (single-label hosts aren't URL-valid; the runner can't reach published ports).
#
# Does NOT manage the stack lifecycle (the caller owns bring-up + teardown), but it
# cleans up the throwaway sink/driver it creates. Plain docker primitives only.
# See ADR-0007.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
SINK_AUTH="Bearer notification-sink-token"
cleanup() { docker rm -f rr-nsink rr-nverify >/dev/null 2>&1 || true; }
trap cleanup EXIT
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
oz="$(docker ps -q --filter 'name=[-_]openzaak[-_]' | head -1)"
nrc="$(docker ps -q --filter 'name=nrc-web' | head -1)"
[ -n "$oz" ] && [ -n "$nrc" ] || { echo "ERROR: OpenZaak and/or NRC not running — bring the stack up first" >&2; exit 1; }
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$oz" | head -1)"
oz_ip="$(ip "$oz")"; nrc_ip="$(ip "$nrc")"
echo ">> network=$net openzaak=$oz_ip nrc=$nrc_ip"
echo ">> seeding a published BIG zaaktype (idempotent)"
sid="$(docker create --network "$net" -e "OZ_BASE=http://$oz_ip:8000" -e OZ_PUBLISH=1 \
python:3-slim python /seed.py)"
docker cp "$here/openzaak/seed_catalogus.py" "$sid:/seed.py" >/dev/null
docker start -a "$sid"
docker rm -f "$sid" >/dev/null
echo ">> starting the webhook sink"
docker rm -f rr-nsink >/dev/null 2>&1 || true
sink="$(docker create --network "$net" --name rr-nsink -e "EXPECTED_AUTH=$SINK_AUTH" \
python:3-slim python /sink.py)"
docker cp "$here/notification-sink.py" "$sink:/sink.py" >/dev/null
docker start "$sink" >/dev/null
sleep 1
sink_ip="$(ip rr-nsink)"
echo ">> sink at $sink_ip:9000"
echo ">> registering abonnement + creating a zaak"
docker rm -f rr-nverify >/dev/null 2>&1 || true
drv="$(docker create --network "$net" --name rr-nverify \
-e "OZ_BASE=http://$oz_ip:8000" -e "NRC_BASE=http://$nrc_ip:8000" \
-e "SINK_CALLBACK=http://$sink_ip:9000/" -e "SINK_AUTH=$SINK_AUTH" \
python:3-slim python /driver.py)"
docker cp "$here/verify-notification-driver.py" "$drv:/driver.py" >/dev/null
docker start -a "$drv"
zaak_url="$(docker logs rr-nverify 2>/dev/null | sed -n 's/^ZAAK_CREATED //p' | head -1)"
docker rm -f rr-nverify >/dev/null
[ -n "$zaak_url" ] || { echo "ERROR: driver did not create a zaak" >&2; exit 1; }
zaak_uuid="${zaak_url##*/}"
echo ">> zaak created: $zaak_url"
echo ">> waiting for the notification to reach the sink"
for _ in $(seq 1 30); do
if docker logs rr-nsink 2>&1 | grep -q "$zaak_uuid"; then
echo "OK — NRC delivered the zaken notification for zaak $zaak_uuid to the sink"
docker logs rr-nsink 2>&1 | grep "$zaak_uuid" | tail -1 | cut -c1-300
exit 0
fi
sleep 2
done
echo "FAIL — the sink never received a notification for zaak $zaak_uuid" >&2
echo "--- sink log ---" >&2; docker logs rr-nsink 2>&1 | tail -8 >&2
exit 1

View File

@@ -33,11 +33,12 @@ populate() { # volume source(file or dir/.)
echo " seeded $vol" echo " seeded $vol"
} }
[ "$#" -gt 0 ] || { echo "usage: seed-config.sh <oz|kc|fl> ..." >&2; exit 2; } [ "$#" -gt 0 ] || { echo "usage: seed-config.sh <oz|nrc|kc|fl> ..." >&2; exit 2; }
for key in "$@"; do for key in "$@"; do
case "$key" in case "$key" in
oz) populate rr-oz-config "$here/openzaak/setup_configuration/." ;; oz) populate rr-oz-config "$here/openzaak/setup_configuration/." ;;
nrc) populate rr-nrc-config "$here/opennotificaties/setup_configuration/." ;;
kc) populate rr-kc-realms "$here/keycloak/realms/." ;; kc) populate rr-kc-realms "$here/keycloak/realms/." ;;
fl) populate rr-fl-bpmn "$here/../workflows/registratie.bpmn" ;; fl) populate rr-fl-bpmn "$here/../workflows/registratie.bpmn" ;;
*) echo "unknown seed key: $key" >&2; exit 2 ;; *) echo "unknown seed key: $key" >&2; exit 2 ;;

View File

@@ -0,0 +1,90 @@
#!/usr/bin/env python3
"""Drive the OpenZaak → NRC notification check from *inside* the compose network.
Registers an abonnement on the `zaken` kanaal pointing at a webhook sink, then
creates a zaak against the published BIG zaaktype. OpenZaak publishes a
`zaken`/`create` notification; NRC delivers it to the sink. The host harness
(infra/verify-notifications.sh) then asserts the sink received it.
Reached by container IP, not service name: OpenZaak/NRC validate URLs with Django's
URLValidator, which rejects a single-label host like `openzaak`. Stdlib only.
Env: OZ_BASE, NRC_BASE, SINK_CALLBACK, SINK_AUTH, OZ_CLIENT_ID, OZ_SECRET.
"""
import base64
import hashlib
import hmac
import json
import os
import sys
import time
import urllib.error
import urllib.request
OZ = os.environ["OZ_BASE"].rstrip("/")
NRC = os.environ["NRC_BASE"].rstrip("/")
SINK_CALLBACK = os.environ["SINK_CALLBACK"]
SINK_AUTH = os.environ.get("SINK_AUTH", "Bearer notification-sink-token")
CID = os.environ.get("OZ_CLIENT_ID", "big-reference-seed")
SECRET = os.environ.get("OZ_SECRET", "insecure-dev-secret-change-me")
RSIN = "517439943"
def token():
b64 = lambda b: base64.urlsafe_b64encode(b).rstrip(b"=")
seg = (
b64(json.dumps({"alg": "HS256", "typ": "JWT"}, separators=(",", ":")).encode())
+ b"."
+ b64(json.dumps(
{"iss": CID, "iat": int(time.time()), "client_id": CID,
"user_id": "verify", "user_representation": "verify"},
separators=(",", ":")).encode())
)
return (seg + b"." + b64(hmac.new(SECRET.encode(), seg, hashlib.sha256).digest())).decode()
def call(method, url, body=None, crs=False):
headers = {"Authorization": "Bearer " + token(),
"Content-Type": "application/json", "Accept": "application/json"}
if crs:
headers["Accept-Crs"] = "EPSG:4326"
headers["Content-Crs"] = "EPSG:4326"
data = json.dumps(body).encode() if body is not None else None
try:
with urllib.request.urlopen(
urllib.request.Request(url, data=data, method=method, headers=headers), 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 main():
status, ab = call("POST", f"{NRC}/api/v1/abonnement", {
"callbackUrl": SINK_CALLBACK,
"auth": SINK_AUTH,
"kanalen": [{"naam": "zaken", "filters": {}}],
})
if status != 201:
sys.exit(f"create abonnement -> {status}: {json.dumps(ab)}")
print(f"abonnement: {ab['url']}")
status, body = call(
"GET", f"{OZ}/catalogi/api/v1/zaaktypen?identificatie=BIG-REGISTRATIE&status=definitief")
results = body.get("results", []) if status == 200 else []
if not results:
sys.exit("no published BIG-REGISTRATIE zaaktype — seed with OZ_PUBLISH=1 first")
zaaktype = results[0]["url"]
status, zaak = call("POST", f"{OZ}/zaken/api/v1/zaken", {
"bronorganisatie": RSIN, "verantwoordelijkeOrganisatie": RSIN,
"zaaktype": zaaktype, "startdatum": time.strftime("%Y-%m-%d"),
"vertrouwelijkheidaanduiding": "openbaar",
}, crs=True)
if status != 201:
sys.exit(f"create zaak -> {status}: {json.dumps(zaak)}")
# The harness greps the sink for this exact URL.
print(f"ZAAK_CREATED {zaak['url']}")
if __name__ == "__main__":
main()

41
infra/verify-notifications.sh Executable file
View File

@@ -0,0 +1,41 @@
#!/usr/bin/env bash
#
# Local convenience: verify the OpenZaak → NRC notification path against a throwaway
# oz+nrc stack. Brings both up (notifications enabled), runs the shared stack-agnostic
# check (infra/run-notification-check.sh), then always tears down.
#
# CI does not use this — there the full stack is brought up once and the same runner
# is invoked as a step (see the `verify-stack` job / Makefile `verify-*`). See ADR-0007.
set -euo pipefail
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
OZ_COMPOSE="$here/openzaak/docker-compose.yml"
NRC_COMPOSE="$here/opennotificaties/docker-compose.yml"
cleanup() {
docker compose -f "$OZ_COMPOSE" -f "$NRC_COMPOSE" down --volumes >/dev/null 2>&1 || true
docker volume rm -f rr-oz-config rr-nrc-config >/dev/null 2>&1 || true
}
trap cleanup EXIT
wait_healthy() { # name-regex
local re="$1" cid
for _ in $(seq 1 140); do
cid="$(docker ps -q --filter "name=$re" | head -1)"
if [ -n "$cid" ] && [ "$(docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{end}}' "$cid" 2>/dev/null || true)" = healthy ]; then
return 0
fi
sleep 3
done
return 1
}
echo ">> bringing up OpenZaak + Open Notificaties (notifications enabled)"
bash "$here/seed-config.sh" oz nrc
OZ_NOTIFICATIONS_DISABLED=false docker compose -f "$OZ_COMPOSE" -f "$NRC_COMPOSE" up -d
echo ">> waiting for OpenZaak + NRC to be healthy"
wait_healthy '[-_]openzaak[-_]' || { echo "ERROR: OpenZaak not healthy" >&2; exit 1; }
wait_healthy 'nrc-web' || { echo "ERROR: NRC not healthy" >&2; exit 1; }
bash "$here/run-notification-check.sh"

View File

@@ -28,6 +28,7 @@ nav:
- "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md - "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md
- "ADR-0005: Mutation testing": architecture/adr-0005-mutation-testing.md - "ADR-0005: Mutation testing": architecture/adr-0005-mutation-testing.md
- "ADR-0006: ACL integration test provisioning": architecture/adr-0006-integration-test-provisioning.md - "ADR-0006: ACL integration test provisioning": architecture/adr-0006-integration-test-provisioning.md
- "ADR-0007: OpenZaak → NRC notification wiring": architecture/adr-0007-notification-wiring.md
- Working in Gitea: gitea-workflow.md - Working in Gitea: gitea-workflow.md
- Runbooks: - Runbooks:
- CI: runbooks/ci.md - CI: runbooks/ci.md