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") + } +}