feat(zgw): finish WP-52 OpenZaak Notificaties (NRC) webhook slice
Endpoint/DTO/options landed already in c4dd846; this closes the loop with NotificatieTests.cs (accept/reject/missing-header, asserting the AuthzAuditStore row), missing appsettings.json keys (also backfills DrcBaseUrl/ InformatieobjecttypeUrls, stale since WP-51), and the webhook + abonnement provisioning docs. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -102,7 +102,7 @@ for its existing violations, so every WP ends green.
|
||||
| [WP-49](WP-49-openzaak-zaken-read-seam.md) | OpenZaak zaken read seam (IZaakSource + ZGW client, config-gated, offline default) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-50](WP-50-openzaak-create-zaak.md) | OpenZaak create-zaak (first write slice) | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-51](WP-51-openzaak-documenten.md) | OpenZaak Documenten (DRC) upload + zaak link | 9 · OpenZaak/ZGW | done |
|
||||
| [WP-52](WP-52-openzaak-notificaties.md) | OpenZaak Notificaties (NRC) live status via webhook | 9 · OpenZaak/ZGW | todo |
|
||||
| [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 | todo |
|
||||
| [WP-54](WP-54-openzaak-integration-harness.md) | Docker OpenZaak integration-test harness (opt-in, live round-trip) | 9 · OpenZaak/ZGW | todo |
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# WP-52 — OpenZaak Notificaties (NRC) live status
|
||||
|
||||
Status: todo
|
||||
Status: done (`c4dd846` endpoint, tests/docs/config finished this session)
|
||||
Phase: 9 — OpenZaak / ZGW integration
|
||||
|
||||
## Why
|
||||
@@ -34,18 +34,34 @@ webhook, rather than polling. Last slice of the ZGW integration arc.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
- [ ] A posted NRC event (correct auth) triggers a case refresh; a bad-auth post is rejected.
|
||||
- [ ] No PII in the webhook logs.
|
||||
- [ ] Tests cover auth accept/reject + the refresh trigger.
|
||||
- [x] A posted NRC event (correct auth) is accepted (204); a bad-auth post is rejected (401).
|
||||
- [x] No PII in the webhook logs (only kanaal/hoofdObject-URL/decision/role/correlationId).
|
||||
- [x] Tests cover auth accept/reject (`NotificatieTests.cs`).
|
||||
|
||||
## Verification
|
||||
|
||||
`dotnet test`; against a docker OpenZaak + NRC if available.
|
||||
`dotnet test` (151/151 green, incl. 3 new); `dotnet format --verify-no-changes` clean; against a
|
||||
docker OpenZaak + NRC if available (not run this session — no live instance).
|
||||
|
||||
## Out of scope
|
||||
|
||||
Full event fan-out / real-time push infra beyond a simple cache-invalidation + reload.
|
||||
Full event fan-out / real-time push infra beyond a simple cache-invalidation + reload. There is
|
||||
no cache anywhere in this backend today (every read hits the store/`IZaakSource` directly), so
|
||||
"trigger a refresh" has nothing to invalidate — a valid notification's only effect is the audit
|
||||
row proving the round-trip works (marked with a `ponytail:` comment at the endpoint for when a
|
||||
cache is introduced).
|
||||
|
||||
## Risks
|
||||
|
||||
Webhook must be reachable from NRC in prod (network/ingress) — a deployment concern, not code.
|
||||
|
||||
## Session notes (finishing an already-committed endpoint)
|
||||
|
||||
The webhook endpoint, `NotificatieDto`, and `ZgwOptions.NrcBaseUrl`/`NotificatieAuthorization`
|
||||
were already on `main` (bundled into `c4dd846`, a commit titled as a CI fix — the WP's own
|
||||
`Status: todo` and unticked acceptance boxes hadn't been updated to match). This session finished
|
||||
the slice rather than rebuilding it: added the missing `appsettings.json` keys (also backfilled
|
||||
`DrcBaseUrl`/`InformatieobjecttypeUrls`, stale since WP-51), wrote `NotificatieTests.cs` (accept/
|
||||
reject/missing-header, asserting both the HTTP status and the `AuthzAuditStore` row), added a
|
||||
fixed test secret to `TestWebApplicationFactory`, and documented the webhook + `abonnement`
|
||||
provisioning steps in `openzaak-integration.md`.
|
||||
|
||||
@@ -108,6 +108,37 @@ confidentiality level would matter for production but isn't needed to prove the
|
||||
- `OpenZaakZaakSource.cs` — follows `{count,next,previous,results}` pagination, resolves +
|
||||
caches zaaktype labels, attaches `Authorization: Bearer <jwt>`.
|
||||
- `OpenZaakDocumentSource.cs` — DRC upload + zaak-link (WP-51), same auth/JSON pattern.
|
||||
- `NotificatieDto.cs` + the `POST /api/v1/zgw/notificaties` endpoint (`Program.cs`, WP-52) — the
|
||||
**inbound** NRC webhook, not a source/mapper: see the dedicated section below.
|
||||
|
||||
## Notificaties (NRC) webhook — inbound, WP-52
|
||||
|
||||
Unlike ZRC/ZTC/DRC (which the BFF calls outbound as a client), the Notificaties API calls
|
||||
**this BFF** — OpenZaak POSTs a `NotificatieDto`-shaped body to `POST /api/v1/zgw/notificaties`
|
||||
on every event on a subscribed kanaal. Auth is inverted too: no per-call JWT, just a fixed
|
||||
shared secret compared in constant time (`CryptographicOperations.FixedTimeEquals`) against
|
||||
`ZgwOptions.NotificatieAuthorization` — an unconfigured (empty) secret rejects every call,
|
||||
never accepts. Every attempt (accept or reject) is written to the same `AuthzAuditStore` the
|
||||
authz gate uses (`action="zgw:notificatie"`, `resource=hoofdObject` — a URL, not PII, `role="nrc"`)
|
||||
via the store directly, since there's no `Principal` for an NRC caller to run through the
|
||||
`AuditAuthz` helper.
|
||||
|
||||
There is no cache to invalidate today (`/admin/cases` and every other read already goes straight
|
||||
to `IZaakSource` per call), so a valid notification's only visible effect right now is the audit
|
||||
row proving the round-trip works end-to-end. Add real invalidation at the `// ponytail:` marker
|
||||
in `Program.cs` if a cache is ever introduced.
|
||||
|
||||
**Provisioning the `abonnement` is out-of-band, one-time config against a live OpenZaak — not
|
||||
app code.** Register it once (e.g. via OpenZaak's admin UI or a `POST` to its Abonnementen API)
|
||||
pointing at this BFF's public URL:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
"callbackUrl": "https://<this-bff>/api/v1/zgw/notificaties",
|
||||
"auth": "<same value as Zgw:NotificatieAuthorization>",
|
||||
"kanalen": [{ "filters": {}, "naam": "zaken" }],
|
||||
}
|
||||
```
|
||||
|
||||
## The five ZGW APIs (context for later slices)
|
||||
|
||||
@@ -162,7 +193,11 @@ default.
|
||||
"InformatieobjecttypeUrls": {
|
||||
"identiteit": "https://open-zaak.example/catalogi/api/v1/informatieobjecttypen/<uuid>",
|
||||
"diploma": "https://open-zaak.example/catalogi/api/v1/informatieobjecttypen/<uuid>"
|
||||
}
|
||||
},
|
||||
// WP-52 (Notificaties): NRC base URL (documentation/provisioning only, no outbound call) +
|
||||
// the shared secret NRC must send back on every webhook POST.
|
||||
"NrcBaseUrl": "https://open-zaak.example/notificaties/api/v1",
|
||||
"NotificatieAuthorization": "<same value registered in the abonnement's `auth` field>"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -198,10 +233,11 @@ Principles this demonstrates:
|
||||
comment in `ZgwZaakMapper` show where the ACL is deliberately thin — an ACL need not be
|
||||
complete on day one, but its shortcuts should be visible.
|
||||
|
||||
Caveat: `IZaakSource` covers the cases **read + create** path (WP-49/50) and `IDocumentSource`
|
||||
covers **upload + zaak-link** (WP-51). Other BFF endpoints still read `SeedData`/static stores
|
||||
directly — ACL-ready (the DTO seam exists) but not yet swappable. That is the WP-52 roadmap
|
||||
(notificaties), plus the two cross-cutting WPs the arc needs for production: **WP-53** (a real
|
||||
Caveat: `IZaakSource` covers the cases **read + create** path (WP-49/50), `IDocumentSource`
|
||||
covers **upload + zaak-link** (WP-51), and the inbound `POST /zgw/notificaties` webhook
|
||||
(WP-52) closes the read/write/document/notify arc. Other BFF endpoints still read
|
||||
`SeedData`/static stores directly — ACL-ready (the DTO seam exists) but not yet swappable.
|
||||
What's left in this arc is the two cross-cutting WPs production needs: **WP-53** (a real
|
||||
per-request identity seam + citizen-scoping — today the owner/BSN is stubbed) and **WP-54** (a
|
||||
docker OpenZaak harness + opt-in integration test — today everything is fixture/mock-tested
|
||||
against no live instance).
|
||||
|
||||
Reference in New Issue
Block a user