CI / build (pull_request) Successful in 1m14s
CI / lint (pull_request) Successful in 1m30s
CI / unit (pull_request) Successful in 1m34s
CI / frontend (pull_request) Successful in 3m14s
CI / mutation (pull_request) Successful in 6m21s
CI / verify-stack (pull_request) Successful in 8m48s
The publish chain works — the sink received it:
{"kanaal": "objecten", "resource": "object", "kenmerken": {"objectType": "…"},
"hoofdObject": "http://objecten.local:8000/api/v2/objects/a68d4c46-…", …}
The check just looked for the wrong thing. An NRC notification carries hoofdObject /
resourceUrl and kenmerken — never the record data — so the `reference` inside the
RegisterRecord it wrote was never going to appear in the delivered message. Grep the sink
for the object URL instead, which is what identifies the write.
122 lines
5.0 KiB
Python
122 lines
5.0 KiB
Python
#!/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())
|