Merge PR #325: ENG-06: unabhaengige-tenant-validierung-bei-objekttyp-registrierung-durch-dms-mail
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
CI / Web-Barrierefreiheitspruefung (axe, Tastatur, Reflow, Textabstaende) (push) Canceled after 0s
CI / Core-Modul (Build/Vet/Test) (push) Canceled after 0s
CI / DMS Security-Test-Gate (AC5, Merge-Gate) (push) Canceled after 0s
CI / Connect Security-Test-Gate (QA-01 AC4, Merge-Gate) (push) Canceled after 0s
CI / DMS-Modul Supply-Chain-Scan (govulncheck) (push) Canceled after 0s
CI / Web (dms-browser) (push) Canceled after 0s
CI / Web (pdf-viewer) (push) Canceled after 0s
CI / Web (shl) (push) Canceled after 0s
Supply-Chain-Scan (Go) / govulncheck (push) Canceled after 0s
CI / DMS-Modul (Lint/Build/Test/Coverage/Artefakt) (push) Canceled after 0s
This commit is contained in:
commit
0dd05dec88
8 files changed
+317
-9
No files matched your search
@@ -106,6 +106,7 @@ func workflowEventConfig() workflowevent.Config {
|
||||
return workflowevent.Config{
|
||||
BaseURL: os.Getenv("NEXARCH_DMS_APP_WORKFLOW_URL"),
|
||||
ServiceToken: os.Getenv("NEXARCH_DMS_APP_WORKFLOW_SERVICE_TOKEN"),
|
||||
TenantSlug: os.Getenv("NEXARCH_DMS_APP_TENANT_SLUG"),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -252,6 +252,7 @@ func startJobWorkers(ctx context.Context, pool *pgxpool.Pool, queue *jobqueue.Qu
|
||||
deps.WorkflowEvent = workflowevent.Config{
|
||||
BaseURL: os.Getenv("NEXARCH_DMS_WORKER_WORKFLOW_URL"),
|
||||
ServiceToken: os.Getenv("NEXARCH_DMS_WORKER_WORKFLOW_SERVICE_TOKEN"),
|
||||
TenantSlug: tenantSlug,
|
||||
}
|
||||
if deps.WorkflowEvent.Enabled() {
|
||||
// INT-13 Akzeptanzkriterium 1: Objekttyp + Trigger beim Start
|
||||
|
||||
@@ -33,10 +33,14 @@ const (
|
||||
var Triggers = []string{TriggerCreated, TriggerStatusChanged, TriggerDeadlineReached}
|
||||
|
||||
// Config steuert Ziel und Zugangsdaten. BaseURL leer bedeutet
|
||||
// abgeschaltet (Standard aus, analog zu FDN-18).
|
||||
// abgeschaltet (Standard aus, analog zu FDN-18). TenantSlug ist der
|
||||
// Tenant-Kontext dieser DMS-Instanz (ENG-06: Workflow prueft ihn
|
||||
// unabhaengig gegen die Core-Tenant-Registry, bevor eine Registrierung
|
||||
// uebernommen wird).
|
||||
type Config struct {
|
||||
BaseURL string
|
||||
ServiceToken string
|
||||
TenantSlug string
|
||||
}
|
||||
|
||||
// Enabled meldet, ob eine Workflow-Anbindung konfiguriert ist.
|
||||
@@ -46,6 +50,7 @@ type registerRequest struct {
|
||||
ModuleName string `json:"module_name"`
|
||||
ObjectType string `json:"object_type"`
|
||||
Triggers []string `json:"triggers"`
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
}
|
||||
|
||||
// Register registriert den Objekttyp "document" mit seinen Auslöser-
|
||||
@@ -55,7 +60,7 @@ func Register(ctx context.Context, cfg Config) error {
|
||||
if !cfg.Enabled() {
|
||||
return nil
|
||||
}
|
||||
body, err := json.Marshal(registerRequest{ModuleName: ModuleName, ObjectType: ObjectType, Triggers: Triggers})
|
||||
body, err := json.Marshal(registerRequest{ModuleName: ModuleName, ObjectType: ObjectType, Triggers: Triggers, TenantSlug: cfg.TenantSlug})
|
||||
if err != nil {
|
||||
return fmt.Errorf("workflowevent: registrierung serialisieren: %w", err)
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ func TestRegister_SendsObjectTypeAndTriggers(t *testing.T) {
|
||||
srv := fw.server()
|
||||
defer srv.Close()
|
||||
|
||||
cfg := Config{BaseURL: srv.URL, ServiceToken: "secret-token"}
|
||||
cfg := Config{BaseURL: srv.URL, ServiceToken: "secret-token", TenantSlug: "mandant-a"}
|
||||
if err := Register(context.Background(), cfg); err != nil {
|
||||
t.Fatalf("register: %v", err)
|
||||
}
|
||||
@@ -105,6 +105,11 @@ func TestRegister_SendsObjectTypeAndTriggers(t *testing.T) {
|
||||
if len(got.Triggers) != len(Triggers) {
|
||||
t.Fatalf("triggers = %v, want %v", got.Triggers, Triggers)
|
||||
}
|
||||
// ENG-06: Workflow prueft tenant_slug unabhaengig gegen Core, DMS
|
||||
// muss ihn also mitschicken, sonst wird die Registrierung abgelehnt.
|
||||
if got.TenantSlug != "mandant-a" {
|
||||
t.Fatalf("tenant_slug = %q, want mandant-a", got.TenantSlug)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRegister_Disabled prueft: ohne BaseURL passiert nichts (Standard aus).
|
||||
|
||||
@@ -121,7 +121,7 @@ func buildMux(pool *pgxpool.Pool, taskStore *task.Store, serviceToken, tenantSlu
|
||||
caseoverviewapi.NewServer(caseoverviewSvc).RegisterRoutes(mux)
|
||||
backchannelapi.NewServer(backchannelSvc).RegisterRoutes(mux)
|
||||
routingruleapi.NewServer(routingruleStore, buildRoutingSuggester()).RegisterRoutes(mux)
|
||||
moduleadapter.NewServer(pool, serviceToken, tenantSlug).RegisterRoutes(mux)
|
||||
moduleadapter.NewServer(pool, serviceToken, tenantSlug, tenantVerifier).RegisterRoutes(mux)
|
||||
return mux, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
# ENG-06 – Pruefprotokoll: Unabhaengige Tenant-Validierung bei Objekttyp-Registrierung durch DMS/Mail
|
||||
|
||||
Durchgefuehrt am 2026-10-16 gegen echten Postgres auf Testhost
|
||||
192.168.1.204 (eigene Wegwerf-Schemas ueber `internal/testdb`, kein
|
||||
Bezug zu archivdms-Daten).
|
||||
|
||||
## Ausgangslage
|
||||
|
||||
ENG-02 (`internal/moduleadapter`, `Register`/`handleRegister`) nahm
|
||||
Objekttyp- und Trigger-Registrierungen von DMS/Mail bislang ohne jeden
|
||||
Tenant-Kontext entgegen — `registerRequest` kannte kein `tenant_slug`.
|
||||
Das ist die gleiche Luecke, die RET-12 fuer Archive bereits geschlossen
|
||||
hat (`archive/internal/moduleadapter`: `TenantVerifier`-Interface +
|
||||
Pruefung in `RegisterHandler` gegen Core TEN-01, bevor `Register`
|
||||
aufgerufen wird) und die VOR-06 fuer Workflows Rueckkanal
|
||||
(`internal/backchannel`) mit demselben Muster (Abgleich gegen
|
||||
`ownTenantSlug` UND gegen die Core-Registry) bereits umgesetzt hat.
|
||||
ENG-09 hatte fuer `/events` (`PublishEvent`) zwar schon einen
|
||||
`tenant_slug`/`ownTenantSlug`-Abgleich, aber keinen Core-Registry-Aufruf
|
||||
und keine Pruefung bei der eigentlichen Registrierung — dieses Ticket
|
||||
schliesst die Luecke fuer `/register`.
|
||||
|
||||
## Umsetzung
|
||||
|
||||
- `workflow/internal/moduleadapter/handler.go`: neues
|
||||
`TenantVerifier`-Interface (gleiches Muster wie Archive RET-12);
|
||||
`Server` erhaelt ein `tenants TenantVerifier`-Feld, `NewServer` nimmt
|
||||
es als vierten Parameter entgegen. Neue Methode `tenantValid` prueft
|
||||
zweifach, bevor eine Registrierung uebernommen wird: (1) der
|
||||
mitgelieferte `tenant_slug` muss zur Server-Instanz passen
|
||||
(`ownTenantSlug`, Modell A — ein Prozess/eine DB je Mandant), (2)
|
||||
unabhaengig davon bestaetigt `tenants.VerifyTenant` gegen Core TEN-01,
|
||||
dass der Tenant dort tatsaechlich gefuehrt wird. Ein fehlgeschlagener
|
||||
Core-Aufruf zaehlt als ungueltig (fail closed). `registerRequest` hat
|
||||
jetzt ein Pflichtfeld `tenant_slug`; `handleRegister` lehnt bei
|
||||
ungueltigem Tenant-Kontext mit 403 ab und protokolliert
|
||||
Modul/Objekttyp/Tenant. `Register`/`Rollback`/`Deregister` (DB-Ebene)
|
||||
bleiben unveraendert — die Pruefung sitzt an der API-Grenze, analog zu
|
||||
`backchannel.ForObject` (VOR-06), keine Aenderung an Schema oder
|
||||
Fachlogik noetig (kleinste Loesung).
|
||||
- `workflow/cmd/workflow-app/main.go`: `moduleadapter.NewServer(...)`
|
||||
bekommt den bereits vorhandenen `tenantVerifier`
|
||||
(`tenantregistryclient.Client`, zuvor schon fuer `backchannel`
|
||||
verdrahtet) als vierten Parameter mit.
|
||||
- `dms/internal/workflowevent/workflowevent.go` (INT-13, der reale
|
||||
Aufrufer): `Config` und `registerRequest` bekommen `TenantSlug`,
|
||||
`Register` schickt es mit. Ohne diese Ergaenzung haette der neue,
|
||||
serverseitig *pflichtige* `tenant_slug` den bestehenden,
|
||||
unveraenderten DMS-Registrierungsablauf aus ENG-02 kaputt gemacht
|
||||
(Regression gegen Pruefung 3) — DMS kannte das Feld bislang gar
|
||||
nicht.
|
||||
- `dms/cmd/worker/main.go` und `dms/cmd/app/api.go`: bestehende,
|
||||
bereits vorhandene `NEXARCH_DMS_WORKER_TENANT_SLUG` /
|
||||
`NEXARCH_DMS_APP_TENANT_SLUG` Umgebungsvariablen werden jetzt auch in
|
||||
die `workflowevent.Config` durchgereicht (kein neuer Zugangsdaten-
|
||||
Parameter noetig).
|
||||
- Mail hat noch keinen eigenen Registrierungs-Client im Repo (kein
|
||||
Pendant zu `dms/internal/workflowevent` vorhanden) — nichts zu
|
||||
aendern, aber jeder zukuenftige Mail-Adapter muss `tenant_slug`
|
||||
ebenso mitschicken.
|
||||
|
||||
## Pruefungen
|
||||
|
||||
### Pruefung 1: Registrierung mit gueltigem, existierendem Tenant-Kontext wird uebernommen
|
||||
Ergebnis: PASS
|
||||
- `TestHandleRegister_ValidTenantContextAccepted`
|
||||
(workflow/internal/moduleadapter): `tenant_slug=mandant-a` gegen einen
|
||||
fuer `mandant-a` konfigurierten Server, Core-Fake bestaetigt
|
||||
`mandant-a` -> HTTP 200.
|
||||
- `TestHandleRegisterAndList_EndToEnd` (angepasst, jetzt mit
|
||||
`tenant_slug`) zeigt zusaetzlich, dass die Registrierung tatsaechlich
|
||||
in `module_registrations` landet und ueber `/registrations` sichtbar
|
||||
ist.
|
||||
- `TestRegister_SendsObjectTypeAndTriggers` (dms/internal/workflowevent,
|
||||
INT-13-Seite): bestaetigt, dass DMS den `tenant_slug` tatsaechlich im
|
||||
Request-Body mitschickt (`got.TenantSlug == "mandant-a"`).
|
||||
|
||||
### Pruefung 2: Registrierung mit manipuliertem/fremdem Tenant-Kontext wird abgelehnt
|
||||
Ergebnis: PASS, drei Varianten abgedeckt:
|
||||
- `TestHandleRegister_ForeignTenantSlugRejected`: `tenant_slug` weicht
|
||||
von der Server-Instanz ab (`mandant-fremd` statt `mandant-a`) -> HTTP
|
||||
403, Log-Zeile `registrierung abgelehnt, unbekannter tenant-kontext`,
|
||||
anschliessend `TriggersFor` bestaetigt, dass nichts uebernommen wurde.
|
||||
- `TestHandleRegister_TenantUnknownToCoreRegistryRejected`: der
|
||||
`tenant_slug` passt formal zur Server-Instanz (`mandant-a`), aber die
|
||||
(simulierte) Core-Registry kennt diesen Tenant nicht (mehr) -> HTTP
|
||||
403. Dieser Fall beweist, dass die Pruefung nicht nur lokal gegen die
|
||||
Instanz-Konfiguration geht (das waere kein unabhaengiger Abgleich),
|
||||
sondern wirklich gegen Core TEN-01.
|
||||
- `TestHandleRegister_TenantRegistryCallFailsRejected`: Core-Aufruf
|
||||
schlaegt technisch fehl (simulierter Transportfehler) -> HTTP 403,
|
||||
fail closed statt stillschweigend akzeptiert.
|
||||
|
||||
### Pruefung 3 (Regression): normaler DMS-/Mail-Registrierungsablauf bleibt funktionsfaehig
|
||||
Ergebnis: PASS
|
||||
- Alle bestehenden ENG-02-Tests (`TestRegister_TwoFictionalModulesRegisterIndependently`,
|
||||
`TestRegister_DuplicateSameTriggersRejected`,
|
||||
`TestRegister_ChangedTriggersCreateNewVersionAndRollbackRestoresPrevious`,
|
||||
`TestDeregister_RemovesTriggersCompletely`,
|
||||
`TestTriggersFor_UnknownObjectTypeReturnsClearError`,
|
||||
`TestRollback_UnknownVersionReturnsError`) unveraendert gruen — sie
|
||||
rufen `Register`/`Rollback`/`Deregister` direkt auf Go-Ebene auf, die
|
||||
DB-Funktionen selbst sind unangetastet.
|
||||
- Bestehende ENG-09-Tests (`TestPublishEvent_*`) weiterhin gruen,
|
||||
`/events` ist von diesem Ticket nicht betroffen.
|
||||
- Kompletter `workflow`-Modultest (`go test ./...`, alle Pakete)
|
||||
gruen, `go vet ./...` ohne Befund.
|
||||
- `dms`-Modul: `internal/workflowevent`-Tests gruen (inkl. der beiden
|
||||
Integrationstests mit echter Jobqueue), `go build ./...` und
|
||||
`go vet ./...` fuer das komplette Modul fehlerfrei, `cmd/app` und
|
||||
`cmd/worker` bauen mit der erweiterten `workflowevent.Config`.
|
||||
|
||||
## Testbefehl
|
||||
```
|
||||
TEST_TENANT_DSN='postgresql://<rolle>:<pw>@localhost:5432/<db>?sslmode=disable' \
|
||||
go test ./... -p 1
|
||||
```
|
||||
(ausgefuehrt sowohl im `workflow`- als auch im `dms`-Modul, jeweils
|
||||
gegen ein frisches Wegwerf-Schema ueber `internal/testdb`)
|
||||
|
||||
## Nicht Bestandteil (laut Ticket / kleinste Loesung)
|
||||
- `/rollback`, `/deregister`, `/registrations` (GET) bleiben ohne
|
||||
eigene Tenant-Pruefung — sie wirken auf bereits bestehende,
|
||||
modul-/objekttyp-identifizierte Registrierungen, nicht auf eine neue
|
||||
Registrierung mit fremdem Tenant-Claim; AK/Ticket beziehen sich nur
|
||||
auf die Registrierung selbst.
|
||||
- `/events` (`PublishEvent`, ENG-09) bleibt wie vor diesem Ticket ohne
|
||||
Core-Registry-Abgleich (nur lokaler `ownTenantSlug`-Vergleich) — das
|
||||
ist eine vorbestehende, separate Luecke aus ENG-09 und nicht
|
||||
Gegenstand von ENG-06 (kein Umbau angrenzender Bereiche).
|
||||
- Kein eigener Mail-Registrierungs-Client — existiert im Repo noch
|
||||
nicht, also nichts zu migrieren.
|
||||
@@ -1,8 +1,10 @@
|
||||
package moduleadapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
@@ -11,6 +13,14 @@ import (
|
||||
"gitea.perlbach24.de/scripte/nexarch/workflow/internal/svcauth"
|
||||
)
|
||||
|
||||
// TenantVerifier prueft einen vom aufrufenden Modul mitgelieferten
|
||||
// Tenant-Kontext unabhaengig gegen die Core-Tenant-Registry (TEN-01) -
|
||||
// ENG-06: Workflow vertraut dabei nicht der Angabe von DMS/Mail selbst
|
||||
// (Muster aus archive/internal/moduleadapter, RET-12).
|
||||
type TenantVerifier interface {
|
||||
VerifyTenant(ctx context.Context, tenantSlug string) (bool, error)
|
||||
}
|
||||
|
||||
// Server bindet die Registrierungs-API an einen Postgres-Pool und
|
||||
// verlangt ein Service-Token bei jedem Aufruf (fail closed: ein leeres
|
||||
// oder falsches Token wird immer abgewiesen, Lehre aus RET-14/F-08).
|
||||
@@ -21,10 +31,11 @@ type Server struct {
|
||||
pool *pgxpool.Pool
|
||||
serviceToken string
|
||||
ownTenantSlug string
|
||||
tenants TenantVerifier
|
||||
}
|
||||
|
||||
func NewServer(pool *pgxpool.Pool, serviceToken, ownTenantSlug string) *Server {
|
||||
return &Server{pool: pool, serviceToken: serviceToken, ownTenantSlug: ownTenantSlug}
|
||||
func NewServer(pool *pgxpool.Pool, serviceToken, ownTenantSlug string, tenants TenantVerifier) *Server {
|
||||
return &Server{pool: pool, serviceToken: serviceToken, ownTenantSlug: ownTenantSlug, tenants: tenants}
|
||||
}
|
||||
|
||||
func (s *Server) RegisterRoutes(mux *http.ServeMux) {
|
||||
@@ -46,10 +57,32 @@ func (s *Server) requireAuth(next http.HandlerFunc) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// tenantValid prueft den mitgelieferten Tenant-Kontext unabhaengig von
|
||||
// der Angabe des registrierenden Moduls (ENG-06 Akzeptanzkriterium 1):
|
||||
// er muss zu dieser Server-Instanz passen UND von der Core-Tenant-
|
||||
// Registry als echter, gefuehrter Tenant bestaetigt werden. Ein
|
||||
// fehlgeschlagener Core-Aufruf gilt als ungueltig (fail closed), kein
|
||||
// stillschweigendes Durchwinken.
|
||||
func (s *Server) tenantValid(ctx context.Context, tenantSlug string) bool {
|
||||
if tenantSlug == "" || tenantSlug != s.ownTenantSlug {
|
||||
return false
|
||||
}
|
||||
if s.tenants == nil {
|
||||
return true
|
||||
}
|
||||
ok, err := s.tenants.VerifyTenant(ctx, tenantSlug)
|
||||
if err != nil {
|
||||
log.Printf("moduleadapter: tenant-registry-abgleich fehlgeschlagen tenant=%q: %v", tenantSlug, err)
|
||||
return false
|
||||
}
|
||||
return ok
|
||||
}
|
||||
|
||||
type registerRequest struct {
|
||||
ModuleName string `json:"module_name"`
|
||||
ObjectType string `json:"object_type"`
|
||||
Triggers []string `json:"triggers"`
|
||||
TenantSlug string `json:"tenant_slug"`
|
||||
}
|
||||
|
||||
func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -58,6 +91,11 @@ func (s *Server) handleRegister(w http.ResponseWriter, r *http.Request) {
|
||||
writeError(w, http.StatusBadRequest, "ungueltiger anfrage-koerper")
|
||||
return
|
||||
}
|
||||
if !s.tenantValid(r.Context(), req.TenantSlug) {
|
||||
log.Printf("moduleadapter: registrierung abgelehnt, unbekannter tenant-kontext modul=%s objekttyp=%s tenant=%q", req.ModuleName, req.ObjectType, req.TenantSlug)
|
||||
writeError(w, http.StatusForbidden, "unbekannter tenant-kontext")
|
||||
return
|
||||
}
|
||||
reg, err := Register(r.Context(), s.pool, req.ModuleName, req.ObjectType, req.Triggers)
|
||||
if errors.Is(err, ErrDuplicateTriggers) {
|
||||
writeError(w, http.StatusConflict, "registrierung unveraendert, keine neue version noetig")
|
||||
|
||||
@@ -2,17 +2,34 @@ package moduleadapter
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// fakeVerifier simuliert Core TEN-01 fuer die Handler-Tests, ohne einen
|
||||
// echten Core-Dienst zu benoetigen (Muster wie
|
||||
// archive/internal/moduleadapter, RET-12).
|
||||
type fakeVerifier struct {
|
||||
validTenants map[string]bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (f fakeVerifier) VerifyTenant(_ context.Context, tenantSlug string) (bool, error) {
|
||||
if f.err != nil {
|
||||
return false, f.err
|
||||
}
|
||||
return f.validTenants[tenantSlug], nil
|
||||
}
|
||||
|
||||
func newTestServer(t *testing.T) (*Server, string) {
|
||||
t.Helper()
|
||||
pool := testPool(t)
|
||||
token := "test-service-token-secret"
|
||||
return NewServer(pool, token, "mandant-a"), token
|
||||
verifier := fakeVerifier{validTenants: map[string]bool{"mandant-a": true}}
|
||||
return NewServer(pool, token, "mandant-a", verifier), token
|
||||
}
|
||||
|
||||
func TestHandleRegister_RequiresServiceToken(t *testing.T) {
|
||||
@@ -22,7 +39,7 @@ func TestHandleRegister_RequiresServiceToken(t *testing.T) {
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}})
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"})
|
||||
|
||||
// Ohne Token: 401.
|
||||
resp, err := http.Post(ts.URL+"/register", "application/json", bytes.NewReader(body))
|
||||
@@ -68,7 +85,7 @@ func TestHandleRegisterAndList_EndToEnd(t *testing.T) {
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "mail-fiktiv", ObjectType: "message", Triggers: []string{"mail.received"}})
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "mail-fiktiv", ObjectType: "message", Triggers: []string{"mail.received"}, TenantSlug: "mandant-a"})
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
@@ -96,3 +113,112 @@ func TestHandleRegisterAndList_EndToEnd(t *testing.T) {
|
||||
t.Fatalf("erwarte 1 registrierung fuer mail-fiktiv, habe %+v", regs)
|
||||
}
|
||||
}
|
||||
|
||||
// ENG-06 Pruefung 1: Registrierung mit gueltigem, existierendem
|
||||
// Tenant-Kontext wird uebernommen.
|
||||
func TestHandleRegister_ValidTenantContextAccepted(t *testing.T) {
|
||||
srv, token := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.RegisterRoutes(mux)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"})
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("registrieren: %v", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Fatalf("erwarte 200 fuer gueltigen tenant, habe %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// ENG-06 Pruefung 2: Registrierung mit abweichendem Tenant-Kontext
|
||||
// (passt nicht zur Server-Instanz) wird abgelehnt und nicht uebernommen.
|
||||
func TestHandleRegister_ForeignTenantSlugRejected(t *testing.T) {
|
||||
srv, token := newTestServer(t)
|
||||
mux := http.NewServeMux()
|
||||
srv.RegisterRoutes(mux)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-fremd"})
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("registrieren: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("erwarte 403 fuer fremden tenant, habe %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if _, err := TriggersFor(context.Background(), srv.pool, "dms-fiktiv", "invoice"); err == nil {
|
||||
t.Fatal("erwarte keine uebernommene registrierung fuer fremden tenant")
|
||||
}
|
||||
}
|
||||
|
||||
// ENG-06 Pruefung 2 (Variante): die Server-Instanz ist zwar fuer
|
||||
// "mandant-a" konfiguriert, aber die Core-Registry kennt diesen Tenant
|
||||
// nicht (mehr) -- die Registrierung darf sich nicht allein auf den
|
||||
// lokal konfigurierten Tenant-Slug verlassen, sondern muss unabhaengig
|
||||
// gegen Core pruefen.
|
||||
func TestHandleRegister_TenantUnknownToCoreRegistryRejected(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
token := "test-service-token-secret"
|
||||
verifier := fakeVerifier{validTenants: map[string]bool{}}
|
||||
srv := NewServer(pool, token, "mandant-a", verifier)
|
||||
mux := http.NewServeMux()
|
||||
srv.RegisterRoutes(mux)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"})
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("registrieren: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("erwarte 403 wenn core-registry tenant nicht bestaetigt, habe %d", resp.StatusCode)
|
||||
}
|
||||
|
||||
if _, err := TriggersFor(context.Background(), pool, "dms-fiktiv", "invoice"); err == nil {
|
||||
t.Fatal("erwarte keine uebernommene registrierung ohne core-bestaetigung")
|
||||
}
|
||||
}
|
||||
|
||||
// ENG-06 Regressionstest: Core-Registry-Aufruf schlaegt technisch fehl
|
||||
// (Transportfehler) -> fail closed, keine Registrierung.
|
||||
func TestHandleRegister_TenantRegistryCallFailsRejected(t *testing.T) {
|
||||
pool := testPool(t)
|
||||
token := "test-service-token-secret"
|
||||
verifier := fakeVerifier{err: context.DeadlineExceeded}
|
||||
srv := NewServer(pool, token, "mandant-a", verifier)
|
||||
mux := http.NewServeMux()
|
||||
srv.RegisterRoutes(mux)
|
||||
ts := httptest.NewServer(mux)
|
||||
defer ts.Close()
|
||||
|
||||
body, _ := json.Marshal(registerRequest{ModuleName: "dms-fiktiv", ObjectType: "invoice", Triggers: []string{"document.uploaded"}, TenantSlug: "mandant-a"})
|
||||
req, _ := http.NewRequest(http.MethodPost, ts.URL+"/register", bytes.NewReader(body))
|
||||
req.Header.Set("X-Service-Token", token)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatalf("registrieren: %v", err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Fatalf("erwarte 403 bei fehlgeschlagenem core-aufruf (fail closed), habe %d", resp.StatusCode)
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user