Compare commits
2
Commits
66f8125ccd
...
3588057a75
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3588057a75 | ||
|
|
f21c3c7ca2 |
@@ -54,3 +54,6 @@ storybook-static
|
||||
|
||||
# WP-54: bootstrap-catalogus.sh's own record of what it seeded into a local OpenZaak run
|
||||
backend/openzaak/seeded.env
|
||||
|
||||
# WP-55: render-prod-secrets.sh's output — the real client secret, never committed
|
||||
backend/openzaak/setup_configuration/data.prod.yaml
|
||||
|
||||
@@ -50,6 +50,40 @@ up, so it never runs where the harness doesn't exist.
|
||||
docker compose -f docker-compose.openzaak.yml down -v
|
||||
```
|
||||
|
||||
## Production (WP-55)
|
||||
|
||||
This dev harness stays dev-only: hardcoded `SECRET_KEY`, `POSTGRES_HOST_AUTH_METHOD=trust`,
|
||||
`IS_HTTPS: 'no'`, a client secret checked into `setup_configuration/data.yaml`. A real
|
||||
deployment layers `docker-compose.openzaak.prod.yml` on top instead of replacing anything:
|
||||
|
||||
```bash
|
||||
export OPENZAAK_SECRET_KEY=... # Django SECRET_KEY — generate, don't reuse the dev value
|
||||
export OPENZAAK_DB_PASSWORD=... # postgres password (switches auth off `trust`)
|
||||
export OPENZAAK_SITE_DOMAIN=... # e.g. open-zaak.example.org — no scheme/port
|
||||
export OPENZAAK_ALLOWED_HOSTS=... # Django ALLOWED_HOSTS, usually the same domain
|
||||
export OPENZAAK_CLIENT_ID=... # the BFF's OpenZaak client id (ZgwOptions:ClientId)
|
||||
export OPENZAAK_CLIENT_SECRET=... # the BFF's JWT signing secret (ZgwOptions:Secret)
|
||||
export OPENZAAK_APPLICATIE_UUID=$(uuidgen)
|
||||
|
||||
./render-prod-secrets.sh # writes the gitignored setup_configuration/data.prod.yaml
|
||||
docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.prod.yml up -d
|
||||
```
|
||||
|
||||
Every one of those env vars is `${VAR:?...}`-checked — compose (and `render-prod-secrets.sh`
|
||||
for the client secret) refuses to start rather than silently falling back to a dev-looking
|
||||
default. There is no env var that "means insecure default"; if it's unset, it's a hard error.
|
||||
|
||||
**TLS**: OpenZaak itself does no certificate handling. Put a reverse proxy/ingress (the same
|
||||
one fronting the BFF) in front of `web`'s `:8000`, terminate TLS there, and forward to
|
||||
`http://web:8000` over the compose network. `IS_HTTPS: 'yes'` in the prod override only tells
|
||||
Django it's being served over HTTPS (secure cookies, `SECURE_*` redirects) — it does not open
|
||||
a TLS listener itself.
|
||||
|
||||
The BFF side needs no code change: `ZgwOptions` already binds `ClientId`/`Secret`/the base
|
||||
URLs from `IConfiguration`, so pointing it at a production OpenZaak is a config change
|
||||
(`Zgw:ClientId`/`Zgw:Secret`/`Zgw:ZrcBaseUrl` etc. via env vars or a secrets manager), not an
|
||||
app change.
|
||||
|
||||
## What's in here / what isn't
|
||||
|
||||
- `docker-compose.openzaak.yml` — postgres (postgis), redis, a one-shot `web-init` (runs
|
||||
@@ -72,3 +106,11 @@ docker compose -f docker-compose.openzaak.yml down -v
|
||||
`procesType` on the public VNG selectielijst API).
|
||||
- **Not here**: Documenten (DRC) / Notificaties (NRC) content — add if a later WP needs to prove
|
||||
those round-trips against a live instance too (WP-51/52 are fixture-tested today).
|
||||
- `docker-compose.openzaak.prod.yml` (WP-55) — production overrides layered on top of
|
||||
`docker-compose.openzaak.yml`: real `SECRET_KEY`/DB password/site domain/allowed-hosts from
|
||||
required env vars (fails fast if unset), password DB auth instead of `trust`, `IS_HTTPS: 'yes'`.
|
||||
Adds no image/service of its own — see "Production" above for the full flow.
|
||||
- `setup_configuration/data.prod.yaml.template` (WP-55) — the prod counterpart of `data.yaml`
|
||||
with no secret in it (`${OPENZAAK_CLIENT_SECRET}` etc. as placeholders); `render-prod-secrets.sh`
|
||||
fills it in to the gitignored `data.prod.yaml`, which the prod compose override mounts over
|
||||
the container's `data.yaml`.
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
# WP-55 — production overrides for docker-compose.openzaak.yml: real secrets, real DB auth,
|
||||
# HTTPS-aware settings. Use ON TOP of the base file, never alone (it has no image/ports of its
|
||||
# own to add — see backend/openzaak/README.md for the required env vars and full flow):
|
||||
#
|
||||
# ./render-prod-secrets.sh # renders setup_configuration/data.prod.yaml (gitignored)
|
||||
# docker compose -f docker-compose.openzaak.yml -f docker-compose.openzaak.prod.yml up -d
|
||||
#
|
||||
# TLS is NOT terminated here — OpenZaak sits behind a reverse proxy/ingress that owns the
|
||||
# certificate; this file only tells OpenZaak (via IS_HTTPS) that it's being served over HTTPS
|
||||
# so it sets secure cookies / redirects correctly.
|
||||
services:
|
||||
db:
|
||||
environment:
|
||||
- POSTGRES_HOST_AUTH_METHOD=md5
|
||||
- POSTGRES_PASSWORD=${OPENZAAK_DB_PASSWORD:?OPENZAAK_DB_PASSWORD must be set}
|
||||
|
||||
web-init:
|
||||
environment:
|
||||
SECRET_KEY: ${OPENZAAK_SECRET_KEY:?OPENZAAK_SECRET_KEY must be set}
|
||||
DB_PASSWORD: ${OPENZAAK_DB_PASSWORD:?OPENZAAK_DB_PASSWORD must be set}
|
||||
IS_HTTPS: 'yes'
|
||||
SITE_DOMAIN: ${OPENZAAK_SITE_DOMAIN:?OPENZAAK_SITE_DOMAIN must be set}
|
||||
ALLOWED_HOSTS: ${OPENZAAK_ALLOWED_HOSTS:?OPENZAAK_ALLOWED_HOSTS must be set}
|
||||
DISABLE_2FA: 'false'
|
||||
volumes:
|
||||
# Shadows the dev data.yaml (still mounted read-only from the base file) with the
|
||||
# secret-free template rendered by render-prod-secrets.sh.
|
||||
- ./setup_configuration/data.prod.yaml:/app/setup_configuration/data.yaml:ro
|
||||
|
||||
web:
|
||||
environment:
|
||||
SECRET_KEY: ${OPENZAAK_SECRET_KEY:?OPENZAAK_SECRET_KEY must be set}
|
||||
DB_PASSWORD: ${OPENZAAK_DB_PASSWORD:?OPENZAAK_DB_PASSWORD must be set}
|
||||
IS_HTTPS: 'yes'
|
||||
SITE_DOMAIN: ${OPENZAAK_SITE_DOMAIN:?OPENZAAK_SITE_DOMAIN must be set}
|
||||
ALLOWED_HOSTS: ${OPENZAAK_ALLOWED_HOSTS:?OPENZAAK_ALLOWED_HOSTS must be set}
|
||||
DISABLE_2FA: 'false'
|
||||
Executable
+16
@@ -0,0 +1,16 @@
|
||||
#!/usr/bin/env bash
|
||||
# WP-55 — renders setup_configuration/data.prod.yaml.template into the gitignored
|
||||
# data.prod.yaml docker-compose.openzaak.prod.yml mounts over the container's data.yaml.
|
||||
# Run this once before `docker compose ... up` in a production deploy; re-run whenever the
|
||||
# secrets rotate. Fails fast (no output file) if a required env var is missing — never
|
||||
# silently falls back to a real-looking default.
|
||||
set -euo pipefail
|
||||
cd "$(dirname "${BASH_SOURCE[0]}")"
|
||||
|
||||
: "${OPENZAAK_SITE_DOMAIN:?OPENZAAK_SITE_DOMAIN must be set (e.g. open-zaak.example.org)}"
|
||||
: "${OPENZAAK_CLIENT_ID:?OPENZAAK_CLIENT_ID must be set}"
|
||||
: "${OPENZAAK_CLIENT_SECRET:?OPENZAAK_CLIENT_SECRET must be set}"
|
||||
: "${OPENZAAK_APPLICATIE_UUID:?OPENZAAK_APPLICATIE_UUID must be set (a fresh UUID, e.g. \$(uuidgen))}"
|
||||
|
||||
envsubst < setup_configuration/data.prod.yaml.template > setup_configuration/data.prod.yaml
|
||||
echo "Wrote setup_configuration/data.prod.yaml"
|
||||
@@ -0,0 +1,28 @@
|
||||
# Prod counterpart of data.yaml (WP-54's dev-only version, kept as-is for local iteration —
|
||||
# see docker-compose.openzaak.yml's own comment on why it hardcodes a client secret). This
|
||||
# template has no secret in it; render-prod-secrets.sh substitutes OPENZAAK_CLIENT_SECRET
|
||||
# into it to produce the gitignored data.prod.yaml that docker-compose.openzaak.prod.yml
|
||||
# mounts over the container's data.yaml.
|
||||
#
|
||||
# Least-privilege client scopes (heeft_alle_autorisaties: true below) are WP-57's job, not
|
||||
# this WP's — left matching the dev harness on purpose.
|
||||
sites_config_enable: true
|
||||
sites_config:
|
||||
items:
|
||||
- domain: ${OPENZAAK_SITE_DOMAIN}
|
||||
name: OpenZaak (production)
|
||||
|
||||
vng_api_common_credentials_config_enable: true
|
||||
vng_api_common_credentials:
|
||||
items:
|
||||
- identifier: ${OPENZAAK_CLIENT_ID}
|
||||
secret: ${OPENZAAK_CLIENT_SECRET}
|
||||
|
||||
vng_api_common_applicaties_config_enable: true
|
||||
vng_api_common_applicaties:
|
||||
items:
|
||||
- uuid: ${OPENZAAK_APPLICATIE_UUID}
|
||||
client_ids:
|
||||
- ${OPENZAAK_CLIENT_ID}
|
||||
label: BIG-register BFF (production)
|
||||
heeft_alle_autorisaties: true
|
||||
@@ -105,6 +105,18 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-53](WP-53-inbound-identity-and-citizen-scoping.md) | Inbound identity seam + citizen-scoping (per-request BSN, ZGW audit claims) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-55](WP-55-openzaak-secrets-tls.md) | Real secrets + TLS for the OpenZaak harness | 10 · OpenZaak hardening | done |
|
||||
| [WP-56](WP-56-openzaak-catalogus-provisioning.md) | Idempotent catalogus provisioning | 10 · OpenZaak hardening | todo |
|
||||
| [WP-57](WP-57-openzaak-least-privilege-scopes.md) | Least-privilege client scopes | 10 · OpenZaak hardening | todo |
|
||||
| [WP-58](WP-58-openzaak-notifications.md) | Real notifications (celery + scripted abonnement) | 10 · OpenZaak hardening | todo |
|
||||
| [WP-59](WP-59-document-confidentialiteit-config.md) | Per-document-type confidentialiteit config | 10 · OpenZaak hardening | todo |
|
||||
| [WP-60](WP-60-write-divergence-resilience.md) | Write-divergence resilience (local + ZGW writes) | 10 · OpenZaak hardening | todo |
|
||||
| [WP-61](WP-61-behandelportal-bootstrap.md) | Bootstrap the behandelportal app | 11 · Behandelportal | todo |
|
||||
| [WP-62](WP-62-medewerker-identity-authz.md) | Backend: medewerker caller identity + authz seam | 11 · Behandelportal | todo |
|
||||
| [WP-63](WP-63-aanvraag-status-lifecycle.md) | Backend: aanvraag status lifecycle as a published DTO | 11 · Behandelportal | todo |
|
||||
| [WP-64](WP-64-behandelportal-werkvoorraad.md) | Behandelportal: werkvoorraad (queue) screen | 11 · Behandelportal | todo |
|
||||
| [WP-65](WP-65-behandelportal-beoordeling.md) | Behandelportal: zaak detail + beoordeling (decision) screen | 11 · Behandelportal | todo |
|
||||
| [WP-66](WP-66-behandelportal-openzaak-write.md) | Wire the decision into OpenZaak | 11 · Behandelportal | todo |
|
||||
|
||||
Sequencing dependencies (stated in the WPs too): 01 before 10–15 (axe covers story churn);
|
||||
03/04 before 05–09 (boundaries stop new violations during refactors); 06 before 07 (typed
|
||||
@@ -137,6 +149,17 @@ deployment of 49–52) and **54** (a docker OpenZaak harness + opt-in integratio
|
||||
CRUD arc and can land any time; 54 depends on 49 (something to read) and unlocks realistic
|
||||
testing for the rest. Both are self-contained (each WP file carries its own current-state
|
||||
handoff) and sized for a fresh Sonnet session.
|
||||
Phase 10 (OpenZaak production hardening, WP-55..60) and Phase 11 (Behandelportal,
|
||||
WP-61..66) are two independent tracks that can be worked concurrently — neither blocks
|
||||
the other. Within phase 10: 55/59/60 are fully independent; 57 and 58 both build on 56's
|
||||
provisioning mechanism, otherwise independent of each other. Within phase 11: 61
|
||||
(bootstrap), 62 (backend medewerker identity), and 63 (backend status DTO) are
|
||||
independent of each other and can land in any order; 64 needs all three (61 for the app
|
||||
to exist, 62 for identity, 63 for the status it reads); 65 needs 64; 66 needs 65 and
|
||||
benefits from — but doesn't strictly require — phase 10's WP-60 landing first (WP-66 is
|
||||
a second, currently-unprotected write pair otherwise). WP-60 is the one slice in phase 10
|
||||
sized for a `planner`-agent kickoff rather than direct implementation — its Decisions
|
||||
block is deliberately left open (outbox vs. retry+reconcile).
|
||||
|
||||
## WP template
|
||||
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
# WP-55 — Real secrets + TLS for the OpenZaak harness
|
||||
|
||||
Status: done
|
||||
Phase: 10 — OpenZaak production hardening
|
||||
|
||||
## Why
|
||||
|
||||
`backend/openzaak/docker-compose.openzaak.yml` is explicitly a throwaway dev/test harness:
|
||||
`SECRET_KEY: wp-54-local-harness-not-for-prod`, `POSTGRES_HOST_AUTH_METHOD=trust` (no DB
|
||||
password), `IS_HTTPS: 'no'`, `DISABLE_2FA: 'true'`. Before anything else in this phase can
|
||||
be called "production," the instance needs real secrets, real DB auth, and TLS. The BFF
|
||||
side is already fine — `ZgwOptions.cs` binds from `IConfiguration`, so this is a deploy-config
|
||||
change, not application code.
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md)
|
||||
- `backend/openzaak/README.md`
|
||||
- [ADR-0005 — OpenZaak behind the BFF](../reference/architecture/0005-openzaak-behind-bff.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Secrets come from the deployment environment (env vars / secrets manager), never
|
||||
checked into compose or appsettings.
|
||||
- TLS termination happens at a reverse proxy/ingress in front of OpenZaak — OpenZaak
|
||||
itself doesn't need built-in cert handling.
|
||||
- The existing dev harness stays as-is for local iteration (WP-54's trimmed rig is
|
||||
intentional and still valuable); this WP adds a production compose/override or an
|
||||
env-driven parameterization of the same file, not a replacement of the dev rig.
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/openzaak/docker-compose.openzaak.yml` (or a new `docker-compose.openzaak.prod.yml` override)
|
||||
- `backend/openzaak/README.md`
|
||||
- `backend/src/BigRegister.Api/appsettings*.json` / `Zgw/ZgwOptions.cs` (confirm only, likely no change)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Parameterize `SECRET_KEY`, DB user/password, and the ZGW JWT secret via env vars;
|
||||
remove hardcoded values from the committed file.
|
||||
2. Switch `POSTGRES_HOST_AUTH_METHOD` from `trust` to password auth, password from env.
|
||||
3. Set `IS_HTTPS: 'yes'`; document the required reverse-proxy/ingress TLS termination.
|
||||
4. Update `backend/openzaak/README.md` with the required env vars and the TLS note.
|
||||
5. Confirm the BFF's JWT secret already comes from config — no code change expected.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [x] No secret value is hardcoded in any committed compose/config file.
|
||||
- [x] The production compose fails fast (or docs state clearly) when secrets aren't
|
||||
supplied — no silent fallback to a real-looking default.
|
||||
- [x] README documents exactly which env vars must be set and how TLS is terminated.
|
||||
|
||||
## Deviation from the original plan
|
||||
|
||||
The WP's own "Files" section expected the secret to be parameterized directly inside
|
||||
`docker-compose.openzaak.yml`'s environment or a straight env-var override. That covers
|
||||
`SECRET_KEY`/DB password/`IS_HTTPS` fine (compose does key-based environment merging across
|
||||
`-f` files even though the base file writes some blocks as YAML mappings and others as
|
||||
anchors), but the ZGW client secret lives inside `setup_configuration/data.yaml`, a file
|
||||
OpenZaak's own `setup_configuration` management command reads — compose has no mechanism to
|
||||
interpolate env vars _inside_ a mounted file's contents. Solved by templating that one file
|
||||
(`data.prod.yaml.template`, no secret) + a tiny host-side `render-prod-secrets.sh`
|
||||
(`envsubst`, fail-fast via `${VAR:?...}`) that produces a gitignored `data.prod.yaml`, which
|
||||
`docker-compose.openzaak.prod.yml` mounts over the container's `data.yaml` (bind-mounting a
|
||||
single file inside an already bind-mounted read-only directory works fine in Docker/Podman —
|
||||
verified via `docker compose config` with the override applied). No new dependency: `envsubst`
|
||||
is part of `gettext`, already present on this machine.
|
||||
|
||||
Verified for real: `docker compose -f docker-compose.openzaak.yml -f
|
||||
docker-compose.openzaak.prod.yml config` succeeds with all required env vars set and both
|
||||
environment overrides (SECRET_KEY, DB password/auth method) present in the merged output;
|
||||
fails with a clear `${VAR:?...}` error when any is missing. `render-prod-secrets.sh` itself
|
||||
fails fast (tested) when `OPENZAAK_CLIENT_SECRET` etc. are unset, and its rendered
|
||||
`data.prod.yaml` was inspected and matched the template with real values substituted.
|
||||
`cd backend && dotnet test` (WP-54 harness untouched): 159/159 green. The dev harness
|
||||
(`docker-compose.openzaak.yml` alone, `setup_configuration/data.yaml`) is untouched.
|
||||
|
||||
## Verification
|
||||
|
||||
`docker compose -f backend/openzaak/docker-compose.openzaak.yml config` with required env
|
||||
vars set; `cd backend && dotnet test` (WP-54 harness tests unaffected); manual: the local
|
||||
dev harness still works with its dev-only values documented as dev-only.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Catalogus provisioning (WP-56), client scope narrowing (WP-57), notifications (WP-58).
|
||||
|
||||
## Risks
|
||||
|
||||
If TLS/secrets docs lag an actual deploy, someone could ship with dev defaults — mitigate
|
||||
by making the prod compose fail without required env vars rather than silently defaulting.
|
||||
@@ -0,0 +1,73 @@
|
||||
# WP-56 — Idempotent catalogus provisioning
|
||||
|
||||
Status: todo
|
||||
Phase: 10 — OpenZaak production hardening
|
||||
|
||||
## Why
|
||||
|
||||
`backend/openzaak/bootstrap-catalogus.sh` seeds catalogus/zaaktype/statustype/roltype/zaak
|
||||
via hand-rolled curl+JWT and is explicitly **not idempotent** (fails on `domein`+`rsin`
|
||||
uniqueness on rerun) — fine for a one-shot WP-54 harness, wrong for an environment that
|
||||
needs to be rebuildable. OpenZaak already ships a documented, scripted alternative — the
|
||||
`setup_configuration` mechanism (already used in the harness for the JWTSecret/Applicatie,
|
||||
see `setup_configuration/data.yaml`) — this WP extends that same mechanism to the catalogus
|
||||
content too.
|
||||
|
||||
## Read first
|
||||
|
||||
- `backend/openzaak/bootstrap-catalogus.sh`
|
||||
- `backend/openzaak/setup_configuration/data.yaml`
|
||||
- `backend/openzaak/docker-compose.openzaak.yml` (`web-init` service)
|
||||
- OpenZaak's own `setup_configuration` / `openzaak_config_cli` docs (upstream)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Move catalogus/zaaktype/statustype/roltype provisioning into `setup_configuration`,
|
||||
run by the existing `web-init` one-shot service, instead of the separate curl script.
|
||||
- Keep `bootstrap-catalogus.sh` only for whatever content `setup_configuration` genuinely
|
||||
can't express (e.g. a demo zaak instance) — confirm what's left at kickoff.
|
||||
- Provisioning must be safe to run against an already-provisioned instance — either
|
||||
genuinely idempotent, or the compose is structured to only run it once per fresh
|
||||
volume (document which, don't leave it ambiguous).
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/openzaak/setup_configuration/data.yaml`
|
||||
- `backend/openzaak/bootstrap-catalogus.sh` (trim to whatever remains)
|
||||
- `backend/openzaak/docker-compose.openzaak.yml`
|
||||
- `backend/openzaak/README.md`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Express the catalogus/zaaktype/statustype/roltype definitions currently created by
|
||||
curl as `setup_configuration` YAML.
|
||||
2. Wire it into the `web-init` command alongside the existing JWTSecret/Applicatie config.
|
||||
3. Trim `bootstrap-catalogus.sh` to only what setup_configuration can't cover, if anything.
|
||||
4. Test: tear down + `docker compose up` twice in a row (fresh volume, then existing
|
||||
volume); confirm no failure on rerun.
|
||||
5. Update the README describing the provisioning flow.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Catalogus/zaaktype/statustype/roltype provisioning is declarative
|
||||
(`setup_configuration`), not imperative curl.
|
||||
- [ ] Running the compose stack up twice in a row doesn't error.
|
||||
- [ ] WP-54's `OpenZaakIntegrationTests` still pass unchanged (same content, different
|
||||
provisioning mechanism).
|
||||
|
||||
## Verification
|
||||
|
||||
`docker compose -f backend/openzaak/docker-compose.openzaak.yml up` twice in a row (fresh
|
||||
volume, then existing volume); `cd backend && dotnet test --filter Category=Integration`
|
||||
against the harness.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Secrets/TLS (WP-55, unrelated but sequenced first in the table only by number), client
|
||||
scopes (WP-57), notifications (WP-58).
|
||||
|
||||
## Risks
|
||||
|
||||
OpenZaak's `setup_configuration` coverage for zaaktype/besluittype content may be
|
||||
incomplete upstream — if a piece genuinely can't be expressed declaratively, keep it in a
|
||||
clearly-labeled idempotent script rather than forcing a bad fit.
|
||||
@@ -0,0 +1,61 @@
|
||||
# WP-57 — Least-privilege client scopes
|
||||
|
||||
Status: todo
|
||||
Phase: 10 — OpenZaak production hardening
|
||||
|
||||
## Why
|
||||
|
||||
The harness's OpenZaak client is granted `heeft_alle_autorisaties: true` in
|
||||
`setup_configuration/data.yaml` — acceptable for a disposable test rig, wrong for anything
|
||||
closer to production, where the BFF's client should hold only the Autorisaties it actually
|
||||
exercises.
|
||||
|
||||
## Read first
|
||||
|
||||
- `backend/openzaak/setup_configuration/data.yaml`
|
||||
- `backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs` and `OpenZaakDocumentSource.cs`
|
||||
(the actual ZGW endpoints/verbs called)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Scope precisely to what the BFF calls today: zaken (aanmaken, bijwerken, lezen),
|
||||
statussen (aanmaken), rollen (aanmaken), documenten/zaakinformatieobjecten (aanmaken,
|
||||
lezen) — enumerate exactly at kickoff from the client code, don't guess broader.
|
||||
- No wildcard/all-scope grant in any environment beyond the pre-WP-56 disposable dev rig.
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/openzaak/setup_configuration/data.yaml` (Autorisaties block)
|
||||
- `backend/openzaak/README.md`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Grep `OpenZaakZaakSource.cs` and `OpenZaakDocumentSource.cs` for every ZGW
|
||||
endpoint/verb called.
|
||||
2. Replace `heeft_alle_autorisaties: true` with an explicit `autorisaties` list matching
|
||||
exactly that set.
|
||||
3. Re-run the full integration suite against the narrowed client; add any scope a 403
|
||||
surfaces.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Client config has no wildcard/all-scopes grant.
|
||||
- [ ] `OpenZaakIntegrationTests` (WP-54) pass unchanged against the narrowed client.
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test --filter Category=Integration` against the harness with the
|
||||
narrowed client.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Rotating/expiring the client credential itself — defer until multi-tenant/production ops
|
||||
actually need it.
|
||||
|
||||
## Risks
|
||||
|
||||
An overlooked scope only surfaces as a runtime 403 against a real instance — mitigated by
|
||||
running the full integration suite, which already exercises every current call path
|
||||
(WP-54).
|
||||
|
||||
Depends on: WP-56 (provisioning mechanism this scopes down).
|
||||
@@ -0,0 +1,68 @@
|
||||
# WP-58 — Real notifications (celery + scripted abonnement)
|
||||
|
||||
Status: todo
|
||||
Phase: 10 — OpenZaak production hardening
|
||||
|
||||
## Why
|
||||
|
||||
The WP-54 harness deliberately trims celery/celery-beat/celery-flower and nginx, and sets
|
||||
`NOTIFICATIONS_DISABLED: 'true'` — without a Celery worker, OpenZaak 500s and rolls back on
|
||||
every write to a notified resource. Fine for a fixture-driven integration harness; a real
|
||||
deployment that wants live Notificaties (WP-52's webhook) needs the workers running and the
|
||||
`abonnement` (subscription) actually registered against the BFF's public callback URL —
|
||||
today that registration step is manual.
|
||||
|
||||
## Read first
|
||||
|
||||
- `backend/openzaak/docker-compose.openzaak.yml` (top-of-file ponytail note)
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md) (Notificaties section)
|
||||
- [WP-52](WP-52-openzaak-notificaties.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Add celery + celery-beat as additional compose services (same `openzaak/open-zaak`
|
||||
image, different command), pointed at the same redis broker already in the harness.
|
||||
- Registering the `abonnement` becomes a scripted, idempotent step — not a manual
|
||||
admin-UI action — parameterized by the BFF's real public URL.
|
||||
- Keep the existing WP-54 harness variant (`NOTIFICATIONS_DISABLED: 'true'`) available for
|
||||
fast local iteration where a live webhook round-trip isn't needed; this WP is additive
|
||||
(a "with notifications" profile/override), not a replacement.
|
||||
|
||||
## Files
|
||||
|
||||
- `backend/openzaak/docker-compose.openzaak.yml` (or an override file)
|
||||
- New script/config for `abonnement` registration
|
||||
- `docs/reference/openzaak-integration.md`
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add celery/celery-beat services to a notifications-enabled compose profile.
|
||||
2. Flip `NOTIFICATIONS_DISABLED` off for that profile.
|
||||
3. Script the `abonnement` registration (POST to the NRC, pointed at the BFF's
|
||||
`/zgw/notificaties` endpoint from WP-52), idempotent on rerun.
|
||||
4. Verify a real write (e.g. a status change) triggers a live webhook delivery to the BFF.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A notifications-enabled harness profile runs celery/celery-beat and delivers a real
|
||||
notification end-to-end to the BFF's webhook.
|
||||
- [ ] The `abonnement` registration step is a script, re-runnable without erroring on an
|
||||
already-registered subscription.
|
||||
|
||||
## Verification
|
||||
|
||||
Bring up the notifications-enabled profile; create a zaak/status change; confirm the BFF's
|
||||
`/zgw/notificaties` endpoint receives and logs it.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Cache invalidation on notification receipt (flagged separately in
|
||||
`openzaak-integration.md` as a `ponytail:` marker, not part of this slice);
|
||||
celery-flower/monitoring UI.
|
||||
|
||||
## Risks
|
||||
|
||||
Celery/celery-beat add real operational surface (another process to keep alive) — scope
|
||||
this WP to "works, documented," not a fully monitored deployment.
|
||||
|
||||
Depends on: WP-56 (provisioning mechanism this extends).
|
||||
@@ -0,0 +1,65 @@
|
||||
# WP-59 — Per-document-type confidentialiteit config
|
||||
|
||||
Status: todo
|
||||
Phase: 10 — OpenZaak production hardening
|
||||
|
||||
## Why
|
||||
|
||||
`OpenZaakDocumentSource` hardcodes `vertrouwelijkheidaanduiding` to `"openbaar"` for every
|
||||
uploaded document, regardless of document type. Real BIG-register documents (diploma's, ID
|
||||
scans) plausibly need different confidentiality levels. This repo already has a house
|
||||
pattern for exactly this kind of business-tunable value — stamdata-as-code (ADR-0004) — so
|
||||
this slice is "apply the existing pattern," not invent a new one.
|
||||
|
||||
## Read first
|
||||
|
||||
- [ADR-0004 — Stamdata as code](../reference/architecture/0004-stamdata-as-code.md)
|
||||
- `backend/src/BigRegister.Api/Stamdata/` (an existing table for the shape to imitate)
|
||||
- `backend/src/BigRegister.Api/Zgw/OpenZaakDocumentSource.cs`
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Confidentiality level is keyed by document type (whatever type already distinguishes
|
||||
uploads, e.g. diploma vs. id-bewijs) via a new Stamdata table, using the existing
|
||||
`StamdataTable.Of<T>` mechanism — not a new ad hoc config format.
|
||||
- Default/fallback value stays `"openbaar"` if a document type isn't in the table, to
|
||||
avoid a silent upload failure.
|
||||
|
||||
## Files
|
||||
|
||||
- `Stamdata/` (new table + validation)
|
||||
- `Zgw/OpenZaakDocumentSource.cs`
|
||||
- `StamdataCatalog.cs` (register the new table)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add a `DocumentConfidentialiteit` stamdata table (document type →
|
||||
vertrouwelijkheidaanduiding), validated at build like every other stamdata table
|
||||
(`StamdataValidationTests`).
|
||||
2. Register it in `StamdataCatalog` so it's editable via the existing `/beheer/stamdata`
|
||||
grid.
|
||||
3. `OpenZaakDocumentSource` looks up the level by document type instead of hardcoding
|
||||
`"openbaar"`.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Confidentiality level for a real upload varies by document type per the new
|
||||
stamdata table.
|
||||
- [ ] `StamdataValidationTests` cover the new table (a bad edit fails CI, per ADR-0004).
|
||||
- [ ] `/beheer/stamdata` can edit the new table without a code change (existing generic
|
||||
editor).
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; manual: `/beheer/stamdata` shows and edits the new table; an
|
||||
upload for a mapped document type carries the mapped confidentiality level (test
|
||||
asserted).
|
||||
|
||||
## Out of scope
|
||||
|
||||
Any UI-facing confidentiality display/change on the citizen side (FE keeps rendering
|
||||
decisions, not recomputing them, per ADR-0001).
|
||||
|
||||
## Risks
|
||||
|
||||
None significant — this is a config/data-shape change reusing an established mechanism.
|
||||
@@ -0,0 +1,74 @@
|
||||
# WP-60 — Write-divergence resilience (local + ZGW writes)
|
||||
|
||||
Status: todo
|
||||
Phase: 10 — OpenZaak production hardening
|
||||
|
||||
## Why
|
||||
|
||||
A citizen action today does a local `Aanvraag`/`Document` write and a paired ZGW write
|
||||
(create zaak/status/document); these aren't transactional. If the ZGW call fails after the
|
||||
local write succeeds (or vice versa), the two diverge silently —
|
||||
`openzaak-integration.md` flags this explicitly as "acceptable for a demo backend; a
|
||||
production arc needs retry/reconciliation or an outbox." This is the one genuine
|
||||
correctness gap standing between the current integration and something safe to call
|
||||
production.
|
||||
|
||||
## Read first
|
||||
|
||||
- [openzaak-integration.md](../reference/openzaak-integration.md) (the section discussing
|
||||
this gap)
|
||||
- `backend/src/BigRegister.Api/Data/ApplicationStore.cs`,
|
||||
`Zgw/OpenZaakZaakSource.cs` (the two write sides)
|
||||
- [ADR-0005 — OpenZaak behind the BFF](../reference/architecture/0005-openzaak-behind-bff.md)
|
||||
|
||||
## Decisions
|
||||
|
||||
Intentionally left open for kickoff — this is exactly the kind of ambiguous-root-cause,
|
||||
multi-file design call the `planner` agent should make, not something pre-decided here.
|
||||
Options to weigh at kickoff:
|
||||
|
||||
- (a) an outbox table — write local + an outbox row in one local transaction, a background
|
||||
worker drains the outbox to ZGW with retry.
|
||||
- (b) a simpler synchronous retry-with-backoff at the call site, plus a reconciliation job
|
||||
that periodically diffs local vs. ZGW state and flags/repairs divergence.
|
||||
|
||||
Pick the smaller one that closes the gap — don't build a generic outbox framework if a
|
||||
bounded retry+reconcile suffices for this POC's actual write volume.
|
||||
|
||||
## Files
|
||||
|
||||
Likely `Data/ApplicationStore.cs`, a new reconciliation/outbox mechanism,
|
||||
`Zgw/OpenZaakZaakSource.cs`, `Program.cs` (background job registration if needed).
|
||||
|
||||
## Steps
|
||||
|
||||
1. Design review with the `planner` agent — pick outbox vs. retry+reconcile.
|
||||
2. Implement the chosen mechanism for the create-zaak and status-transition write paths.
|
||||
3. Add a test that simulates a ZGW failure mid-write and asserts the system recovers
|
||||
(retries successfully, or is left in a detectably-inconsistent-but-flagged state)
|
||||
rather than silently diverging.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A simulated ZGW failure after a successful local write no longer leaves permanent
|
||||
silent divergence — either it retries to consistency or the divergence is
|
||||
detectable/flagged.
|
||||
- [ ] No new synchronous latency added to the happy path beyond what the chosen mechanism
|
||||
requires.
|
||||
|
||||
## Verification
|
||||
|
||||
A new integration test that fails a stubbed ZGW call mid-write and asserts
|
||||
recovery/flagging behavior; `cd backend && dotnet test`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
A general-purpose outbox framework reusable beyond this one write pair (YAGNI unless a
|
||||
second write pair appears — note WP-66 is exactly that second pair, so revisit scope if
|
||||
WP-66 lands first); UI surfacing of reconciliation state (backend-only fix for now).
|
||||
|
||||
## Risks
|
||||
|
||||
Over-building this (a generic outbox/saga framework) for a POC's actual write volume —
|
||||
ladder check at kickoff: does a bounded retry + periodic reconcile job cover it before
|
||||
reaching for an outbox table?
|
||||
@@ -0,0 +1,62 @@
|
||||
# WP-61 — Bootstrap the behandelportal app
|
||||
|
||||
Status: todo
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
|
||||
ADR-0002 already designed the Behandelaar/backoffice as a separate sibling frontend app,
|
||||
not a folder in this repo. Nothing exists yet — `/beheer/zaken` is confirmed to be only a
|
||||
cross-owner list+delete, no treatment workflow. The `new-ssp` skill exists precisely to
|
||||
bootstrap a new portal from this template; this slice is running that recipe for real,
|
||||
with no business context yet — an empty, correctly-scaffolded shell.
|
||||
|
||||
## Read first
|
||||
|
||||
- `.claude/skills/new-ssp/SKILL.md`
|
||||
- [ADR-0002 — user groups & bounded contexts](../reference/architecture/0002-user-groups-and-bounded-contexts.md)
|
||||
- [ADR-0001 — BFF-lite decision DTOs](../reference/architecture/0001-bff-lite-decision-dtos.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Follow `new-ssp`'s own "keep vs. strip" checklist as-is: keep the `shared/ui` kernel,
|
||||
tooling/CI gates, ADRs 0001-0003; strip the four citizen contexts and citizen branding.
|
||||
- The new app talks to the same `BigRegister.Api` backend — no new backend service
|
||||
(confirmed by ADR-0002: contexts integrate through the backend).
|
||||
- Repo layout for the new app (separate repo vs. a second app in this monorepo) — decide
|
||||
at kickoff based on how `new-ssp` is meant to be invoked.
|
||||
|
||||
## Files
|
||||
|
||||
Whatever `new-ssp`'s recipe touches (new app root, `package.json`, shared/ui copy or
|
||||
workspace reference, CI config) — enumerate at kickoff by following the skill.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run the `new-ssp` bootstrap per its own checklist.
|
||||
2. Confirm the known un-genericizable rough edges it flags (`shared/ui/debug-state/`, the
|
||||
`/dashboard` route) are handled per the skill's own guidance (delete / TODO stopgap)
|
||||
rather than re-solved from scratch.
|
||||
3. Land an empty landing/login page only — no `behandeling` context yet (that's WP-64+).
|
||||
4. Get the new app's own CI green.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] New app boots and its own `npm run ci` is green.
|
||||
- [ ] No citizen-facing business context (`registratie`, `herregistratie`, `brief`,
|
||||
`showcase`) present.
|
||||
- [ ] Points at the same backend (`BigRegister.Api`) as this repo, no new backend stood
|
||||
up.
|
||||
|
||||
## Verification
|
||||
|
||||
`npm run ci` in the new app; manual smoke — app loads to an empty shell page.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Any `behandeling` screens (WP-64/65), identity (WP-62), status lifecycle (WP-63).
|
||||
|
||||
## Risks
|
||||
|
||||
`new-ssp`'s own docs already name its rough edges (`debug-state`, `/dashboard`) — budget
|
||||
time for those rather than being surprised by them.
|
||||
@@ -0,0 +1,73 @@
|
||||
# WP-62 — Backend: medewerker caller identity + authz seam
|
||||
|
||||
Status: todo
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
|
||||
The backend's only identity today is `CallerIdentity` (BSN + display name, from WP-53)
|
||||
modeling a single zorgverlener actor. ADR-0002 requires a second actor kind
|
||||
(medewerker/employee) that authenticates differently (no BSN, has `rollen`) and needs its
|
||||
own capability checks for backoffice calls. This slice adds that identity + authz surface
|
||||
on the backend only — unused by any frontend until WP-64 calls it, matching the same
|
||||
"seam, not provider" discipline WP-53 used for citizen identity (stub, no real employee
|
||||
SSO — out of scope per CLAUDE.md, same as DigiD).
|
||||
|
||||
## Read first
|
||||
|
||||
- [ADR-0002 §3 — Principal union](../reference/architecture/0002-user-groups-and-bounded-contexts.md)
|
||||
- `backend/src/BigRegister.Api/Domain/Authorization/CallerIdentity.cs`,
|
||||
`IIdentityProvider.cs`, `StubIdentityProvider.cs` (WP-53's pattern to extend/mirror)
|
||||
- [WP-53](WP-53-inbound-identity-and-citizen-scoping.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Model the two actor kinds as a discriminated union (mirroring ADR-0002 §3:
|
||||
`{ kind: 'zorgverlener'; bsn }` | `{ kind: 'medewerker'; medewerkerId; rollen }`),
|
||||
backend-side, extending `CallerIdentity` rather than introducing a parallel type.
|
||||
- Stub the medewerker identity the same way WP-53 stubbed citizen identity (a
|
||||
header-driven `StubIdentityProvider` variant) — no real employee SSO/eHerkenning.
|
||||
- New capability checks (e.g. `canBeoordelen`) are computed backend-side and exposed only
|
||||
as decision flags, never a permission matrix shipped to a frontend (ADR-0001 discipline,
|
||||
reaffirmed by ADR-0002 §3).
|
||||
|
||||
## Files
|
||||
|
||||
- `Domain/Authorization/CallerIdentity.cs` (extend to the union)
|
||||
- `Domain/Authorization/StubIdentityProvider.cs` (medewerker variant)
|
||||
- `Domain/Authorization/Authz.cs` (medewerker capability checks)
|
||||
- Tests
|
||||
|
||||
## Steps
|
||||
|
||||
1. Extend `CallerIdentity` to the two-actor-kind union.
|
||||
2. Extend the stub identity provider to produce a `medewerker` identity from a
|
||||
header/config, alongside the existing zorgverlener stub.
|
||||
3. Add capability checks a backoffice caller needs (start with `canBeoordelen`; extend as
|
||||
WP-65 needs more).
|
||||
4. Unit tests for both identity kinds and the new capability checks — no consumer exists
|
||||
yet (WP-64+ will call this).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] `CallerIdentity` represents both actor kinds without breaking any existing
|
||||
zorgverlener call site (WP-53's tests still green).
|
||||
- [ ] A stub medewerker identity resolves from a request header, mirroring the existing
|
||||
citizen stub.
|
||||
- [ ] At least one capability flag (`canBeoordelen`) computable for a medewerker
|
||||
identity, unit-tested.
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test` (existing WP-53 tests unaffected + new medewerker tests
|
||||
green).
|
||||
|
||||
## Out of scope
|
||||
|
||||
Any actual backoffice endpoint using this (WP-64+); real employee SSO/eHerkenning.
|
||||
|
||||
## Risks
|
||||
|
||||
If the union is modeled as a bolt-on rather than replacing the flat type, existing
|
||||
zorgverlener call sites could break — mitigated by keeping WP-53's existing tests as a
|
||||
regression gate.
|
||||
@@ -0,0 +1,69 @@
|
||||
# WP-63 — Backend: aanvraag status lifecycle as a published DTO
|
||||
|
||||
Status: todo
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
|
||||
The FE currently infers "in behandeling" from a single boolean, `pendingHerregistratie`
|
||||
(`big-profile.store.ts:53`) — explicitly called out in ADR-0002 as "a temporary stand-in
|
||||
for a real, backend-owned status." The full lifecycle (`Ingediend → In behandeling →
|
||||
(Meer info gevraagd ⇄) → Goedgekeurd/Afgewezen`) needs to become a real backend-published
|
||||
value before either frontend can render it meaningfully — the SSP needs it as a richer
|
||||
read (this WP), the behandelportal needs it as the thing it advances (WP-65).
|
||||
|
||||
## Read first
|
||||
|
||||
- [ADR-0002](../reference/architecture/0002-user-groups-and-bounded-contexts.md) (status
|
||||
lifecycle diagram)
|
||||
- `src/app/registratie/application/big-profile.store.ts` (the current boolean)
|
||||
- [ADR-0001 — BFF-lite decision DTOs](../reference/architecture/0001-bff-lite-decision-dtos.md)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Status lives on the existing `Aanvraag`/`ApplicationSummaryDto` aggregate (extend,
|
||||
don't invent a parallel status resource).
|
||||
- The DTO change is additive: the SSP's `pendingHerregistratie` boolean can be derived
|
||||
from the new status field (or kept as a computed convenience) so this ships with zero
|
||||
required FE behavior change — a pure backend + contract widening.
|
||||
- Only the status _value_ is published here; any transition (advancing it) is a separate
|
||||
write endpoint, not part of this slice (that's WP-65's mutation).
|
||||
|
||||
## Files
|
||||
|
||||
- `Data/ApplicationStore.cs` (status field/enum)
|
||||
- `Contracts/Dtos.cs` (extend `ApplicationSummaryDto`/status DTO)
|
||||
- The FE `infrastructure/*.adapter.ts` + `parse*` boundary consuming it
|
||||
- `big-profile.store.ts` (derive the existing boolean from the new field)
|
||||
|
||||
## Steps
|
||||
|
||||
1. Model the full status enum backend-side (`Ingediend`, `InBehandeling`,
|
||||
`MeerInfoGevraagd`, `Goedgekeurd`, `Afgewezen`) on `Aanvraag`.
|
||||
2. Publish it on the existing DTO the SSP already consumes.
|
||||
3. Regenerate the typed client (`npm run gen:api`); update the FE `parse*` boundary to
|
||||
read the new field.
|
||||
4. Point `pendingHerregistratie` (or its replacement) at the new field so the SSP's
|
||||
existing behavior is unchanged, just backed by a real value.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Backend publishes the full status lifecycle value on the existing aanvraag DTO.
|
||||
- [ ] `npm run gen:api` leaves no drift; SSP's existing "pending" display is unchanged in
|
||||
behavior, now backed by the real status.
|
||||
- [ ] `dotnet test` + `npm run ci` green.
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test`; `npm run gen:api` (no drift); `npm run ci`; manual: SSP
|
||||
dashboard still shows the same pending/approved states it does today.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Any endpoint that _advances_ the status (WP-65); the behandelportal consuming it (WP-64).
|
||||
|
||||
## Risks
|
||||
|
||||
If the enum doesn't anticipate a state WP-65 needs (e.g. distinguishing who can transition
|
||||
from what), it gets revised there — acceptable, this slice only needs to cover the states
|
||||
already named in ADR-0002's diagram.
|
||||
@@ -0,0 +1,65 @@
|
||||
# WP-64 — Behandelportal: werkvoorraad (queue) screen
|
||||
|
||||
Status: todo
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
|
||||
First real screen in the new app — a read-only list of aanvragen needing treatment (the
|
||||
"werkvoorraad"), gated by the medewerker identity from WP-62 and backed by the real status
|
||||
DTO from WP-63. This is the smallest useful vertical slice of actual case-treatment
|
||||
functionality — usable and demoable on its own, even before any decision can be recorded
|
||||
(WP-65).
|
||||
|
||||
## Read first
|
||||
|
||||
- WP-61/62/63 outcomes
|
||||
- `bff-endpoint` skill (screen-shaped decision DTO recipe)
|
||||
- `src/app/registratie/ui/admin-cases.page.ts` (the existing cross-owner list, for what
|
||||
to avoid repeating — that page is audit/delete, this one is a queue)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- New BFF-lite endpoint (decision-enriched DTO) shaped for a werkvoorraad screen — not a
|
||||
reuse of the existing `/admin/cases` endpoint, which is audit-shaped, not queue-shaped
|
||||
(per CLAUDE.md's per-screen endpoint discipline).
|
||||
- Gated by the `canBeoordelen`-style capability from WP-62, not a new ad hoc role check.
|
||||
- Domain first, then infrastructure, application, UI — per the house `new-feature`
|
||||
recipe.
|
||||
|
||||
## Files
|
||||
|
||||
New backend endpoint + DTO in `BigRegister.Api`; new `behandeling` context in the
|
||||
behandelportal app (domain/infrastructure/application/ui per the house layering).
|
||||
|
||||
## Steps
|
||||
|
||||
1. Backend: new screen-shaped endpoint returning aanvragen needing treatment
|
||||
(status = `InBehandeling`/`Ingediend`), gated by WP-62's capability.
|
||||
2. FE: scaffold the `behandeling` context (domain → infrastructure → application → ui),
|
||||
following `new-feature`.
|
||||
3. UI: a list page (queue), composed from the shared `shared/ui` kernel — no new atoms
|
||||
unless nothing existing fits.
|
||||
4. Storybook story for the new list component/page, a11y-checked.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] Werkvoorraad screen lists aanvragen needing treatment for an authenticated
|
||||
medewerker.
|
||||
- [ ] `npm run ci` green in the behandelportal app; Storybook story present.
|
||||
- [ ] Endpoint follows BFF-lite discipline (decision-enriched, not raw passthrough).
|
||||
|
||||
## Verification
|
||||
|
||||
`npm run ci` in the behandelportal app; `cd backend && dotnet test`; manual: log in as a
|
||||
stub medewerker, see the queue populated from seeded aanvragen.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Viewing a single zaak's detail (WP-65); recording any decision (WP-65).
|
||||
|
||||
## Risks
|
||||
|
||||
None major — this is a read-only composition slice once WP-61-63 exist.
|
||||
|
||||
Depends on: WP-61, WP-62, WP-63.
|
||||
@@ -0,0 +1,72 @@
|
||||
# WP-65 — Behandelportal: zaak detail + beoordeling (decision) screen
|
||||
|
||||
Status: todo
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
|
||||
The core case-treatment write path — a medewerker opens one aanvraag's detail (including
|
||||
its documents) and records a decision (goedkeuren/afwijzen/meer info opvragen), advancing
|
||||
the status lifecycle WP-63 published. This is the first genuinely new _write_ capability
|
||||
in the system beyond what the citizen SSP already does to itself.
|
||||
|
||||
## Read first
|
||||
|
||||
- `mutation-command` skill
|
||||
- `form-machine` skill (the decision action is a state-changing form, same idiom as
|
||||
everywhere else in this house)
|
||||
- [WP-63](WP-63-aanvraag-status-lifecycle.md) (the status field being advanced)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- The decision action is modeled as a `*.machine.ts` (Model/Msg/reduce) + a `submit-*`
|
||||
command returning `Result`, per this house's one-idiom-for-forms rule — not a
|
||||
hand-rolled mutable field.
|
||||
- The mutation endpoint is a new BFF-lite write (per `mutation-command` recipe) that
|
||||
transitions the status field from WP-63; it validates the transition is legal
|
||||
server-side (e.g. can't approve an already-approved case) — the backend remains the
|
||||
authority.
|
||||
- Runs against `LocalZaakSource` for this slice; wiring the decision into real OpenZaak is
|
||||
explicitly WP-66, not bundled here — keeps this slice's surface to app-level behavior
|
||||
only.
|
||||
|
||||
## Files
|
||||
|
||||
New mutation endpoint + command in `BigRegister.Api`; `behandeling/ui` detail page +
|
||||
`behandeling/application` decision machine in the behandelportal app.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Backend: mutation endpoint advancing aanvraag status (goedkeuren/afwijzen/meer-info-
|
||||
opvragen), validating the transition.
|
||||
2. FE: zaak-detail page (documents + current status) + a decision form machine + submit
|
||||
command.
|
||||
3. Wire the werkvoorraad list (WP-64) to link into this detail page.
|
||||
4. Storybook stories + a11y for the new detail/decision UI.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A medewerker can view one aanvraag's detail and record a decision that advances its
|
||||
status.
|
||||
- [ ] Illegal transitions are rejected server-side (tested).
|
||||
- [ ] End-to-end smoke: werkvoorraad → detail → decision → status change reflected back
|
||||
in the queue.
|
||||
- [ ] `npm run ci` (behandelportal app) + `dotnet test` green.
|
||||
|
||||
## Verification
|
||||
|
||||
Manual/automated smoke test of the full werkvoorraad → beoordeling → besluit flow against
|
||||
`LocalZaakSource`; `npm run ci`; `cd backend && dotnet test`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Writing the decision to real OpenZaak (WP-66).
|
||||
|
||||
## Risks
|
||||
|
||||
This is the largest FE slice in Phase 11 — if it feels too big at kickoff, split
|
||||
detail-view (read) from decision-recording (write) into two sessions; the WP as scoped
|
||||
already keeps them in one slice because a detail view with no decision action isn't
|
||||
independently useful for a caseworker.
|
||||
|
||||
Depends on: WP-64.
|
||||
@@ -0,0 +1,69 @@
|
||||
# WP-66 — Wire the decision into OpenZaak
|
||||
|
||||
Status: todo
|
||||
Phase: 11 — Behandelportal
|
||||
|
||||
## Why
|
||||
|
||||
WP-65's decision currently only updates local state (`LocalZaakSource`). For the
|
||||
behandelportal to actually function against a real register, the recorded decision needs
|
||||
to also write a besluit/status transition to ZGW — extending the write capability that
|
||||
already partially exists (`CreateStatusRequest`, `CreateRolRequest` in
|
||||
`OpenZaakZaakSource.cs`) rather than building a new ZGW client from scratch.
|
||||
|
||||
## Read first
|
||||
|
||||
- `backend/src/BigRegister.Api/Zgw/OpenZaakZaakSource.cs` (existing write records)
|
||||
- ZGW's Besluiten API (referenced in `openzaak-integration.md` if covered, or the ZGW
|
||||
standard docs) for besluit creation
|
||||
- [WP-50](WP-50-openzaak-create-zaak.md) (the first ZGW write slice, for the pattern to
|
||||
follow)
|
||||
|
||||
## Decisions (pre-made, don't relitigate)
|
||||
|
||||
- Extend `IZaakSource`/`OpenZaakZaakSource` with a besluit/status-transition write,
|
||||
following the same pattern WP-50 established for create-zaak (a records + mapper
|
||||
addition, not a new abstraction).
|
||||
- Gated by `Zgw:Enabled` like every other ZGW write — the behandelportal keeps working
|
||||
against `LocalZaakSource` when it's off.
|
||||
- Best done after Phase 10's WP-60 (write-divergence resilience) lands, since this is
|
||||
exactly the second write pair that resilience work should already cover — but not
|
||||
strictly blocked on it if Phase 10 is still in progress (call out the residual risk
|
||||
explicitly if shipped first).
|
||||
|
||||
## Files
|
||||
|
||||
`Zgw/OpenZaakZaakSource.cs` (besluit/status write), `Data/IZaakSource.cs` (new write
|
||||
method), tests.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Add the besluit/status-transition write to `OpenZaakZaakSource`, mirroring WP-50's
|
||||
create-zaak pattern.
|
||||
2. Wire WP-65's decision command to call it when `Zgw:Enabled=true`.
|
||||
3. Integration test against the WP-54 harness (extend `OpenZaakIntegrationTests`).
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A recorded decision writes a real besluit/status transition to OpenZaak when
|
||||
`Zgw:Enabled=true`.
|
||||
- [ ] Behandelportal still works unchanged against `LocalZaakSource` when
|
||||
`Zgw:Enabled=false`.
|
||||
- [ ] `OpenZaakIntegrationTests` covers the new write.
|
||||
|
||||
## Verification
|
||||
|
||||
`cd backend && dotnet test --filter Category=Integration` against the (ideally
|
||||
Phase-10-hardened) OpenZaak harness; manual smoke with `Zgw:Enabled=true`.
|
||||
|
||||
## Out of scope
|
||||
|
||||
Any further behandelportal screens beyond beoordeling.
|
||||
|
||||
## Risks
|
||||
|
||||
If Phase 10's WP-60 (write-divergence resilience) hasn't landed yet, this introduces a
|
||||
second unprotected write pair — call this out explicitly if the two phases aren't
|
||||
sequenced together in practice.
|
||||
|
||||
Depends on: WP-65. Benefits from (but doesn't strictly require) WP-60.
|
||||
Reference in New Issue
Block a user