Compare commits

..

2 Commits

Author SHA1 Message Date
e1883c2d0e chore(infra): make openzaak-* targets + runbook (refs #10)
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / unit (pull_request) Has been cancelled
CI / compose-smoke (pull_request) Has been cancelled
Add `make openzaak-up`, `make openzaak-smoke`, and `make openzaak-down` so
the OpenZaak stack is testable in one command, matching the `make ci`
pattern. openzaak-smoke polls until the API responds, then asserts auth is
enforced (403) and the admin/schema endpoints are reachable (302/200).
Document it in docs/runbooks/openzaak.md (kept out of `make ci` — it's a
heavier, separate check).

Verified: `make openzaak-smoke` is green end-to-end.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 15:08:00 +02:00
ae2169b6ee feat(infra): OpenZaak + Postgres + Redis up in compose (refs #10)
Some checks failed
CI / lint (pull_request) Has been cancelled
CI / build (pull_request) Has been cancelled
CI / unit (pull_request) Has been cancelled
CI / compose-smoke (pull_request) Has been cancelled
Add infra/openzaak/docker-compose.yml — a lean adaptation of the upstream
open-zaak dev stack: PostGIS db, redis, a one-shot init that runs database
migrations, the OpenZaak API, and a celery worker. (Dropped nginx, beat,
flower, OTEL for leanness.) Images fully qualified (docker.io/...) for
rootless Podman.

Verified locally: stack comes up; migrations run; GET /admin/ -> 302,
GET /zaken/api/v1/ -> 200, and an unauthenticated GET /zaken/api/v1/zaken
-> 403 PermissionDenied (a proper ZGW fout) — i.e. auth is enforced.

Note: the S-01 acceptance says "401", but OpenZaak's ZGW APIs return 403
for missing/invalid JWT. Auth-enforced intent is met; the issue text
should be corrected to 403. Remaining S-01 work: BIG catalogus + zaaktype
seed + JWT client (then list zaaktypen), and Open Notificaties — so this
refs (not closes) #10.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-03 14:28:20 +02:00
17 changed files with 14 additions and 781 deletions

View File

@@ -8,13 +8,8 @@
SLN := services/bff/Bff.slnx
COMPOSE := infra/docker-compose.yml
HEALTH_URL := http://localhost:8080/health
OZ_COMPOSE := infra/openzaak/docker-compose.yml
OZ_BASE := http://localhost:8000
NRC_COMPOSE := infra/opennotificaties/docker-compose.yml
NRC_BASE := http://localhost:8001
KC_COMPOSE := infra/keycloak/docker-compose.yml
KC_BASE := http://localhost:8180
STACK_FILES := -f $(OZ_COMPOSE) -f $(NRC_COMPOSE)
OZ_COMPOSE := infra/openzaak/docker-compose.yml
OZ_BASE := http://localhost:8000
# On a rootless Podman dev box, point Docker CLI/Compose at the Podman socket —
# but only if that socket exists and DOCKER_HOST isn't already set, so real
@@ -26,7 +21,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
endif
endif
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down help
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-down help
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
ci: lint build unit smoke
@@ -76,54 +71,10 @@ openzaak-smoke:
echo "GET /zaken/api/v1/ -> $$root (expect 200)"; test "$$root" = "200"; \
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:
docker compose -f $(OZ_COMPOSE) down --volumes
## stack-up: start OpenZaak + Open Notificaties together (shared network)
stack-up:
docker compose $(STACK_FILES) up -d
## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable
stack-smoke: stack-up
@bash -c 'set -e; \
echo "waiting for OpenZaak + Open Notificaties..."; \
for i in $$(seq 1 60); do \
oz=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/admin/ || true); \
nrc=$$(curl -s -o /dev/null -w "%{http_code}" $(NRC_BASE)/admin/ || true); \
[ "$$oz" = "302" ] && [ "$$nrc" = "302" ] && break; sleep 3; done; \
z=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/zaken); \
echo "OpenZaak /zaken (unauth) -> $$z (expect 403)"; test "$$z" = "403"; \
echo "OpenZaak /admin/ -> $$oz (expect 302)"; test "$$oz" = "302"; \
echo "Open Notificaties /admin/-> $$nrc (expect 302)"; test "$$nrc" = "302"; \
echo "stack smoke OK"'
## stack-down: stop and remove both stacks (wipes data)
stack-down:
docker compose $(STACK_FILES) down --volumes
## keycloak-up: start Keycloak with the four imported realms
keycloak-up:
docker compose -f $(KC_COMPOSE) up -d
## keycloak-smoke: start Keycloak, then verify each realm logs in + returns its claim
keycloak-smoke: keycloak-up
@bash -c 'for i in $$(seq 1 60); do \
c=$$(curl -s -o /dev/null -w "%{http_code}" $(KC_BASE)/realms/digid/.well-known/openid-configuration || true); \
[ "$$c" = "200" ] && break; sleep 3; done; echo "Keycloak ready ($$c)"'
python3 infra/keycloak/check_realms.py
## keycloak-down: stop and remove Keycloak
keycloak-down:
docker compose -f $(KC_COMPOSE) down --volumes
## help: list available targets
help:
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'

View File

@@ -1,53 +0,0 @@
# 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.

View File

@@ -1,37 +0,0 @@
# Keycloak runbook
Keycloak (`infra/keycloak/docker-compose.yml`) runs in dev mode with four realms
imported at boot from `infra/keycloak/realms/`: **digid**, **eherkenning**, **eidas**,
**medewerker**. It mocks the Dutch identity brokers so portals can do real OIDC logins
locally. Host port **:8180**.
## Quick test (`make`)
```bash
make keycloak-up # start Keycloak + import realms (~30-60s first boot)
make keycloak-smoke # start + verify every realm logs in and returns its claim
make keycloak-down # stop + wipe
```
`make keycloak-smoke` runs `infra/keycloak/check_realms.py`, which does a password-grant
login per realm and asserts the identifying claim:
| Realm | User | Claim asserted |
|---|---|---|
| digid | jan-burger | `bsn` |
| eherkenning | acme-ondernemer | `kvk` |
| eidas | pierre-dupont | `eidas_id` |
| medewerker | merel-behandelaar | role `behandelaar` |
All test users / credentials are in [../synthetic-data.md](../synthetic-data.md).
## Notes
- **Admin console:** <http://localhost:8180/> — `admin` / `admin` (dev only).
- **Client `big-portal`** is public with `standardFlowEnabled` (browser redirect login)
*and* `directAccessGrantsEnabled` (password grant, used by the smoke test).
- **Dev store:** in-memory H2 via `start-dev`; realms re-import on each boot, so changes
made in the admin UI don't persist. Edit the realm JSONs to make durable changes.
- **Image** pinned to `quay.io/keycloak/keycloak:26.1`.
- Claims are injected by OIDC protocol mappers on `big-portal` (user attribute → token
claim); `medewerker` roles come through `realm_access.roles`.

View File

@@ -1,44 +0,0 @@
# Open Notificaties (NRC) runbook
The Open Notificaties stack (`infra/opennotificaties/docker-compose.yml`) is a lean
adaptation of the upstream dev compose: PostGIS db, redis (also the Celery broker), a
one-shot migrate-init, the API, and a celery worker. It shares the **`cg`** Docker
network with the OpenZaak stack so the two can reach each other by service name.
NRC is published on host **:8001** (OpenZaak holds :8000).
## Quick test (`make`) — both platforms together
```bash
make stack-up # OpenZaak + Open Notificaties on the shared network
make stack-smoke # start both + assert reachable (OZ 403/302/200, NRC 302)
make stack-down # stop + wipe both
```
`make stack-smoke` runs `docker compose -f infra/openzaak/... -f infra/opennotificaties/... up -d`
and asserts:
| Check | Expected |
|---|---|
| OpenZaak `GET /zaken/api/v1/zaken` (no JWT) | 403 (auth enforced) |
| OpenZaak `GET /admin/` | 302 |
| Open Notificaties `GET /admin/` | 302 |
NRC admin UI: <http://localhost:8001/admin/> (dev superuser **admin / admin**).
## Notification wiring is deferred to S-06
Both platforms are **up and reachable**, but OpenZaak→NRC notification *delivery* is not
wired yet, and OpenZaak still runs with `NOTIFICATIONS_DISABLED=true`. The bidirectional
auth wiring (NRC `setup_configuration`: Services + Authorization to OpenZaak's
Autorisaties API + JWT secrets + Kanalen + Abonnementen; OpenZaak's NotificationConfig)
lands with **S-06 (Event Subscriber)** — the slice that actually consumes events. NRC's
`setup_configuration/data.yaml` is intentionally minimal (migrations only) until then.
This matches S-01's acceptance, which asks only that the platforms *come up in compose*
and a health check confirms them reachable.
## Prerequisites
Same rootless-Podman setup as the rest of the repo — see [ci.md](ci.md) and
[openzaak.md](openzaak.md). `systemctl --user start podman.socket` once per session.

View File

@@ -9,32 +9,9 @@ migrations, the OpenZaak API, and a celery worker.
```bash
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-seed # start + seed the BIG catalogus (idempotent)
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:
| Check | Expected |
@@ -66,10 +43,8 @@ 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
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.
- **Notifications disabled.** `NOTIFICATIONS_DISABLED=true` — otherwise ZTC writes 500
trying to notify. Open Notificaties is now up (see [opennotificaties.md](opennotificaties.md)),
but OZ→NRC delivery wiring + re-enabling lands with **S-06**.
- **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/
resultaattypen — beyond the lean seed). List with `?status=alles`.
- **No catalogus/zaaktypen yet.** Seeding the `BIG` catalogus + `BIG-registratie`
zaaktype and a JWT client is the next slice (**S-01-b**); authenticated zaaktype
listing isn't testable until then.
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
a known-good tag (ADR-0002 follow-up).
a known-good tag as part of the catalogus-design ADR.

View File

@@ -1,35 +0,0 @@
# Synthetic data
All credentials here are **dev-only** synthetic test data — never real personal data,
never used outside local development.
## Keycloak realms (S-02)
Keycloak runs at <http://localhost:8180> (admin console: **admin / admin**). Four realms
are imported at boot from `infra/keycloak/realms/`. Each has a public OIDC client
**`big-portal`** (standard flow + direct access grants enabled, redirect URIs `*` for dev).
All test users share the password **`test123`**.
| Realm | Mimics | User | Identifying claim |
|---|---|---|---|
| `digid` | DigiD (burgers) | `jan-burger` | `bsn` = `123456782` |
| `eherkenning` | eHerkenning (bedrijven) | `acme-ondernemer` | `kvk` = `12345678` |
| `eidas` | eIDAS (EU) | `pierre-dupont` | `eidas_id` = `FR/NL/AB-1234-5678` |
| `medewerker` | Internal staff | `merel-behandelaar` | role `behandelaar` |
| `medewerker` | Internal staff | `tom-teamlead` | roles `behandelaar`, `teamlead` |
The identifying claims are injected via OIDC protocol mappers on `big-portal`
(user-attribute → token claim); `medewerker` roles appear in `realm_access.roles`.
## Get a token (for testing)
```bash
curl -s -X POST \
http://localhost:8180/realms/digid/protocol/openid-connect/token \
-d grant_type=password -d client_id=big-portal \
-d username=jan-burger -d password=test123 -d scope=openid | jq -r .access_token
```
Decode the JWT payload to see the `bsn` claim. `make keycloak-smoke` checks every realm
automatically.

View File

@@ -1,60 +0,0 @@
#!/usr/bin/env python3
"""Smoke-check the Keycloak realms: each realm's OIDC login works (password grant)
and returns its expected identifying claim. Stdlib only. Exits non-zero on failure.
"""
import base64, json, sys, urllib.error, urllib.parse, urllib.request
BASE = "http://localhost:8180"
CLIENT = "big-portal"
PWD = "test123"
# realm, user, claim ("__roles__" => check realm_access.roles), expected-contains
CHECKS = [
("digid", "jan-burger", "bsn", "123456782"),
("eherkenning", "acme-ondernemer", "kvk", "12345678"),
("eidas", "pierre-dupont", "eidas_id", "FR/NL"),
("medewerker", "merel-behandelaar", "__roles__", "behandelaar"),
]
def decode(jwt):
p = jwt.split(".")[1]
p += "=" * (-len(p) % 4)
return json.loads(base64.urlsafe_b64decode(p))
def grant(realm, user):
data = urllib.parse.urlencode({
"grant_type": "password", "client_id": CLIENT,
"username": user, "password": PWD, "scope": "openid",
}).encode()
req = urllib.request.Request(
f"{BASE}/realms/{realm}/protocol/openid-connect/token", data=data,
headers={"Content-Type": "application/x-www-form-urlencoded"})
with urllib.request.urlopen(req, timeout=20) as r:
return json.loads(r.read())
def main():
ok = True
for realm, user, claim, expect in CHECKS:
try:
at = decode(grant(realm, user)["access_token"])
if claim == "__roles__":
val = at.get("realm_access", {}).get("roles", [])
good = expect in val
else:
val = at.get(claim)
good = val is not None and expect in str(val)
print(f"{realm:12} {user:18} login OK | {claim} = {val} "
f"[{'OK' if good else 'UNEXPECTED'}]")
ok = ok and good
except urllib.error.HTTPError as e:
ok = False
print(f"{realm:12} {user:18} LOGIN FAILED {e.code}: {e.read()[:200]!r}")
print("keycloak smoke OK" if ok else "keycloak smoke FAILED")
sys.exit(0 if ok else 1)
if __name__ == "__main__":
main()

View File

@@ -1,29 +0,0 @@
# Keycloak (S-02) with four pre-seeded realms imported at boot:
# digid · eherkenning · eidas · medewerker
# Dev mode, H2 in-memory store, realm JSONs imported from ./realms.
#
# docker compose -f infra/keycloak/docker-compose.yml up -d
# curl -s -o /dev/null -w '%{http_code}\n' \
# http://localhost:8180/realms/digid/.well-known/openid-configuration # -> 200
#
# Admin console: http://localhost:8180/ (admin / admin — dev only)
services:
keycloak:
image: quay.io/keycloak/keycloak:26.1
command: ["start-dev", "--import-realm"]
environment:
KC_BOOTSTRAP_ADMIN_USERNAME: admin
KC_BOOTSTRAP_ADMIN_PASSWORD: admin
# Older var names too, harmless on 26.x:
KEYCLOAK_ADMIN: admin
KEYCLOAK_ADMIN_PASSWORD: admin
KC_HEALTH_ENABLED: "true"
KC_HTTP_ENABLED: "true"
ports:
- "8180:8080"
volumes:
- ./realms:/opt/keycloak/data/import:ro,z
networks: [cg]
networks:
cg:

View File

@@ -1,43 +0,0 @@
{
"realm": "digid",
"enabled": true,
"displayName": "Mock DigiD",
"clients": [
{
"clientId": "big-portal",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["*"],
"webOrigins": ["*"],
"protocolMappers": [
{
"name": "bsn",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "bsn",
"claim.name": "bsn",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"users": [
{
"username": "jan-burger",
"enabled": true,
"firstName": "Jan",
"lastName": "Burger",
"email": "jan.burger@example.nl",
"emailVerified": true,
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
"attributes": { "bsn": ["123456782"] }
}
]
}

View File

@@ -1,43 +0,0 @@
{
"realm": "eherkenning",
"enabled": true,
"displayName": "Mock eHerkenning",
"clients": [
{
"clientId": "big-portal",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["*"],
"webOrigins": ["*"],
"protocolMappers": [
{
"name": "kvk",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "kvk",
"claim.name": "kvk",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"users": [
{
"username": "acme-ondernemer",
"enabled": true,
"firstName": "Anita",
"lastName": "Ondernemer",
"email": "anita@acme.nl",
"emailVerified": true,
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
"attributes": { "kvk": ["12345678"] }
}
]
}

View File

@@ -1,43 +0,0 @@
{
"realm": "eidas",
"enabled": true,
"displayName": "Mock eIDAS",
"clients": [
{
"clientId": "big-portal",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["*"],
"webOrigins": ["*"],
"protocolMappers": [
{
"name": "eidas_id",
"protocol": "openid-connect",
"protocolMapper": "oidc-usermodel-attribute-mapper",
"config": {
"user.attribute": "eidas_id",
"claim.name": "eidas_id",
"jsonType.label": "String",
"id.token.claim": "true",
"access.token.claim": "true",
"userinfo.token.claim": "true"
}
}
]
}
],
"users": [
{
"username": "pierre-dupont",
"enabled": true,
"firstName": "Pierre",
"lastName": "Dupont",
"email": "pierre.dupont@example.fr",
"emailVerified": true,
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
"attributes": { "eidas_id": ["FR/NL/AB-1234-5678"] }
}
]
}

View File

@@ -1,44 +0,0 @@
{
"realm": "medewerker",
"enabled": true,
"displayName": "Medewerkers",
"roles": {
"realm": [
{ "name": "behandelaar", "description": "Behandelt registratieaanvragen" },
{ "name": "teamlead", "description": "Teamleider behandeling" }
]
},
"clients": [
{
"clientId": "big-portal",
"enabled": true,
"publicClient": true,
"standardFlowEnabled": true,
"directAccessGrantsEnabled": true,
"redirectUris": ["*"],
"webOrigins": ["*"]
}
],
"users": [
{
"username": "merel-behandelaar",
"enabled": true,
"firstName": "Merel",
"lastName": "Behandelaar",
"email": "merel@big.example.nl",
"emailVerified": true,
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
"realmRoles": ["behandelaar"]
},
{
"username": "tom-teamlead",
"enabled": true,
"firstName": "Tom",
"lastName": "Teamlead",
"email": "tom@big.example.nl",
"emailVerified": true,
"credentials": [{ "type": "password", "value": "test123", "temporary": false }],
"realmRoles": ["behandelaar", "teamlead"]
}
]
}

View File

@@ -1,91 +0,0 @@
# Open Notificaties (NRC) stack (S-01-c). Lean adaptation of the upstream dev compose:
# db (PostGIS) + redis (also the Celery broker) + migrate-init + the API + a celery worker.
#
# Shares the `cg` network with the OpenZaak stack so the two can reach each other.
# Run BOTH together (NRC needs OpenZaak's Autorisaties API for auth wiring):
#
# docker compose -f infra/openzaak/docker-compose.yml -f infra/opennotificaties/docker-compose.yml up -d
# curl -s -o /dev/null -w '%{http_code}\n' http://localhost:8001/admin/ # -> 302
#
# NRC is published on host :8001 (OpenZaak holds :8000).
services:
nrc-db:
image: docker.io/postgis/postgis:17-3.5
environment:
POSTGRES_USER: opennotificaties
POSTGRES_PASSWORD: opennotificaties
POSTGRES_DB: opennotificaties
command: postgres -c max_connections=300
volumes:
- nrc-db:/var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U opennotificaties -d opennotificaties"]
interval: 5s
timeout: 3s
retries: 10
networks: [cg]
nrc-redis:
image: docker.io/library/redis:7
networks: [cg]
nrc-init:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
environment: &nrc-env
DJANGO_SETTINGS_MODULE: nrc.conf.docker
SECRET_KEY: ${NRC_SECRET_KEY:-dev-only-not-for-production}
DB_HOST: nrc-db
DB_NAME: opennotificaties
DB_USER: opennotificaties
DB_PASSWORD: opennotificaties
IS_HTTPS: "no"
ALLOWED_HOSTS: "*"
CACHE_DEFAULT: nrc-redis:6379/0
CACHE_AXES: nrc-redis:6379/0
CELERY_BROKER_URL: redis://nrc-redis:6379/1
CELERY_RESULT_BACKEND: redis://nrc-redis:6379/1
DISABLE_2FA: "true"
OPENNOTIFICATIES_SUPERUSER_USERNAME: admin
DJANGO_SUPERUSER_PASSWORD: admin
OPENNOTIFICATIES_SUPERUSER_EMAIL: admin@localhost
RUN_SETUP_CONFIG: "true"
command: /setup_configuration.sh
volumes:
- ./setup_configuration:/app/setup_configuration:ro,z
depends_on:
nrc-db:
condition: service_healthy
nrc-redis:
condition: service_started
networks: [cg]
nrc-web:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
environment: *nrc-env
healthcheck:
test: ["CMD", "python", "-c", "import requests,sys; sys.exit(0 if requests.head('http://localhost:8000/admin/').status_code in (200,302) else 1)"]
interval: 10s
timeout: 5s
retries: 10
start_period: 30s
ports:
- "8001:8000"
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
nrc-celery:
image: docker.io/openzaak/open-notificaties:${OPENNOTIFICATIES_TAG:-latest}
environment: *nrc-env
command: /celery_worker.sh
depends_on:
nrc-init:
condition: service_completed_successfully
networks: [cg]
volumes:
nrc-db:
networks:
cg:

View File

@@ -1,5 +0,0 @@
# Open Notificaties setup_configuration.
# Stage 1 (this commit): intentionally minimal — the init container runs
# migrations; no steps enabled yet. The OpenZaak<->NRC notification wiring
# (Services, Authorization, JWT, Kanalen) is added next. See ADR-0002 / S-01-c.
{}

View File

@@ -22,11 +22,11 @@ services:
interval: 5s
timeout: 3s
retries: 10
networks: [cg]
networks: [oz]
oz-redis:
image: docker.io/library/redis:7
networks: [cg]
networks: [oz]
oz-init:
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
@@ -44,24 +44,17 @@ services:
CELERY_BROKER_URL: redis://oz-redis:6379/1
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
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
DJANGO_SUPERUSER_PASSWORD: admin
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
RUN_SETUP_CONFIG: "true"
RUN_SETUP_CONFIG: "false"
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:
oz-db:
condition: service_healthy
oz-redis:
condition: service_started
networks: [cg]
networks: [oz]
openzaak:
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
@@ -77,7 +70,7 @@ services:
depends_on:
oz-init:
condition: service_completed_successfully
networks: [cg]
networks: [oz]
oz-celery:
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
@@ -86,10 +79,10 @@ services:
depends_on:
oz-init:
condition: service_completed_successfully
networks: [cg]
networks: [oz]
volumes:
oz-db:
networks:
cg:
oz:

View File

@@ -1,137 +0,0 @@
#!/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()

View File

@@ -1,22 +0,0 @@
# 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