diff --git a/BACKLOG.md b/BACKLOG.md index d00afac..476d485 100644 --- a/BACKLOG.md +++ b/BACKLOG.md @@ -285,7 +285,7 @@ Split into independently deployable sub-slices (CLAUDE.md §13): - **S-18a** (#139, ✅) · Objecttypen API up in compose (own DB + seeded config + health + static token). - **S-18b** (#140, ✅) · Objecten API up in compose, wired to Objecttypen. Depends on S-18a. -- **S-18c** (#141) · RegisterRecord objecttype defined + registered (public-safe JSON schema). Depends on S-18a/b. +- **S-18c** (#141, ✅) · RegisterRecord objecttype defined + registered (public-safe JSON schema). Depends on S-18a/b. ### S-19 · ACL extension: write register-record to Objecten on approval diff --git a/docs/architecture/adr-0027-registerrecord-objecttype-schema.md b/docs/architecture/adr-0027-registerrecord-objecttype-schema.md new file mode 100644 index 0000000..a64dfa5 --- /dev/null +++ b/docs/architecture/adr-0027-registerrecord-objecttype-schema.md @@ -0,0 +1,81 @@ +# ADR-0027: The RegisterRecord objecttype is public-safe by construction + +- **Status:** Accepted +- **Date:** 2026-07-27 +- **Deciders:** Respellion engineering +- **Slice:** S-18c (#141), third of the S-18 (#19) split + +## Context + +S-18 stands up Objecttypen (S-18a) and Objecten (S-18b) as the authoritative +register-record store (PRD §"Objecten as the authoritative register record store"). +S-19 (#20) will, on approval, write the canonical register record to the Objecten API +instead of OpenZaak zaak-eigenschappen, and the openbaar (public) register will read it. + +Objecten validates every object against a **objecttype version's JSON schema**. So the +schema is a contract: it fixes which fields a register record may carry. The register is +read **anonymously** by the openbaar portal (ADR-0010), so the schema is also a +disclosure boundary — anything the schema allows can end up public. + +Two questions: **which fields** the schema defines, and **how** the objecttype gets into +the Objecttypen API (which has no declarative objecttype step). + +## Decision + +**Define a `RegisterRecord` objecttype whose published schema carries exactly the +public-safe fields — `id`, `status`, `reference` — and register it over the API at +startup with a one-shot, idempotently.** + +### The schema mirrors the BFF's public projection, not the internal one + +The public-safe field set already exists: the BFF's `OpenbaarEntry` +(`services/bff/Bff.Api/DownstreamClients.cs`) — `id`, `status`, `reference` — is what +`OpenbaarProjection.PublicView` narrows every row down to, dropping `bsn` and +`naamPlaceholder` at the boundary (S-09). The RegisterRecord schema mirrors that record, +**not** the internal `RegisterEntry` / `RegisterEntryRow` (which carry bsn/naam): + +| field | type | notes | +|-------|------|-------| +| `id` | string (required) | zaak id — the entry's stable key | +| `status` | string (required) | enum `INGEDIEND` \| `INGESCHREVEN` (`RegistrationStatus`) | +| `reference` | string \| null | citizen-facing zaak identificatie (ADR-0012) | + +`additionalProperties: false` so a record can't smuggle a field the schema didn't +sanction, and `dataClassification: "open"` records the intent that this objecttype is +public. **`bsn` and `naamPlaceholder` are deliberately absent** — public-safe by +construction, so S-19 cannot write a personal-data field into the public register even by +mistake. + +### Registered over the API by a one-shot, not setup_configuration + +The Objecttypen API's `setup_configuration` (3.4.2) provisions only tokens — it has no +declarative step to create an objecttype with a schema. So a `registerrecord-init` +compose one-shot (stdlib Python, on the stack network) creates the objecttype + a +**published** version over the API once Objecttypen is healthy, following the ADR-0020 +self-seed pattern. It is **idempotent**: if a `RegisterRecord` with a version already +exists it is a no-op, so it is safe on every `up`. + +- ponytail ceiling: no schema-migration/versioning story — a schema change means editing + `registerrecord.schema.json` and bumping the version by hand; the one-shot only ever + adds v1 if none exists. +- Upgrade path: if the schema evolves, have the one-shot diff the published schema and + POST a new version, or move to a declarative step once the upstream supports one. + +## Consequences + +**Positive** + +- The public register's disclosure surface is fixed in one reviewed artifact + (`registerrecord.schema.json`) and enforced by Objecten's own validation. +- Self-seeds on a fresh `make up` / bare local compose; no manual step, no built image. + +**Negative / costs** + +- The public-safe field set now lives in two places — the BFF's `OpenbaarEntry` and this + schema — that must be kept in sync by hand (a drift check is a candidate for later). +- Hand-managed schema version (ceiling above). + +## Coupling rules touched (CLAUDE.md §8) + +None new. Registration talks to the Objecttypen API over its documented API. S-19 will +write records via the ACL (§8.1) — this ADR only fixes the schema they conform to. diff --git a/docs/demo-script.md b/docs/demo-script.md index 363a905..e3f133a 100644 --- a/docs/demo-script.md +++ b/docs/demo-script.md @@ -5,6 +5,35 @@ copy-pasteable walkthrough against a local `make up` stack. --- +## S-18c — RegisterRecord objecttype defined + registered (#141, ADR-0027) + +**Outcome:** a **RegisterRecord** objecttype with a **published** JSON schema is registered in the +Objecttypen API at startup. The schema is public-safe by construction — `id`, `status`, `reference` +only, mirroring the BFF's `OpenbaarEntry` (no `bsn`/`naam`), `dataClassification: open`. This is the +schema S-19 writes register records against on approval. A `registerrecord-init` one-shot creates it +over the API once Objecttypen is healthy (the Objecttypen `setup_configuration` has no objecttype +step), idempotently. + +```bash +make up +# The RegisterRecord objecttype exists with a published version: +curl -s -H "Authorization: Token 0123456789abcdef0123456789abcdef01234567" \ + "http://localhost:8020/api/v2/objecttypes" | python3 -c \ + 'import sys,json; o=[x for x in json.load(sys.stdin)["results"] if x["name"]=="RegisterRecord"][0]; print(o["name"], o["dataClassification"], o["versions"])' +# → RegisterRecord open ['http://.../objecttypes//versions/1'] +# +# Automated (a CI verify-stack step): asserts the objecttype exists, has a published version, and +# that version's schema carries id/status/reference. +make verify-registerrecord # → OK — RegisterRecord v1 published, fields=['id', 'reference', 'status'] +``` + +**The path:** `infra/objecttypen-registerrecord/registerrecord.schema.json` (the reviewed public-safe +contract) + `register.py` are streamed into an external config volume by `infra/seed-config.sh +registerrecord` (bind-mounted locally); the `registerrecord-init` one-shot POSTs the objecttype + a +published version. Re-running is a no-op. S-19 (#20) writes records against this schema in Objecten. + +--- + ## S-18b — Objecten API up in compose, wired to Objecttypen (#140) **Outcome:** the upstream Maykin **Objecten API** runs in the stack — own **PostGIS** DB + redis,