From 4d4c24c6503b70055cfae14582b9f3bf64be203d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Strausmann?= Date: Tue, 23 Jun 2026 11:55:55 +0000 Subject: [PATCH] =?UTF-8?q?fix(frontend):=20forwardAuth=20erg=C3=A4nzt=20X?= =?UTF-8?q?-Pangolin-Token=20+=20Remote-User=20(Admin-Routes)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #130/#132 hatten WithAuthFrom im oapi-Client um X-Pangolin-Token und Remote-User ergänzt. forwardAuth in admin_api_keys.go ist eine zweite, parallele Implementierung für die Admin-Routes (/admin/printers, /admin/api-keys) die raw http.DefaultClient verwendet — die Header-Liste dort wurde nicht mitgezogen. Konsequenz: SSO-User konnten Read-Routes nutzen, alle Admin-Routes gaben weiterhin 503 weil das Backend mit "missing_credentials" 401 zurück lieferte. Fix: dieselbe Header-Liste in forwardAuth. Plus Regression-Test TestListPrintersPage_ForwardetPangolinSSOHeaders der httptest-Server-seitig beide Header verifiziert. Race-Detector grün, alle 4 Frontend-Packages grün. Refs PR #130 (X-Pangolin-Token), PR #132 (Remote-User), PR #133 (ADR 0014 Backend-Seite) --- frontend/internal/handlers/admin_api_keys.go | 26 ++++++++++-- .../internal/handlers/admin_printers_test.go | 41 +++++++++++++++++++ 2 files changed, 63 insertions(+), 4 deletions(-) diff --git a/frontend/internal/handlers/admin_api_keys.go b/frontend/internal/handlers/admin_api_keys.go index 1aca0ff..4cf19ab 100644 --- a/frontend/internal/handlers/admin_api_keys.go +++ b/frontend/internal/handlers/admin_api_keys.go @@ -236,11 +236,29 @@ func (h *PageHandler) revokeAPIKey(r *http.Request, id string) error { return nil } -// forwardAuth copies auth-related headers from the incoming request to the -// outgoing backend request. This ensures Pangolin SSO tokens and API keys -// are forwarded to the backend for authentication. +// forwardAuth copies auth-related headers from the incoming browser request +// to the outgoing backend request, so that the backend's auth middleware sees +// the same credentials the Pangolin-edge handed us. +// +// Forwarded headers (must stay in sync with api.HubClient.WithAuthFrom): +// - X-Label-Hub-Key — App-side API key +// - X-Pangolin-User — Legacy Pangolin SSO identity header +// - X-Pangolin-Token — Pangolin Resource custom upstream trust token +// - Remote-User — Standard Pangolin SSO identity header +// - Authorization — Pangolin Basic-Auth bypass (claude-automation) +// +// The X-Pangolin-Token / Remote-User pair is what makes the SSO-trust path +// work for browser users without an API key. Forgetting one of them means +// every /admin/* route returns 503 because the backend rejects the call as +// unauthenticated. func (h *PageHandler) forwardAuth(from *http.Request, to *http.Request) { - for _, hdr := range []string{"X-Label-Hub-Key", "X-Pangolin-User", "Authorization"} { + for _, hdr := range []string{ + "X-Label-Hub-Key", + "X-Pangolin-User", + "X-Pangolin-Token", + "Remote-User", + "Authorization", + } { if v := from.Header.Get(hdr); v != "" { to.Header.Set(hdr, v) } diff --git a/frontend/internal/handlers/admin_printers_test.go b/frontend/internal/handlers/admin_printers_test.go index c9d9a50..42ae7de 100644 --- a/frontend/internal/handlers/admin_printers_test.go +++ b/frontend/internal/handlers/admin_printers_test.go @@ -700,3 +700,44 @@ func TestEnablePrinter_CallsBackendUndRedirectZuDetail(t *testing.T) { t.Errorf("EnablePrinter Redirect: %q enthält nicht 'lager-sued'", loc) } } + +// TestListPrintersPage_ForwardetPangolinSSOHeaders prüft dass forwardAuth +// X-Pangolin-Token + Remote-User an das Backend weitergibt — sonst kann der +// SSO-Trust-Pfad nicht greifen und das Backend lehnt mit 401 ab, was zu 503 +// im Frontend führt. +// +// Regression-Test für die Lücke die PR #130/#132 (WithAuthFrom) zwar im +// oapi-Client gefixt haben, aber forwardAuth blieb auf der alten 3-Header- +// Liste hängen. +func TestListPrintersPage_ForwardetPangolinSSOHeaders(t *testing.T) { + t.Parallel() + var receivedToken, receivedRemoteUser string + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/v1/admin/printers" { + receivedToken = r.Header.Get("X-Pangolin-Token") + receivedRemoteUser = r.Header.Get("Remote-User") + w.Header().Set("Content-Type", "application/json") + fmt.Fprint(w, `[]`) + return + } + http.NotFound(w, r) + })) + t.Cleanup(srv.Close) + + ph := handlers.NewPageHandlerFromURL(t, srv.URL) + req := httptest.NewRequest(http.MethodGet, "/admin/printers", nil) + req.Header.Set("X-Pangolin-Token", "trust-token-abc123") + req.Header.Set("Remote-User", "strausmann") + w := httptest.NewRecorder() + ph.ListPrintersPage(w, req) + + if w.Code != http.StatusOK { + t.Errorf("ListPrintersPage: Status %d, erwartet 200", w.Code) + } + if receivedToken != "trust-token-abc123" { + t.Errorf("X-Pangolin-Token forwarded als %q, erwartet %q", receivedToken, "trust-token-abc123") + } + if receivedRemoteUser != "strausmann" { + t.Errorf("Remote-User forwarded als %q, erwartet %q", receivedRemoteUser, "strausmann") + } +}