Compare commits
1
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cd227af244 |
@@ -1,24 +0,0 @@
|
||||
---
|
||||
name: ADR proposal
|
||||
about: Propose a decision that needs recording before coding (CLAUDE.md §14)
|
||||
title: "ADR: "
|
||||
labels:
|
||||
- type:adr-proposal
|
||||
---
|
||||
|
||||
**Decision to be made:**
|
||||
|
||||
**Context / forces:** <!-- what makes this non-obvious; constraints, trade-offs -->
|
||||
|
||||
**Options considered:**
|
||||
1.
|
||||
2.
|
||||
|
||||
**Proposed option + why:**
|
||||
|
||||
**Consequences:** <!-- what becomes easier/harder; what we commit to -->
|
||||
|
||||
**Coupling rules touched (CLAUDE.md §8):** <!-- none, or which and why -->
|
||||
|
||||
> On acceptance, the ADR file (`docs/architecture/adr-NNNN-title.md`, Nygard
|
||||
> template) lands in the PR that implements the decision.
|
||||
@@ -1,21 +0,0 @@
|
||||
---
|
||||
name: Bug
|
||||
about: Something behaves incorrectly
|
||||
title: ""
|
||||
labels:
|
||||
- type:bug
|
||||
---
|
||||
|
||||
**What happened:**
|
||||
|
||||
**What you expected:**
|
||||
|
||||
**Steps to reproduce:**
|
||||
1.
|
||||
2.
|
||||
|
||||
**Environment:** <!-- branch/commit, OS, container engine, anything relevant -->
|
||||
|
||||
**Logs / evidence:**
|
||||
|
||||
**Suspected area:** <!-- e.g. area:bff, area:acl — add the matching area label -->
|
||||
@@ -1,31 +0,0 @@
|
||||
---
|
||||
name: Slice (user story)
|
||||
about: A backlog slice — independently demoable, encodes the Definition of Done
|
||||
title: "S-NN · "
|
||||
labels:
|
||||
- type:slice
|
||||
---
|
||||
|
||||
**Outcome:** <!-- one sentence; user-visible if possible -->
|
||||
|
||||
**Acceptance:**
|
||||
<!-- Gherkin scenarios or testable assertions -->
|
||||
-
|
||||
|
||||
**Touches:** <!-- services and folders -->
|
||||
|
||||
**Out of scope:** <!-- explicit non-goals -->
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] This linked Gitea issue exists and is on the right milestone.
|
||||
- [ ] Failing test written and committed first (`test(scope): … (refs #NN)`).
|
||||
- [ ] Implementation makes the test pass (`feat(scope): … (refs #NN)`).
|
||||
- [ ] Refactor commit follows if structure improved.
|
||||
- [ ] Conventional Commit messages referencing this issue.
|
||||
- [ ] All Gitea Actions CI jobs green (or `make ci` green while no runner exists).
|
||||
- [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes.
|
||||
- [ ] Docs touched if behaviour, contracts, or operations changed.
|
||||
- [ ] ADR added in `docs/architecture/` if a non-obvious decision was made.
|
||||
- [ ] Demo note appended to `docs/demo-script.md` if the slice is user-visible.
|
||||
- [ ] This issue closed by the merging PR (`closes #NN`).
|
||||
@@ -1,23 +0,0 @@
|
||||
<!-- Title: Conventional Commit style, e.g. feat(bff): … (closes #NN) -->
|
||||
|
||||
## What & why
|
||||
|
||||
<!-- Summary of the change and the slice/bug it addresses. -->
|
||||
|
||||
Closes #
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Linked Gitea issue (above).
|
||||
- [ ] Failing test committed before the implementation.
|
||||
- [ ] Implementation makes the test pass; refactor commit if structure improved.
|
||||
- [ ] Conventional Commits referencing the issue (`refs #NN`).
|
||||
- [ ] CI green — all Gitea Actions jobs (or `make ci` green while no runner exists).
|
||||
- [ ] `docker compose up` from a fresh clone reaches green health checks within 3 minutes.
|
||||
- [ ] Docs updated if behaviour, contracts, or operations changed.
|
||||
- [ ] ADR added in `docs/architecture/` if a non-obvious decision was made.
|
||||
- [ ] Demo note in `docs/demo-script.md` if user-visible.
|
||||
|
||||
## Notes for reviewers
|
||||
|
||||
<!-- Anything that helps review: trade-offs, follow-ups, known gaps. -->
|
||||
@@ -1,50 +0,0 @@
|
||||
name: CI
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
# Self-hosted runner — see docs/runbooks/ci.md for the runner setup.
|
||||
# `uses:` are absolute, tag-pinned URLs (CLAUDE.md §8.7 / §15).
|
||||
|
||||
# Each job calls a `make` target — the same one developers run locally
|
||||
# (`make ci`). The Makefile is the single source of truth; see docs/runbooks/ci.md.
|
||||
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: respellion-linux
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- run: make lint
|
||||
|
||||
build:
|
||||
runs-on: respellion-linux
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- run: make build
|
||||
|
||||
unit:
|
||||
runs-on: respellion-linux
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- uses: https://github.com/actions/setup-dotnet@v4
|
||||
with:
|
||||
dotnet-version: '10.0.x'
|
||||
- run: make unit
|
||||
|
||||
compose-smoke:
|
||||
runs-on: respellion-linux
|
||||
steps:
|
||||
- uses: https://github.com/actions/checkout@v4
|
||||
- run: make smoke
|
||||
-34
@@ -1,34 +0,0 @@
|
||||
# .NET build output
|
||||
bin/
|
||||
obj/
|
||||
[Dd]ebug/
|
||||
[Rr]elease/
|
||||
*.user
|
||||
|
||||
# Reqnroll-generated test code (regenerated from *.feature on build)
|
||||
*.feature.cs
|
||||
|
||||
# Test results / coverage
|
||||
[Tt]est[Rr]esults/
|
||||
*.trx
|
||||
coverage*.json
|
||||
coverage*.xml
|
||||
*.coverage
|
||||
|
||||
# Rider / VS / VS Code
|
||||
.idea/
|
||||
.vs/
|
||||
.vscode/
|
||||
|
||||
# Node / Angular (added as the frontend lands)
|
||||
node_modules/
|
||||
dist/
|
||||
.angular/
|
||||
|
||||
# Python / MkDocs
|
||||
.venv/
|
||||
site/
|
||||
|
||||
# OS
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
@@ -1,20 +0,0 @@
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project. Generated from Conventional Commits by git-cliff.
|
||||
|
||||
## Unreleased
|
||||
|
||||
### CI
|
||||
- Gitea Actions pipeline + runner runbook (refs #30) (#37)
|
||||
|
||||
### Chores
|
||||
- Add idempotent Gitea backlog seeder
|
||||
- Remove bootstrap scripts from main (#35)
|
||||
|
||||
### Documentation
|
||||
- Split S-00 into sub-slices (refs #1) (#33)
|
||||
|
||||
### Features
|
||||
- Placeholder BFF + /health endpoint (closes #28) (#34)
|
||||
- Containerize BFF + compose-up smoke (closes #29) (#36)
|
||||
|
||||
@@ -1,146 +0,0 @@
|
||||
# Developer + CI entrypoints.
|
||||
#
|
||||
# These targets are the single source of truth for the checks. The Gitea
|
||||
# Actions workflow (.gitea/workflows/ci.yaml) invokes the SAME targets, so
|
||||
# `make ci` locally runs exactly what the pipeline runs — no drift. Until a
|
||||
# self-hosted runner is registered, `make ci` is the gate (see docs/runbooks/ci.md).
|
||||
|
||||
SLN := register-referentie.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
|
||||
FL_COMPOSE := infra/flowable/docker-compose.yml
|
||||
FL_BASE := http://localhost:8090/flowable-rest/service
|
||||
STACK_FILES := -f $(OZ_COMPOSE) -f $(NRC_COMPOSE)
|
||||
|
||||
# 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
|
||||
# Docker hosts and CI runners are left untouched.
|
||||
PODMAN_SOCK := /run/user/$(shell id -u)/podman/podman.sock
|
||||
ifeq ($(wildcard $(PODMAN_SOCK)),$(PODMAN_SOCK))
|
||||
ifeq ($(origin DOCKER_HOST),undefined)
|
||||
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 flowable-up flowable-smoke flowable-down help
|
||||
|
||||
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
|
||||
ci: lint build unit smoke
|
||||
|
||||
## lint: verify formatting (no changes)
|
||||
lint:
|
||||
dotnet format $(SLN) --verify-no-changes
|
||||
|
||||
## build: release build
|
||||
build:
|
||||
dotnet build $(SLN) -c Release
|
||||
|
||||
## unit: run unit tests
|
||||
unit:
|
||||
dotnet test $(SLN) -c Release
|
||||
|
||||
## smoke: compose up (wait for healthy), curl /health, then tear down
|
||||
smoke:
|
||||
docker compose -f $(COMPOSE) up -d --build --wait
|
||||
bash -c 'curl -fsS $(HEALTH_URL); rc=$$?; docker compose -f $(COMPOSE) down --volumes; exit $$rc'
|
||||
|
||||
## down: stop and remove the local stack
|
||||
down:
|
||||
docker compose -f $(COMPOSE) down --volumes
|
||||
|
||||
## changelog: regenerate CHANGELOG.md from Conventional Commits (git-cliff)
|
||||
changelog:
|
||||
git-cliff --output CHANGELOG.md
|
||||
|
||||
## openzaak-up: start the OpenZaak stack (migrations run on first start)
|
||||
openzaak-up:
|
||||
docker compose -f $(OZ_COMPOSE) up -d
|
||||
|
||||
## openzaak-smoke: start OpenZaak, then assert it is up with auth enforced
|
||||
openzaak-smoke:
|
||||
docker compose -f $(OZ_COMPOSE) up -d
|
||||
@bash -c 'set -e; \
|
||||
echo "waiting for OpenZaak to respond..."; \
|
||||
for i in $$(seq 1 60); do \
|
||||
code=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/zaken || true); \
|
||||
[ -n "$$code" ] && [ "$$code" != "000" ] && break; sleep 3; \
|
||||
done; \
|
||||
echo "GET /zaken/api/v1/zaken (unauth) -> $$code (expect 403, auth enforced)"; test "$$code" = "403"; \
|
||||
admin=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/admin/); \
|
||||
echo "GET /admin/ -> $$admin (expect 302)"; test "$$admin" = "302"; \
|
||||
root=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/); \
|
||||
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
|
||||
|
||||
## flowable-up: start Flowable (deploys registratie.bpmn on boot)
|
||||
flowable-up:
|
||||
docker compose -f $(FL_COMPOSE) up -d
|
||||
|
||||
## flowable-smoke: start Flowable, then verify a started instance waits on the external task
|
||||
flowable-smoke: flowable-up
|
||||
@bash -c 'for i in $$(seq 1 80); do \
|
||||
c=$$(curl -s -o /dev/null -w "%{http_code}" -u rest-admin:test $(FL_BASE)/repository/process-definitions?key=registratie || true); \
|
||||
[ "$$c" = "200" ] && break; sleep 3; done; echo "Flowable ready ($$c)"'
|
||||
python3 infra/flowable/verify.py
|
||||
|
||||
## flowable-down: stop and remove Flowable
|
||||
flowable-down:
|
||||
docker compose -f $(FL_COMPOSE) down --volumes
|
||||
|
||||
## help: list available targets
|
||||
help:
|
||||
@grep -E '^## ' $(MAKEFILE_LIST) | sed 's/^## //'
|
||||
@@ -41,32 +41,22 @@ For the architecture rationale, see [docs/PRD.md §3](docs/PRD.md) and [docs/arc
|
||||
|
||||
**Prerequisites**
|
||||
|
||||
- .NET 10 SDK (for `make lint/build/unit`)
|
||||
- A container engine with Compose v2 — Docker, or rootless Podman (see [docs/runbooks/ci.md](docs/runbooks/ci.md) for the Podman + Compose-provider setup)
|
||||
- `make`, `curl`, `git`
|
||||
- ~4 GB free RAM, ~5 GB free disk (grows as services land)
|
||||
- Docker Engine (or Docker Desktop) with Compose v2
|
||||
- ~8 GB free RAM, ~10 GB free disk
|
||||
- Bash or PowerShell
|
||||
|
||||
**Clone**
|
||||
**Bring the stack up**
|
||||
|
||||
```bash
|
||||
git clone git@git.labs.respellion.tech:eho/register-referentie.git
|
||||
cd register-referentie
|
||||
git clone https://gitea.respellion.local/respellion/register-reference.git
|
||||
cd register-reference
|
||||
cp .env.example .env # edit if you change ports
|
||||
docker compose -f infra/docker-compose.yml up -d
|
||||
```
|
||||
|
||||
**Wired today (Iteration 0):** only the placeholder BFF exists so far. Get to green in under 10 minutes — run the full check gate, or just the running service:
|
||||
Health checks should be green within ~3 minutes on a developer machine. If something fails, see [docs/runbooks/local-startup.md](docs/runbooks/local-startup.md).
|
||||
|
||||
```bash
|
||||
make ci # lint + build + unit + container smoke — the CI gate
|
||||
```
|
||||
|
||||
```bash
|
||||
docker compose -f infra/docker-compose.yml up -d --build --wait
|
||||
curl http://localhost:8080/health # -> Healthy
|
||||
```
|
||||
|
||||
`--wait` exits non-zero unless the container reports healthy, so it doubles as the compose-up smoke test. The remaining services and the URLs below land in later slices.
|
||||
|
||||
**Target service URLs** *(most land in later slices)*
|
||||
**Default URLs**
|
||||
|
||||
| Service | URL |
|
||||
|---|---|
|
||||
@@ -74,7 +64,7 @@ curl http://localhost:8080/health # -> Healthy
|
||||
| Openbaar register | http://localhost:4201 |
|
||||
| Behandel-portal | http://localhost:4202 |
|
||||
| Beheer-portal | http://localhost:4203 |
|
||||
| BFF | http://localhost:8080 |
|
||||
| BFF | http://localhost:5000 |
|
||||
| OpenZaak | http://localhost:8000 |
|
||||
| Open Notificaties | http://localhost:8001 |
|
||||
| Flowable | http://localhost:8080 |
|
||||
@@ -83,12 +73,10 @@ curl http://localhost:8080/health # -> Healthy
|
||||
|
||||
Test credentials, BSNs, and personas: see [docs/synthetic-data.md](docs/synthetic-data.md).
|
||||
|
||||
**Build the docs site**
|
||||
**Re-seed synthetic data**
|
||||
|
||||
```bash
|
||||
python3 -m venv .venv && .venv/bin/pip install mkdocs-material
|
||||
.venv/bin/mkdocs serve # live preview at http://localhost:8000
|
||||
.venv/bin/mkdocs build # static site in ./site
|
||||
./tools/seed.sh # or pwsh ./tools/seed.ps1
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
-45
@@ -1,45 +0,0 @@
|
||||
# git-cliff configuration — generates CHANGELOG.md from Conventional Commits.
|
||||
# Run via `make changelog`. See https://git-cliff.org.
|
||||
|
||||
[changelog]
|
||||
header = """
|
||||
# Changelog
|
||||
|
||||
All notable changes to this project. Generated from Conventional Commits by git-cliff.\n
|
||||
"""
|
||||
body = """
|
||||
{% if version %}\
|
||||
## {{ version }} — {{ timestamp | date(format="%Y-%m-%d") }}
|
||||
{% else %}\
|
||||
## Unreleased
|
||||
{% endif %}\
|
||||
{% for group, commits in commits | group_by(attribute="group") %}
|
||||
### {{ group | upper_first }}
|
||||
{% for commit in commits %}\
|
||||
- {{ commit.message | upper_first }}{% if commit.breaking %} **[BREAKING]**{% endif %}
|
||||
{% endfor %}\
|
||||
{% endfor %}\n
|
||||
"""
|
||||
trim = true
|
||||
|
||||
[git]
|
||||
conventional_commits = true
|
||||
filter_unconventional = true
|
||||
split_commits = false
|
||||
protect_breaking_commits = true
|
||||
tag_pattern = "v[0-9]*"
|
||||
# CalVer tags: YYYY.MM.PATCH
|
||||
filter_commits = false
|
||||
commit_parsers = [
|
||||
{ message = "^feat", group = "Features" },
|
||||
{ message = "^fix", group = "Bug Fixes" },
|
||||
{ message = "^perf", group = "Performance" },
|
||||
{ message = "^refactor", group = "Refactor" },
|
||||
{ message = "^docs", group = "Documentation" },
|
||||
{ message = "^test", group = "Tests" },
|
||||
{ message = "^ci", group = "CI" },
|
||||
{ message = "^build", group = "Build" },
|
||||
{ message = "^arch", group = "Architecture" },
|
||||
{ message = "^chore", group = "Chores" },
|
||||
{ message = ".*", group = "Other" },
|
||||
]
|
||||
+1
-1
@@ -85,7 +85,7 @@ The five flows form the BDD acceptance backbone (Gherkin scenarios in `tests/acc
|
||||
|
||||
- **Source control & collaboration:** **Gitea** (Respellion self-hosted) — repository, issues, milestones, labels, projects, releases, container registry, wiki, packages.
|
||||
- **CI/CD:** **Gitea Actions** running on Respellion-hosted `act_runner` instances. Workflow files live in `.gitea/workflows/`. Marketplace actions are referenced via absolute URLs (`uses: https://github.com/actions/checkout@v4` or Gitea-hosted equivalents where available) for reproducibility.
|
||||
- **Backend:** .NET 10 (LTS at iteration time), C#, minimal APIs for BFF, MediatR for in-process messaging within Domain Service, EF Core for the projection store and domain DB.
|
||||
- **Backend:** .NET 9 (LTS at iteration time), C#, minimal APIs for BFF, MediatR for in-process messaging within Domain Service, EF Core for the projection store and domain DB.
|
||||
- **Frontend:** Angular (latest LTS) + TypeScript, standalone components + signals, Nx monorepo, NL Design System component library, Angular Testing Library + Playwright.
|
||||
- **Workflow:** Flowable (BPMN + DMN) via Docker image; Postgres for engine store.
|
||||
- **Identity:** Keycloak with pre-seeded realms.
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
# ADR-0001: Loose coupling to upstream Common Ground modules
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-06-03
|
||||
- **Deciders:** Respellion engineering
|
||||
- **Template note:** This is the first ADR and doubles as the worked example of
|
||||
the Nygard template. Copy its shape for new ADRs (`adr-NNNN-title.md`).
|
||||
|
||||
## Context
|
||||
|
||||
This reference application orchestrates several upstream Common Ground modules —
|
||||
OpenZaak (ZGW APIs), Open Notificaties (NRC), Objecten/Objecttypen, Flowable,
|
||||
Keycloak. Each is an independently developed, independently deployed peer. The
|
||||
temptation in a demo is to reach straight into a peer's database or couple to its
|
||||
internal schema to move faster. That coupling is exactly what makes Common Ground
|
||||
landscapes brittle and un-upgradeable in practice.
|
||||
|
||||
We need a stance, recorded up front, on how our services may talk to these peers.
|
||||
|
||||
## Decision
|
||||
|
||||
**We integrate with upstream modules only through their documented public APIs, and
|
||||
we isolate that integration behind explicit anti-corruption boundaries.**
|
||||
|
||||
Concretely (mirrors CLAUDE.md §8):
|
||||
|
||||
1. The **ACL** is the only code that talks to ZGW APIs; no other service constructs
|
||||
ZGW URLs.
|
||||
2. The **Workflow Client** is the only code that talks to Flowable; BPMN models hold
|
||||
no OpenZaak knowledge.
|
||||
3. **Portals talk only to the BFF** — never directly to a backend or a peer module.
|
||||
4. **No direct database access across services or to any peer.** Each service owns
|
||||
its schema; the Read Projection is a rebuildable derived artefact.
|
||||
5. **Idempotency at every event boundary** (the Event Subscriber tolerates duplicate
|
||||
and out-of-order NRC events).
|
||||
|
||||
Bending any of these is an ADR-worthy moment (CLAUDE.md §14): stop and open an
|
||||
`adr-proposal` issue first.
|
||||
|
||||
## Consequences
|
||||
|
||||
**Positive**
|
||||
|
||||
- Upstream modules can be upgraded or swapped behind their APIs without rippling
|
||||
through our services.
|
||||
- Coupling is visible and minimal — anti-corruption code lives in one named place.
|
||||
- The architecture teaches the Common Ground pattern by enforcing it.
|
||||
|
||||
**Negative / costs**
|
||||
|
||||
- More indirection: a translation layer (ACL, Workflow Client) instead of direct
|
||||
calls. Accepted — it's the point.
|
||||
- Eventual consistency across aggregates must be designed for, not assumed away.
|
||||
|
||||
**Follow-up**
|
||||
|
||||
- Each integration slice that touches a boundary references this ADR; new boundary
|
||||
decisions get their own ADR.
|
||||
@@ -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.
|
||||
@@ -1,44 +0,0 @@
|
||||
# ADR-0003: ACL default-fill strategy
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-06-04
|
||||
- **Deciders:** Respellion engineering
|
||||
- **Relates to:** S-04 (#5); builds on ADR-0001 (loose coupling)
|
||||
|
||||
## Context
|
||||
|
||||
The ACL is the only code that talks to ZGW APIs (ADR-0001 / CLAUDE.md §8.1). When the
|
||||
domain asks it to "open a zaak", the domain payload is intentionally free of ZGW
|
||||
specifics — it carries domain facts (e.g. the registrant's BSN), not OpenZaak fields. But
|
||||
OpenZaak's `POST /zaken` requires ZGW-mandatory fields: `bronorganisatie`,
|
||||
`verantwoordelijkeOrganisatie`, `startdatum`, `vertrouwelijkheidaanduiding`, and a
|
||||
`zaaktype` URL. Something has to supply those, and it must not leak into the domain.
|
||||
|
||||
## Decision
|
||||
|
||||
**The ACL default-fills the ZGW-mandatory zaak fields; the domain never sees them.**
|
||||
|
||||
- `bronorganisatie`, `verantwoordelijkeOrganisatie`, `vertrouwelijkheidaanduiding`, and the
|
||||
`zaaktype` URL come from **ACL configuration** (`AclDefaults` options) — not hardcoded,
|
||||
not from the domain. This keeps them operationally manageable (the beheer portal will
|
||||
edit them in S-15) and environment-specific (the seeded BIG zaaktype URL differs per env).
|
||||
- `startdatum` is derived from an injected **clock** (today's date), so it is
|
||||
deterministic in tests.
|
||||
- The mapping from domain payload → ZGW `ZaakRequest` lives entirely inside the ACL
|
||||
(`Application` builds the request from payload + defaults; `Infrastructure` serialises and
|
||||
POSTs it). No other service constructs ZGW payloads or URLs.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive:** the domain stays ZGW-agnostic; ZGW knowledge is in one named place; defaults
|
||||
are config (testable, env-specific, later editable via the beheer portal).
|
||||
- **Cost:** the ACL must be configured per environment (the seeded zaaktype URL, the
|
||||
organisation RSINs). Missing/invalid config fails fast at the ACL boundary.
|
||||
- **Follow-ups:** mapping the BSN onto the zaak (eigenschap/rol), status transitions, and
|
||||
documents are explicitly out of scope for S-04 and get their own slices.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **Defaults in the domain payload** — rejected: leaks ZGW concerns into the domain,
|
||||
violating ADR-0001.
|
||||
- **Hardcoded defaults in code** — rejected: not env-specific, not operationally editable.
|
||||
@@ -1,52 +0,0 @@
|
||||
# ADR-0004: Reqnroll as the BDD acceptance framework
|
||||
|
||||
- **Status:** Accepted
|
||||
- **Date:** 2026-06-04
|
||||
- **Deciders:** Respellion engineering
|
||||
- **Relates to:** S-04 (#5); supports CLAUDE.md §3 (BDD at the use-case level) and §11 (tests pyramid)
|
||||
|
||||
## Context
|
||||
|
||||
CLAUDE.md §11 mandates that each user-visible flow is driven by a Gherkin acceptance
|
||||
scenario living in `tests/acceptance/`, and §3 names "BDD at the use-case level" as a core
|
||||
engineering principle. The foundational slices (S-00…S-03) added no acceptance layer; S-04
|
||||
is the first slice with real domain behaviour to drive, so it is where the BDD framework is
|
||||
introduced. We need a .NET tool that:
|
||||
|
||||
- parses Gherkin `.feature` files and binds steps to C#,
|
||||
- integrates with the existing xUnit test runner (the repo standardises on xUnit), so
|
||||
acceptance tests run under the same `dotnet test` / `make ci` gate as everything else,
|
||||
- is actively maintained on modern .NET (we target net10.0).
|
||||
|
||||
## Decision
|
||||
|
||||
**Use [Reqnroll](https://reqnroll.net/) (`Reqnroll.xUnit`) for acceptance tests.**
|
||||
|
||||
- Reqnroll is the actively-maintained, open-source successor to SpecFlow (which is no longer
|
||||
maintained). It keeps the same Gherkin + `[Binding]` model, so the knowledge transfers.
|
||||
- `Reqnroll.xUnit` generates one xUnit test per scenario, so acceptance tests are discovered
|
||||
and run by the same runner as the unit tests — no second test framework, no extra CI step.
|
||||
- Acceptance projects live under `tests/acceptance/` per the PRD §9 layout. Generated
|
||||
`*.feature.cs` files are build artefacts and are git-ignored.
|
||||
|
||||
## Consequences
|
||||
|
||||
- **Positive:** one assertion/runner stack (xUnit) across unit and acceptance tests; scenarios
|
||||
are written in business language (Dutch domain terms inline) and reviewed as the slice's
|
||||
contract; maintained tooling on net10.0.
|
||||
- **Cost:** a new dependency (`Reqnroll.xUnit`) and its xUnit v2 transitive graph. Reqnroll
|
||||
pulls `xunit.core` but not the assertion library, so the `xunit` metapackage is referenced
|
||||
explicitly to get `Assert`.
|
||||
- **Replaceable by:** hand-written xUnit "scenario" tests with a Given/When/Then helper, at
|
||||
the cost of losing Gherkin as the shared, readable contract — which is the whole point of §3.
|
||||
- **Follow-ups:** the real-OpenZaak integration test (Testcontainers) and the Stryker mutation
|
||||
baseline for S-04 are tracked as their own issues split off #5.
|
||||
|
||||
## Alternatives considered
|
||||
|
||||
- **SpecFlow** — rejected: unmaintained and without an official net10.0 story; Reqnroll is its
|
||||
drop-in successor.
|
||||
- **Plain xUnit Given/When/Then helpers** — rejected for user-visible flows: loses the
|
||||
business-readable Gherkin contract that §3/§11 require. Still fine for unit-level tests.
|
||||
- **Xunit.Gherkin.Quick** — rejected: lighter but less featureful (no hooks/scoped contexts,
|
||||
smaller community) than Reqnroll.
|
||||
@@ -1,46 +0,0 @@
|
||||
# FDS-architectuur — Open Register
|
||||
|
||||
Deze map bevat de architectuurbesluiten en de engineer-documentatie voor de FDS-kant van deze
|
||||
referentie-applicatie: deelnemen aan het Federatief Datastelsel als **afnemer**.
|
||||
|
||||
De strategische inzet, de slices en de portfoliostatus staan in het Innovation Lab-repo,
|
||||
`Respellion/innovation-lab`, onder `projects/open-register-fd/`. Daar staan ook de
|
||||
architectuurblauwdruk, de FDS gap-analyse en de privacy-views.
|
||||
|
||||
## Documenten
|
||||
|
||||
| Document | Waarvoor |
|
||||
|---|---|
|
||||
| [`c4-component-view.md`](c4-component-view.md) | Componentview op niveau 3: ports en adapters, en welke views nog waarde toevoegen |
|
||||
| [`slice-1-proposal.md`](slice-1-proposal.md) | Het bouwbare eerste increment; plak dit in een `poc-voorstel`-issue |
|
||||
| `adr/` | De geaccepteerde architectuurbesluiten, ADR-0001 tot en met ADR-0006. Zie de tabel hieronder. |
|
||||
|
||||
## Architecture Decision Records
|
||||
|
||||
Een ADR legt een besluit vast dat **vaststaat**, met de context en de gevolgen, zodat het niet stil
|
||||
opnieuw wordt uitgevochten. Statuswaarden: `proposed` → `accepted` → (`vervangen door ADR-NNNN` |
|
||||
`deprecated`).
|
||||
|
||||
Een geaccepteerde ADR wijzigen betekent een nieuwe ADR schrijven die de oude vervangt. Wij
|
||||
herschrijven de historie nooit.
|
||||
|
||||
ADRs liggen naast governance. Acceptatie volgt de asynchrone bezwaarronde uit
|
||||
`Respellion/innovation-lab`, `operating-model/operating-model.md`, sectie *Besluitvorming*.
|
||||
|
||||
| ADR | Besluit | Status |
|
||||
|---|---|---|
|
||||
| [0001](adr/0001-acl-at-every-register-boundary.md) | Anti-Corruption Layer op elke registergrens | accepted |
|
||||
| [0002](adr/0002-fsc-for-connectivity.md) | FSC voor connectiviteit tussen organisaties, geen ruwe REST | accepted |
|
||||
| [0003](adr/0003-pbac-via-opa.md) | Policy-based access control via OPA, FTV-klaar | accepted |
|
||||
| [0004](adr/0004-bounded-cache.md) | Begrensde cache; registers blijven systeem van registratie | accepted |
|
||||
| [0005](adr/0005-ldv-verwerkingenlog.md) | Verwerkingenlog via event-emissie, in lijn met LDV | accepted |
|
||||
| [0006](adr/0006-module-boundary-and-reuse.md) | Modulegrens en hergebruikstrategie: in-process → .NET-module → OpenMetadata-feed → gateway op verzoek | accepted |
|
||||
|
||||
## Nummering
|
||||
|
||||
Deze reeks staat los van de ADR-reeks over de referentie-applicatie zelf, die begint bij
|
||||
[`adr-0001-loose-coupling.md`](../adr-0001-loose-coupling.md). Vandaar de eigen map `fds/`: beide
|
||||
reeksen beginnen bij 0001, en de nummers zouden anders botsen.
|
||||
|
||||
Nieuwe FDS-ADR: kopieer [`adr/template.md`](adr/template.md), neem het volgende nummer, en open een
|
||||
pull request.
|
||||
@@ -1,42 +0,0 @@
|
||||
# ADR-0001: Anti-Corruption Layer op elke registergrens
|
||||
|
||||
- **Status:** accepted
|
||||
- **Datum:** 2026-06-13
|
||||
- **Deciders:** Lab Circle (Build, Lead Link)
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
De applicatie bevraagt meerdere registers: BRP, NHR/KVK, en ZGW via OpenZaak. Hun vocabulaires en
|
||||
schema's verschillen van elkaar en van ons domein. Zij veranderen ook zelf mee met de FDS-standaarden.
|
||||
|
||||
Lekt registervocabulaire het domeinmodel in, dan werkt elke wijziging aan de registerzijde door in de
|
||||
bedrijfslogica. Het domein wordt dan een lappendeken van vreemde begrippen in plaats van ubiquitous
|
||||
language.
|
||||
|
||||
## Besluit
|
||||
|
||||
Elk register is bereikbaar via een Anti-Corruption Layer: **één adapter per register**, die een
|
||||
**port** vervult die het domein definieert.
|
||||
|
||||
Adapters doen alleen vertalen en velden versmallen. Zij bevatten geen bedrijfslogica. Het domein
|
||||
spreekt `Persoon` en `Organisatie`, en nooit veldnamen uit BRP of NHR.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** verloop in registers en FDS-standaarden blijft bij de adapter. Het domein blijft stabiel
|
||||
en testbaar. Adapters zijn onafhankelijk vervangbaar, en dat is precies wat de FSC-wissel uit
|
||||
ADR-0002 goedkoop maakt. Het patroon generaliseert naar een herbruikbare ACL-template per register,
|
||||
een Foundations-kandidaat.
|
||||
|
||||
**Negatief en kosten:** één vertaalmap per register om te schrijven en te onderhouden, plus een extra
|
||||
indirectie die engineers moeten respecteren in plaats van omzeilen.
|
||||
|
||||
**Vervolgwerk:** extraheer de ACL-template zodra de tweede adapter bestaat (slice 3).
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
- **Registers direct aanroepen uit de applicatieservices** — afgewezen: dit koppelt bedrijfscode aan
|
||||
registerschema's en aan versies van FDS-standaarden.
|
||||
- **Eén generieke registeradapter** — afgewezen: registers verschillen genoeg dat een generieke
|
||||
abstractie zou gaan lekken of opzwellen. Adapters per register zijn duidelijker.
|
||||
@@ -1,44 +0,0 @@
|
||||
# ADR-0002: FSC voor connectiviteit tussen organisaties, geen ruwe REST
|
||||
|
||||
- **Status:** accepted
|
||||
- **Datum:** 2026-06-13
|
||||
- **Deciders:** Lab Circle, Upstream Liaison
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
Registerbevragingen kruisen een organisatiegrens naar systemen van bronhouders met
|
||||
persoonsgegevens. Het FDS noemt Federatieve Service Connectiviteit (FSC, de opvolger van NLX) als de
|
||||
richting voor connectiviteit: wederzijdse authenticatie op organisatieniveau, autorisatie
|
||||
gecontroleerd tegen een contract en gehandhaafd bij de bron, en symmetrische transactielogging.
|
||||
|
||||
Een ruwe REST-client met mTLS geeft ons geen van de contractadministratie, delegatie of onafhankelijke
|
||||
tweezijdige verantwoording die een FG of auditor nodig heeft.
|
||||
|
||||
## Besluit
|
||||
|
||||
Het FSC Client-component stuurt alle registerbevragingen via een **FSC outway**, de
|
||||
EUPL-referentie-implementatie. De ACL-adapter hangt af van de FSC Client, en niet van een HTTP-client.
|
||||
|
||||
FSC-zaken — contracten, identiteiten, delegatie — leven in dit component, achter de Register Port.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** de autorisatie wordt bij de bron gehandhaafd, en niet op gezag van de aanroeper
|
||||
vertrouwd. Onweerlegbaar loggen aan beide uiteinden maakt onafhankelijke afstemming tegen ons LDV-log
|
||||
mogelijk. Delegatie wordt expliciet meegedragen. Wij lopen in lijn met de FDS-richting, vóór er een
|
||||
verplichting is.
|
||||
|
||||
**Negatief en kosten:** FSC is operationeel zwaarder dan een REST-aanroep — beheer van certificaten en
|
||||
identiteiten, plus een outway die op De Werf moet draaien. De vergelijking FSC tegenover DSP loopt
|
||||
binnen het FDS nog, dus sommige details kunnen schuiven.
|
||||
|
||||
**Vervolgwerk:** valideer het contract- en logginggedrag van de huidige fsc-nlx-implementatie
|
||||
(slice 2). Herzie dit als het FDS voor DSP kiest; ADR-0001 houdt die wissel beperkt tot één component.
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
- **Ruwe REST met mTLS** — afgewezen: geen contractlaag, geen tweezijdig log, en het wijkt af van het
|
||||
FDS.
|
||||
- **Wachten tot het FDS FSC tegenover DSP heeft beslist** — afgewezen: de naad uit ADR-0001 laat ons nu
|
||||
adopteren en later aanpassen. Wachten geeft het voordeel van vroege expertise weg.
|
||||
@@ -1,44 +0,0 @@
|
||||
# ADR-0003: Policy-based access control via OPA, FTV-klaar
|
||||
|
||||
- **Status:** accepted
|
||||
- **Datum:** 2026-06-13
|
||||
- **Deciders:** Lab Circle, FG (geconsulteerd)
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
Elke bevraging van persoonsgegevens uit BRP of NHR is een verwerking die een grondslag en een
|
||||
begrensde doelbinding nodig heeft. Toegangsregels moeten handhaafbaar en auditeerbaar zijn, en
|
||||
wijzigbaar zonder de bedrijfscode opnieuw uit te rollen.
|
||||
|
||||
De Federatieve Toegangsverlening (FTV) van het FDS beweegt naar policy-based access control, maar is
|
||||
nog geen afgeronde standaard.
|
||||
|
||||
## Besluit
|
||||
|
||||
Introduceer een Policy Decision Point met Open Policy Agent (OPA). De applicatieservices roepen de
|
||||
PDP aan — via een Authorisation Port en een PDP Client — **vóór elke registerbevraging**, en geven
|
||||
rol, doel en grondslag mee.
|
||||
|
||||
Policies schrijven wij als code, **geversioneerd in Gitea**, en zij gaan via review naar productie. De
|
||||
PDP staat zo gepositioneerd dat wij bij de komst van FTV alleen het policy-dialect opnieuw uitdrukken,
|
||||
zonder de architectuurgrens te verplaatsen.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** doelbinding en grondslag worden gehandhaafd, en niet alleen gedocumenteerd. De FG kan de
|
||||
werkelijke regels in versiebeheer lezen, waardoor het verwerkingenregister en de gehandhaafde policy
|
||||
naar elkaar toe groeien. Toegangswijzigingen zijn reviewbaar en gedateerd.
|
||||
|
||||
**Negatief en kosten:** BRP-autorisatiebesluiten correct modelleren is juridisch werk, geen
|
||||
engineering. De PDP maakt de handhaving betrouwbaar, niet de policy juist. Daarnaast komt er een
|
||||
component bij om te exploiteren.
|
||||
|
||||
**Vervolgwerk:** een promotiepijplijn voor policies in Gitea Actions. Policies opnieuw uitdrukken zodra
|
||||
FTV stabiliseert. Een FG-review van de policy-set vóórdat er echte persoonsgegevens in komen.
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
- **Rolcontroles in de applicatiecode** — afgewezen: niet auditeerbaar, niet wijzigbaar zonder deploy,
|
||||
en het verspreidt toegangslogica over de codebase.
|
||||
- **Wachten op FTV** — afgewezen: de PBAC-vorm is al duidelijk. Nu OPA, later het FTV-dialect.
|
||||
@@ -1,48 +0,0 @@
|
||||
# ADR-0004: Begrensde cache; registers blijven systeem van registratie
|
||||
|
||||
- **Status:** accepted
|
||||
- **Datum:** 2026-06-13
|
||||
- **Deciders:** Lab Circle, FG (geconsulteerd)
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
*Data bij de bron* verbiedt het behandelen van registerdata als lokale bron van waarheid. Maar BRP of
|
||||
NHR bij elke interactie bevragen is onpraktisch en vergroot de blootstelling.
|
||||
|
||||
Persoonsgegevens zijn de data die wij het minst willen opbouwen. Een onbegrensde cache wordt stil een
|
||||
schaduwregister, met een onbeheerde bewaarverplichting als gevolg.
|
||||
|
||||
## Besluit
|
||||
|
||||
Een **begrensde cache** staat achter een Cache Port, beheerd door een Cache Manager. Vier grenzen
|
||||
gelden.
|
||||
|
||||
| Grens | Wat die betekent |
|
||||
|---|---|
|
||||
| **Tijd** | Een TTL die aan het doel hangt |
|
||||
| **Omvang** | Alleen de werkset van een actieve zaak |
|
||||
| **Gezag** | Antwoordt nooit wat de bron niet zou antwoorden; geen systeem van registratie |
|
||||
| **Adresseerbaarheid** | Gesleuteld op subject, zodat verwijderen op verzoek kan |
|
||||
|
||||
Purge-triggers: het verstrijken van de TTL, het sluiten van de zaak, en een verwijderingsverzoek.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** de prestaties van een lokale kopie, zonder een onbevoegd register te worden. Bewaartermijn
|
||||
en het recht op verwijdering zijn echte operaties, geen hoop. Dit is consistent met zowel
|
||||
AVG-dataminimalisatie als FDS-data-bij-de-bron.
|
||||
|
||||
**Negatief en kosten:** de mapping van doel naar TTL is een beleidsbesluit, samen met de FG en de
|
||||
autorisatievoorwaarden, en geen engineeringconstante. Die is dus makkelijk fout te krijgen. Daarnaast
|
||||
komt de complexiteit van cache-invalidatie erbij.
|
||||
|
||||
**Vervolgwerk:** definieer het beleid voor doel naar TTL met de FG. Maak een toestandsdiagram voor de
|
||||
levensloop van een cache-entry. Documenteer de aanvaardbare veroudering per register.
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
- **Geen cache; altijd de bron bevragen** — afgewezen: onpraktische latency en belasting, en meer
|
||||
blootstelling per aanroep.
|
||||
- **Een onbegrensde of algemene cache** — afgewezen: die wordt een schaduwregister, precies de
|
||||
faalvorm waar de AVG en het FDS beide tegen duwen.
|
||||
@@ -1,42 +0,0 @@
|
||||
# ADR-0005: Verwerkingenlog via event-emissie, in lijn met LDV
|
||||
|
||||
- **Status:** accepted
|
||||
- **Datum:** 2026-06-13
|
||||
- **Deciders:** Lab Circle, FG (geconsulteerd)
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
AVG art. 30 vereist een register van verwerkingsactiviteiten. De FDS-bouwsteen Logboek
|
||||
Dataverwerkingen (LDV) wijst naar een gestandaardiseerd verwerkingslog dat de burger kan bevragen.
|
||||
|
||||
Database-CDC met Debezium legt *datawijzigingen* vast, en niet *verwerkingsgebeurtenissen met
|
||||
doelbinding*. Het is dus geen verwerkingenlog.
|
||||
|
||||
## Besluit
|
||||
|
||||
Elke registeradapter stuurt een **verwerkingsactiviteit-event** naar een eigen Redpanda-topic, via een
|
||||
Verwerking Port en een LDV Emitter. Het event bevat: subjectcategorie, register, velden, doel en
|
||||
doelbinding, grondslag, bevragende rol, en tijdstempel. **Nooit de opgehaalde waarden.**
|
||||
|
||||
Een projectie maakt het log bevraagbaar. De emissie is asynchroon, maar niet over te slaan: de adapter
|
||||
die de Register Port vervult, is dezelfde code die het event uitstuurt.
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** het spoor voor art. 30 en LDV ontstaat als neveneffect van de bevraging, dus het kan niet
|
||||
uit de pas lopen met de werkelijkheid. Het is af te stemmen tegen de tweezijdige logs van FSC
|
||||
(ADR-0002). Het is onderscheidend in een tender.
|
||||
|
||||
**Negatief en kosten:** een topic en een projectie om te exploiteren. Het ontsluiten van het log naar
|
||||
de burger valt buiten de huidige scope; wij produceren het log. Het eventschema vraagt governance.
|
||||
|
||||
**Vervolgwerk:** definieer het schema van het verwerkingsevent. Bouw de bevraagbare projectie. Sluit
|
||||
aan op de LDV-standaard zodra die volwassen wordt; dit is een upstream-kandidaat.
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
- **Debezium-CDC hergebruiken als log** — afgewezen: dat legt datawijzigingen vast, en geen verwerking
|
||||
met doelbinding. Verkeerde semantiek.
|
||||
- **Synchroon loggen in het aanroeppad** — afgewezen: dat koppelt de latency van de bevraging aan het
|
||||
log. Asynchroon maar niet over te slaan geeft zowel snelheid als garantie.
|
||||
@@ -1,68 +0,0 @@
|
||||
# ADR-0006: Modulegrens en hergebruikstrategie voor de governed-access spine
|
||||
|
||||
- **Status:** accepted
|
||||
- **Datum:** 2026-06-13
|
||||
- **Deciders:** Lab Circle (Lead Link, Build, Upstream Liaison)
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
De compliance-spine uit slice 1 bestaat uit de PDP-controle (ADR-0003), gegoverneerd uitgaand verkeer
|
||||
via FSC (ADR-0002), emissie van het verwerkingenlog (ADR-0005), en de begrensde cache (ADR-0004),
|
||||
allemaal achter ports (ADR-0001). Die spine is mogelijk breder herbruikbaar dan alleen in de
|
||||
referentie-applicatie.
|
||||
|
||||
Er spelen twee hergebruikvragen: welke verpakkingsvorm kiezen wij, en hoe verhoudt de spine zich tot
|
||||
andere omgevingen zoals het OpenMetadata-datagovernanceproject?
|
||||
|
||||
Twee verduidelijkingen bepalen het besluit.
|
||||
|
||||
1. **OpenMetadata is geen afnemer.** In het datagovernanceproject is het de catalogus- en
|
||||
lineage-laag over (synthetische) data. Het bevraagt geen BRP of NHR. FSC of de begrensde cache
|
||||
daarin inbouwen zou zinloos zijn. De juiste aansluiting is **integratie van de output van de
|
||||
spine**, en niet het inbouwen van de spine.
|
||||
2. **FSC en de begrensde cache zijn zaken die alleen een afnemer aangaan.** "Maak het herbruikbaar"
|
||||
mag deze niet uitsmeren over componenten die geen registerdata bevragen.
|
||||
|
||||
Nu al een taalonafhankelijke gateway bouwen — vóórdat er een tweede, niet-.NET afnemer bestaat — zou
|
||||
de valkuil van speculatieve architectuur herhalen, die wij voor de capability-laag al hebben
|
||||
afgewezen.
|
||||
|
||||
## Besluit
|
||||
|
||||
Wij nemen een **vraaggestuurde reeks van vier stappen** aan. Elke stap hangt af van echte behoefte, en
|
||||
niet van verwachte behoefte.
|
||||
|
||||
| Stap | Wat | Wanneer |
|
||||
|---|---|---|
|
||||
| 1 | **In-process bewijzen.** Bouw de spine als gewone componenten achter ports, binnen de .NET register-applicatie. Nog geen extractie. Doel: de compliance-invarianten één keer echt valideren. | Slice 1 |
|
||||
| 2 | **Extraheren als .NET-module.** Zodra een tweede .NET-afnemer in zicht is, haal de spine eruit als een geversioneerde .NET-library of SDK. Dit is de ACL-template-extractie die het charter al plant. Herbruikbaar voor .NET-afnemers, en dat is genoeg voor register-reference en zijn broertjes. | Slice 3 |
|
||||
| 3 | **De feed LDV naar OpenMetadata aansluiten.** Route verwerkingsevents uit de LDV-emitter naar OpenMetadata als access- en usage-metadata bij het geclassificeerde asset: wie las welk persoonsgegevensveld, met welk doel, hoe vaak. Optioneel laten classificatietags uit OpenMetadata terugstromen om veldminimalisatie in de ACL aan te sturen. Dit is de concrete brug tussen beide anchor-projecten: integratie, geen inbouw. | Na stap 2 |
|
||||
| 4 | **Alleen op verzoek een taalonafhankelijke gateway bouwen.** Heeft een echte niet-.NET afnemer gegoverneerde registertoegang nodig, verpak de spine dan als zelfstandige sidecar of proxy met een dunne lokale API, met PDP, FSC-egress en LDV erachter. Niet eerder. | Op verzoek |
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** eigen software blijft minimaal. Hergebruik volgt op validatie in plaats van eraan vooraf
|
||||
te gaan. Beide anchor-projecten krijgen een concreet, benoemd integratiepunt (stap 3). Zaken die
|
||||
alleen een afnemer aangaan, blijven ingesloten.
|
||||
|
||||
**Negatief en kosten:** de .NET-module uit stap 2 dient geen niet-.NET afnemers. Dat aanvaarden wij,
|
||||
omdat stap 4 dat geval dekt zodra het echt is. Stap 3 vraagt een afgesproken schema voor het
|
||||
verwerkingsevent, stabiel genoeg voor OpenMetadata om te consumeren.
|
||||
|
||||
**Vervolgwerk:**
|
||||
|
||||
1. Neem stap 3 als expliciet integratiepunt op in beide projectpagina's in het Innovation Lab-repo:
|
||||
`projects/open-register-fd/README.md` en `projects/openmetadata/README.md`.
|
||||
2. Herzie de trigger van stap 4 bij elke portfolio-review. Bouw niet vooruit.
|
||||
3. Regel governance op het schema van het verwerkingsevent; dat is een gedeelde afhankelijkheid van
|
||||
stap 1 en stap 3.
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
- **De taalonafhankelijke gateway vooraf bouwen** — afgewezen: speculatieve architectuur voordat er een
|
||||
tweede afnemer bestaat. De latency en de operationele kosten zijn niet te rechtvaardigen.
|
||||
- **De spine in OpenMetadata inbouwen** — afgewezen: OpenMetadata is geen afnemer. Dit is een
|
||||
categoriefout.
|
||||
- **De spine permanent in-process houden, zonder extractie** — afgewezen: dat geeft het hergebruik
|
||||
tussen projecten en applicaties weg, en dat is een kerndoel van de Open Register-inzet.
|
||||
@@ -1,27 +0,0 @@
|
||||
# ADR-NNNN: <titel>
|
||||
|
||||
- **Status:** proposed
|
||||
- **Datum:** JJJJ-MM-DD
|
||||
- **Deciders:** <rollen>
|
||||
- **Vervangt / vervangen door:** —
|
||||
|
||||
## Context
|
||||
|
||||
<De krachten die spelen: het probleem, de beperkingen, de FDS- en AVG-drijfveren. Waarom er nu een
|
||||
besluit nodig is.>
|
||||
|
||||
## Besluit
|
||||
|
||||
<De keuze, eenvoudig gesteld.>
|
||||
|
||||
## Gevolgen
|
||||
|
||||
**Positief:** <wat dit oplevert>
|
||||
|
||||
**Negatief en kosten:** <wat het kost, en wat wij aanvaarden>
|
||||
|
||||
**Vervolgwerk:** <welk werk dit oproept>
|
||||
|
||||
## Overwogen alternatieven
|
||||
|
||||
<De afgewezen opties, en waarom.>
|
||||
@@ -1,127 +0,0 @@
|
||||
# C4-componentview — register-applicatie en capability-laag
|
||||
|
||||
> Niveau 3, de componentview. Deze view zoomt in op de container van de .NET register-applicatie uit
|
||||
> het L2-containerdiagram. Zij verbindt het geheel op componentniveau — domein, ports, adapters en de
|
||||
> FDS-capability-componenten — en toont waar elk onderdeel externe tooling raakt.
|
||||
>
|
||||
> De hexagonale structuur is expliciet: het domein hangt alleen af van **ports** (interfaces). Elke
|
||||
> concrete capability is een **adapter** die aan een port is gebonden.
|
||||
>
|
||||
> De containerview (L2), de blauwdruk en de privacy-datastroomviews staan in het Innovation Lab-repo,
|
||||
> `Respellion/innovation-lab`, onder `projects/open-register-fd/`.
|
||||
|
||||
```mermaid
|
||||
C4Component
|
||||
title Componentview — register-applicatie (.NET) en de FDS-capability-laag
|
||||
|
||||
Person(user, "Behandelaar", "Behandelt zaken")
|
||||
Container(spa, "Frontend", "Angular + NL Design System", "Zaakinterface")
|
||||
|
||||
Container_Boundary(app, "Register-applicatie (.NET, hexagonaal)") {
|
||||
Component(api, "API / application services", ".NET", "Orkestreert use cases; verklaart doelbinding per vraag")
|
||||
Component(domain, "Domeinmodel", ".NET / DDD", "Ubiquitous language; geen registervocabulaire")
|
||||
|
||||
Component(portReg, "Register Port", "interface", "De vraag van het domein: Personen / Organisaties")
|
||||
Component(portPol, "Authorisation Port", "interface", "mag-deze-verwerking-doorgaan?")
|
||||
Component(portLog, "Verwerking Port", "interface", "leg de verwerkingsgebeurtenis vast")
|
||||
Component(portTm, "Terugmelding Port", "interface", "meld een vermoedelijke fout")
|
||||
Component(portCache, "Cache Port", "interface", "doelgebonden lezen, schrijven en verwijderen")
|
||||
|
||||
Component(aclBrp, "BRP-adapter", ".NET", "Vertaalt domein<->BRP; minimale velden")
|
||||
Component(aclKvk, "NHR/KVK-adapter", ".NET", "Vertaalt domein<->NHR; UBO-bewust")
|
||||
Component(pdpClient, "PDP Client", ".NET -> OPA", "Roept de policy engine; geeft doel en grondslag mee")
|
||||
Component(ldvEmit, "LDV Emitter", ".NET", "Bouwt het verwerkingsevent; publiceert naar Redpanda")
|
||||
Component(fscClient, "FSC Client", ".NET", "Stuurt contractuele aanroepen via de outway")
|
||||
Component(cacheMgr, "Cache Manager", ".NET", "TTL en verwijderen op subjectsleutel")
|
||||
Component(tmHandler, "Terugmelding Handler", ".NET -> Flowable", "Start het terugmeldproces")
|
||||
Component(procClient, "Process Client", ".NET -> Flowable", "Uitvoering van BPMN en DMN")
|
||||
}
|
||||
|
||||
System_Ext(opa, "OPA (PDP)", "Policies geversioneerd in Gitea")
|
||||
System_Ext(fsc, "FSC Outway", "EUPL-referentie-implementatie")
|
||||
System_Ext(flowable, "Flowable", "BPMN + DMN")
|
||||
ContainerDb_Ext(cache, "Begrensde cache", "PostgreSQL")
|
||||
System_Ext(redpanda, "Redpanda", "LDV-topic + CDC")
|
||||
System_Ext(brp, "BRP", "via FSC inway")
|
||||
System_Ext(kvk, "NHR / KVK", "via FSC inway")
|
||||
System_Ext(kanidm, "Kanidm", "OIDC")
|
||||
|
||||
Rel(user, spa, "Gebruikt")
|
||||
Rel(spa, api, "REST/JSON")
|
||||
Rel(kanidm, api, "OIDC", "authenticatie")
|
||||
Rel(api, domain, "Roept aan")
|
||||
Rel(api, portPol, "Controleert vóór de bevraging")
|
||||
Rel(api, portReg, "Vraagt data")
|
||||
Rel(api, portTm, "Dient melding in")
|
||||
Rel(api, procClient, "Voert proces uit")
|
||||
|
||||
Rel(portPol, pdpClient, "gebonden aan")
|
||||
Rel(pdpClient, opa, "besluitverzoek")
|
||||
|
||||
Rel(portReg, aclBrp, "gebonden aan")
|
||||
Rel(portReg, aclKvk, "gebonden aan")
|
||||
Rel(aclBrp, fscClient, "via")
|
||||
Rel(aclKvk, fscClient, "via")
|
||||
Rel(aclBrp, portLog, "stuurt event")
|
||||
Rel(aclKvk, portLog, "stuurt event")
|
||||
Rel(aclBrp, portCache, "leest en schrijft")
|
||||
Rel(aclKvk, portCache, "leest en schrijft")
|
||||
Rel(fscClient, fsc, "contractuele aanroep")
|
||||
Rel(fsc, brp, "mTLS + contract")
|
||||
Rel(fsc, kvk, "mTLS + contract")
|
||||
|
||||
Rel(portLog, ldvEmit, "gebonden aan")
|
||||
Rel(ldvEmit, redpanda, "publiceert")
|
||||
Rel(portCache, cacheMgr, "gebonden aan")
|
||||
Rel(cacheMgr, cache, "slaat op")
|
||||
Rel(portTm, tmHandler, "gebonden aan")
|
||||
Rel(tmHandler, flowable, "start proces")
|
||||
Rel(procClient, flowable, "voert uit")
|
||||
```
|
||||
|
||||
## Hoe je dit leest
|
||||
|
||||
1. **De ports zijn de naad.** Het domein en de application services hangen af van de vijf interfaces,
|
||||
en nooit van adapters. FSC wisselen voor DSP, of OPA voor de latere FTV-client, verandert een
|
||||
adapter — geen port, en niet het domein. Dit is de clock-speed boundary, concreet gemaakt.
|
||||
2. **De compliance-componenten zijn adapters, geen domeinlogica.** De PDP-client, de LDV-emitter, de
|
||||
FSC-client en de cache manager staan allemaal aan de adapterzijde. Een bevraging kan er fysiek niet
|
||||
langs, omdat de adapter die de Register Port vervult dezelfde code is die het LDV-event uitstuurt
|
||||
en via FSC routeert.
|
||||
3. **Slechts twee componenten raken de registers**: de BRP-adapter en de NHR/KVK-adapter. Beide
|
||||
bereiken ze uitsluitend via de FSC-client. Er is geen vierde pad.
|
||||
|
||||
## Componenten tegenover verplichtingen
|
||||
|
||||
| Component | Omvang eigen bouw | Verplichting die het afdekt |
|
||||
|---|---|---|
|
||||
| Domeinmodel | het product | correctheid van de bedrijfsregels |
|
||||
| BRP- en NHR-adapters | dun | dataminimalisatie: vertalen en velden versmallen |
|
||||
| PDP Client | klein | handhaven van grondslag en doelbinding |
|
||||
| LDV Emitter | klein | verwerkingenlog (AVG art. 30 en LDV) |
|
||||
| FSC Client | klein | geautoriseerde, gelogde connectiviteit |
|
||||
| Cache Manager | klein | grenzen aan bewaring, en verwijdering |
|
||||
| Terugmelding Handler | klein | de terugmeldplicht van de afnemer |
|
||||
|
||||
---
|
||||
|
||||
## Aanvullende views die voor engineers waarde hebben
|
||||
|
||||
De diagrammen tot hier verklaren *structuur* en *compliance-intentie*. Engineers die dit bouwen,
|
||||
hebben er nog een aantal nodig. Wij tekenen geen view voordat er iets echt is om te beschrijven, dus
|
||||
elke regel noemt de trigger.
|
||||
|
||||
| # | View | Wat het toevoegt | Trigger |
|
||||
|---|---|---|---|
|
||||
| 1 | **Deploymentview** (C4 deployment, topologie) | Waar elke container op De Werf draait: k3s-namespaces, welke services sidecar zijn en welke een eigen pod (is OPA een sidecar of centraal? waar eindigt de FSC outway?), netwerkpolicies tussen de vlakken van de vertrouwensgrens, en beheer van secrets en mTLS-certificaten voor FSC. Hier worden de privacy*grenzen* echte firewall- en netwerkregels. | Vóór de eerste deploy met meerdere services. **Hoogste waarde als volgende.** |
|
||||
| 2 | **Sequences voor de niet-gelukkige paden** | Wij hebben het gelukkige pad. Engineers hebben de lastige nodig: PDP-*deny* midden in een transactie, een verlopen of ingetrokken FSC-contract, een register-timeout terwijl er een verouderde cache-entry ligt, en een gedeeltelijk NHR-antwoord waarbij een UBO-veld is achtergehouden. Dit bepaalt de foutafhandeling, en hier verstoppen de compliance-randgevallen zich. | Direct na slice 1. |
|
||||
| 3 | **Domeinmodel en ERD** | De bounded contexts en aggregates in het domein, plus het cacheschema: welke persoonsgegevens blijven staan, op welke sleutel, en met welke purge-kolom. Dit is tegelijk het artefact dat de FG beoordeelt voor bewaartermijnen. | Zodra het domein in slice 1 stabiliseert. |
|
||||
| 4 | **Dataclassificatie- en catalogusview** | Elk veld dat een grens kruist, getagd — persoonsgegeven? bijzondere categorie? UBO-beperkt? — en gemapt op zijn classificatie in OpenMetadata. Dit stuurt de GDPR-scrubbingregels en de lineage-tags. | Beter *uit* OpenMetadata gegenereerd zodra die gevuld is, dan met de hand getekend. |
|
||||
| 5 | **Toestandsdiagram: levensloop van een cache-entry** | `fetched` → `valid` (binnen TTL) → `stale` → `purged` (TTL verstreken \| zaak gesloten \| verwijderingsverzoek). Klein, maar het pint de bewaarsemantiek vast die "begrensde cache" nu alleen in prose beschrijft. | Samen met ADR-0004-vervolgwerk. |
|
||||
| 6 | **BPMN-view: de terugmelding-workflow** | Het Flowable-proces zelf: ingediend → verstuurd naar bronhouder → bevestigd → opgelost of afgewezen. Dit is uitvoerbaar BPMN, dus het diagram en de implementatie zijn hetzelfde artefact. | Wanneer de terugmelding-slice start. |
|
||||
| 7 | **Threat model en vertrouwensgrensview** (STRIDE-stijl) | Dreigingen over de vertrouwensgrens leggen: tokendiefstal, cache poisoning, replay tegen FSC, policy bypass, en manipulatie van logs. Past natuurlijk bij de FSC-zoom, en is het anker van het securitygesprek. | Vóór het verwerken van echte persoonsgegevens. |
|
||||
| 8 | **CI/CD- en policy-promotieview** | Hoe OPA-policies en BPMN/DMN-modellen van een pull request naar draaiende configuratie gaan. "Toegangsbeheer is configuratie in Gitea" geldt alleen als er een pijplijn is die review en promotie handhaaft. | Samen met het vervolgwerk uit ADR-0003. |
|
||||
|
||||
**Voorstel voor de volgende twee.** De **deploymentview**, omdat die de privacygrenzen omzet in
|
||||
handhaafbare netwerkpolicy. En de **sequences voor de niet-gelukkige paden**, omdat compliance daar
|
||||
werkelijk breekt.
|
||||
@@ -1,103 +0,0 @@
|
||||
# POC-voorstel — slice 1: walking skeleton (één register, gegoverneerde bevraging)
|
||||
|
||||
> Klaar om in een `poc-voorstel`-issue te plakken, met de labels `build` en `poc`. Dit is het bouwbare
|
||||
> eerste increment dat de architectuurdocumenten beschrijven. Het bewijst met opzet de
|
||||
> *compliance-spine* end-to-end op de dunst mogelijke functionaliteit.
|
||||
|
||||
## Probleem en strategische vraag
|
||||
|
||||
Kunnen wij een registerbevraging demonstreren die *structureel* gegoverneerd is — onmogelijk uit te
|
||||
voeren zonder gehandhaafde grondslag en een automatische regel in het verwerkingenlog — op onze
|
||||
soevereine stack?
|
||||
|
||||
Dit is de geloofwaardigheidstoets achter de hele Open Register-inzet (slice 1 van het charter) en
|
||||
achter de FDS gap-analyse.
|
||||
|
||||
## Hypothese
|
||||
|
||||
Wij verwachten dat het doorverbinden van één registerbevraging door de volledige capability-spine —
|
||||
Register Port → ACL-adapter → PDP-controle → FSC-aanroep → LDV-emissie → begrensde cache — de claim
|
||||
"compliance is structureel" bewijst.
|
||||
|
||||
Wij weten dat wij het goed hebben als een geautomatiseerde test aantoont dat een bevraging **niet** kan
|
||||
voltooien als de PDP weigert, en **altijd** een LDV-event oplevert als de PDP toestaat.
|
||||
|
||||
## Scope ter grootte van één blok
|
||||
|
||||
**Wel in scope**
|
||||
|
||||
| Onderdeel | Wat |
|
||||
|---|---|
|
||||
| Register | **NHR/KVK**, basisgegevens over onderneming en bestuurder. Gekozen boven BRP; zie de slotnotitie. |
|
||||
| Use case | Geef bij een KVK-nummer de geregistreerde organisatie terug aan het domein, voor één verklaard doel. |
|
||||
| Ports | De vijf ports als interface. Concrete adapters: NHR-ACL, PDP-client (OPA), FSC-client met sandbox- of test-outway, LDV-emitter (Redpanda-topic), en cache manager (PostgreSQL met TTL). |
|
||||
| Policy | OPA draait met één handgeschreven voorbeeldpolicy in Gitea: één allow-regel en één deny-geval. |
|
||||
| Log | Verwerkingsevent-schema v0 plus een minimale bevraagbare projectie; een tabelweergave is genoeg. |
|
||||
| Tests | Tests die de twee compliance-invarianten vastleggen: deny blokkeert, allow logt. |
|
||||
|
||||
**Niet in scope** — even belangrijk om op te schrijven.
|
||||
|
||||
1. Afgewerkte interface of NL Design System-schermen, verder dan een dev-harness.
|
||||
2. BRP en paden met veel persoonsgegevens. Die gaan naar slice 2, met een door de FG beoordeelde
|
||||
policy.
|
||||
3. UBO-data. Het regime van beperkte toegankelijkheid valt buiten deze slice.
|
||||
4. De terugmelding-workflow (latere slice), DCAT-export, en Superset-dashboards.
|
||||
5. Echte register-endpoints. Alleen sandbox en stubs.
|
||||
|
||||
## Definition of Done
|
||||
|
||||
- [ ] Een bevraging op KVK-nummer geeft een domein-`Organisatie` terug via de NHR-ACL-adapter, zonder
|
||||
registervocabulaire in het domein (ADR-0001).
|
||||
- [ ] De aanroep loopt via de FSC-client naar een sandbox-outway, en niet via een ruwe HTTP-client
|
||||
(ADR-0002).
|
||||
- [ ] Er vindt geen bevraging plaats tenzij de PDP allow teruggeeft voor de combinatie rol, doel en
|
||||
grondslag (ADR-0003).
|
||||
- [ ] Elke toegestane bevraging stuurt precies één verwerkingsevent naar Redpanda, bevraagbaar in de
|
||||
projectie, zonder opgehaalde waarden (ADR-0005).
|
||||
- [ ] Cache-entries dragen een TTL en een subjectsleutel; een purge-aanroep verwijdert ze (ADR-0004).
|
||||
- [ ] **De tests op de compliance-invarianten slagen in CI:** (a) PDP-deny betekent geen FSC-aanroep;
|
||||
(b) PDP-allow betekent precies één LDV-event; (c) te ruim gevraagde velden bereiken het domein
|
||||
nooit.
|
||||
- [ ] Het geheel draait lokaal uit een gedocumenteerd `compose`- of k3s-manifest met stubs, zonder
|
||||
echte registertoegang.
|
||||
- [ ] ADR-0001 tot en met ADR-0005 zijn vanuit de code gelinkt. Eén nieuwe ADR als er in slice 1 een
|
||||
besluit ontstaat.
|
||||
|
||||
## Acceptatiedemo (bewijs voor de week-3-toets)
|
||||
|
||||
Live: een geslaagde bevraging plus de bijbehorende LDV-regel. Zet daarna de policy op deny en toon
|
||||
dezelfde bevraging geweigerd, zonder registeraanroep en zonder data.
|
||||
|
||||
Dat contrast *is* de demo.
|
||||
|
||||
## Ontvangende Delivery Circle (voorlopig)
|
||||
|
||||
De register-reference Delivery Circle. De Handoff-ontvanger krijgt bij de kickoff een naam.
|
||||
|
||||
Waarschijnlijke adoptie: de capability-spine wordt het herbruikbare substraat voor de
|
||||
register-reference-applicatie.
|
||||
|
||||
## Upstream-kandidaten
|
||||
|
||||
| Project | Wat wij kunnen bijdragen |
|
||||
|---|---|
|
||||
| fsc-nlx | Ergonomie van de sandbox en testomgeving, plus documentatie |
|
||||
| OPA | Policy-patronen voor het modelleren van Nederlandse grondslagen |
|
||||
| OpenMetadata | Later een DCAT-AP-NL exporter; dit verbindt het OpenMetadata-project |
|
||||
|
||||
## AVG- en soevereiniteitsoverwegingen
|
||||
|
||||
Alleen NHR-basisgegevens, over onderneming en bestuurder, en in slice 1 **gestubd**. Er worden geen
|
||||
echte persoonsgegevens verwerkt.
|
||||
|
||||
Een FG-review is een voorwaarde voor slice 2, met echte data en BRP. Alle componenten draaien
|
||||
zelfgehost op De Werf; OPA-policies en BPMN staan in Gitea.
|
||||
|
||||
## Slotnotitie: waarom NHR vóór BRP voor het skeleton
|
||||
|
||||
Beide registers bevatten persoonsgegevens, dus geen van beide is "gratis". NHR-basisgegevens over
|
||||
onderneming en bestuurder zijn echter minder gevoelig dan BRP-gegevens over inwoners, en er is een
|
||||
duidelijker verhaal rond een publieke sandbox.
|
||||
|
||||
Zo bewijst slice 1 het *mechanisme*, voordat slice 2 BRP oppakt onder een door de FG beoordeelde
|
||||
policy. UBO-data blijft buiten scope tot het toegangsregime is gemodelleerd.
|
||||
@@ -1,52 +0,0 @@
|
||||
# Working with Gitea: issues, milestones, PRs
|
||||
|
||||
Gitea is the **system of record** (CLAUDE.md §7). `BACKLOG.md` is a human-readable
|
||||
mirror of the active milestone — when they disagree, Gitea wins.
|
||||
|
||||
## Issues
|
||||
|
||||
- Open issues from the templates in `.gitea/ISSUE_TEMPLATE/`:
|
||||
- **Slice** — a backlog user story (`S-NN · …`), encodes the Definition of Done.
|
||||
- **Bug** — a defect.
|
||||
- **ADR proposal** — a decision to record before coding (CLAUDE.md §14).
|
||||
- Every issue gets `type:*` plus the relevant `area:*` label(s), and is assigned to
|
||||
its iteration **milestone** (`Iteration N — …`).
|
||||
- Splitting a slice that grew too big: see CLAUDE.md §13 and the "How to split a
|
||||
slice" section of `BACKLOG.md`.
|
||||
|
||||
## Branches & commits
|
||||
|
||||
- Trunk-based: short-lived branches off `main`, squash-merged. Never push to `main`.
|
||||
- Branch name: `<type>/<issue-number>-<slug>`, e.g. `feat/28-bff-health`.
|
||||
- Conventional Commits, each referencing its issue: `feat(bff): … (refs #28)`.
|
||||
- TDD order: the `test(...)` red commit precedes the `feat(...)` green commit.
|
||||
|
||||
## Pull requests
|
||||
|
||||
- Open with `tea pr create` (the Gitea CLI) or the web UI; the body uses
|
||||
`.gitea/PULL_REQUEST_TEMPLATE.md` and its DoD checklist.
|
||||
- The merging PR closes its issue via `closes #NN` in the squash-commit body.
|
||||
Work that isn't finished (e.g. CI green pending a runner) uses `refs #NN` and the
|
||||
issue stays open.
|
||||
- A PR needs a linked issue. Don't open one without it.
|
||||
|
||||
## CI gate
|
||||
|
||||
Until a self-hosted `respellion-linux` runner is registered, `make ci` is the gate
|
||||
(it runs the same checks the workflow does). See [runbooks/ci.md](runbooks/ci.md).
|
||||
|
||||
## CLI cheatsheet (`tea`)
|
||||
|
||||
```bash
|
||||
tea issues list --state open # backlog
|
||||
tea issue create --title "S-NN · …" --labels type:slice,area:bff --milestone "Iteration 1 — Walking Skeleton"
|
||||
tea pr create --base main --head <branch> --title "…" --description "… closes #NN"
|
||||
tea pr list # open PRs
|
||||
tea pr merge <n> --style squash # merge (after review)
|
||||
```
|
||||
|
||||
## Changelog & releases
|
||||
|
||||
`CHANGELOG.md` is generated from commits by `git-cliff` (`make changelog`), refreshed
|
||||
on tag. Versioning is **CalVer** `YYYY.MM.PATCH`; releases are published via Gitea
|
||||
Releases.
|
||||
@@ -1,26 +0,0 @@
|
||||
# register-referentie
|
||||
|
||||
A reference application demonstrating Respellion's Common Ground architecture
|
||||
pattern. Quality and architectural clarity over feature throughput — every commit
|
||||
should teach.
|
||||
|
||||
## Where to go
|
||||
|
||||
- **[Product Requirements](PRD.md)** — what we're building and why.
|
||||
- **[ADR-0001: Loose coupling](architecture/adr-0001-loose-coupling.md)** — the
|
||||
non-negotiable integration stance; the template for future ADRs.
|
||||
- **[FDS architecture](architecture/fds/README.md)** — participating in the Federatief
|
||||
Datastelsel as an afnemer: ADR-0001…0006, the L3 component view, the slice-1 proposal.
|
||||
In Dutch; the strategic framing lives in `Respellion/innovation-lab`.
|
||||
- **[Working in Gitea](gitea-workflow.md)** — issues, milestones, branches, PRs.
|
||||
- **[CI runbook](runbooks/ci.md)** — the pipeline and the `make ci` local gate.
|
||||
|
||||
## Quickstart
|
||||
|
||||
See the repository `README.md`. In short: clone, then either run the checks with
|
||||
`make ci`, or bring the BFF up with
|
||||
`docker compose -f infra/docker-compose.yml up -d --build --wait` and
|
||||
`curl http://localhost:8080/health`.
|
||||
|
||||
> This site is built with MkDocs Material (`mkdocs build`). It grows with the
|
||||
> backlog; sections appear as their slices land.
|
||||
@@ -1,104 +0,0 @@
|
||||
# CI runbook — Gitea Actions
|
||||
|
||||
> **Status: no runner yet → run CI locally with `make ci`.** The workflow
|
||||
> `.gitea/workflows/ci.yaml` is in place, but the pipeline cannot go green until a
|
||||
> self-hosted `respellion-linux` runner is registered against the Gitea instance.
|
||||
> Until then, **`make ci` is the gate** — it runs the exact same checks locally
|
||||
> (the workflow calls the same `make` targets). Issue **#30 (S-00-c)** stays open
|
||||
> until CI is verified green on a runner.
|
||||
|
||||
## The pipeline
|
||||
|
||||
`.gitea/workflows/ci.yaml` runs on every push and pull request to `main`. Each job
|
||||
calls a `make` target — the **single source of truth** for the checks, so local
|
||||
and CI cannot drift:
|
||||
|
||||
| Job | Target | Needs |
|
||||
|---|---|---|
|
||||
| `lint` | `make lint` → `dotnet format … --verify-no-changes` | .NET 10 SDK |
|
||||
| `build` | `make build` → `dotnet build … -c Release` | .NET 10 SDK |
|
||||
| `unit` | `make unit` → `dotnet test … -c Release` | .NET 10 SDK |
|
||||
| `compose-smoke` | `make smoke` → compose up `--wait` → `curl /health` → `down` | container engine + compose v2 |
|
||||
|
||||
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
|
||||
Actions resolves them from GitHub.
|
||||
|
||||
## Running CI locally (`make ci`)
|
||||
|
||||
Until the runner exists, run the full pipeline yourself before pushing:
|
||||
|
||||
```bash
|
||||
make ci # lint + build + unit + smoke — what the pipeline runs
|
||||
make lint # or a single stage
|
||||
make smoke # compose up --wait, curl /health, tear down
|
||||
```
|
||||
|
||||
**Prerequisites:** .NET 10 SDK, a container engine with Compose v2, and `curl`.
|
||||
|
||||
On a **rootless Podman** box (the default dev setup here), the `smoke` target needs
|
||||
the Podman API socket and a Compose provider:
|
||||
|
||||
```bash
|
||||
systemctl --user enable --now podman.socket # start the API socket
|
||||
ln -sf "$(command -v podman)" ~/.local/bin/docker # docker -> podman shim
|
||||
# install Docker Compose v2 into ~/.local/bin as `docker-compose` (the provider)
|
||||
```
|
||||
|
||||
The Makefile auto-points `DOCKER_HOST` at `/run/user/$(id -u)/podman/podman.sock`
|
||||
when that socket exists and `DOCKER_HOST` is unset, so `make smoke` "just works"
|
||||
locally while leaving real Docker hosts / CI runners untouched.
|
||||
|
||||
## Runner: `respellion-linux`
|
||||
|
||||
The single self-hosted runner label this repo targets is **`respellion-linux`**
|
||||
(declared here per §15). It is intended to run **co-located on the Gitea server**
|
||||
(`git.labs.respellion.tech` / `46.224.220.37`) so CI is durable and independent of
|
||||
any developer machine.
|
||||
|
||||
### Host prerequisites
|
||||
|
||||
The runner executes jobs in **host mode** (see registration below), so the host
|
||||
must have, on `PATH`:
|
||||
|
||||
- .NET 10 SDK (or let `setup-dotnet` install it into the runner tool cache)
|
||||
- A container engine with Compose v2 — Docker, or Podman with the Docker-compatible
|
||||
socket and the `docker-compose` provider (as configured on the dev box)
|
||||
- `curl`
|
||||
|
||||
### Install & register `act_runner` (on the Gitea server)
|
||||
|
||||
```bash
|
||||
# 1. Install the binary (pick the version matching the Gitea release line)
|
||||
VER=0.2.11
|
||||
curl -fsSL -o /usr/local/bin/act_runner \
|
||||
"https://dl.gitea.com/act_runner/${VER}/act_runner-${VER}-linux-amd64"
|
||||
chmod +x /usr/local/bin/act_runner
|
||||
|
||||
# 2. Obtain a registration token from the Gitea UI:
|
||||
# Site Administration → Actions → Runners → "Create new Runner" (instance-level)
|
||||
# (or Repo → Settings → Actions → Runners for a repo-scoped runner)
|
||||
|
||||
# 3. Register with the respellion-linux label in HOST execution mode.
|
||||
# The ":host" suffix means jobs run directly on the host shell, so
|
||||
# `docker compose` in compose-smoke uses the host engine (no docker-in-docker).
|
||||
act_runner register --no-interactive \
|
||||
--instance https://git.labs.respellion.tech \
|
||||
--token <REGISTRATION_TOKEN> \
|
||||
--name respellion-ci-1 \
|
||||
--labels "respellion-linux:host"
|
||||
|
||||
# 4. Run it (foreground to verify, then install as a systemd service)
|
||||
act_runner daemon
|
||||
```
|
||||
|
||||
Verify in the Gitea UI (Actions → Runners) that `respellion-ci-1` shows **Idle**,
|
||||
then re-run the `CI` workflow; all four jobs should pass.
|
||||
|
||||
## Security note
|
||||
|
||||
A self-hosted runner in **host mode** executes workflow code directly on the Gitea
|
||||
server host. Anyone who can push a workflow can run code there. This is acceptable
|
||||
for a **private lab** instance with trusted contributors. For anything
|
||||
internet-facing, switch to container/VM isolation (`--labels "respellion-linux:docker://..."`)
|
||||
or a dedicated runner host, and gate workflow runs on approval for outside PRs.
|
||||
@@ -1,40 +0,0 @@
|
||||
# Flowable runbook
|
||||
|
||||
Flowable (`infra/flowable/docker-compose.yml`) runs the **flowable-rest** engine on
|
||||
Postgres. The `workflows/registratie.bpmn` model is deployed via the REST API at boot by
|
||||
the `flowable-init` container. Host port **:8090**; REST API under
|
||||
`http://localhost:8090/flowable-rest/service/` (basic auth **rest-admin / test**, dev only).
|
||||
|
||||
## The model — `registratie`
|
||||
|
||||
A minimal "Registratie ontvangen" process: **start → external-worker task
|
||||
`OpenZaakAanmaken` → end**. The external task is where the Workflow Client / ACL will
|
||||
later create the zaak in OpenZaak (S-04/S-05); for now a started instance parks there.
|
||||
|
||||
## Quick test (`make`)
|
||||
|
||||
```bash
|
||||
make flowable-up # start engine + deploy registratie.bpmn on boot
|
||||
make flowable-smoke # start + verify a new instance waits on the external task
|
||||
make flowable-down # stop + wipe
|
||||
```
|
||||
|
||||
`make flowable-smoke` runs `infra/flowable/verify.py`, which:
|
||||
1. waits for the `registratie` process definition to be deployed,
|
||||
2. starts an instance and asserts it did **not** end immediately,
|
||||
3. asserts an execution is parked at activity **`OpenZaakAanmaken`**,
|
||||
4. deletes the test instance.
|
||||
|
||||
## Notes
|
||||
|
||||
- **Deploy on boot** is idempotent: `flowable-init` skips if a deployment named
|
||||
`registratie` already exists (so restarts on the same volume don't pile up versions).
|
||||
- **Dev creds:** `rest-admin` / `test`. Override via the flowable-rest app config for
|
||||
anything beyond local dev.
|
||||
- **Image** `flowable/flowable-rest:latest` — pin a tag when stabilising.
|
||||
- Start an instance by hand:
|
||||
```bash
|
||||
curl -s -u rest-admin:test -H 'Content-Type: application/json' \
|
||||
-d '{"processDefinitionKey":"registratie"}' \
|
||||
http://localhost:8090/flowable-rest/service/runtime/process-instances
|
||||
```
|
||||
@@ -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`.
|
||||
@@ -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.
|
||||
@@ -1,75 +0,0 @@
|
||||
# OpenZaak runbook
|
||||
|
||||
The OpenZaak stack (`infra/openzaak/docker-compose.yml`) is a lean adaptation of the
|
||||
upstream open-zaak dev compose: PostGIS db, redis, a one-shot init that runs
|
||||
migrations, the OpenZaak API, and a celery worker.
|
||||
|
||||
## Quick test (`make`)
|
||||
|
||||
```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 |
|
||||
|---|---|
|
||||
| `GET /zaken/api/v1/zaken` (no JWT) | **403** — `PermissionDenied` ZGW fout (auth enforced) |
|
||||
| `GET /admin/` | **302** — admin login redirect |
|
||||
| `GET /zaken/api/v1/` | **200** — ZGW API schema root |
|
||||
|
||||
> **403, not 401.** OpenZaak's ZGW APIs return `403 PermissionDenied` for a missing
|
||||
> or invalid JWT. The S-01 acceptance text says "401" — that's inaccurate; 403 is the
|
||||
> correct auth-enforced response.
|
||||
|
||||
The admin UI is at <http://localhost:8000/admin/>; the dev superuser is **admin /
|
||||
admin** (from the compose env — dev only).
|
||||
|
||||
## Prerequisites (rootless Podman)
|
||||
|
||||
Same setup as the rest of the repo (see [ci.md](ci.md)):
|
||||
|
||||
```bash
|
||||
systemctl --user start podman.socket # the Docker-API socket the shim talks to
|
||||
```
|
||||
|
||||
The Makefile auto-points `DOCKER_HOST` at the Podman socket when it exists, so the
|
||||
`make openzaak-*` targets work without extra env.
|
||||
|
||||
## Notes
|
||||
|
||||
- **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`.
|
||||
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
|
||||
a known-good tag (ADR-0002 follow-up).
|
||||
@@ -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.
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.203",
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
# Local development stack. Grows service-by-service with each slice.
|
||||
# S-00-b: the placeholder BFF with a /health check.
|
||||
#
|
||||
# docker compose -f infra/docker-compose.yml up -d --build --wait
|
||||
# curl http://localhost:8080/health # -> Healthy
|
||||
services:
|
||||
bff:
|
||||
build:
|
||||
context: ../services/bff
|
||||
dockerfile: Dockerfile
|
||||
image: register-referentie/bff:dev
|
||||
ports:
|
||||
- "8080:8080"
|
||||
healthcheck:
|
||||
test: ["CMD", "curl", "-fsS", "http://localhost:8080/health"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
@@ -1,65 +0,0 @@
|
||||
# Flowable (S-03): the flowable-rest engine on Postgres.
|
||||
# The registratie.bpmn model is deployed via the REST API on startup by flowable-init.
|
||||
#
|
||||
# docker compose -f infra/flowable/docker-compose.yml up -d
|
||||
# # REST API (basic auth) under http://localhost:8090/flowable-rest/service/
|
||||
#
|
||||
# Host port 8090 (8000/8001/8080/8180 are taken by OpenZaak/NRC/BFF/Keycloak).
|
||||
services:
|
||||
flowable-db:
|
||||
image: docker.io/library/postgres:16
|
||||
environment:
|
||||
POSTGRES_USER: flowable
|
||||
POSTGRES_PASSWORD: flowable
|
||||
POSTGRES_DB: flowable
|
||||
volumes:
|
||||
- flowable-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U flowable -d flowable"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks: [cg]
|
||||
|
||||
flowable-rest:
|
||||
image: docker.io/flowable/flowable-rest:latest
|
||||
environment:
|
||||
SPRING_DATASOURCE_DRIVER-CLASS-NAME: org.postgresql.Driver
|
||||
SPRING_DATASOURCE_URL: jdbc:postgresql://flowable-db:5432/flowable
|
||||
SPRING_DATASOURCE_USERNAME: flowable
|
||||
SPRING_DATASOURCE_PASSWORD: flowable
|
||||
ports:
|
||||
- "8090:8080"
|
||||
depends_on:
|
||||
flowable-db:
|
||||
condition: service_healthy
|
||||
networks: [cg]
|
||||
|
||||
# Deploys workflows/registratie.bpmn via the REST API once flowable-rest is up.
|
||||
# Idempotent: skips if a deployment named "registratie" already exists.
|
||||
flowable-init:
|
||||
image: docker.io/curlimages/curl:latest
|
||||
restart: "no"
|
||||
volumes:
|
||||
- ../../workflows/registratie.bpmn:/work/registratie.bpmn:ro,z
|
||||
command:
|
||||
- sh
|
||||
- -c
|
||||
- |
|
||||
base=http://flowable-rest:8080/flowable-rest/service/repository/deployments
|
||||
until curl -sf -u rest-admin:test "$$base" >/dev/null 2>&1; do echo "waiting for flowable-rest..."; sleep 3; done
|
||||
if curl -s -u rest-admin:test "$$base?name=registratie" | grep -q '"name":"registratie"'; then
|
||||
echo "registratie already deployed; skip"
|
||||
else
|
||||
curl -sf -u rest-admin:test -F 'file=@/work/registratie.bpmn;filename=registratie.bpmn' "$$base" >/dev/null && echo "deployed registratie"
|
||||
fi
|
||||
depends_on:
|
||||
flowable-rest:
|
||||
condition: service_started
|
||||
networks: [cg]
|
||||
|
||||
volumes:
|
||||
flowable-db:
|
||||
|
||||
networks:
|
||||
cg:
|
||||
@@ -1,54 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Smoke-check Flowable: the registratie process is deployed, and starting an
|
||||
instance parks it on the OpenZaakAanmaken external task. Stdlib only.
|
||||
"""
|
||||
import base64, json, sys, time, urllib.error, urllib.request
|
||||
|
||||
BASE = "http://localhost:8090/flowable-rest/service"
|
||||
AUTH = "Basic " + base64.b64encode(b"rest-admin:test").decode()
|
||||
|
||||
|
||||
def call(method, path, payload=None):
|
||||
data = json.dumps(payload).encode() if payload is not None else None
|
||||
req = urllib.request.Request(BASE + path, data=data, method=method, headers={
|
||||
"Authorization": AUTH, "Content-Type": "application/json", "Accept": "application/json"})
|
||||
with urllib.request.urlopen(req, timeout=30) as r:
|
||||
return r.status, json.loads(r.read() or "null")
|
||||
|
||||
|
||||
def main():
|
||||
# 1. process definition deployed? (wait for the async init-container deploy)
|
||||
defs = {"total": 0}
|
||||
for _ in range(40):
|
||||
_, defs = call("GET", "/repository/process-definitions?key=registratie")
|
||||
if defs["total"] >= 1:
|
||||
break
|
||||
time.sleep(3)
|
||||
assert defs["total"] >= 1, "registratie process definition not deployed"
|
||||
print(f"process definition 'registratie' deployed (total={defs['total']})")
|
||||
|
||||
# 2. start an instance
|
||||
st, pi = call("POST", "/runtime/process-instances", {"processDefinitionKey": "registratie"})
|
||||
assert st == 201, f"start failed: {st} {pi}"
|
||||
pid = pi["id"]
|
||||
assert pi.get("ended") is False, "instance ended immediately — external task not reached"
|
||||
print(f"started instance {pid} (ended={pi.get('ended')})")
|
||||
|
||||
# 3. waiting on the external task?
|
||||
_, ex = call("GET", f"/runtime/executions?processInstanceId={pid}")
|
||||
activities = [e.get("activityId") for e in ex["data"]]
|
||||
assert "OpenZaakAanmaken" in activities, f"not waiting at OpenZaakAanmaken: {activities}"
|
||||
print(f"instance is waiting at the external task: {activities}")
|
||||
|
||||
# 4. cleanup
|
||||
try:
|
||||
call("DELETE", f"/runtime/process-instances/{pid}")
|
||||
print("cleaned up instance")
|
||||
except urllib.error.HTTPError:
|
||||
pass
|
||||
|
||||
print("flowable smoke OK")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -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()
|
||||
@@ -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:
|
||||
@@ -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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"] }
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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:
|
||||
@@ -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.
|
||||
{}
|
||||
@@ -1,95 +0,0 @@
|
||||
# OpenZaak stack (S-01). Lean adaptation of the upstream open-zaak dev compose:
|
||||
# db (PostGIS) + redis + a one-shot init (migrations) + the API + a celery worker.
|
||||
# Dropped from upstream for leanness: nginx, celery-beat, flower, OTEL.
|
||||
#
|
||||
# docker compose -f infra/openzaak/docker-compose.yml up -d --wait
|
||||
# curl -i http://localhost:8000/zaken/api/v1/zaken # -> 401 (auth required)
|
||||
#
|
||||
# NOTE: image pinned to a tag (not :latest) once a known-good tag is chosen; see
|
||||
# the catalogus-design ADR. Using a tag var with a sensible default for now.
|
||||
services:
|
||||
oz-db:
|
||||
image: docker.io/postgis/postgis:17-3.5
|
||||
environment:
|
||||
POSTGRES_USER: openzaak
|
||||
POSTGRES_PASSWORD: openzaak
|
||||
POSTGRES_DB: openzaak
|
||||
command: postgres -c max_connections=300
|
||||
volumes:
|
||||
- oz-db:/var/lib/postgresql/data
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U openzaak -d openzaak"]
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks: [cg]
|
||||
|
||||
oz-redis:
|
||||
image: docker.io/library/redis:7
|
||||
networks: [cg]
|
||||
|
||||
oz-init:
|
||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
||||
environment: &oz-env
|
||||
DJANGO_SETTINGS_MODULE: openzaak.conf.docker
|
||||
SECRET_KEY: ${OZ_SECRET_KEY:-dev-only-not-for-production}
|
||||
DB_HOST: oz-db
|
||||
DB_NAME: openzaak
|
||||
DB_USER: openzaak
|
||||
DB_PASSWORD: openzaak
|
||||
IS_HTTPS: "no"
|
||||
ALLOWED_HOSTS: "*"
|
||||
CACHE_DEFAULT: oz-redis:6379/0
|
||||
CACHE_AXES: oz-redis:6379/0
|
||||
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"
|
||||
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]
|
||||
|
||||
openzaak:
|
||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
||||
environment: *oz-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:
|
||||
- "8000:8000"
|
||||
depends_on:
|
||||
oz-init:
|
||||
condition: service_completed_successfully
|
||||
networks: [cg]
|
||||
|
||||
oz-celery:
|
||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
||||
environment: *oz-env
|
||||
command: /celery_worker.sh
|
||||
depends_on:
|
||||
oz-init:
|
||||
condition: service_completed_successfully
|
||||
networks: [cg]
|
||||
|
||||
volumes:
|
||||
oz-db:
|
||||
|
||||
networks:
|
||||
cg:
|
||||
@@ -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()
|
||||
@@ -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
|
||||
-59
@@ -1,59 +0,0 @@
|
||||
site_name: register-referentie
|
||||
site_description: Reference application for Respellion's Common Ground architecture pattern
|
||||
docs_dir: docs
|
||||
|
||||
theme:
|
||||
name: material
|
||||
features:
|
||||
- navigation.sections
|
||||
- navigation.top
|
||||
- content.code.copy
|
||||
palette:
|
||||
- scheme: default
|
||||
toggle:
|
||||
icon: material/brightness-7
|
||||
name: Switch to dark mode
|
||||
- scheme: slate
|
||||
toggle:
|
||||
icon: material/brightness-4
|
||||
name: Switch to light mode
|
||||
|
||||
nav:
|
||||
- Home: index.md
|
||||
- Product Requirements: PRD.md
|
||||
- Architecture:
|
||||
- "ADR-0001: Loose coupling": architecture/adr-0001-loose-coupling.md
|
||||
- "ADR-0002: Catalogus design": architecture/adr-0002-catalogus-design.md
|
||||
- "ADR-0003: ACL default-fill": architecture/adr-0003-default-fill.md
|
||||
- "ADR-0004: BDD framework": architecture/adr-0004-bdd-framework.md
|
||||
- FDS-architectuur:
|
||||
- Overzicht: architecture/fds/README.md
|
||||
- Componentview (L3): architecture/fds/c4-component-view.md
|
||||
- "Slice 1: walking skeleton": architecture/fds/slice-1-proposal.md
|
||||
- "ADR-0001: ACL op elke registergrens": architecture/fds/adr/0001-acl-at-every-register-boundary.md
|
||||
- "ADR-0002: FSC voor connectiviteit": architecture/fds/adr/0002-fsc-for-connectivity.md
|
||||
- "ADR-0003: PBAC via OPA": architecture/fds/adr/0003-pbac-via-opa.md
|
||||
- "ADR-0004: Begrensde cache": architecture/fds/adr/0004-bounded-cache.md
|
||||
- "ADR-0005: Verwerkingenlog via events": architecture/fds/adr/0005-ldv-verwerkingenlog.md
|
||||
- "ADR-0006: Modulegrens en hergebruik": architecture/fds/adr/0006-module-boundary-and-reuse.md
|
||||
- "ADR-template (FDS)": architecture/fds/adr/template.md
|
||||
- Working in Gitea: gitea-workflow.md
|
||||
- Runbooks:
|
||||
- CI: runbooks/ci.md
|
||||
|
||||
markdown_extensions:
|
||||
- admonition
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
|
||||
# Many docs referenced by PRD.md land in later slices; don't fail the build on them.
|
||||
validation:
|
||||
nav:
|
||||
omitted_files: warn
|
||||
links:
|
||||
not_found: warn
|
||||
@@ -1,16 +0,0 @@
|
||||
<Solution>
|
||||
<Folder Name="/services/" />
|
||||
<Folder Name="/services/acl/">
|
||||
<Project Path="services/acl/Acl.Api/Acl.Api.csproj" />
|
||||
<Project Path="services/acl/Acl.Application/Acl.Application.csproj" />
|
||||
<Project Path="services/acl/Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
||||
<Project Path="services/acl/Acl.Tests/Acl.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/services/bff/">
|
||||
<Project Path="services/bff/Bff.Api/Bff.Api.csproj" />
|
||||
<Project Path="services/bff/Bff.Tests/Bff.Tests.csproj" />
|
||||
</Folder>
|
||||
<Folder Name="/tests/">
|
||||
<Project Path="tests/acceptance/Acceptance.csproj" />
|
||||
</Folder>
|
||||
</Solution>
|
||||
@@ -1,14 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Acl.Application\Acl.Application.csproj" />
|
||||
<ProjectReference Include="..\Acl.Infrastructure\Acl.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,31 +0,0 @@
|
||||
using Acl.Application;
|
||||
using Acl.Infrastructure;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddSingleton<IClock, SystemClock>();
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
.GetSection("Acl:Defaults").Get<AclDefaults>()
|
||||
?? throw new InvalidOperationException("Missing configuration section 'Acl:Defaults'"));
|
||||
builder.Services.AddSingleton(sp => sp.GetRequiredService<IConfiguration>()
|
||||
.GetSection("Acl:OpenZaak").Get<OpenZaakOptions>()
|
||||
?? throw new InvalidOperationException("Missing configuration section 'Acl:OpenZaak'"));
|
||||
builder.Services.AddHttpClient<IZaakGateway, OpenZaakGateway>();
|
||||
builder.Services.AddScoped<AclService>();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/health", () => "Healthy");
|
||||
|
||||
// The ACL's single operation, exposed as a service endpoint.
|
||||
app.MapPost("/zaken", async (OpenZaakRequest body, AclService acl, CancellationToken ct) =>
|
||||
{
|
||||
var zaakUrl = await acl.OpenZaakAsync(new DomainRegistration(body.Bsn), ct);
|
||||
return Results.Ok(new { zaakUrl = zaakUrl.ToString() });
|
||||
});
|
||||
|
||||
app.Run();
|
||||
|
||||
public sealed record OpenZaakRequest(string Bsn);
|
||||
|
||||
public partial class Program;
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5041",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7260;http://localhost:5041",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,10 +0,0 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>Configured ZGW defaults the ACL fills in (ADR-0003).</summary>
|
||||
public sealed class AclDefaults
|
||||
{
|
||||
public required string Bronorganisatie { get; init; }
|
||||
public required string VerantwoordelijkeOrganisatie { get; init; }
|
||||
public required string Vertrouwelijkheidaanduiding { get; init; }
|
||||
public required Uri ZaaktypeUrl { get; init; }
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>The ACL's single operation: open a zaak from a domain payload,
|
||||
/// default-filling the ZGW-mandatory fields (ADR-0003).</summary>
|
||||
public sealed class AclService(IZaakGateway gateway, AclDefaults defaults, IClock clock)
|
||||
{
|
||||
public Task<Uri> OpenZaakAsync(DomainRegistration registration, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(registration);
|
||||
|
||||
var request = new ZaakRequest(
|
||||
defaults.Bronorganisatie,
|
||||
defaults.VerantwoordelijkeOrganisatie,
|
||||
defaults.Vertrouwelijkheidaanduiding,
|
||||
defaults.ZaaktypeUrl,
|
||||
clock.Today);
|
||||
|
||||
return gateway.OpenZaakAsync(request, ct);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>Domain-language payload handed to the ACL. No ZGW concepts here.</summary>
|
||||
public sealed record DomainRegistration(string Bsn);
|
||||
@@ -1,7 +0,0 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>Abstracts "today" so startdatum default-fill is deterministic in tests.</summary>
|
||||
public interface IClock
|
||||
{
|
||||
DateOnly Today { get; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>Port to the ZGW Zaken API. Implemented in Infrastructure — the only
|
||||
/// code that talks to OpenZaak (ADR-0001).</summary>
|
||||
public interface IZaakGateway
|
||||
{
|
||||
Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Acl.Application;
|
||||
|
||||
/// <summary>The fully default-filled zaak the gateway will create in OpenZaak.</summary>
|
||||
public sealed record ZaakRequest(
|
||||
string Bronorganisatie,
|
||||
string VerantwoordelijkeOrganisatie,
|
||||
string Vertrouwelijkheidaanduiding,
|
||||
Uri Zaaktype,
|
||||
DateOnly Startdatum);
|
||||
@@ -1,13 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Acl.Application\Acl.Application.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,48 +0,0 @@
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
using System.Text.Json.Serialization;
|
||||
using Acl.Application;
|
||||
|
||||
namespace Acl.Infrastructure;
|
||||
|
||||
/// <summary>The only code that talks to OpenZaak's Zaken API (ADR-0001).</summary>
|
||||
public sealed class OpenZaakGateway(HttpClient http, OpenZaakOptions options) : IZaakGateway
|
||||
{
|
||||
public async Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(request);
|
||||
|
||||
using var message = new HttpRequestMessage(
|
||||
HttpMethod.Post, new Uri(options.BaseUrl, "/zaken/api/v1/zaken"))
|
||||
{
|
||||
Content = JsonContent.Create(new ZaakDto(
|
||||
request.Bronorganisatie,
|
||||
request.Zaaktype.ToString(),
|
||||
request.VerantwoordelijkeOrganisatie,
|
||||
request.Startdatum.ToString("yyyy-MM-dd"),
|
||||
request.Vertrouwelijkheidaanduiding)),
|
||||
};
|
||||
message.Headers.Authorization =
|
||||
new AuthenticationHeaderValue("Bearer", ZgwToken.Mint(options.ClientId, options.Secret));
|
||||
// ZRC is a geo API; it requires the CRS headers.
|
||||
message.Headers.Add("Accept-Crs", "EPSG:4326");
|
||||
message.Content.Headers.Add("Content-Crs", "EPSG:4326");
|
||||
|
||||
using var response = await http.SendAsync(message, ct);
|
||||
response.EnsureSuccessStatusCode();
|
||||
|
||||
var created = await response.Content.ReadFromJsonAsync<ZaakCreatedDto>(ct)
|
||||
?? throw new InvalidOperationException("OpenZaak returned an empty zaak response");
|
||||
return new Uri(created.Url);
|
||||
}
|
||||
|
||||
private sealed record ZaakDto(
|
||||
[property: JsonPropertyName("bronorganisatie")] string Bronorganisatie,
|
||||
[property: JsonPropertyName("zaaktype")] string Zaaktype,
|
||||
[property: JsonPropertyName("verantwoordelijkeOrganisatie")] string VerantwoordelijkeOrganisatie,
|
||||
[property: JsonPropertyName("startdatum")] string Startdatum,
|
||||
[property: JsonPropertyName("vertrouwelijkheidaanduiding")] string Vertrouwelijkheidaanduiding);
|
||||
|
||||
private sealed record ZaakCreatedDto(
|
||||
[property: JsonPropertyName("url")] string Url);
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
namespace Acl.Infrastructure;
|
||||
|
||||
/// <summary>Connection + credential config for OpenZaak's ZGW APIs.</summary>
|
||||
public sealed class OpenZaakOptions
|
||||
{
|
||||
public required Uri BaseUrl { get; init; }
|
||||
public required string ClientId { get; init; }
|
||||
public required string Secret { get; init; }
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
using Acl.Application;
|
||||
|
||||
namespace Acl.Infrastructure;
|
||||
|
||||
public sealed class SystemClock : IClock
|
||||
{
|
||||
public DateOnly Today => DateOnly.FromDateTime(DateTime.UtcNow);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Acl.Infrastructure;
|
||||
|
||||
/// <summary>Mints a ZGW (vng-api-common) JWT: HS256 over the standard claims.</summary>
|
||||
internal static class ZgwToken
|
||||
{
|
||||
public static string Mint(string clientId, string secret)
|
||||
{
|
||||
var header = B64Url(JsonSerializer.SerializeToUtf8Bytes(new { alg = "HS256", typ = "JWT" }));
|
||||
var payload = B64Url(JsonSerializer.SerializeToUtf8Bytes(new
|
||||
{
|
||||
iss = clientId,
|
||||
iat = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
client_id = clientId,
|
||||
user_id = "acl",
|
||||
user_representation = "acl",
|
||||
}));
|
||||
var signingInput = $"{header}.{payload}";
|
||||
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
|
||||
var signature = B64Url(hmac.ComputeHash(Encoding.UTF8.GetBytes(signingInput)));
|
||||
return $"{signingInput}.{signature}";
|
||||
}
|
||||
|
||||
private static string B64Url(byte[] bytes) =>
|
||||
Convert.ToBase64String(bytes).TrimEnd('=').Replace('+', '-').Replace('/', '_');
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Acl.Application\Acl.Application.csproj" />
|
||||
<ProjectReference Include="..\Acl.Infrastructure\Acl.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,47 +0,0 @@
|
||||
using Acl.Application;
|
||||
|
||||
namespace Acl.Tests;
|
||||
|
||||
public class AclServiceTests
|
||||
{
|
||||
private sealed class FakeGateway : IZaakGateway
|
||||
{
|
||||
public ZaakRequest? Captured;
|
||||
public Uri Result { get; } = new("http://openzaak/zaken/api/v1/zaken/abc");
|
||||
|
||||
public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Captured = request;
|
||||
return Task.FromResult(Result);
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class FixedClock(DateOnly today) : IClock
|
||||
{
|
||||
public DateOnly Today { get; } = today;
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Opening_a_zaak_default_fills_zgw_fields_and_returns_the_zaak_url()
|
||||
{
|
||||
var gateway = new FakeGateway();
|
||||
var defaults = new AclDefaults
|
||||
{
|
||||
Bronorganisatie = "517439943",
|
||||
VerantwoordelijkeOrganisatie = "517439943",
|
||||
Vertrouwelijkheidaanduiding = "openbaar",
|
||||
ZaaktypeUrl = new("http://openzaak/catalogi/api/v1/zaaktypen/big"),
|
||||
};
|
||||
var service = new AclService(gateway, defaults, new FixedClock(new DateOnly(2026, 6, 4)));
|
||||
|
||||
var url = await service.OpenZaakAsync(new DomainRegistration("123456782"));
|
||||
|
||||
Assert.Equal(gateway.Result, url);
|
||||
var req = Assert.IsType<ZaakRequest>(gateway.Captured);
|
||||
Assert.Equal("517439943", req.Bronorganisatie);
|
||||
Assert.Equal("517439943", req.VerantwoordelijkeOrganisatie);
|
||||
Assert.Equal("openbaar", req.Vertrouwelijkheidaanduiding);
|
||||
Assert.Equal(defaults.ZaaktypeUrl, req.Zaaktype);
|
||||
Assert.Equal(new DateOnly(2026, 6, 4), req.Startdatum);
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
using System.Net;
|
||||
using System.Net.Http.Json;
|
||||
using Acl.Application;
|
||||
using Acl.Infrastructure;
|
||||
|
||||
namespace Acl.Tests;
|
||||
|
||||
public class OpenZaakGatewayTests
|
||||
{
|
||||
private sealed class StubHandler(Func<HttpRequestMessage, Task<HttpResponseMessage>> onSend)
|
||||
: HttpMessageHandler
|
||||
{
|
||||
protected override Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken ct)
|
||||
=> onSend(request);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task Posts_zaak_to_openzaak_with_bearer_and_default_fields_and_returns_url()
|
||||
{
|
||||
HttpRequestMessage? seen = null;
|
||||
string? body = null;
|
||||
var handler = new StubHandler(async req =>
|
||||
{
|
||||
seen = req;
|
||||
body = await req.Content!.ReadAsStringAsync();
|
||||
return new HttpResponseMessage(HttpStatusCode.Created)
|
||||
{
|
||||
Content = JsonContent.Create(new { url = "http://openzaak/zaken/api/v1/zaken/xyz" }),
|
||||
};
|
||||
});
|
||||
var gateway = new OpenZaakGateway(
|
||||
new HttpClient(handler),
|
||||
new OpenZaakOptions { BaseUrl = new("http://openzaak"), ClientId = "cid", Secret = "sec" });
|
||||
var request = new ZaakRequest(
|
||||
"517439943", "517439943", "openbaar",
|
||||
new("http://openzaak/catalogi/api/v1/zaaktypen/big"), new DateOnly(2026, 6, 4));
|
||||
|
||||
var url = await gateway.OpenZaakAsync(request);
|
||||
|
||||
Assert.Equal("http://openzaak/zaken/api/v1/zaken/xyz", url.ToString());
|
||||
Assert.Equal(HttpMethod.Post, seen!.Method);
|
||||
Assert.Equal("http://openzaak/zaken/api/v1/zaken", seen.RequestUri!.ToString());
|
||||
Assert.Equal("Bearer", seen.Headers.Authorization!.Scheme);
|
||||
Assert.False(string.IsNullOrWhiteSpace(seen.Headers.Authorization.Parameter));
|
||||
Assert.Contains("\"bronorganisatie\":\"517439943\"", body);
|
||||
Assert.Contains("\"verantwoordelijkeOrganisatie\":\"517439943\"", body);
|
||||
Assert.Contains("\"vertrouwelijkheidaanduiding\":\"openbaar\"", body);
|
||||
Assert.Contains("\"startdatum\":\"2026-06-04\"", body);
|
||||
Assert.Contains("\"zaaktype\":\"http://openzaak/catalogi/api/v1/zaaktypen/big\"", body);
|
||||
}
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
<Solution>
|
||||
<Project Path="Acl.Api/Acl.Api.csproj" />
|
||||
<Project Path="Acl.Application/Acl.Application.csproj" />
|
||||
<Project Path="Acl.Infrastructure/Acl.Infrastructure.csproj" />
|
||||
<Project Path="Acl.Tests/Acl.Tests.csproj" />
|
||||
</Solution>
|
||||
@@ -1,3 +0,0 @@
|
||||
**/bin
|
||||
**/obj
|
||||
**/*.user
|
||||
@@ -1,9 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,12 +0,0 @@
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
builder.Services.AddHealthChecks();
|
||||
|
||||
var app = builder.Build();
|
||||
|
||||
app.MapGet("/", () => "BFF placeholder");
|
||||
app.MapHealthChecks("/health");
|
||||
|
||||
app.Run();
|
||||
|
||||
// Exposed so the test host (WebApplicationFactory<Program>) can boot the app.
|
||||
public partial class Program;
|
||||
@@ -1,23 +0,0 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/launchsettings.json",
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5249",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7106;http://localhost:5249",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,8 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*"
|
||||
}
|
||||
@@ -1,26 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="10.0.8" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Using Include="Xunit" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Bff.Api\Bff.Api.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,20 +0,0 @@
|
||||
using System.Net;
|
||||
using Microsoft.AspNetCore.Mvc.Testing;
|
||||
|
||||
namespace Bff.Tests;
|
||||
|
||||
public class HealthEndpointTests(WebApplicationFactory<Program> factory)
|
||||
: IClassFixture<WebApplicationFactory<Program>>
|
||||
{
|
||||
[Fact]
|
||||
public async Task Health_endpoint_returns_200_and_reports_healthy()
|
||||
{
|
||||
var client = factory.CreateClient();
|
||||
|
||||
var response = await client.GetAsync("/health");
|
||||
|
||||
Assert.Equal(HttpStatusCode.OK, response.StatusCode);
|
||||
var body = await response.Content.ReadAsStringAsync();
|
||||
Assert.Contains("Healthy", body);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +0,0 @@
|
||||
<Solution>
|
||||
<Project Path="Bff.Api/Bff.Api.csproj" />
|
||||
<Project Path="Bff.Tests/Bff.Tests.csproj" />
|
||||
</Solution>
|
||||
@@ -1,30 +0,0 @@
|
||||
# Multi-stage build for the placeholder BFF (.NET 10).
|
||||
# Build context is services/bff (see infra/docker-compose.yml).
|
||||
FROM mcr.microsoft.com/dotnet/sdk:10.0 AS build
|
||||
WORKDIR /src
|
||||
|
||||
# Restore first (cached unless the csproj changes).
|
||||
COPY Bff.Api/Bff.Api.csproj Bff.Api/
|
||||
RUN dotnet restore Bff.Api/Bff.Api.csproj
|
||||
|
||||
# Then build + publish.
|
||||
COPY Bff.Api/ Bff.Api/
|
||||
RUN dotnet publish Bff.Api/Bff.Api.csproj -c Release -o /app/publish /p:UseAppHost=false
|
||||
|
||||
FROM mcr.microsoft.com/dotnet/aspnet:10.0 AS runtime
|
||||
WORKDIR /app
|
||||
|
||||
# curl is used by the container HEALTHCHECK / compose healthcheck.
|
||||
RUN apt-get update \
|
||||
&& apt-get install -y --no-install-recommends curl \
|
||||
&& rm -rf /var/lib/apt/lists/*
|
||||
|
||||
COPY --from=build /app/publish .
|
||||
|
||||
ENV ASPNETCORE_URLS=http://+:8080
|
||||
EXPOSE 8080
|
||||
|
||||
HEALTHCHECK --interval=5s --timeout=3s --start-period=10s --retries=5 \
|
||||
CMD curl -fsS http://localhost:8080/health || exit 1
|
||||
|
||||
ENTRYPOINT ["dotnet", "Bff.Api.dll"]
|
||||
@@ -1,23 +0,0 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="coverlet.collector" Version="6.0.4" />
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
|
||||
<PackageReference Include="Reqnroll.xUnit" Version="3.3.4" />
|
||||
<PackageReference Include="xunit" Version="2.9.3" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\services\acl\Acl.Application\Acl.Application.csproj" />
|
||||
<ProjectReference Include="..\..\services\acl\Acl.Infrastructure\Acl.Infrastructure.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,27 +0,0 @@
|
||||
# language: en
|
||||
# Drives S-04 (#5). The ACL is the only code that talks to ZGW (ADR-0001); it
|
||||
# default-fills the ZGW-mandatory zaak fields the domain never sees (ADR-0003).
|
||||
# Real-OpenZaak verification is a separate slice — this scenario exercises the
|
||||
# use case against an in-memory stand-in for the Zaken API.
|
||||
Feature: Een zaak openen vanuit een registratie
|
||||
Als domein wil ik de ACL vragen een zaak te openen
|
||||
zodat de ZGW-verplichte velden worden ingevuld zonder dat het domein ze kent.
|
||||
|
||||
Scenario: De ACL vult de ZGW-verplichte velden default in
|
||||
Given a domain registration for BSN "123456782"
|
||||
And the ACL is configured with these defaults:
|
||||
| field | value |
|
||||
| bronorganisatie | 517439943 |
|
||||
| verantwoordelijkeOrganisatie | 517439943 |
|
||||
| vertrouwelijkheidaanduiding | openbaar |
|
||||
| zaaktype | http://openzaak/catalogi/api/v1/zaaktypen/big |
|
||||
And today is "2026-06-04"
|
||||
When the domain asks the ACL to open a zaak
|
||||
Then a zaak is created with these default-filled fields
|
||||
| field | value |
|
||||
| bronorganisatie | 517439943 |
|
||||
| verantwoordelijkeOrganisatie | 517439943 |
|
||||
| vertrouwelijkheidaanduiding | openbaar |
|
||||
| zaaktype | http://openzaak/catalogi/api/v1/zaaktypen/big |
|
||||
| startdatum | 2026-06-04 |
|
||||
And the ACL returns the URL of the created zaak
|
||||
@@ -1,65 +0,0 @@
|
||||
using Acceptance.Support;
|
||||
using Acl.Application;
|
||||
using Reqnroll;
|
||||
using Xunit;
|
||||
|
||||
namespace Acceptance.Steps;
|
||||
|
||||
/// <summary>Bindings for <c>EenZaakOpenen.feature</c>. Reqnroll creates one
|
||||
/// instance per scenario, so instance fields hold scenario-scoped state.</summary>
|
||||
[Binding]
|
||||
public sealed class EenZaakOpenenSteps
|
||||
{
|
||||
private readonly InMemoryZaakGateway _gateway = new();
|
||||
private DomainRegistration? _registration;
|
||||
private AclDefaults? _defaults;
|
||||
private DateOnly _today;
|
||||
private Uri? _returnedUrl;
|
||||
|
||||
[Given("a domain registration for BSN \"(.*)\"")]
|
||||
public void GivenADomainRegistrationForBsn(string bsn)
|
||||
=> _registration = new DomainRegistration(bsn);
|
||||
|
||||
[Given("the ACL is configured with these defaults:")]
|
||||
public void GivenTheAclIsConfiguredWithTheseDefaults(DataTable defaults)
|
||||
{
|
||||
var values = ToFieldMap(defaults);
|
||||
_defaults = new AclDefaults
|
||||
{
|
||||
Bronorganisatie = values["bronorganisatie"],
|
||||
VerantwoordelijkeOrganisatie = values["verantwoordelijkeOrganisatie"],
|
||||
Vertrouwelijkheidaanduiding = values["vertrouwelijkheidaanduiding"],
|
||||
ZaaktypeUrl = new Uri(values["zaaktype"]),
|
||||
};
|
||||
}
|
||||
|
||||
[Given("today is \"(.*)\"")]
|
||||
public void GivenTodayIs(string date)
|
||||
=> _today = DateOnly.Parse(date);
|
||||
|
||||
[When("the domain asks the ACL to open a zaak")]
|
||||
public async Task WhenTheDomainAsksTheAclToOpenAZaak()
|
||||
{
|
||||
var service = new AclService(_gateway, _defaults!, new FixedClock(_today));
|
||||
_returnedUrl = await service.OpenZaakAsync(_registration!);
|
||||
}
|
||||
|
||||
[Then("a zaak is created with these default-filled fields")]
|
||||
public void ThenAZaakIsCreatedWithTheseDefaultFilledFields(DataTable expected)
|
||||
{
|
||||
var request = Assert.IsType<ZaakRequest>(_gateway.Captured);
|
||||
var fields = ToFieldMap(expected);
|
||||
Assert.Equal(fields["bronorganisatie"], request.Bronorganisatie);
|
||||
Assert.Equal(fields["verantwoordelijkeOrganisatie"], request.VerantwoordelijkeOrganisatie);
|
||||
Assert.Equal(fields["vertrouwelijkheidaanduiding"], request.Vertrouwelijkheidaanduiding);
|
||||
Assert.Equal(fields["zaaktype"], request.Zaaktype.ToString());
|
||||
Assert.Equal(fields["startdatum"], request.Startdatum.ToString("yyyy-MM-dd"));
|
||||
}
|
||||
|
||||
[Then("the ACL returns the URL of the created zaak")]
|
||||
public void ThenTheAclReturnsTheUrlOfTheCreatedZaak()
|
||||
=> Assert.Equal(InMemoryZaakGateway.CreatedZaakUrl, _returnedUrl);
|
||||
|
||||
private static Dictionary<string, string> ToFieldMap(DataTable table)
|
||||
=> table.Rows.ToDictionary(r => r["field"], r => r["value"]);
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
using Acl.Application;
|
||||
|
||||
namespace Acceptance.Support;
|
||||
|
||||
/// <summary>A clock pinned to a known date so <c>startdatum</c> is deterministic
|
||||
/// in scenarios (ADR-0003).</summary>
|
||||
public sealed class FixedClock(DateOnly today) : IClock
|
||||
{
|
||||
public DateOnly Today { get; } = today;
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
using Acl.Application;
|
||||
|
||||
namespace Acceptance.Support;
|
||||
|
||||
/// <summary>An in-memory stand-in for the OpenZaak Zaken API. It captures the
|
||||
/// fully default-filled <see cref="ZaakRequest"/> the ACL builds and returns a
|
||||
/// fixed zaak URL, so the acceptance scenario can verify the use case without a
|
||||
/// running OpenZaak (real-OpenZaak verification is a separate slice).</summary>
|
||||
public sealed class InMemoryZaakGateway : IZaakGateway
|
||||
{
|
||||
public static readonly Uri CreatedZaakUrl = new("http://openzaak/zaken/api/v1/zaken/created-123");
|
||||
|
||||
public ZaakRequest? Captured { get; private set; }
|
||||
|
||||
public Task<Uri> OpenZaakAsync(ZaakRequest request, CancellationToken ct = default)
|
||||
{
|
||||
Captured = request;
|
||||
return Task.FromResult(CreatedZaakUrl);
|
||||
}
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL"
|
||||
xmlns:flowable="http://flowable.org/bpmn"
|
||||
xmlns:bpmndi="http://www.omg.org/spec/BPMN/20100524/DI"
|
||||
xmlns:omgdc="http://www.omg.org/spec/DD/20100524/DC"
|
||||
xmlns:omgdi="http://www.omg.org/spec/DD/20100524/DI"
|
||||
targetNamespace="http://respellion.nl/big">
|
||||
|
||||
<!-- S-03: minimal "Registratie ontvangen" flow.
|
||||
start -> external-worker task (OpenZaakAanmaken) -> end.
|
||||
The external task is handled by the Workflow Client / ACL in later slices. -->
|
||||
<process id="registratie" name="Registratie ontvangen" isExecutable="true">
|
||||
|
||||
<startEvent id="start" name="Registratie ontvangen"/>
|
||||
|
||||
<sequenceFlow id="flow1" sourceRef="start" targetRef="OpenZaakAanmaken"/>
|
||||
|
||||
<serviceTask id="OpenZaakAanmaken" name="OpenZaak aanmaken"
|
||||
flowable:type="external-worker"
|
||||
flowable:topic="OpenZaakAanmaken"/>
|
||||
|
||||
<sequenceFlow id="flow2" sourceRef="OpenZaakAanmaken" targetRef="end"/>
|
||||
|
||||
<endEvent id="end" name="Zaak aangemaakt"/>
|
||||
</process>
|
||||
|
||||
<bpmndi:BPMNDiagram id="diagram">
|
||||
<bpmndi:BPMNPlane id="plane" bpmnElement="registratie">
|
||||
<bpmndi:BPMNShape id="s_start" bpmnElement="start">
|
||||
<omgdc:Bounds x="100" y="100" width="30" height="30"/>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="s_task" bpmnElement="OpenZaakAanmaken">
|
||||
<omgdc:Bounds x="200" y="85" width="120" height="60"/>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNShape id="s_end" bpmnElement="end">
|
||||
<omgdc:Bounds x="400" y="100" width="30" height="30"/>
|
||||
</bpmndi:BPMNShape>
|
||||
<bpmndi:BPMNEdge id="e_flow1" bpmnElement="flow1">
|
||||
<omgdi:waypoint x="130" y="115"/>
|
||||
<omgdi:waypoint x="200" y="115"/>
|
||||
</bpmndi:BPMNEdge>
|
||||
<bpmndi:BPMNEdge id="e_flow2" bpmnElement="flow2">
|
||||
<omgdi:waypoint x="320" y="115"/>
|
||||
<omgdi:waypoint x="400" y="115"/>
|
||||
</bpmndi:BPMNEdge>
|
||||
</bpmndi:BPMNPlane>
|
||||
</bpmndi:BPMNDiagram>
|
||||
</definitions>
|
||||
Reference in New Issue
Block a user