From a256db1a2326c6710f75272689a5593d59e0e4e3 Mon Sep 17 00:00:00 2001 From: Niek Otten Date: Mon, 29 Jun 2026 14:29:12 +0200 Subject: [PATCH] =?UTF-8?q?arch(infra):=20ADR-0007=20+=20runbooks=20for=20?= =?UTF-8?q?the=20OZ=E2=86=92NRC=20notification=20wiring=20(refs=20#56)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Records the wiring decision (AC-delegated auth, required celery-beat) and the two non-obvious gotchas: single-label hosts aren't URL-valid (reach services by IP) and abonnement callbacks must enforce auth. Documents the new notifications CI job. Co-Authored-By: Claude Opus 4.8 (1M context) --- .../adr-0007-notification-wiring.md | 76 +++++++++++++++++++ docs/runbooks/ci.md | 1 + docs/runbooks/gitea-actions-gotchas.md | 32 ++++++-- mkdocs.yml | 1 + 4 files changed, 105 insertions(+), 5 deletions(-) create mode 100644 docs/architecture/adr-0007-notification-wiring.md diff --git a/docs/architecture/adr-0007-notification-wiring.md b/docs/architecture/adr-0007-notification-wiring.md new file mode 100644 index 0000000..87845ae --- /dev/null +++ b/docs/architecture/adr-0007-notification-wiring.md @@ -0,0 +1,76 @@ +# 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 (`make verify-notifications`, +`infra/verify-notifications.sh` + a `notifications` CI job): it brings the stack up, +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). + +## 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. diff --git a/docs/runbooks/ci.md b/docs/runbooks/ci.md index abd035e..0b61cd7 100644 --- a/docs/runbooks/ci.md +++ b/docs/runbooks/ci.md @@ -18,6 +18,7 @@ and CI cannot drift: | `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 | | `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`) | +| `notifications` | `make verify-notifications` → bring up OpenZaak + NRC → seed → assert a zaak-create notification reaches a subscriber (containers on the network) → tear down | container engine + egress (base images, `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 diff --git a/docs/runbooks/gitea-actions-gotchas.md b/docs/runbooks/gitea-actions-gotchas.md index c9c9ae4..0c02b68 100644 --- a/docs/runbooks/gitea-actions-gotchas.md +++ b/docs/runbooks/gitea-actions-gotchas.md @@ -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 `docker run --network _cg …`. -**Applied** — `make integration` (the ACL ↔ real-OpenZaak test, ADR-0006) does -exactly this: `infra/run-integration.sh` runs the seed and the test as containers on -the OpenZaak network and reaches it by **container IP** (a single-label service name -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). +**Applied** — `make integration` (ADR-0006) and `make verify-notifications` (ADR-0007) +do exactly this: they run the seed/test/driver as containers on the stack network and +reach services by **container IP** (see §6). + +--- + +## 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-integration.sh`, `infra/verify-notifications.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. diff --git a/mkdocs.yml b/mkdocs.yml index 8173c11..b4b3949 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -28,6 +28,7 @@ nav: - "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.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-0007: OpenZaak → NRC notification wiring": architecture/adr-0007-notification-wiring.md - Working in Gitea: gitea-workflow.md - Runbooks: - CI: runbooks/ci.md