feat(infra): beheer portal in compose + e2e + ADR-0025 + demo note (refs #130)
This commit is contained in:
@@ -177,7 +177,7 @@ jobs:
|
|||||||
# Log dump must precede teardown (which removes the containers).
|
# Log dump must precede teardown (which removes the containers).
|
||||||
- name: Dump container logs on failure
|
- name: Dump container logs on failure
|
||||||
if: failure()
|
if: failure()
|
||||||
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel tempo prometheus grafana 2>&1 || true
|
run: docker compose -f infra/docker-compose.yml logs --no-color --tail=100 oz-init openzaak nrc-init nrc-web nrc-celery nrc-beat flowable-db flowable-rest flowable-init keycloak acl bff domain projection-db event-subscriber projection-api self-service openbaar behandel beheer tempo prometheus grafana 2>&1 || true
|
||||||
- name: Tear down
|
- name: Tear down
|
||||||
if: always()
|
if: always()
|
||||||
run: make down
|
run: make down
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ COMPOSE := infra/docker-compose.yml
|
|||||||
# Long-running services with a healthcheck — the smoke polls these for readiness
|
# Long-running services with a healthcheck — the smoke polls these for readiness
|
||||||
# (infra/wait-healthy.sh). One-shot init jobs (oz-init, nrc-init, flowable-init)
|
# (infra/wait-healthy.sh). One-shot init jobs (oz-init, nrc-init, flowable-init)
|
||||||
# are not polled; they only need to have run. See docs/runbooks/gitea-actions-gotchas.md.
|
# are not polled; they only need to have run. See docs/runbooks/gitea-actions-gotchas.md.
|
||||||
WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api self-service openbaar behandel
|
WAIT_SVCS := openzaak nrc-web acl bff domain event-subscriber projection-api self-service openbaar behandel beheer
|
||||||
# Config files (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are streamed
|
# Config files (OpenZaak data.yaml, Keycloak realms, Flowable BPMN) are streamed
|
||||||
# into external named volumes via `docker cp` (infra/seed-config.sh) instead of
|
# into external named volumes via `docker cp` (infra/seed-config.sh) instead of
|
||||||
# bind-mounted, because bind mounts don't reach sibling containers on the
|
# bind-mounted, because bind mounts don't reach sibling containers on the
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
# ADR-0025: The BFF reads the catalogus directly from the ACL
|
||||||
|
|
||||||
|
- **Status:** Accepted
|
||||||
|
- **Date:** 2026-07-24
|
||||||
|
- **Deciders:** Respellion engineering
|
||||||
|
- **Slice:** S-15a (#130), first of the S-15 (#16) split
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
The beheer portal shows a read-only view of the ZTC catalogus (the published
|
||||||
|
zaaktypen). Two coupling rules constrain where that data can come from:
|
||||||
|
|
||||||
|
- **§8.1** — only the ACL may talk to the ZGW APIs (Catalogi included). So the
|
||||||
|
catalogus read *must* originate in the ACL.
|
||||||
|
- **§8.3** — portals talk only to the BFF. So the portal reaches the ACL only
|
||||||
|
through the BFF.
|
||||||
|
|
||||||
|
That leaves the question of *how the BFF gets the data*. Until now the BFF fanned
|
||||||
|
out to exactly two backends — the Domain Service and the read projection. The
|
||||||
|
catalogus is neither: it is not a registration (domain) nor a projected read model.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
**The BFF calls the ACL directly for the beheer catalogus read** — a new typed
|
||||||
|
`IAclClient` (`GET /catalogi/zaaktypen`), configured by `Downstream:Acl:BaseUrl`,
|
||||||
|
mirroring the existing `IDomainClient` / `IProjectionClient` pattern.
|
||||||
|
|
||||||
|
Rejected alternative — **route it through the Domain Service** (BFF → domain →
|
||||||
|
ACL): the catalogus is not a domain concern, so the domain would gain a
|
||||||
|
pass-through endpoint that owns no aggregate and no invariant, blurring the
|
||||||
|
domain's responsibility purely to avoid a new edge. That is worse coupling, not
|
||||||
|
better.
|
||||||
|
|
||||||
|
This adds one service-to-service edge (BFF → ACL) — an architecturally
|
||||||
|
significant boundary change (§14), hence this ADR. It does **not** bend §8: the
|
||||||
|
ACL stays the only code that reads ZGW, and the portal still talks only to the
|
||||||
|
BFF. The ACL endpoint is a plain read that trusts its callers (§8.3); the
|
||||||
|
beheerder authorization lives at the BFF (medewerker realm + `beheerder` role).
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
**Positive**
|
||||||
|
|
||||||
|
- The catalogus read follows the shortest honest path; the domain stays about
|
||||||
|
registrations.
|
||||||
|
- Symmetric with the other downstream clients — nothing new to learn.
|
||||||
|
|
||||||
|
**Negative / costs**
|
||||||
|
|
||||||
|
- The BFF now depends on three backends instead of two. The ACL must be reachable
|
||||||
|
for the beheer portal to load (it already is — the BFF is on the same network).
|
||||||
|
- A second consumer of the ACL (alongside the domain and event-subscriber), so
|
||||||
|
ACL read endpoints are now part of more than one caller's contract.
|
||||||
|
|
||||||
|
## Coupling rules touched (CLAUDE.md §8)
|
||||||
|
|
||||||
|
A new BFF → ACL edge. §8.1 and §8.3 remain intact; §14 (boundary change) is the
|
||||||
|
reason this ADR exists.
|
||||||
@@ -5,6 +5,32 @@ copy-pasteable walkthrough against a local `make up` stack.
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## S-15a — Beheer-portal: read-only catalogus viewer (#130, ADR-0025)
|
||||||
|
|
||||||
|
**Outcome:** a new **beheer** portal (medewerker realm, like behandel) shows the ZTC catalogus —
|
||||||
|
the published zaaktypen — **read-only**. A beheerder logs in and sees the seeded BIG-REGISTRATIE
|
||||||
|
zaaktype. The read path is portal → BFF `GET /beheer/catalogi/zaaktypen` (medewerker realm +
|
||||||
|
`beheerder` role) → ACL `GET /catalogi/zaaktypen` → ZGW Catalogi API. The BFF reaches the ACL
|
||||||
|
directly (ADR-0025); managing the default-fill config (S-15b) and MFA (S-15c) come next.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make up
|
||||||
|
# 1. Log in as bram-beheerder / test123 → the catalogus lists the published zaaktypen.
|
||||||
|
open http://localhost:8143
|
||||||
|
#
|
||||||
|
# 2. Automated (a CI verify-stack e2e): a beheerder logs in and sees BIG-REGISTRATIE.
|
||||||
|
make verify-e2e # → catalogus.spec: "a beheerder sees the published zaaktypen in the catalogus"
|
||||||
|
#
|
||||||
|
# 3. The BFF endpoint is behind the beheerder role — a plain behandelaar gets 403 (BFF unit tests):
|
||||||
|
# Bff.Tests → BeheerEndpointTests.
|
||||||
|
```
|
||||||
|
|
||||||
|
**Auth:** the `beheerder` realm role + `bram-beheerder` user live in the medewerker realm
|
||||||
|
(`infra/keycloak/realms/medewerker-realm.json`); the BFF reuses the medewerker bearer scheme and its
|
||||||
|
realm-role lifting, requiring `beheerder` rather than `behandelaar`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## S-16c — Prometheus metrics + golden-signal Grafana dashboard (#124, ADR-0023)
|
## S-16c — Prometheus metrics + golden-signal Grafana dashboard (#124, ADR-0023)
|
||||||
|
|
||||||
**Outcome:** the five .NET services now expose OpenTelemetry metrics in Prometheus format at `/metrics`
|
**Outcome:** the five .NET services now expose OpenTelemetry metrics in Prometheus format at `/metrics`
|
||||||
|
|||||||
@@ -380,6 +380,8 @@ services:
|
|||||||
Keycloak__MedewerkerAuthority: http://keycloak:8080/realms/medewerker
|
Keycloak__MedewerkerAuthority: http://keycloak:8080/realms/medewerker
|
||||||
Downstream__Domain__BaseUrl: http://domain:8080/
|
Downstream__Domain__BaseUrl: http://domain:8080/
|
||||||
Downstream__Projection__BaseUrl: http://projection-api:8080/
|
Downstream__Projection__BaseUrl: http://projection-api:8080/
|
||||||
|
# The beheer catalogus read reaches the ACL directly (S-15a, ADR-0025).
|
||||||
|
Downstream__Acl__BaseUrl: http://acl:8080/
|
||||||
ports:
|
ports:
|
||||||
- "8080:8080"
|
- "8080:8080"
|
||||||
healthcheck:
|
healthcheck:
|
||||||
@@ -544,6 +546,29 @@ services:
|
|||||||
condition: service_started
|
condition: service_started
|
||||||
networks: [cg]
|
networks: [cg]
|
||||||
|
|
||||||
|
# The beheer portal: nginx serves the Angular app and reverse-proxies /beheer to the BFF.
|
||||||
|
# Beheerders log in against the Keycloak medewerker realm (same realm as behandel, S-15a).
|
||||||
|
beheer:
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: apps/beheer/Dockerfile
|
||||||
|
image: register-referentie/beheer:dev
|
||||||
|
ports:
|
||||||
|
- "8143:80"
|
||||||
|
healthcheck:
|
||||||
|
# 127.0.0.1, not localhost: nginx listens on IPv4 only, but localhost resolves to ::1 first.
|
||||||
|
test: ["CMD-SHELL", "wget -q -O /dev/null http://127.0.0.1/ || exit 1"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 5
|
||||||
|
start_period: 10s
|
||||||
|
depends_on:
|
||||||
|
bff:
|
||||||
|
condition: service_healthy
|
||||||
|
keycloak:
|
||||||
|
condition: service_started
|
||||||
|
networks: [cg]
|
||||||
|
|
||||||
# ── Observability backplane (S-16a, ADR-0023) ──────────────────────────────
|
# ── Observability backplane (S-16a, ADR-0023) ──────────────────────────────
|
||||||
# Grafana-native stack: Tempo ingests OTLP traces (the .NET services export
|
# Grafana-native stack: Tempo ingests OTLP traces (the .NET services export
|
||||||
# straight to it — no collector hop, S-16b), Prometheus scrapes service
|
# straight to it — no collector hop, S-16b), Prometheus scrapes service
|
||||||
|
|||||||
@@ -0,0 +1,19 @@
|
|||||||
|
import { expect, test } from '@playwright/test';
|
||||||
|
|
||||||
|
// S-15a walking skeleton: a beheerder logs in to the beheer portal (medewerker realm) and sees the
|
||||||
|
// read-only ZTC catalogus. The verify stack seeds and publishes the BIG-REGISTRATIE zaaktype (the
|
||||||
|
// same one verify-domain relies on), so it must appear in the catalogus. Runs against the shared
|
||||||
|
// verify stack, so it asserts on that stable seeded zaaktype rather than anything test-specific.
|
||||||
|
test('a beheerder sees the published zaaktypen in the catalogus', async ({ page }) => {
|
||||||
|
await page.goto('http://beheer/');
|
||||||
|
|
||||||
|
// The beheer portal redirects to the Keycloak medewerker realm login (same realm as behandel).
|
||||||
|
await page.locator('#username').fill('bram-beheerder');
|
||||||
|
await page.locator('#password').fill('test123');
|
||||||
|
await page.locator('#kc-login').click();
|
||||||
|
|
||||||
|
await expect(page.getByRole('heading', { name: /Catalogus/i })).toBeVisible();
|
||||||
|
|
||||||
|
// The seeded, published BIG zaaktype is shown by its business identificatie.
|
||||||
|
await expect(page.getByText('BIG-REGISTRATIE')).toBeVisible();
|
||||||
|
});
|
||||||
@@ -6,6 +6,9 @@ const baseURL = process.env.SELF_SERVICE_URL ?? 'http://self-service';
|
|||||||
// The behandel portal is a second origin the happy path visits (staff approve from the werkbak);
|
// The behandel portal is a second origin the happy path visits (staff approve from the werkbak);
|
||||||
// it needs the same insecure-origin-as-secure treatment as self-service for the PKCE login (below).
|
// it needs the same insecure-origin-as-secure treatment as self-service for the PKCE login (below).
|
||||||
const behandelURL = process.env.BEHANDEL_URL ?? 'http://behandel';
|
const behandelURL = process.env.BEHANDEL_URL ?? 'http://behandel';
|
||||||
|
// The beheer portal is a third medewerker-realm origin (the read-only catalogus viewer, S-15a); it
|
||||||
|
// needs the same insecure-origin-as-secure treatment as the others for the PKCE login (below).
|
||||||
|
const beheerURL = process.env.BEHEER_URL ?? 'http://beheer';
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
testDir: '.',
|
testDir: '.',
|
||||||
@@ -33,7 +36,7 @@ export default defineConfig({
|
|||||||
channel: 'chromium',
|
channel: 'chromium',
|
||||||
launchOptions: {
|
launchOptions: {
|
||||||
args: [
|
args: [
|
||||||
`--unsafely-treat-insecure-origin-as-secure=${baseURL},${behandelURL}`,
|
`--unsafely-treat-insecure-origin-as-secure=${baseURL},${behandelURL},${beheerURL}`,
|
||||||
// Write Chromium's shared memory to /tmp instead of the container's small /dev/shm, so a
|
// Write Chromium's shared memory to /tmp instead of the container's small /dev/shm, so a
|
||||||
// large DOM/heap can't crash the renderer on the memory-constrained runner (belt-and-braces
|
// large DOM/heap can't crash the renderer on the memory-constrained runner (belt-and-braces
|
||||||
// alongside the single worker above).
|
// alongside the single worker above).
|
||||||
|
|||||||
Reference in New Issue
Block a user