From df9581cf450cabf2c51169eddec24f82ef131e55 Mon Sep 17 00:00:00 2001 From: patrick Date: Thu, 29 Oct 2026 23:41:06 +0100 Subject: [PATCH] workflow, connect: bearerauth aus common/bearerauth Die beiden gleichen Kopien entfallen; common v0.2.2. Das Testpflicht-Muster fuer bearerauth wandert vom workflow-Gate in das common-Gate. Co-Authored-By: Claude Sonnet 5.5 --- common/cmd/pflichttestgate/main.go | 1 + connect/cmd/connect-app/main.go | 2 +- connect/go.mod | 2 +- connect/go.sum | 4 +- connect/internal/bearerauth/bearerauth.go | 71 ------------------ .../internal/bearerauth/bearerauth_test.go | 75 ------------------- workflow/cmd/workflow-app/main.go | 2 +- workflow/docs/TESTSTRATEGIE.md | 5 +- workflow/go.mod | 2 +- workflow/go.sum | 4 +- workflow/internal/bearerauth/bearerauth.go | 71 ------------------ .../internal/bearerauth/bearerauth_test.go | 75 ------------------- workflow/internal/pflichttestgate/gate.go | 1 - .../internal/pflichttestgate/gate_test.go | 8 -- 14 files changed, 12 insertions(+), 311 deletions(-) delete mode 100644 connect/internal/bearerauth/bearerauth.go delete mode 100644 connect/internal/bearerauth/bearerauth_test.go delete mode 100644 workflow/internal/bearerauth/bearerauth.go delete mode 100644 workflow/internal/bearerauth/bearerauth_test.go diff --git a/common/cmd/pflichttestgate/main.go b/common/cmd/pflichttestgate/main.go index 48e3e947..0047043b 100644 --- a/common/cmd/pflichttestgate/main.go +++ b/common/cmd/pflichttestgate/main.go @@ -20,6 +20,7 @@ var sensitive = []*regexp.Regexp{ regexp.MustCompile(`(^|/)common/policyclient/`), regexp.MustCompile(`(^|/)common/apierror/`), regexp.MustCompile(`(^|/)common/httpharden/`), + regexp.MustCompile(`(^|/)common/bearerauth/`), } func main() { diff --git a/connect/cmd/connect-app/main.go b/connect/cmd/connect-app/main.go index 7745b7b5..52a6d898 100644 --- a/connect/cmd/connect-app/main.go +++ b/connect/cmd/connect-app/main.go @@ -15,9 +15,9 @@ import ( "github.com/jackc/pgx/v5/pgxpool" + "gitea.perlbach24.de/scripte/nexarch/common/bearerauth" "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" "gitea.perlbach24.de/scripte/nexarch/common/policyclient" - "gitea.perlbach24.de/scripte/nexarch/connect/internal/bearerauth" "gitea.perlbach24.de/scripte/nexarch/connect/internal/connectorsetupapi" "gitea.perlbach24.de/scripte/nexarch/connect/internal/credential" "gitea.perlbach24.de/scripte/nexarch/connect/internal/crypto" diff --git a/connect/go.mod b/connect/go.mod index d6c45234..f3353a84 100644 --- a/connect/go.mod +++ b/connect/go.mod @@ -5,7 +5,7 @@ go 1.25.0 toolchain go1.25.13 require ( - gitea.perlbach24.de/scripte/nexarch/common v0.2.1 + gitea.perlbach24.de/scripte/nexarch/common v0.2.2 github.com/jackc/pgx/v5 v5.9.2 golang.org/x/text v0.29.0 ) diff --git a/connect/go.sum b/connect/go.sum index c33711d0..60820eeb 100644 --- a/connect/go.sum +++ b/connect/go.sum @@ -1,5 +1,5 @@ -gitea.perlbach24.de/scripte/nexarch/common v0.2.1 h1:vIreSocK/F4+uid+lZ7u0CSNlPFvA1N7A77k+RhNeAc= -gitea.perlbach24.de/scripte/nexarch/common v0.2.1/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= +gitea.perlbach24.de/scripte/nexarch/common v0.2.2 h1:PupmTMm3xysOP8J+zgqRAgwro1XlaQn0njsuUgbqN14= +gitea.perlbach24.de/scripte/nexarch/common v0.2.2/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/connect/internal/bearerauth/bearerauth.go b/connect/internal/bearerauth/bearerauth.go deleted file mode 100644 index bea3e87b..00000000 --- a/connect/internal/bearerauth/bearerauth.go +++ /dev/null @@ -1,71 +0,0 @@ -// Package bearerauth setzt die Identitaet der Connect-APIs aus einem -// verifizierten Core-Modul-Token. Die Handler lesen weiter X-User-Id und -// X-Tenant-Slug; die Middleware ueberschreibt beide aus den Claims, sodass ein -// Client sie nicht frei setzen kann. X-User-Role kommt nur aus dem Token. -package bearerauth - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" -) - -const ( - headerUser = "X-User-Id" - headerTenant = "X-Tenant-Slug" - headerRole = "X-User-Role" -) - -// Middleware verlangt fuer Anfragen mit Authorization: Bearer ein gueltiges -// Token. Ohne Bearer werden X-User-Id und X-Tenant-Slug entfernt, ausser -// allowHeaderAuth ist gesetzt (Uebergang, bis alle Clients Bearer senden). -func Middleware(v *moduleauth.Verifier, allowHeaderAuth bool, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.Header.Del(headerRole) - token, ok := bearer(r) - if !ok { - if !allowHeaderAuth { - r.Header.Del(headerUser) - r.Header.Del(headerTenant) - } - next.ServeHTTP(w, r) - return - } - if v == nil { - writeError(w, http.StatusUnauthorized, "nicht angemeldet") - return - } - claims, err := v.Verify(r.Context(), token) - if err != nil { - if errors.Is(err, moduleauth.ErrInvalidToken) { - writeError(w, http.StatusUnauthorized, "nicht angemeldet") - } else { - writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar") - } - return - } - r.Header.Set(headerUser, claims.Subject) - r.Header.Set(headerTenant, claims.Tenant) - if claims.Role != "" { - r.Header.Set(headerRole, claims.Role) - } - next.ServeHTTP(w, r) - }) -} - -func bearer(r *http.Request) (string, bool) { - h := r.Header.Get("Authorization") - if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") { - return strings.TrimSpace(h[7:]), true - } - return "", false -} - -func writeError(w http.ResponseWriter, status int, message string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) -} diff --git a/connect/internal/bearerauth/bearerauth_test.go b/connect/internal/bearerauth/bearerauth_test.go deleted file mode 100644 index 685f2fbe..00000000 --- a/connect/internal/bearerauth/bearerauth_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package bearerauth - -import ( - "net/http" - "net/http/httptest" - "testing" - - "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" -) - -func echo() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(r.Header.Get("X-User-Id") + "|" + r.Header.Get("X-Tenant-Slug") + "|" + r.Header.Get("X-User-Role"))) - }) -} - -func do(h http.Handler, hdr map[string]string) *httptest.ResponseRecorder { - req := httptest.NewRequest(http.MethodGet, "/x", nil) - for k, v := range hdr { - req.Header.Set(k, v) - } - rec := httptest.NewRecorder() - h.ServeHTTP(rec, req) - return rec -} - -func TestBearerOverridesClientSuppliedHeaders(t *testing.T) { - v, iss := moduleauth.NewTestVerifierAndIssuer() - h := Middleware(v, false, echo()) - rec := do(h, map[string]string{ - "Authorization": "Bearer " + iss.Token("u1", "acme", "user", "", ""), - "X-User-Id": "fremd", "X-Tenant-Slug": "fremd", - }) - if rec.Code != 200 || rec.Body.String() != "u1|acme|user" { - t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String()) - } -} - -func TestInvalidBearerIs401(t *testing.T) { - v, _ := moduleauth.NewTestVerifierAndIssuer() - _, other := moduleauth.NewTestVerifierAndIssuer() - h := Middleware(v, true, echo()) - if rec := do(h, map[string]string{"Authorization": "Bearer " + other.Token("u1", "acme", "user", "", ""), "X-User-Id": "evil"}); rec.Code != 401 { - t.Fatalf("code=%d", rec.Code) - } - if rec := do(Middleware(nil, true, echo()), map[string]string{"Authorization": "Bearer x.y.z"}); rec.Code != 401 { - t.Fatalf("ohne Verifier code=%d", rec.Code) - } -} - -func TestWithoutBearerHeadersStrippedUnlessAllowed(t *testing.T) { - v, _ := moduleauth.NewTestVerifierAndIssuer() - hdr := map[string]string{"X-User-Id": "u9", "X-Tenant-Slug": "t9"} - if rec := do(Middleware(v, false, echo()), hdr); rec.Body.String() != "||" { - t.Fatalf("Header muessen entfernt werden, bekam %q", rec.Body.String()) - } - if rec := do(Middleware(v, true, echo()), hdr); rec.Body.String() != "u9|t9|" { - t.Fatalf("Uebergangsmodus muss Header durchlassen, bekam %q", rec.Body.String()) - } -} - -func TestRoleOnlyFromToken(t *testing.T) { - v, iss := moduleauth.NewTestVerifierAndIssuer() - h := Middleware(v, true, echo()) - rec := do(h, map[string]string{ - "Authorization": "Bearer " + iss.Token("u1", "acme", "tenant_admin", "", ""), - "X-User-Role": "tenant_admin_fremd", - }) - if rec.Body.String() != "u1|acme|tenant_admin" { - t.Fatalf("Rolle muss aus dem Token kommen, bekam %q", rec.Body.String()) - } - if rec := do(h, map[string]string{"X-User-Role": "tenant_admin"}); rec.Body.String() != "||" { - t.Fatalf("Rolle ohne Token muss entfernt werden, bekam %q", rec.Body.String()) - } -} diff --git a/workflow/cmd/workflow-app/main.go b/workflow/cmd/workflow-app/main.go index e241dc21..143758c3 100644 --- a/workflow/cmd/workflow-app/main.go +++ b/workflow/cmd/workflow-app/main.go @@ -16,6 +16,7 @@ import ( "github.com/jackc/pgx/v5/pgxpool" + "gitea.perlbach24.de/scripte/nexarch/common/bearerauth" "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" "gitea.perlbach24.de/scripte/nexarch/common/policyclient" "gitea.perlbach24.de/scripte/nexarch/common/tenantregistryclient" @@ -23,7 +24,6 @@ import ( "gitea.perlbach24.de/scripte/nexarch/workflow/internal/approvalapi" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannel" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/backchannelapi" - "gitea.perlbach24.de/scripte/nexarch/workflow/internal/bearerauth" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseapi" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseobj" "gitea.perlbach24.de/scripte/nexarch/workflow/internal/caseoverview" diff --git a/workflow/docs/TESTSTRATEGIE.md b/workflow/docs/TESTSTRATEGIE.md index 9832f6c3..3bda9886 100644 --- a/workflow/docs/TESTSTRATEGIE.md +++ b/workflow/docs/TESTSTRATEGIE.md @@ -64,8 +64,9 @@ aendert — **jede geaenderte `.go`-Datei muss von einer geaenderten oder neuen - **Auth-nahe Logik:** `workflow/internal/svcauth/` (Service-Token-Vergleich in konstanter Zeit), `workflow/internal/moduleadapter/` (prueft `X-Service-Token` gegen `svcauth.Equal` — der tatsaechliche Durchsetzungspunkt), - `workflow/internal/bearerauth/` (prueft das Bearer-Token der Frontends gegen - die Core-JWKS; `X-User-Id` nur noch ueber den Uebergangsschalter) + `common/bearerauth/` (prueft das Bearer-Token der Frontends gegen die + Core-JWKS; `X-User-Id` nur noch ueber den Uebergangsschalter; Gate im + common-Modul) - **Tenant-Scoping:** `workflow/internal/tenantregistryclient/` (fragt Core TEN-01, muss bei Fehler fail-closed bleiben), `workflow/internal/backchannel/` und `workflow/internal/backchannelapi/` (extern erreichbarer Eingang, der den diff --git a/workflow/go.mod b/workflow/go.mod index 85494818..24984d1d 100644 --- a/workflow/go.mod +++ b/workflow/go.mod @@ -3,7 +3,7 @@ module gitea.perlbach24.de/scripte/nexarch/workflow go 1.25.0 require ( - gitea.perlbach24.de/scripte/nexarch/common v0.2.1 + gitea.perlbach24.de/scripte/nexarch/common v0.2.2 github.com/jackc/pgx/v5 v5.9.2 ) diff --git a/workflow/go.sum b/workflow/go.sum index c33711d0..60820eeb 100644 --- a/workflow/go.sum +++ b/workflow/go.sum @@ -1,5 +1,5 @@ -gitea.perlbach24.de/scripte/nexarch/common v0.2.1 h1:vIreSocK/F4+uid+lZ7u0CSNlPFvA1N7A77k+RhNeAc= -gitea.perlbach24.de/scripte/nexarch/common v0.2.1/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= +gitea.perlbach24.de/scripte/nexarch/common v0.2.2 h1:PupmTMm3xysOP8J+zgqRAgwro1XlaQn0njsuUgbqN14= +gitea.perlbach24.de/scripte/nexarch/common v0.2.2/go.mod h1:CiEImG++QMt1xZqSMeDJhPiB7sfWTLZOwjS6OY/Iguc= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= diff --git a/workflow/internal/bearerauth/bearerauth.go b/workflow/internal/bearerauth/bearerauth.go deleted file mode 100644 index efef3e4b..00000000 --- a/workflow/internal/bearerauth/bearerauth.go +++ /dev/null @@ -1,71 +0,0 @@ -// Package bearerauth setzt die Identitaet der Workflow-APIs aus einem -// verifizierten Core-Modul-Token. Die Handler lesen weiter X-User-Id und -// X-Tenant-Slug; die Middleware ueberschreibt beide aus den Claims, sodass ein -// Client sie nicht frei setzen kann. X-User-Role kommt nur aus dem Token. -package bearerauth - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" -) - -const ( - headerUser = "X-User-Id" - headerTenant = "X-Tenant-Slug" - headerRole = "X-User-Role" -) - -// Middleware verlangt fuer Anfragen mit Authorization: Bearer ein gueltiges -// Token. Ohne Bearer werden X-User-Id und X-Tenant-Slug entfernt, ausser -// allowHeaderAuth ist gesetzt (Uebergang, bis alle Clients Bearer senden). -func Middleware(v *moduleauth.Verifier, allowHeaderAuth bool, next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - r.Header.Del(headerRole) - token, ok := bearer(r) - if !ok { - if !allowHeaderAuth { - r.Header.Del(headerUser) - r.Header.Del(headerTenant) - } - next.ServeHTTP(w, r) - return - } - if v == nil { - writeError(w, http.StatusUnauthorized, "nicht angemeldet") - return - } - claims, err := v.Verify(r.Context(), token) - if err != nil { - if errors.Is(err, moduleauth.ErrInvalidToken) { - writeError(w, http.StatusUnauthorized, "nicht angemeldet") - } else { - writeError(w, http.StatusServiceUnavailable, "identitaetsdienst nicht erreichbar") - } - return - } - r.Header.Set(headerUser, claims.Subject) - r.Header.Set(headerTenant, claims.Tenant) - if claims.Role != "" { - r.Header.Set(headerRole, claims.Role) - } - next.ServeHTTP(w, r) - }) -} - -func bearer(r *http.Request) (string, bool) { - h := r.Header.Get("Authorization") - if len(h) > 7 && strings.EqualFold(h[:7], "bearer ") { - return strings.TrimSpace(h[7:]), true - } - return "", false -} - -func writeError(w http.ResponseWriter, status int, message string) { - w.Header().Set("Content-Type", "application/json") - w.WriteHeader(status) - _ = json.NewEncoder(w).Encode(map[string]string{"error": message}) -} diff --git a/workflow/internal/bearerauth/bearerauth_test.go b/workflow/internal/bearerauth/bearerauth_test.go deleted file mode 100644 index 685f2fbe..00000000 --- a/workflow/internal/bearerauth/bearerauth_test.go +++ /dev/null @@ -1,75 +0,0 @@ -package bearerauth - -import ( - "net/http" - "net/http/httptest" - "testing" - - "gitea.perlbach24.de/scripte/nexarch/common/moduleauth" -) - -func echo() http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - _, _ = w.Write([]byte(r.Header.Get("X-User-Id") + "|" + r.Header.Get("X-Tenant-Slug") + "|" + r.Header.Get("X-User-Role"))) - }) -} - -func do(h http.Handler, hdr map[string]string) *httptest.ResponseRecorder { - req := httptest.NewRequest(http.MethodGet, "/x", nil) - for k, v := range hdr { - req.Header.Set(k, v) - } - rec := httptest.NewRecorder() - h.ServeHTTP(rec, req) - return rec -} - -func TestBearerOverridesClientSuppliedHeaders(t *testing.T) { - v, iss := moduleauth.NewTestVerifierAndIssuer() - h := Middleware(v, false, echo()) - rec := do(h, map[string]string{ - "Authorization": "Bearer " + iss.Token("u1", "acme", "user", "", ""), - "X-User-Id": "fremd", "X-Tenant-Slug": "fremd", - }) - if rec.Code != 200 || rec.Body.String() != "u1|acme|user" { - t.Fatalf("code=%d body=%q", rec.Code, rec.Body.String()) - } -} - -func TestInvalidBearerIs401(t *testing.T) { - v, _ := moduleauth.NewTestVerifierAndIssuer() - _, other := moduleauth.NewTestVerifierAndIssuer() - h := Middleware(v, true, echo()) - if rec := do(h, map[string]string{"Authorization": "Bearer " + other.Token("u1", "acme", "user", "", ""), "X-User-Id": "evil"}); rec.Code != 401 { - t.Fatalf("code=%d", rec.Code) - } - if rec := do(Middleware(nil, true, echo()), map[string]string{"Authorization": "Bearer x.y.z"}); rec.Code != 401 { - t.Fatalf("ohne Verifier code=%d", rec.Code) - } -} - -func TestWithoutBearerHeadersStrippedUnlessAllowed(t *testing.T) { - v, _ := moduleauth.NewTestVerifierAndIssuer() - hdr := map[string]string{"X-User-Id": "u9", "X-Tenant-Slug": "t9"} - if rec := do(Middleware(v, false, echo()), hdr); rec.Body.String() != "||" { - t.Fatalf("Header muessen entfernt werden, bekam %q", rec.Body.String()) - } - if rec := do(Middleware(v, true, echo()), hdr); rec.Body.String() != "u9|t9|" { - t.Fatalf("Uebergangsmodus muss Header durchlassen, bekam %q", rec.Body.String()) - } -} - -func TestRoleOnlyFromToken(t *testing.T) { - v, iss := moduleauth.NewTestVerifierAndIssuer() - h := Middleware(v, true, echo()) - rec := do(h, map[string]string{ - "Authorization": "Bearer " + iss.Token("u1", "acme", "tenant_admin", "", ""), - "X-User-Role": "tenant_admin_fremd", - }) - if rec.Body.String() != "u1|acme|tenant_admin" { - t.Fatalf("Rolle muss aus dem Token kommen, bekam %q", rec.Body.String()) - } - if rec := do(h, map[string]string{"X-User-Role": "tenant_admin"}); rec.Body.String() != "||" { - t.Fatalf("Rolle ohne Token muss entfernt werden, bekam %q", rec.Body.String()) - } -} diff --git a/workflow/internal/pflichttestgate/gate.go b/workflow/internal/pflichttestgate/gate.go index 2f3e3856..0e95a2d0 100644 --- a/workflow/internal/pflichttestgate/gate.go +++ b/workflow/internal/pflichttestgate/gate.go @@ -18,7 +18,6 @@ import ( // workflow/docs/TESTSTRATEGIE.md Abschnitt 4. var sensitivePathPatterns = []*regexp.Regexp{ regexp.MustCompile(`(^|/)workflow/internal/moduleadapter/`), - regexp.MustCompile(`(^|/)workflow/internal/bearerauth/`), regexp.MustCompile(`(^|/)workflow/internal/backchannel/`), regexp.MustCompile(`(^|/)workflow/internal/backchannelapi/`), regexp.MustCompile(`(^|/)workflow/internal/dualcheck/`), diff --git a/workflow/internal/pflichttestgate/gate_test.go b/workflow/internal/pflichttestgate/gate_test.go index 2f6a900d..a0d0aa81 100644 --- a/workflow/internal/pflichttestgate/gate_test.go +++ b/workflow/internal/pflichttestgate/gate_test.go @@ -35,7 +35,6 @@ func TestCheckDiff_IgnoresNonSensitivePaths(t *testing.T) { func TestCheckDiff_CoversAllSensitivePackages(t *testing.T) { files := []string{ "workflow/internal/moduleadapter/x.go", - "workflow/internal/bearerauth/x.go", "workflow/internal/backchannel/x.go", "workflow/internal/backchannelapi/x.go", "workflow/internal/dualcheck/x.go", @@ -46,10 +45,3 @@ func TestCheckDiff_CoversAllSensitivePackages(t *testing.T) { t.Fatalf("erwartet %d verstoesse (je package einer), habe %d: %+v", len(files), len(violations), violations) } } - -func TestCheckDiff_FlagsBearerAuthChangeWithoutTest(t *testing.T) { - violations := CheckDiff([]string{"workflow/internal/bearerauth/bearerauth.go"}) - if len(violations) != 1 || violations[0].Package != "workflow/internal/bearerauth" { - t.Fatalf("erwartet 1 verstoss fuer bearerauth, habe: %+v", violations) - } -}