Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d783448b7 | ||
|
|
10b784cc05 | ||
|
|
2bb7d9c165 | ||
|
|
4a047c618c | ||
|
|
43b45ad756 | ||
|
|
5502e4c099 | ||
|
|
3705a18e18 | ||
|
|
400bdcafc4 | ||
|
|
c67ee7d3f5 | ||
|
|
d14f379358 | ||
|
|
66f8322580 |
@@ -219,9 +219,6 @@ jobs:
|
|||||||
- name: OpenZaak → NRC → Event Subscriber → projection-api
|
- name: OpenZaak → NRC → Event Subscriber → projection-api
|
||||||
id: projection
|
id: projection
|
||||||
run: make verify-projection
|
run: make verify-projection
|
||||||
- name: Objecten → NRC notification delivery
|
|
||||||
id: objecten_nrc
|
|
||||||
run: make verify-objecten-notifications
|
|
||||||
- name: Domain → Flowable → ACL → OpenZaak
|
- name: Domain → Flowable → ACL → OpenZaak
|
||||||
id: domain
|
id: domain
|
||||||
run: make verify-domain
|
run: make verify-domain
|
||||||
@@ -248,7 +245,6 @@ jobs:
|
|||||||
OBJECTTYPEN: ${{ steps.objecttypen.outcome }}
|
OBJECTTYPEN: ${{ steps.objecttypen.outcome }}
|
||||||
OBJECTEN: ${{ steps.objecten.outcome }}
|
OBJECTEN: ${{ steps.objecten.outcome }}
|
||||||
REGISTERRECORD: ${{ steps.registerrecord.outcome }}
|
REGISTERRECORD: ${{ steps.registerrecord.outcome }}
|
||||||
OBJECTEN_NOTIFICATIONS: ${{ steps.objecten_nrc.outcome }}
|
|
||||||
ACL: ${{ steps.acl.outcome }}
|
ACL: ${{ steps.acl.outcome }}
|
||||||
NRC: ${{ steps.nrc.outcome }}
|
NRC: ${{ steps.nrc.outcome }}
|
||||||
PROJECTION: ${{ steps.projection.outcome }}
|
PROJECTION: ${{ steps.projection.outcome }}
|
||||||
@@ -270,7 +266,6 @@ jobs:
|
|||||||
echo "| Objecttypen API + token | $(icon "$OBJECTTYPEN") |"
|
echo "| Objecttypen API + token | $(icon "$OBJECTTYPEN") |"
|
||||||
echo "| Objecten API + token | $(icon "$OBJECTEN") |"
|
echo "| Objecten API + token | $(icon "$OBJECTEN") |"
|
||||||
echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |"
|
echo "| RegisterRecord objecttype | $(icon "$REGISTERRECORD") |"
|
||||||
echo "| Objecten → NRC | $(icon "$OBJECTEN_NOTIFICATIONS") |"
|
|
||||||
echo "| ACL ↔ OpenZaak | $(icon "$ACL") |"
|
echo "| ACL ↔ OpenZaak | $(icon "$ACL") |"
|
||||||
echo "| OpenZaak → NRC | $(icon "$NRC") |"
|
echo "| OpenZaak → NRC | $(icon "$NRC") |"
|
||||||
echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |"
|
echo "| NRC → Event Subscriber → projection | $(icon "$PROJECTION") |"
|
||||||
@@ -290,7 +285,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 beheer objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten objecten-celery registerrecord-init 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 objecttypen-db objecttypen-redis objecttypen-init objecttypen objecten-db objecten-redis objecten-init objecten registerrecord-init tempo prometheus grafana 2>&1 || true
|
||||||
- name: Tear down
|
- name: Tear down
|
||||||
if: always()
|
if: always()
|
||||||
run: make down
|
run: make down
|
||||||
|
|||||||
+1
-3
@@ -296,9 +296,7 @@ Split into independently deployable sub-slices (CLAUDE.md §13):
|
|||||||
Split into independently deployable sub-slices (CLAUDE.md §13):
|
Split into independently deployable sub-slices (CLAUDE.md §13):
|
||||||
|
|
||||||
- **S-19a** (#149, ✅) · ACL writes the `RegisterRecord` to Objecten on approval, idempotently, alongside the ZGW eindstatus. Carries the ADR (ADR-0028).
|
- **S-19a** (#149, ✅) · ACL writes the `RegisterRecord` to Objecten on approval, idempotently, alongside the ZGW eindstatus. Carries the ADR (ADR-0028).
|
||||||
- **S-19b** (#150) · Read projection sourced from Objecten instead of NRC zaak events. *(split — #150 closed)*
|
- **S-19b** (#150) · Read projection sourced from Objecten instead of NRC zaak events. Depends on S-19a.
|
||||||
- **S-19b-1** (#152) · Objecten publishes to NRC — broker, celery worker, `objecten` kanaal, notifications config. Turns back on what ADR-0028 deliberately disabled.
|
|
||||||
- **S-19b-2** (#153) · Projection derived from `RegisterRecord` objects, rebuildable from the Objecten-derived log. Depends on S-19b-1.
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ export DOCKER_HOST := unix://$(PODMAN_SOCK)
|
|||||||
endif
|
endif
|
||||||
endif
|
endif
|
||||||
|
|
||||||
.PHONY: ci lint build unit mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-objecten-notifications verify-notifications smoke up down local verify-local local-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 mutation frontend integration verify verify-up verify-acl verify-nrc verify-projection verify-bff verify-domain verify-observability verify-tracing verify-metrics verify-objecttypen verify-objecten verify-registerrecord verify-notifications smoke up down local verify-local local-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
|
||||||
|
|
||||||
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
## ci: run the full pipeline — lint, build, unit, mutation, frontend, verify (mirrors Gitea Actions)
|
||||||
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
## `verify` is the live-stack stage (full stack up once → ACL + notification checks).
|
||||||
@@ -201,11 +201,6 @@ verify-objecten:
|
|||||||
verify-registerrecord:
|
verify-registerrecord:
|
||||||
bash infra/run-registerrecord-check.sh
|
bash infra/run-registerrecord-check.sh
|
||||||
|
|
||||||
## verify-objecten-notifications: assert a RegisterRecord write in Objecten is DELIVERED as an
|
|
||||||
## `objecten` notification via NRC (S-19b-1), against the already-running stack.
|
|
||||||
verify-objecten-notifications:
|
|
||||||
bash infra/run-objecten-notifications-check.sh
|
|
||||||
|
|
||||||
## verify: local mirror of the CI verify-stack job — full stack up once, all checks,
|
## verify: local mirror of the CI verify-stack job — full stack up once, all checks,
|
||||||
## tear down (always). For fast single-concern local iteration use `integration`
|
## tear down (always). For fast single-concern local iteration use `integration`
|
||||||
## (oz-only) or `verify-notifications` (oz+nrc) instead.
|
## (oz-only) or `verify-notifications` (oz+nrc) instead.
|
||||||
@@ -217,7 +212,6 @@ verify:
|
|||||||
&& bash infra/run-acl-integration.sh \
|
&& bash infra/run-acl-integration.sh \
|
||||||
&& bash infra/run-notification-check.sh \
|
&& bash infra/run-notification-check.sh \
|
||||||
&& bash infra/run-projection-check.sh \
|
&& bash infra/run-projection-check.sh \
|
||||||
&& bash infra/run-objecten-notifications-check.sh \
|
|
||||||
&& bash infra/run-domain-check.sh \
|
&& bash infra/run-domain-check.sh \
|
||||||
&& bash infra/run-bff-check.sh \
|
&& bash infra/run-bff-check.sh \
|
||||||
&& bash infra/run-e2e-check.sh || rc=$$?; \
|
&& bash infra/run-e2e-check.sh || rc=$$?; \
|
||||||
|
|||||||
@@ -119,8 +119,8 @@ every message was dropped on the floor — a delivery path that looks wired and
|
|||||||
|
|
||||||
- ponytail ceiling: Objecten emits no notifications, so nothing downstream can react to a
|
- ponytail ceiling: Objecten emits no notifications, so nothing downstream can react to a
|
||||||
register write yet.
|
register write yet.
|
||||||
- **Lifted by ADR-0029** (S-19b-1, #152): broker, worker, `objecten` kanaal and
|
- Upgrade path: S-19b (#150) needs those notifications to source the projection from
|
||||||
notifications config now exist, and `NOTIFICATIONS_DISABLED` is `false`.
|
Objecten, and turns them on together with the broker, worker, kanaal and abonnement.
|
||||||
|
|
||||||
## Consequences
|
## Consequences
|
||||||
|
|
||||||
|
|||||||
@@ -1,122 +0,0 @@
|
|||||||
# ADR-0029: Objecten publishes register events to NRC
|
|
||||||
|
|
||||||
- **Status:** Accepted
|
|
||||||
- **Date:** 2026-08-14
|
|
||||||
- **Deciders:** Respellion engineering
|
|
||||||
- **Slice:** S-19b-1 (#152), first of the S-19b (#150) split
|
|
||||||
- **Supersedes in part:** ADR-0028's "Objecten's notifications are off for this slice"
|
|
||||||
|
|
||||||
## Context
|
|
||||||
|
|
||||||
ADR-0028 put the authoritative register record in the Objecten API and had the ACL write
|
|
||||||
it on approval. It also switched Objecten's notifications **off** — deliberately, with a
|
|
||||||
stated ceiling: there was no broker, no worker, no `objecten` kanaal and no abonnement, so
|
|
||||||
turning the client side on alone would have produced a delivery path that looks wired and
|
|
||||||
drops every message.
|
|
||||||
|
|
||||||
S-19b-2 (#153) wants the read projection sourced from register writes rather than
|
|
||||||
re-derived from ZGW zaak events. That needs the notifications to actually arrive. This ADR
|
|
||||||
builds the four missing pieces and lifts the ceiling.
|
|
||||||
|
|
||||||
## Decision
|
|
||||||
|
|
||||||
**Objecten publishes to the same NRC OpenZaak already publishes to, on the `objecten`
|
|
||||||
kanaal, delivered by its own Celery worker — provisioned declaratively on both sides,
|
|
||||||
exactly as ADR-0007 did for OpenZaak.**
|
|
||||||
|
|
||||||
- **Objecten** (`infra/objecten/setup_configuration/data.yaml`): a `zgw_consumers` service
|
|
||||||
`nrc` (api_type `nrc`) plus a `notifications_config` step naming it, and
|
|
||||||
`NOTIFICATIONS_DISABLED: "false"` in both compose files.
|
|
||||||
- **NRC** (`infra/opennotificaties/setup_configuration/data.yaml`): an `objecten` kanaal
|
|
||||||
alongside `zaken`.
|
|
||||||
- **`objecten-celery`**: a worker container on the Objecten image (`/celery_worker.sh`),
|
|
||||||
mirroring `oz-celery`, with `CELERY_BROKER_URL`/`CELERY_RESULT_BACKEND` on
|
|
||||||
`objecten-redis` db 1 (db 0 is already the cache).
|
|
||||||
|
|
||||||
### One NRC, one credential, one kanaal per publisher
|
|
||||||
|
|
||||||
Objecten reuses the `big-reference-seed` client OpenZaak publishes with. NRC verifies its
|
|
||||||
JWT and authorizes it against OpenZaak's Autorisaties API (ADR-0007), which grants that
|
|
||||||
client `heeft_alle_autorisaties` — so no second credential and no publisher-specific
|
|
||||||
authorization is needed. A second NRC, or a second credential, would buy isolation this
|
|
||||||
reference application has no use for.
|
|
||||||
|
|
||||||
The kanaal name is **not ours to choose**: the Objects API sends
|
|
||||||
`NOTIFICATIONS_KANAAL = "objecten"`. NRC rejects a publish to an unregistered kanaal
|
|
||||||
(`"Kanaal met deze naam bestaat niet"`), which is precisely what the failing check for this
|
|
||||||
slice reported first. Its filter set (`object_type`) matches the kenmerken the Objects API
|
|
||||||
sends, so an abonnement can narrow to one objecttype instead of receiving every write.
|
|
||||||
|
|
||||||
### Writers address Objecten as `objecten.local` — NRC rejects single-label hosts
|
|
||||||
|
|
||||||
NRC types a notification's `hoofdObject` and `resourceUrl` as DRF `URLField`s, so Django's
|
|
||||||
`URLValidator` runs on them — and it refuses a **single-label** host. Objecten fills both
|
|
||||||
from the object `url` that DRF built with `request.build_absolute_uri`, i.e. **the Host the
|
|
||||||
caller used**. Write to `http://objecten:8000` and NRC answers every publish with
|
|
||||||
|
|
||||||
```
|
|
||||||
{"hoofdObject":["Voer een geldige URL in."],"resourceUrl":["Voer een geldige URL in."]}
|
|
||||||
```
|
|
||||||
|
|
||||||
which `objecten-celery` then retries with exponential backoff, forever, in the background —
|
|
||||||
the write itself having returned 201.
|
|
||||||
|
|
||||||
`SITE_DOMAIN` does **not** fix this; it is not what builds those URLs. The fix is on the
|
|
||||||
caller side: the `objecten` service carries an `objecten.local` network alias, and every
|
|
||||||
component whose writes must be notified — the ACL (`Acl__Objecten__BaseUrl`), the gateway
|
|
||||||
integration tests, this slice's verify check — addresses it there. An alias rather than a
|
|
||||||
plain dotted `SITE_DOMAIN` so the host still **resolves in-network**: a subscriber that
|
|
||||||
follows `resourceUrl` reaches the record it points at, which S-19b-2 will do. Readers are
|
|
||||||
unaffected and keep using the plain service name.
|
|
||||||
|
|
||||||
This is the same class of constraint as ADR-0028's "the ACL's Objecttypen base URL must
|
|
||||||
match Objecten's configured `api_root`": these modules put request-derived hosts into data
|
|
||||||
another module then validates or dereferences.
|
|
||||||
|
|
||||||
- ponytail ceiling: nothing *enforces* that a new writer uses the alias — it would get a 201
|
|
||||||
and silently no notification.
|
|
||||||
- Upgrade path: if a second writer ever appears, rename the compose service to `objecten.local`
|
|
||||||
so the plain name stops working, rather than adding a lint.
|
|
||||||
|
|
||||||
### A worker, not a synchronous send
|
|
||||||
|
|
||||||
`notifications_api_common` only schedules the send on transaction commit. Without a worker
|
|
||||||
the task sits in redis forever and every register write is silently undelivered — the exact
|
|
||||||
half-wired state ADR-0028 refused to ship. No `beat` for Objecten: it is a publisher, not a
|
|
||||||
subscriber, and `nrc-beat` already drains NRC's delivery queue.
|
|
||||||
|
|
||||||
## Verification
|
|
||||||
|
|
||||||
`make verify-objecten-notifications` (`infra/run-objecten-notifications-check.sh`, in the
|
|
||||||
CI `verify-stack` job) registers an abonnement on the `objecten` kanaal pointing at a
|
|
||||||
throwaway webhook sink, writes a `RegisterRecord` exactly as the ACL does on approval, and
|
|
||||||
asserts the notification reaches the sink. That is the whole chain in one assertion:
|
|
||||||
Objecten → `objecten-celery` → NRC → `nrc-beat` → the callback. Any missing piece — broker,
|
|
||||||
worker, kanaal, notifications config — shows up as a non-delivery rather than as a green
|
|
||||||
config.
|
|
||||||
|
|
||||||
## Consequences
|
|
||||||
|
|
||||||
**Positive**
|
|
||||||
|
|
||||||
- A register write is now observable by anything that subscribes, which is what S-19b-2
|
|
||||||
(#153) needs to make the projection a cache of Objecten rather than a re-derivation of ZGW.
|
|
||||||
- ADR-0028's ceiling is lifted: the delivery path is proven end to end, not merely configured.
|
|
||||||
|
|
||||||
**Negative / costs**
|
|
||||||
|
|
||||||
- One more long-running container (`objecten-celery`) on an already memory-tight CI runner.
|
|
||||||
- A second publisher on the shared `big-reference-seed` credential — a credential rotation
|
|
||||||
now touches two modules.
|
|
||||||
- Objecten now has two in-network names, and which one a caller uses silently decides
|
|
||||||
whether its writes are notified (ceiling above).
|
|
||||||
- ponytail ceiling: notification delivery has no dead-letter or alerting — a failed publish
|
|
||||||
is visible only in the worker log.
|
|
||||||
- Upgrade path: if undelivered register events start mattering, subscribe an audit sink or
|
|
||||||
read NRC's own delivery admin rather than building a retry layer here.
|
|
||||||
|
|
||||||
## Coupling rules touched (CLAUDE.md §8)
|
|
||||||
|
|
||||||
None bent. This is infrastructure between two upstream modules, over their documented
|
|
||||||
APIs; no service reaches another's database. §8.6 (idempotency at every event boundary)
|
|
||||||
applies to whatever consumes the new kanaal — S-19b-2's problem, not this slice's.
|
|
||||||
@@ -341,8 +341,7 @@ services:
|
|||||||
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
||||||
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
||||||
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
||||||
# Dotted host on purpose — see the `objecten.local` alias below (ADR-0029).
|
Acl__Objecten__BaseUrl: http://objecten:8000/
|
||||||
Acl__Objecten__BaseUrl: http://objecten.local:8000/
|
|
||||||
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||||||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||||||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||||||
@@ -692,14 +691,12 @@ services:
|
|||||||
CACHE_AXES: objecten-redis:6379/0
|
CACHE_AXES: objecten-redis:6379/0
|
||||||
DISABLE_2FA: "true"
|
DISABLE_2FA: "true"
|
||||||
OTEL_SDK_DISABLED: "true"
|
OTEL_SDK_DISABLED: "true"
|
||||||
CELERY_BROKER_URL: redis://objecten-redis:6379/1
|
# S-19a: Objecten refuses every write while its Notificaties config is absent
|
||||||
CELERY_RESULT_BACKEND: redis://objecten-redis:6379/1
|
# (notifications_api_common raises rather than skipping, so POST /objects 500s). Objecten →
|
||||||
# Publish register-record events to NRC on the `objecten` kanaal (S-19b-1, ADR-0029). The NRC
|
# NRC is not wired yet — there is no broker, worker, kanaal or abonnement for it — so turn
|
||||||
# service + notifications_config are provisioned by setup_configuration
|
# notifications off rather than fake a delivery path that silently drops every message.
|
||||||
# (infra/objecten/setup_configuration/data.yaml), and objecten-celery below actually sends
|
# S-19b (#150) sources the projection from Objecten and turns this back on for real.
|
||||||
# them — notifications_api_common only queues the task. See ADR-0028 for why S-19a left this
|
NOTIFICATIONS_DISABLED: "true"
|
||||||
# off until all four pieces existed.
|
|
||||||
NOTIFICATIONS_DISABLED: "false"
|
|
||||||
RUN_SETUP_CONFIG: "true"
|
RUN_SETUP_CONFIG: "true"
|
||||||
command: /setup_configuration.sh
|
command: /setup_configuration.sh
|
||||||
volumes:
|
volumes:
|
||||||
@@ -724,28 +721,6 @@ services:
|
|||||||
start_period: 30s
|
start_period: 30s
|
||||||
ports:
|
ports:
|
||||||
- "8021:8000"
|
- "8021:8000"
|
||||||
depends_on:
|
|
||||||
objecten-init:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
networks:
|
|
||||||
cg:
|
|
||||||
# Objecten reflects the *request* Host into the `url` it returns, and
|
|
||||||
# notifications_api_common publishes that url as the notification's hoofdObject /
|
|
||||||
# resourceUrl — which NRC types as a URLField, and Django's URLValidator rejects a
|
|
||||||
# single-label host ("Voer een geldige URL in."). So every caller whose writes must be
|
|
||||||
# notified addresses Objecten by this dotted alias instead of `objecten` (ADR-0029).
|
|
||||||
# Reads are unaffected and still use the plain service name.
|
|
||||||
aliases:
|
|
||||||
- objecten.local
|
|
||||||
|
|
||||||
# The celery worker that actually delivers Objecten's notifications to NRC (S-19b-1, ADR-0029).
|
|
||||||
# notifications_api_common only schedules the send on transaction commit; without a worker the
|
|
||||||
# task sits in redis forever and every register write is silently undelivered. Mirrors oz-celery.
|
|
||||||
# No beat: Objecten is a publisher, not a subscriber — nrc-beat drains the delivery queue.
|
|
||||||
objecten-celery:
|
|
||||||
image: docker.io/maykinmedia/objects-api:${OBJECTS_TAG:-3.4.0}
|
|
||||||
environment: *objecten-env-local
|
|
||||||
command: /celery_worker.sh
|
|
||||||
depends_on:
|
depends_on:
|
||||||
objecten-init:
|
objecten-init:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|||||||
@@ -326,8 +326,7 @@ services:
|
|||||||
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
# Objecten holds the register, OpenZaak holds the process (S-19a, ADR-0028). Both APIs take a
|
||||||
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
# static token, not a ZGW JWT. The objecttype URL is assigned at seed time, so the ACL resolves
|
||||||
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
# it by name — lazily, on the first approval, so no depends_on is needed here.
|
||||||
# Dotted host on purpose — see the `objecten.local` alias below (ADR-0029).
|
Acl__Objecten__BaseUrl: http://objecten:8000/
|
||||||
Acl__Objecten__BaseUrl: http://objecten.local:8000/
|
|
||||||
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
Acl__Objecten__Token: ${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}
|
||||||
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
Acl__Objecten__ObjecttypenBaseUrl: http://objecttypen:8000/
|
||||||
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
Acl__Objecten__ObjecttypenToken: ${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}
|
||||||
@@ -718,14 +717,12 @@ services:
|
|||||||
CACHE_AXES: objecten-redis:6379/0
|
CACHE_AXES: objecten-redis:6379/0
|
||||||
DISABLE_2FA: "true"
|
DISABLE_2FA: "true"
|
||||||
OTEL_SDK_DISABLED: "true"
|
OTEL_SDK_DISABLED: "true"
|
||||||
CELERY_BROKER_URL: redis://objecten-redis:6379/1
|
# S-19a: Objecten refuses every write while its Notificaties config is absent
|
||||||
CELERY_RESULT_BACKEND: redis://objecten-redis:6379/1
|
# (notifications_api_common raises rather than skipping, so POST /objects 500s). Objecten →
|
||||||
# Publish register-record events to NRC on the `objecten` kanaal (S-19b-1, ADR-0029). The NRC
|
# NRC is not wired yet — there is no broker, worker, kanaal or abonnement for it — so turn
|
||||||
# service + notifications_config are provisioned by setup_configuration
|
# notifications off rather than fake a delivery path that silently drops every message.
|
||||||
# (infra/objecten/setup_configuration/data.yaml), and objecten-celery below actually sends
|
# S-19b (#150) sources the projection from Objecten and turns this back on for real.
|
||||||
# them — notifications_api_common only queues the task. See ADR-0028 for why S-19a left this
|
NOTIFICATIONS_DISABLED: "true"
|
||||||
# off until all four pieces existed.
|
|
||||||
NOTIFICATIONS_DISABLED: "false"
|
|
||||||
RUN_SETUP_CONFIG: "true"
|
RUN_SETUP_CONFIG: "true"
|
||||||
command: /setup_configuration.sh
|
command: /setup_configuration.sh
|
||||||
# data.yaml is streamed into this external volume by infra/seed-config.sh before start.
|
# data.yaml is streamed into this external volume by infra/seed-config.sh before start.
|
||||||
@@ -753,28 +750,6 @@ services:
|
|||||||
start_period: 30s
|
start_period: 30s
|
||||||
ports:
|
ports:
|
||||||
- "8021:8000"
|
- "8021:8000"
|
||||||
depends_on:
|
|
||||||
objecten-init:
|
|
||||||
condition: service_completed_successfully
|
|
||||||
networks:
|
|
||||||
cg:
|
|
||||||
# Objecten reflects the *request* Host into the `url` it returns, and
|
|
||||||
# notifications_api_common publishes that url as the notification's hoofdObject /
|
|
||||||
# resourceUrl — which NRC types as a URLField, and Django's URLValidator rejects a
|
|
||||||
# single-label host ("Voer een geldige URL in."). So every caller whose writes must be
|
|
||||||
# notified addresses Objecten by this dotted alias instead of `objecten` (ADR-0029).
|
|
||||||
# Reads are unaffected and still use the plain service name.
|
|
||||||
aliases:
|
|
||||||
- objecten.local
|
|
||||||
|
|
||||||
# The celery worker that actually delivers Objecten's notifications to NRC (S-19b-1, ADR-0029).
|
|
||||||
# notifications_api_common only schedules the send on transaction commit; without a worker the
|
|
||||||
# task sits in redis forever and every register write is silently undelivered. Mirrors oz-celery.
|
|
||||||
# No beat: Objecten is a publisher, not a subscriber — nrc-beat drains the delivery queue.
|
|
||||||
objecten-celery:
|
|
||||||
image: docker.io/maykinmedia/objects-api:${OBJECTS_TAG:-3.4.0}
|
|
||||||
environment: *objecten-env
|
|
||||||
command: /celery_worker.sh
|
|
||||||
depends_on:
|
depends_on:
|
||||||
objecten-init:
|
objecten-init:
|
||||||
condition: service_completed_successfully
|
condition: service_completed_successfully
|
||||||
|
|||||||
@@ -1,121 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""S-19b-1 (#152): driver for the Objecten → NRC notification check.
|
|
||||||
|
|
||||||
Registers an abonnement on the `objecten` kanaal pointing at the webhook sink, then writes a
|
|
||||||
RegisterRecord object exactly as the ACL's ObjectenGateway does (S-19a). The caller
|
|
||||||
(run-objecten-notifications-check.sh) watches the sink for the delivery — this only sets it up,
|
|
||||||
and prints `OBJECT_URL <url>` for the caller to grep on.
|
|
||||||
|
|
||||||
Delivery exercises the whole chain: Objecten → its celery worker → NRC → nrc-beat → the callback.
|
|
||||||
Anything missing (broker, worker, kanaal, notifications config) shows up as a non-delivery.
|
|
||||||
|
|
||||||
Stdlib only so it runs in a bare python:3-slim container on the compose network.
|
|
||||||
"""
|
|
||||||
import base64
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.error
|
|
||||||
import urllib.request
|
|
||||||
|
|
||||||
OBJECTEN = os.environ["OBJECTEN"] # http://objecten:8000
|
|
||||||
OBJECTEN_TOKEN = os.environ["OBJECTEN_TOKEN"]
|
|
||||||
OBJECTTYPEN = os.environ["OBJECTTYPEN"] # http://objecttypen:8000
|
|
||||||
OBJECTTYPEN_TOKEN = os.environ["OBJECTTYPEN_TOKEN"]
|
|
||||||
NRC_BASE = os.environ["NRC_BASE"] # http://<nrc-ip>:8000
|
|
||||||
SINK_CALLBACK = os.environ["SINK_CALLBACK"] # http://<sink-ip>:9000/
|
|
||||||
SINK_AUTH = os.environ["SINK_AUTH"]
|
|
||||||
CLIENT_ID = os.environ.get("NRC_CLIENT_ID", "big-reference-seed")
|
|
||||||
SECRET = os.environ.get("NRC_SECRET", "insecure-dev-secret-change-me")
|
|
||||||
KANAAL = "objecten"
|
|
||||||
|
|
||||||
|
|
||||||
def mint():
|
|
||||||
"""The HS256 JWT NRC expects (same shape as infra/local/register-abonnement.py)."""
|
|
||||||
def seg(d):
|
|
||||||
return base64.urlsafe_b64encode(json.dumps(d).encode()).rstrip(b"=")
|
|
||||||
|
|
||||||
payload = seg({
|
|
||||||
"iss": CLIENT_ID, "iat": int(time.time()), "client_id": CLIENT_ID,
|
|
||||||
"user_id": CLIENT_ID, "user_representation": CLIENT_ID,
|
|
||||||
})
|
|
||||||
signing_input = seg({"typ": "JWT", "alg": "HS256"}) + b"." + payload
|
|
||||||
signature = base64.urlsafe_b64encode(
|
|
||||||
hmac.new(SECRET.encode(), signing_input, hashlib.sha256).digest()).rstrip(b"=")
|
|
||||||
return (signing_input + b"." + signature).decode()
|
|
||||||
|
|
||||||
|
|
||||||
def nrc(method, url, body=None):
|
|
||||||
"""Call NRC. `url` may be a path or an absolute URL (the list returns absolute ones)."""
|
|
||||||
data = json.dumps(body).encode() if body is not None else None
|
|
||||||
req = urllib.request.Request(
|
|
||||||
url if url.startswith("http") else f"{NRC_BASE}{url}", data=data, method=method,
|
|
||||||
headers={"Authorization": f"Bearer {mint()}", "Content-Type": "application/json"})
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(req, timeout=15) as r:
|
|
||||||
return json.load(r) if r.length != 0 else {}
|
|
||||||
except urllib.error.HTTPError as e:
|
|
||||||
# The body carries the reason (e.g. an unregistered kanaal); the status alone does not.
|
|
||||||
raise SystemExit(f"FAIL — NRC {method} {url} → {e.code}: {e.read().decode(errors='replace')[:400]}")
|
|
||||||
|
|
||||||
|
|
||||||
def token_api(base, token, method, path, body=None, crs=False):
|
|
||||||
data = json.dumps(body).encode() if body is not None else None
|
|
||||||
headers = {"Authorization": f"Token {token}"}
|
|
||||||
if body is not None:
|
|
||||||
headers["Content-Type"] = "application/json"
|
|
||||||
if crs:
|
|
||||||
headers["Accept-Crs"] = "EPSG:4326"
|
|
||||||
if body is not None:
|
|
||||||
headers["Content-Crs"] = "EPSG:4326"
|
|
||||||
req = urllib.request.Request(f"{base}{path}", data=data, method=method, headers=headers)
|
|
||||||
with urllib.request.urlopen(req, timeout=15) as r:
|
|
||||||
return json.load(r) if r.length != 0 else {}
|
|
||||||
|
|
||||||
|
|
||||||
def subscribe():
|
|
||||||
"""Register an abonnement on the objecten kanaal, replacing a stale one for the same callback."""
|
|
||||||
# NRC returns a bare list here, not a paginated envelope.
|
|
||||||
for existing in nrc("GET", "/api/v1/abonnement") or []:
|
|
||||||
if existing.get("callbackUrl") == SINK_CALLBACK:
|
|
||||||
nrc("DELETE", existing["url"])
|
|
||||||
nrc("POST", "/api/v1/abonnement", {
|
|
||||||
"callbackUrl": SINK_CALLBACK,
|
|
||||||
"auth": SINK_AUTH,
|
|
||||||
"kanalen": [{"naam": KANAAL, "filters": {}}],
|
|
||||||
})
|
|
||||||
print(f">> abonnement on '{KANAAL}' -> {SINK_CALLBACK}")
|
|
||||||
|
|
||||||
|
|
||||||
def objecttype_url():
|
|
||||||
results = token_api(OBJECTTYPEN, OBJECTTYPEN_TOKEN, "GET", "/api/v2/objecttypes").get("results", [])
|
|
||||||
match = next((o for o in results if o.get("name") == "RegisterRecord"), None)
|
|
||||||
if not match:
|
|
||||||
print("FAIL — no RegisterRecord objecttype in Objecttypen", file=sys.stderr)
|
|
||||||
raise SystemExit(1)
|
|
||||||
return match["url"]
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
subscribe()
|
|
||||||
reference = f"NOTIF-{int(time.time())}"
|
|
||||||
created = token_api(OBJECTEN, OBJECTEN_TOKEN, "POST", "/api/v2/objects", {
|
|
||||||
"type": objecttype_url(),
|
|
||||||
"record": {
|
|
||||||
"typeVersion": 1,
|
|
||||||
"data": {"id": f"zaak-{reference}", "status": "INGESCHREVEN", "reference": reference},
|
|
||||||
"startAt": time.strftime("%Y-%m-%d"),
|
|
||||||
},
|
|
||||||
}, crs=True)
|
|
||||||
print(f">> wrote RegisterRecord {created['url']}")
|
|
||||||
# An NRC notification carries no record data — only hoofdObject/resourceUrl — so the object
|
|
||||||
# URL, not the reference in its data, is what the caller can correlate the delivery on.
|
|
||||||
print(f"OBJECT_URL {created['url']}")
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -18,16 +18,6 @@ zgw_consumers:
|
|||||||
auth_type: api_key
|
auth_type: api_key
|
||||||
header_key: Authorization
|
header_key: Authorization
|
||||||
header_value: Token 0123456789abcdef0123456789abcdef01234567
|
header_value: Token 0123456789abcdef0123456789abcdef01234567
|
||||||
# (1b) The NRC Objecten publishes register-record events to (S-19b-1, ADR-0029). Same shape and
|
|
||||||
# same big-reference-seed credential OpenZaak publishes with — NRC verifies the JWT and
|
|
||||||
# authorizes it via OpenZaak's AC, which grants that client heeft_alle_autorisaties.
|
|
||||||
- identifier: nrc
|
|
||||||
label: Open Notificaties
|
|
||||||
api_type: nrc
|
|
||||||
api_root: http://nrc-web:8000/api/v1/
|
|
||||||
auth_type: zgw
|
|
||||||
client_id: big-reference-seed
|
|
||||||
secret: insecure-dev-secret-change-me
|
|
||||||
|
|
||||||
# (2) Permit the RegisterRecord objecttype (S-19a). Objecten refuses to store an object whose
|
# (2) Permit the RegisterRecord objecttype (S-19a). Objecten refuses to store an object whose
|
||||||
# objecttype it has not been configured with ("ObjectType with url=… is not configured"), and it
|
# objecttype it has not been configured with ("ObjectType with url=… is not configured"), and it
|
||||||
@@ -50,10 +40,3 @@ tokenauth:
|
|||||||
email: admin@localhost
|
email: admin@localhost
|
||||||
organization: Respellion
|
organization: Respellion
|
||||||
is_superuser: true
|
is_superuser: true
|
||||||
|
|
||||||
# (4) Point Objecten's notifications at that NRC service (S-19b-1, ADR-0029). Requires
|
|
||||||
# NOTIFICATIONS_DISABLED=false plus a celery broker + worker — without the worker the message is
|
|
||||||
# queued and never sent, which is exactly the half-wired state S-19a refused to ship (ADR-0028).
|
|
||||||
notifications_config_enable: true
|
|
||||||
notifications_config:
|
|
||||||
notifications_api_service_identifier: nrc
|
|
||||||
|
|||||||
@@ -29,9 +29,7 @@ autorisaties_api_config_enable: true
|
|||||||
autorisaties_api:
|
autorisaties_api:
|
||||||
authorizations_api_service_identifier: openzaak-ac
|
authorizations_api_service_identifier: openzaak-ac
|
||||||
|
|
||||||
# 4. The kanalen publishers announce on: `zaken` (OpenZaak) and `objecten` (Objecten, S-19b-1).
|
# 4. The kanaal OpenZaak publishes zaak events on.
|
||||||
# Both authenticate with the big-reference-seed credential above, which OpenZaak's AC grants
|
|
||||||
# heeft_alle_autorisaties — so no separate publisher authorization is needed for Objecten.
|
|
||||||
notifications_kanalen_config_enable: true
|
notifications_kanalen_config_enable: true
|
||||||
notifications_kanalen_config:
|
notifications_kanalen_config:
|
||||||
items:
|
items:
|
||||||
@@ -41,11 +39,3 @@ notifications_kanalen_config:
|
|||||||
- bronorganisatie
|
- bronorganisatie
|
||||||
- zaaktype
|
- zaaktype
|
||||||
- vertrouwelijkheidaanduiding
|
- vertrouwelijkheidaanduiding
|
||||||
# 5. The kanaal Objecten publishes register-record events on (S-19b-1, ADR-0029). Its name is
|
|
||||||
# fixed by the Objects API itself (NOTIFICATIONS_KANAAL = "objecten"), not chosen here. The
|
|
||||||
# filter set matches what the Objects API sends as kenmerken, so an abonnement can narrow by
|
|
||||||
# objecttype rather than receiving every object write in the register.
|
|
||||||
- naam: objecten
|
|
||||||
documentatie_link: https://objects-and-objecttypes-api.readthedocs.io/
|
|
||||||
filters:
|
|
||||||
- object_type
|
|
||||||
|
|||||||
@@ -1,83 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
#
|
|
||||||
# S-19b-1 (#152): verify the Objecten → NRC notification path against an ALREADY-RUNNING full
|
|
||||||
# stack. Registers an abonnement on the `objecten` kanaal pointing at a throwaway webhook sink,
|
|
||||||
# writes a RegisterRecord object (exactly as the ACL does on approval, S-19a), and asserts the sink
|
|
||||||
# receives the notification.
|
|
||||||
#
|
|
||||||
# This is the whole publish chain in one assertion: Objecten → its celery worker → NRC → nrc-beat →
|
|
||||||
# the subscriber callback. S-19a deliberately left it disconnected (ADR-0028); this proves it is
|
|
||||||
# connected for real, rather than merely configured.
|
|
||||||
#
|
|
||||||
# All in-network, reaching services by container IP (a single-label host isn't URL-valid for NRC's
|
|
||||||
# callbackUrl validator; the runner can't reach published ports — gitea-actions-gotchas.md §5/§6).
|
|
||||||
# EXCEPT Objecttypen, which must be reached by SERVICE NAME: it echoes the request Host into the
|
|
||||||
# objecttype `url` and Objecten only accepts the one matching its configured api_root (ADR-0028);
|
|
||||||
# and Objecten, reached by its `objecten.local` alias because it reflects the request Host into the
|
|
||||||
# notification's hoofdObject/resourceUrl, which NRC validates as a URL (ADR-0029).
|
|
||||||
#
|
|
||||||
# Does NOT manage the stack lifecycle, but cleans up the sink/driver it creates.
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
||||||
SINK_AUTH="Bearer objecten-notification-sink-token"
|
|
||||||
|
|
||||||
cleanup() { docker rm -f rr-osink rr-overify >/dev/null 2>&1 || true; }
|
|
||||||
trap cleanup EXIT
|
|
||||||
|
|
||||||
ip() { docker inspect -f '{{range .NetworkSettings.Networks}}{{.IPAddress}}{{end}}' "$1"; }
|
|
||||||
|
|
||||||
# Anchored on the compose replica suffix so they don't also match objecten-db / objecten-redis.
|
|
||||||
obj="$(docker ps -q --filter 'name=objecten[-_][0-9]+$' | head -1)"
|
|
||||||
nrc="$(docker ps -q --filter 'name=nrc-web' | head -1)"
|
|
||||||
[ -n "$obj" ] || { echo "ERROR: no running objecten container — bring the stack up first" >&2; exit 1; }
|
|
||||||
[ -n "$nrc" ] || { echo "ERROR: no running nrc-web container — bring the stack up first" >&2; exit 1; }
|
|
||||||
net="$(docker inspect -f '{{range $k,$_ := .NetworkSettings.Networks}}{{$k}}{{"\n"}}{{end}}' "$obj" | head -1)"
|
|
||||||
nrc_ip="$(ip "$nrc")"
|
|
||||||
echo ">> network=$net nrc=$nrc_ip"
|
|
||||||
|
|
||||||
echo ">> starting the webhook sink"
|
|
||||||
docker rm -f rr-osink >/dev/null 2>&1 || true
|
|
||||||
sink="$(docker create --network "$net" --name rr-osink -e "EXPECTED_AUTH=$SINK_AUTH" \
|
|
||||||
python:3-slim python /sink.py)"
|
|
||||||
docker cp "$here/notification-sink.py" "$sink:/sink.py" >/dev/null
|
|
||||||
docker start "$sink" >/dev/null
|
|
||||||
sleep 1
|
|
||||||
sink_ip="$(ip rr-osink)"
|
|
||||||
echo ">> sink at $sink_ip:9000"
|
|
||||||
|
|
||||||
echo ">> registering the abonnement + writing a RegisterRecord"
|
|
||||||
docker rm -f rr-overify >/dev/null 2>&1 || true
|
|
||||||
drv="$(docker create --network "$net" --name rr-overify \
|
|
||||||
-e "OBJECTEN=http://objecten.local:8000" \
|
|
||||||
-e "OBJECTEN_TOKEN=${OBJECTEN_TOKEN:-1234567890abcdef1234567890abcdef12345678}" \
|
|
||||||
-e "OBJECTTYPEN=http://objecttypen:8000" \
|
|
||||||
-e "OBJECTTYPEN_TOKEN=${OBJECTTYPEN_TOKEN:-0123456789abcdef0123456789abcdef01234567}" \
|
|
||||||
-e "NRC_BASE=http://$nrc_ip:8000" \
|
|
||||||
-e "SINK_CALLBACK=http://$sink_ip:9000/" -e "SINK_AUTH=$SINK_AUTH" \
|
|
||||||
python:3-slim python /driver.py)"
|
|
||||||
docker cp "$here/objecten-notifications-check.py" "$drv:/driver.py" >/dev/null
|
|
||||||
docker start -a "$drv"
|
|
||||||
object_url="$(docker logs rr-overify 2>/dev/null | sed -n 's/^OBJECT_URL //p' | head -1)"
|
|
||||||
docker rm -f rr-overify >/dev/null
|
|
||||||
[ -n "$object_url" ] || { echo "FAIL — the driver did not write a RegisterRecord" >&2; exit 1; }
|
|
||||||
echo ">> wrote $object_url"
|
|
||||||
|
|
||||||
# Correlate on the object URL: a notification carries hoofdObject/resourceUrl, never the record
|
|
||||||
# data, so the reference inside the record is not in the delivered message.
|
|
||||||
echo ">> waiting for the notification to reach the sink"
|
|
||||||
for _ in $(seq 1 "${NOTIFICATION_TRIES:-40}"); do
|
|
||||||
if docker logs rr-osink 2>&1 | grep -qF "$object_url"; then
|
|
||||||
echo "OK — Objecten published to NRC and the abonnement delivered it:"
|
|
||||||
docker logs rr-osink 2>&1 | grep -F "$object_url" | tail -1 | cut -c1-500
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
sleep 2
|
|
||||||
done
|
|
||||||
|
|
||||||
echo "FAIL — no 'objecten' notification for $object_url reached the sink." >&2
|
|
||||||
echo " Objecten accepted the write, so the gap is downstream: the celery broker/worker," >&2
|
|
||||||
echo " the kanaal registration, or Objecten's notifications_config." >&2
|
|
||||||
echo "--- sink log ---" >&2; docker logs rr-osink 2>&1 | tail -8 >&2
|
|
||||||
echo "--- objecten log ---" >&2; docker logs "$obj" 2>&1 | tail -15 >&2
|
|
||||||
exit 1
|
|
||||||
@@ -20,7 +20,7 @@ public sealed class ObjectenGatewayIntegrationTests
|
|||||||
new HttpClient(),
|
new HttpClient(),
|
||||||
new ObjectenOptions
|
new ObjectenOptions
|
||||||
{
|
{
|
||||||
BaseUrl = new(Env("OBJECTEN_BASE", "http://objecten.local:8000")),
|
BaseUrl = new(Env("OBJECTEN_BASE", "http://objecten:8000")),
|
||||||
Token = Env("OBJECTEN_TOKEN", "1234567890abcdef1234567890abcdef12345678"),
|
Token = Env("OBJECTEN_TOKEN", "1234567890abcdef1234567890abcdef12345678"),
|
||||||
ObjecttypenBaseUrl = new(Env("OBJECTTYPEN_BASE", "http://objecttypen:8000")),
|
ObjecttypenBaseUrl = new(Env("OBJECTTYPEN_BASE", "http://objecttypen:8000")),
|
||||||
ObjecttypenToken = Env("OBJECTTYPEN_TOKEN", "0123456789abcdef0123456789abcdef01234567"),
|
ObjecttypenToken = Env("OBJECTTYPEN_TOKEN", "0123456789abcdef0123456789abcdef01234567"),
|
||||||
@@ -65,7 +65,7 @@ public sealed class ObjectenGatewayIntegrationTests
|
|||||||
{
|
{
|
||||||
using var http = new HttpClient();
|
using var http = new HttpClient();
|
||||||
var objecttype = await ResolveObjecttypeUrlAsync(http);
|
var objecttype = await ResolveObjecttypeUrlAsync(http);
|
||||||
var query = new Uri(new Uri(Env("OBJECTEN_BASE", "http://objecten.local:8000")),
|
var query = new Uri(new Uri(Env("OBJECTEN_BASE", "http://objecten:8000")),
|
||||||
"/api/v2/objects?type=" + Uri.EscapeDataString(objecttype) +
|
"/api/v2/objects?type=" + Uri.EscapeDataString(objecttype) +
|
||||||
"&data_attrs=id__exact__" + Uri.EscapeDataString(id));
|
"&data_attrs=id__exact__" + Uri.EscapeDataString(id));
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user