Connect-seitige Webhook-Abonnements ueber Core API-07 (zentrale Webhook-Registry): connect/internal/webhookclient als schlanker HTTP-Client gegen die Registry (Subscribe/Unsubscribe), connect/internal/ webhooksub mit lokaler, verschluesselter Abonnementverwaltung und HTTP-Zustellempfaenger inkl. HMAC-Signaturpruefung. Core API-07 exponiert bislang keinen HTTP-Wrapper fuer physisch getrennte Module (anders als z.B. AUD-08) -- Tests laufen deshalb gegen einen den dokumentierten Core-Vertrag nachbildenden Stub, echte Postgres-Testdatenbank, echte HTTP-Verbindungen. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
8.8 KiB
API-03 – Prüfprotokoll: Externe Webhook-Abonnements (nutzt Core API-07)
Datum: 2026-10-16. Host: 204 (Wegwerf-Postgres, TEST_TENANT_DSN, isoliertes Schema pro Testlauf über connect/internal/testdb).
Ausgangslage: Schnittstelle zu Core API-07
Core API-07 (internal/webhook, Modul gitea.perlbach24.de/scripte/nexarch) implementiert Registrierung (Store.Subscribe), Zustellung mit Retry/Backoff (Dispatcher) und HMAC-Signatur (Sign/VerifySignature) als reines Go-Paket. Recherche im Core-Modul (gleicher Checkout, internal/webhook/*.go, internal/contracttest/contracttest_test.go) ergab: es existiert aktuell kein HTTP-Handler/cmd/*-Dienst, der Store.Subscribe für physisch getrennte Module (Connect, DMS, Mail, Archive) über HTTP erreichbar macht — anders als z. B. AUD-08 (internal/audit/ingest_handler.go + cmd/auditlog-devserver), das exakt dieses Muster für den Audit-Log bereits umsetzt. Core-seitig fehlt zusätzlich eine Unsubscribe-Methode auf Store völlig.
Das ist eine Lücke auf Core-Seite (core-kanban), nicht Gegenstand dieser Kachel (Connect ist laut Auftrag physisch getrennt und darf Cores internal/-Pakete nicht importieren — nur über HTTP koppeln). Diese Kachel erlaubt für genau diesen Fall ausdrücklich einen „minimal gestubten Core-Webhook-Registry-Endpunkt" für den End-zu-Ende-Test. Umgesetzt wurde daher:
- Der HTTP-Vertrag, den ein künftiger Core-HTTP-Wrapper für API-07 erfüllen müsste (
POST /webhooks/subscriptions,DELETE /webhooks/subscriptions/{id},X-Service-Token-Auth, Feldertenant_slug/event_type/target_url/secret), dokumentiert als Vertrag inconnect/internal/webhookclient. - Alle Tests laufen über einen echten
httptest.Server, der genau diesen Vertrag nachbildet (echte HTTP-Verbindung über Loopback-TCP, keine Mock-Schnittstelle des Clients selbst) — dasselbe Muster wieconnect/internal/policyclient/connect/internal/auditclientbereits für andere Core-Schnittstellen verwenden. - HMAC-Signaturvertrag (
X-Nexarch-Signature-256, hex-kodiertes HMAC-SHA256) 1:1 ausinternal/webhook.SignatureHeader/VerifySignatureübernommen, da Connect das Core-Paket nicht importieren kann (physische Modultrennung).
Umsetzung
connect/internal/webhookclient— schlanker HTTP-Client gegen Core API-07 (Subscribe,Unsubscribe), Muster ausconnect/internal/auditclient/dms/internal/retentionclient.connect/internal/webhooksub— Connect-seitige Verwaltung:Manager.RegisterAction(connectorName, eventType, action): verdrahtet prozesslokal, welche Connector-Aktion ein Ereignis auslöst (Muster wieregistry.Registry).Manager.Subscribe: registriert bei Core (AK1), verschlüsselt das Zustellgeheimnis perconnect/internal/crypto(Envelope-Verschlüsselung, gleiches Muster wiecredential.Store) und speichert erst danach lokal. Schlägt die Core-Registrierung fehl, bleibt nichts lokal zurück.Manager.Unsubscribe: bestellt zuerst bei Core ab, löscht die lokale Zeile erst danach vollständig (AK3). Schlägt die Core-Abbestellung fehl, bleibt die lokale Zeile für einen Wiederholungsversuch erhalten statt ein verwaistes Core-Abonnement zu hinterlassen.Manager.RegisterRoutes/handleDelivery(delivery.go): HTTP-EndpunktPOST /connector-webhooks/deliveries/{id}, an den Core lauttarget_urlzustellt. Prüft die HMAC-Signatur zeitkonstant (crypto/subtle.ConstantTimeCompare) gegen das entschlüsselte Geheimnis, löst bei Erfolg die registrierte Connector-Aktion aus (AK2).
- Migration
0013_connector_webhook_subscriptions— Tabelleconnector_webhook_subscriptions(lokale id = Zustellpfad-Segment,core_subscription_id, verschlüsseltes Geheimnis,UNIQUE(tenant_slug, connector_name, event_type)). connect/scripts/check-security-test-coverage.sh—internal/webhookclient/internal/webhooksubergänzt (beide verarbeiten tenant-gebundene Geheimnisse/Core-Authentifizierung, gleiche Kategorie wiecredential/auditclient).
Akzeptanzkriterien
- Connector kann ein Webhook-Ereignis über die Core-Webhook-Registry abonnieren — erfüllt:
Manager.Subscriberuftwebhookclient.Subscribe(POST an die Registry) auf und verlangt vorab eine registrierte Connector-Aktion. - Zugestelltes Ereignis löst die zugeordnete Connector-Aktion aus — erfüllt:
handleDeliveryprüft Signatur und ruft die perRegisterActionhinterlegte Funktion mit dem zugestellten Payload auf. - Abbestellen entfernt das Abonnement vollständig — erfüllt:
Manager.Unsubscribeentfernt sowohl bei Core (webhookclient.Unsubscribe) als auch die lokale Zeile; eine erneute Zustellung an die alte URL schlägt danach mit 404 fehl.
Prüfungen vor der Abnahme
| # | Prüfung | Ergebnis |
|---|---|---|
| 1 | Testabonnement über Core-Webhook-Registry im Test erfolgreich registriert | Bestanden. TestSubscribe_RegistersAtCoreAndPersistsLocally: echter HTTP-POST an den (gestubten) Core-Endpunkt, lokale Zeile mit core_subscription_id entsteht, Core-Server bestätigt das Abonnement. Negativ abgesichert durch TestSubscribe_WithoutRegisteredAction_Fails und TestSubscribe_CoreFailure_PersistsNothingLocally (kein verwaister lokaler Datensatz bei Core-Ausfall). |
| 2 | Testereignis löst im Test die erwartete Connector-Aktion aus | Bestanden. TestHandleDelivery_ValidSignature_TriggersConnectorAction: echter HTTP-POST (eigener httptest.Server, echte TCP-Verbindung) mit gültiger HMAC-Signatur an die beim Subscribe erzeugte Zustell-URL löst die registrierte Aktion mit dem exakten Payload aus. Negativ abgesichert durch TestHandleDelivery_InvalidSignature_Rejected (401, Aktion bleibt aus) und TestHandleDelivery_UnknownSubscription_NotFound. |
| 3 | Abbestellung im Test vollständig entfernt | Bestanden. TestUnsubscribe_RemovesAtCoreAndLocally_Completely: nach Unsubscribe ist die lokale Zeile weg und der Core-Stub bestätigt das entfernte Abonnement. Zusätzlich TestHandleDelivery_AfterUnsubscribe_NotFound (Zustellung an die alte URL schlägt danach mit 404 fehl) und TestUnsubscribe_CoreFailure_KeepsLocalRowForRetry (kein Datenverlust bei Core-Ausfall während der Abbestellung). |
Testlauf (real, Host 204, gegen echte Postgres-Testdatenbank)
go build ./... → keine Ausgabe (sauber)
go vet ./... → keine Ausgabe (sauber)
gofmt -l internal/webhookclient internal/webhooksub → keine Ausgabe (bereits formatiert)
go test ./internal/webhookclient/... -v
PASS TestSubscribe_SendsRequestFields_ReturnsSubscription
PASS TestSubscribe_UnexpectedStatus_ReturnsError
PASS TestSubscribe_TransportError_ReturnsError
PASS TestUnsubscribe_SendsServiceTokenAndTenant_Succeeds
PASS TestUnsubscribe_UnexpectedStatus_ReturnsError
PASS TestUnsubscribe_TransportError_ReturnsError
ok connect/internal/webhookclient 0.007s
go test ./internal/webhooksub/... -v (TEST_TENANT_DSN gesetzt, echte Postgres-DB)
PASS TestHandleDelivery_ValidSignature_TriggersConnectorAction
PASS TestHandleDelivery_InvalidSignature_Rejected
PASS TestHandleDelivery_UnknownSubscription_NotFound
PASS TestHandleDelivery_AfterUnsubscribe_NotFound
PASS TestSubscribe_RegistersAtCoreAndPersistsLocally
PASS TestSubscribe_WithoutRegisteredAction_Fails
PASS TestSubscribe_CoreFailure_PersistsNothingLocally
PASS TestUnsubscribe_RemovesAtCoreAndLocally_Completely
PASS TestUnsubscribe_UnknownSubscription_ReturnsNotFound
PASS TestUnsubscribe_CoreFailure_KeepsLocalRowForRetry
ok connect/internal/webhooksub 1.572s–1.778s (mehrere Läufe)
go test ./... (gesamtes connect-Modul, TEST_TENANT_DSN gesetzt) → alle Pakete ok, kein FAIL
bash scripts/check-security-test-coverage.sh → "Merge-Gate bestanden: alle sicherheitsrelevanten Pakete haben Tests."
Keine Regressionen: alle zuvor bestehenden Pakete (u. a. credential, statusapi, syncjob, audittrail) bleiben unverändert grün.
Nicht Bestandteil dieser Kachel
QA-02Prüfgate Connector-Engine — separates Folgeticket, nicht umgesetzt.- Core-seitiger HTTP-Wrapper für API-07 (
Store.Subscribe/neueUnsubscribe-Methode übercmd/*-devserver, analog AUD-08) — Lücke auf Core-Seite, gehört ins core-kanban-Board, nicht in diese Kachel. Bis dahin bindetconnect/internal/webhookclientan den hier dokumentierten, noch nicht Core-seitig existierenden Vertrag; Umstellung auf die reale Core-Basis-URL ist eine reine Konfigurationsänderung (NEXARCH_*_BASE_URL), keine Codeänderung. - Verdrahtung in
connect-appsmain.go(Produktions-KEKProvidergegen Core API-10) — bewusst unterlassen: auchconnect/internal/credential(ältere, grundlegendere Kachel) ist dort noch nicht verdrahtet; ein produktiverKEKProviderfür Connect existiert im Modul noch nirgends und wäre eine eigene, hier nicht beauftragte Erweiterung.