From 1168e3058a8e66f5d073a22831c2082255da7c86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bj=C3=B6rn=20Strausmann?= Date: Tue, 23 Jun 2026 10:32:01 +0000 Subject: [PATCH] fix(frontend): forward X-Pangolin-Token zum Backend (Browser-User 503-Fix) Pangolin Resources koennen via Header-Auth einen statischen Trust-Token als Custom Upstream-Header injecten (X-Pangolin-Token). Das Backend akzeptiert ihn ueber den sso_trust_header Mechanismus als SSO-equivalentes Signal. Bisher hat WithAuthFrom in client.go diesen Header NICHT an Backend-Calls weitergereicht, sondern nur X-Pangolin-User, X-Label-Hub-Key und Authorization. Konsequenz: Browser-User ohne aktive Pangolin-SSO-Session konnten KEINE UI-Route nutzen die Backend-Daten holt - Dashboard (/), /printers, /jobs, /templates, /admin/* gaben alle 503 zurueck. Nur /healthz funktionierte weil der nicht zum Backend ruft. Fix: X-Pangolin-Token zur Forwarding-Liste hinzugefuegt. Test: TestWithAuthFromForwardsPangolinTokenHeader prueft via httptest-Backend dass der Header beim Backend ankommt. Test war RED vor Fix, GREEN danach. Live-Verifikation (vor Fix): - /healthz: 200 OK - /: 503 (Frontend forwarded X-Pangolin-Token nicht) - Direkter Backend-Call mit X-Pangolin-User: 200 OK + JSON - Direkter Frontend-Call mit X-Pangolin-User: 200 OK + UI Refs Issue-Tracker (kein eigenes Issue, akut entdeckt 2026-06-23) --- frontend/internal/api/client.go | 13 +++++++-- frontend/internal/api/client_test.go | 42 ++++++++++++++++++++++++++++ 2 files changed, 52 insertions(+), 3 deletions(-) diff --git a/frontend/internal/api/client.go b/frontend/internal/api/client.go index c9d361e..780d5cd 100644 --- a/frontend/internal/api/client.go +++ b/frontend/internal/api/client.go @@ -115,19 +115,26 @@ func (c *HubClient) BaseURL() string { // // The headers forwarded are: // - X-Label-Hub-Key — API-key auth (Phase 7c) -// - X-Pangolin-User — Pangolin SSO session token +// - X-Pangolin-User — Pangolin SSO session header (Legacy / Standard SSO) +// - X-Pangolin-Token — Pangolin Resource custom upstream header (Trust-Token) // - Authorization — Pangolin Basic-Auth bypass (claude-automation) // // This is required because the frontend-to-backend calls are internal // (Docker-network, no Pangolin in between) and the backend's Phase 7c auth // middleware rejects requests that carry none of these headers with 401. // +// X-Pangolin-Token is the static trust-token that a Pangolin Resource can +// inject into every upstream request via its Header-Auth configuration. The +// backend's sso_trust_header mechanism accepts it as a SSO-equivalent signal, +// which is what allows browser users without an active SSO session to still +// reach the UI. +// // The original HubClient is not mutated; the returned copy shares the same // underlying http.Client and gen client but adds a RequestEditorFn that // injects the auth headers. func (c *HubClient) WithAuthFrom(r *http.Request) *HubClient { - headers := make(map[string]string, 3) - for _, h := range []string{"X-Label-Hub-Key", "X-Pangolin-User", "Authorization"} { + headers := make(map[string]string, 4) + for _, h := range []string{"X-Label-Hub-Key", "X-Pangolin-User", "X-Pangolin-Token", "Authorization"} { if v := r.Header.Get(h); v != "" { headers[h] = v } diff --git a/frontend/internal/api/client_test.go b/frontend/internal/api/client_test.go index 312c806..55a3e09 100644 --- a/frontend/internal/api/client_test.go +++ b/frontend/internal/api/client_test.go @@ -223,3 +223,45 @@ func TestWithAuthFromForwardsAPIKeyHeader(t *testing.T) { t.Errorf("X-Label-Hub-Key forwarded as %q, want %q", receivedKey, "lh_testapikey1234567890") } } + +// TestWithAuthFromForwardsPangolinTokenHeader verifies that WithAuthFrom +// propagates the X-Pangolin-Token header from the incoming browser request +// to the backend. Pangolin Resources can be configured with a custom upstream +// header (via Pangolin Header-Auth) that injects a static trust token into +// every request reaching the frontend container. The backend accepts this +// token as a Phase 7c SSO-trust signal (sso_trust_header). Without this +// forwarding, browser users without an SSO session see a 503 on every route +// that needs backend data (Dashboard, Jobs, Templates, /admin/*). +func TestWithAuthFromForwardsPangolinTokenHeader(t *testing.T) { + t.Parallel() + var receivedToken string + now := time.Now().Format(time.RFC3339) + backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if r.URL.Path == "/api/printers" { + receivedToken = r.Header.Get("X-Pangolin-Token") + w.Header().Set("Content-Type", "application/json") + json.NewEncoder(w).Encode([]map[string]any{ + {"id": "aaaaaaaa-0000-0000-0000-000000000001", "name": "PT-P750W", + "model": "pt_series", "backend": "tcp", + "connection": map[string]any{"host": "198.51.100.10", "port": 9100}, + "enabled": true, "paused": false, + "created_at": now, "updated_at": now}, + }) + return + } + http.NotFound(w, r) + })) + defer backend.Close() + + incomingReq := httptest.NewRequest(http.MethodGet, "/", nil) + incomingReq.Header.Set("X-Pangolin-Token", "pangolin-trust-token-abc123") + + client := api.NewHubClient(backend.URL).WithAuthFrom(incomingReq) + _, err := client.ListPrinters(context.Background()) + if err != nil { + t.Fatalf("ListPrinters: %v", err) + } + if receivedToken != "pangolin-trust-token-abc123" { + t.Errorf("X-Pangolin-Token forwarded as %q, want %q", receivedToken, "pangolin-trust-token-abc123") + } +}