Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e1883c2d0e | ||
|
|
ae2169b6ee |
@@ -5,9 +5,6 @@ obj/
|
||||
[Rr]elease/
|
||||
*.user
|
||||
|
||||
# Reqnroll-generated test code (regenerated from *.feature on build)
|
||||
*.feature.cs
|
||||
|
||||
# Test results / coverage
|
||||
[Tt]est[Rr]esults/
|
||||
*.trx
|
||||
|
||||
@@ -5,18 +5,11 @@
|
||||
# `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
|
||||
SLN := services/bff/Bff.slnx
|
||||
COMPOSE := infra/docker-compose.yml
|
||||
HEALTH_URL := http://localhost:8080/health
|
||||
OZ_COMPOSE := infra/openzaak/docker-compose.yml
|
||||
OZ_BASE := http://localhost:8000
|
||||
NRC_COMPOSE := infra/opennotificaties/docker-compose.yml
|
||||
NRC_BASE := http://localhost:8001
|
||||
KC_COMPOSE := infra/keycloak/docker-compose.yml
|
||||
KC_BASE := http://localhost:8180
|
||||
FL_COMPOSE := infra/flowable/docker-compose.yml
|
||||
FL_BASE := http://localhost:8090/flowable-rest/service
|
||||
STACK_FILES := -f $(OZ_COMPOSE) -f $(NRC_COMPOSE)
|
||||
OZ_COMPOSE := infra/openzaak/docker-compose.yml
|
||||
OZ_BASE := http://localhost:8000
|
||||
|
||||
# On a rootless Podman dev box, point Docker CLI/Compose at the Podman socket —
|
||||
# but only if that socket exists and DOCKER_HOST isn't already set, so real
|
||||
@@ -28,7 +21,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
||||
endif
|
||||
endif
|
||||
|
||||
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-seed openzaak-down stack-up stack-smoke stack-down keycloak-up keycloak-smoke keycloak-down flowable-up flowable-smoke flowable-down help
|
||||
.PHONY: ci lint build unit smoke down changelog openzaak-up openzaak-smoke openzaak-down help
|
||||
|
||||
## ci: run the full pipeline — lint, build, unit, smoke (mirrors Gitea Actions)
|
||||
ci: lint build unit smoke
|
||||
@@ -78,69 +71,10 @@ openzaak-smoke:
|
||||
echo "GET /zaken/api/v1/ -> $$root (expect 200)"; test "$$root" = "200"; \
|
||||
echo "OpenZaak smoke OK"'
|
||||
|
||||
## openzaak-seed: bring OpenZaak up and seed the BIG catalogus (idempotent)
|
||||
openzaak-seed: openzaak-up
|
||||
@bash -c 'for i in $$(seq 1 50); do \
|
||||
c=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/catalogi/api/v1/ || true); \
|
||||
[ "$$c" = "200" ] && break; sleep 3; done; echo "OpenZaak ready ($$c)"'
|
||||
python3 infra/openzaak/seed_catalogus.py
|
||||
|
||||
## openzaak-down: stop and remove the OpenZaak stack (wipes data)
|
||||
openzaak-down:
|
||||
docker compose -f $(OZ_COMPOSE) down --volumes
|
||||
|
||||
## stack-up: start OpenZaak + Open Notificaties together (shared network)
|
||||
stack-up:
|
||||
docker compose $(STACK_FILES) up -d
|
||||
|
||||
## stack-smoke: start both, assert OpenZaak (403/302/200) and NRC (302) are reachable
|
||||
stack-smoke: stack-up
|
||||
@bash -c 'set -e; \
|
||||
echo "waiting for OpenZaak + Open Notificaties..."; \
|
||||
for i in $$(seq 1 60); do \
|
||||
oz=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/admin/ || true); \
|
||||
nrc=$$(curl -s -o /dev/null -w "%{http_code}" $(NRC_BASE)/admin/ || true); \
|
||||
[ "$$oz" = "302" ] && [ "$$nrc" = "302" ] && break; sleep 3; done; \
|
||||
z=$$(curl -s -o /dev/null -w "%{http_code}" $(OZ_BASE)/zaken/api/v1/zaken); \
|
||||
echo "OpenZaak /zaken (unauth) -> $$z (expect 403)"; test "$$z" = "403"; \
|
||||
echo "OpenZaak /admin/ -> $$oz (expect 302)"; test "$$oz" = "302"; \
|
||||
echo "Open Notificaties /admin/-> $$nrc (expect 302)"; test "$$nrc" = "302"; \
|
||||
echo "stack smoke OK"'
|
||||
|
||||
## stack-down: stop and remove both stacks (wipes data)
|
||||
stack-down:
|
||||
docker compose $(STACK_FILES) down --volumes
|
||||
|
||||
## keycloak-up: start Keycloak with the four imported realms
|
||||
keycloak-up:
|
||||
docker compose -f $(KC_COMPOSE) up -d
|
||||
|
||||
## keycloak-smoke: start Keycloak, then verify each realm logs in + returns its claim
|
||||
keycloak-smoke: keycloak-up
|
||||
@bash -c 'for i in $$(seq 1 60); do \
|
||||
c=$$(curl -s -o /dev/null -w "%{http_code}" $(KC_BASE)/realms/digid/.well-known/openid-configuration || true); \
|
||||
[ "$$c" = "200" ] && break; sleep 3; done; echo "Keycloak ready ($$c)"'
|
||||
python3 infra/keycloak/check_realms.py
|
||||
|
||||
## keycloak-down: stop and remove Keycloak
|
||||
keycloak-down:
|
||||
docker compose -f $(KC_COMPOSE) down --volumes
|
||||
|
||||
## 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/^## //'
|
||||
|
||||
@@ -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.
|
||||
@@ -9,9 +9,6 @@ should teach.
|
||||
- **[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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
@@ -9,32 +9,9 @@ migrations, the OpenZaak API, and a celery worker.
|
||||
```bash
|
||||
make openzaak-up # start the stack (first run pulls images + migrates: 1-3 min)
|
||||
make openzaak-smoke # start + assert it's up with auth enforced (403/302/200)
|
||||
make openzaak-seed # start + seed the BIG catalogus (idempotent)
|
||||
make openzaak-down # stop and wipe data
|
||||
```
|
||||
|
||||
## Seed the BIG catalogus
|
||||
|
||||
`make openzaak-seed` brings the stack up and runs `infra/openzaak/seed_catalogus.py`,
|
||||
which creates (idempotently, via the ZTC API):
|
||||
|
||||
- catalogus **BIG**
|
||||
- a lean **BIG-REGISTRATIE** zaaktype (concept; only schema-mandatory fields)
|
||||
- a **bsn** eigenschap on it
|
||||
|
||||
then confirms the JWT client can list it. See **ADR-0002** for the design (why the
|
||||
zaaktype stays a concept, why notifications are disabled, why the API not a fixture).
|
||||
|
||||
**JWT client** (provisioned declaratively by `setup_configuration/data.yaml`, **dev only**):
|
||||
|
||||
| | |
|
||||
|---|---|
|
||||
| client_id | `big-reference-seed` |
|
||||
| secret | `insecure-dev-secret-change-me` |
|
||||
| authorizations | `heeft_alle_autorisaties` (all) |
|
||||
|
||||
The seed mints a ZGW JWT (HS256) from these and calls `/catalogi/api/v1/...`.
|
||||
|
||||
`make openzaak-smoke` polls until the API responds, then asserts:
|
||||
|
||||
| Check | Expected |
|
||||
@@ -66,10 +43,8 @@ The Makefile auto-points `DOCKER_HOST` at the Podman socket when it exists, so t
|
||||
- **Not in `make ci`.** The OpenZaak smoke is a separate, heavier check (large image
|
||||
pull + migrations); it is intentionally kept out of `make ci` so the core gate
|
||||
stays fast. Run `make openzaak-smoke` when you touch the OpenZaak stack.
|
||||
- **Notifications disabled.** `NOTIFICATIONS_DISABLED=true` — otherwise ZTC writes 500
|
||||
trying to notify. Open Notificaties is now up (see [opennotificaties.md](opennotificaties.md)),
|
||||
but OZ→NRC delivery wiring + re-enabling lands with **S-06**.
|
||||
- **Zaaktype is a concept**, not published (publishing needs roltypen/statustypen/
|
||||
resultaattypen — beyond the lean seed). List with `?status=alles`.
|
||||
- **No catalogus/zaaktypen yet.** Seeding the `BIG` catalogus + `BIG-registratie`
|
||||
zaaktype and a JWT client is the next slice (**S-01-b**); authenticated zaaktype
|
||||
listing isn't testable until then.
|
||||
- **Image tag.** Currently `openzaak/open-zaak:latest` via `${OPENZAAK_TAG}`; pin to
|
||||
a known-good tag (ADR-0002 follow-up).
|
||||
a known-good tag as part of the catalogus-design ADR.
|
||||
|
||||
@@ -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,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.
|
||||
{}
|
||||
@@ -22,11 +22,11 @@ services:
|
||||
interval: 5s
|
||||
timeout: 3s
|
||||
retries: 10
|
||||
networks: [cg]
|
||||
networks: [oz]
|
||||
|
||||
oz-redis:
|
||||
image: docker.io/library/redis:7
|
||||
networks: [cg]
|
||||
networks: [oz]
|
||||
|
||||
oz-init:
|
||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
||||
@@ -44,24 +44,17 @@ services:
|
||||
CELERY_BROKER_URL: redis://oz-redis:6379/1
|
||||
CELERY_RESULT_BACKEND: redis://oz-redis:6379/1
|
||||
DISABLE_2FA: "true"
|
||||
# Notifications go to Open Notificaties (NRC), which arrives in S-01-c.
|
||||
# Until then, disable outbound notifications so writes don't 500.
|
||||
NOTIFICATIONS_DISABLED: "true"
|
||||
OPENZAAK_SUPERUSER_USERNAME: admin
|
||||
DJANGO_SUPERUSER_PASSWORD: admin
|
||||
OPENZAAK_SUPERUSER_EMAIL: admin@localhost
|
||||
RUN_SETUP_CONFIG: "true"
|
||||
RUN_SETUP_CONFIG: "false"
|
||||
command: /setup_configuration.sh
|
||||
volumes:
|
||||
# :z relabels for SELinux; the dir/file must be world-readable for the
|
||||
# container user (rootless Podman uid mapping). See docs/runbooks/openzaak.md.
|
||||
- ./setup_configuration:/app/setup_configuration:ro,z
|
||||
depends_on:
|
||||
oz-db:
|
||||
condition: service_healthy
|
||||
oz-redis:
|
||||
condition: service_started
|
||||
networks: [cg]
|
||||
networks: [oz]
|
||||
|
||||
openzaak:
|
||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
||||
@@ -77,7 +70,7 @@ services:
|
||||
depends_on:
|
||||
oz-init:
|
||||
condition: service_completed_successfully
|
||||
networks: [cg]
|
||||
networks: [oz]
|
||||
|
||||
oz-celery:
|
||||
image: docker.io/openzaak/open-zaak:${OPENZAAK_TAG:-latest}
|
||||
@@ -86,10 +79,10 @@ services:
|
||||
depends_on:
|
||||
oz-init:
|
||||
condition: service_completed_successfully
|
||||
networks: [cg]
|
||||
networks: [oz]
|
||||
|
||||
volumes:
|
||||
oz-db:
|
||||
|
||||
networks:
|
||||
cg:
|
||||
oz:
|
||||
|
||||
@@ -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
|
||||
+1
-19
@@ -23,20 +23,6 @@ nav:
|
||||
- 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
|
||||
@@ -45,11 +31,7 @@ markdown_extensions:
|
||||
- admonition
|
||||
- toc:
|
||||
permalink: true
|
||||
- pymdownx.superfences:
|
||||
custom_fences:
|
||||
- name: mermaid
|
||||
class: mermaid
|
||||
format: !!python/name:pymdownx.superfences.fence_code_format
|
||||
- pymdownx.superfences
|
||||
|
||||
# Many docs referenced by PRD.md land in later slices; don't fail the build on them.
|
||||
validation:
|
||||
|
||||
@@ -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,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