From 98f1166685b0564143defe9658c4175b201a8eed Mon Sep 17 00:00:00 2001 From: Brad Stein Date: Sun, 12 Apr 2026 11:36:22 -0300 Subject: [PATCH] auth: accept oauth2-proxy forwarded headers --- README.md | 5 ++++- internal/server/server.go | 26 +++++++++++++++++++++++--- internal/server/server_test.go | 20 ++++++++++++++++++++ 3 files changed, 47 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 9d1eda9..827b86b 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,11 @@ When `SOTERIA_AUTH_REQUIRED=true`, Soteria expects trusted auth headers from a f - `X-Auth-Request-User` - `X-Auth-Request-Email` - `X-Auth-Request-Groups` +- `X-Forwarded-User` (fallback) +- `X-Forwarded-Email` (fallback) +- `X-Forwarded-Groups` (fallback) -Allowed groups are configured with `SOTERIA_ALLOWED_GROUPS` and compared after normalizing leading `/` prefixes, so both `maintenance` and `/maintenance` are accepted. +Allowed groups are configured with `SOTERIA_ALLOWED_GROUPS` and compared after normalizing leading `/` prefixes, so both `maintenance` and `/maintenance` are accepted. Group lists may be comma- or semicolon-separated. Optional machine-to-machine access can be enabled with `SOTERIA_AUTH_BEARER_TOKENS`, which accepts a comma-separated list of bearer tokens. diff --git a/internal/server/server.go b/internal/server/server.go index 203dc06..74d351c 100644 --- a/internal/server/server.go +++ b/internal/server/server.go @@ -564,9 +564,9 @@ func (s *Server) authorize(r *http.Request) (authIdentity, int, error) { identity := authIdentity{ Authenticated: true, - User: strings.TrimSpace(r.Header.Get("X-Auth-Request-User")), - Email: strings.TrimSpace(r.Header.Get("X-Auth-Request-Email")), - Groups: normalizeGroups(strings.Split(strings.TrimSpace(r.Header.Get("X-Auth-Request-Groups")), ",")), + User: firstHeader(r, "X-Auth-Request-User", "X-Forwarded-User"), + Email: firstHeader(r, "X-Auth-Request-Email", "X-Forwarded-Email"), + Groups: normalizeGroups(splitGroups(firstHeader(r, "X-Auth-Request-Groups", "X-Forwarded-Groups"))), } if identity.User == "" && identity.Email == "" { return authIdentity{}, http.StatusUnauthorized, fmt.Errorf("authentication required") @@ -638,6 +638,26 @@ func normalizeGroups(values []string) []string { return groups } +func firstHeader(r *http.Request, names ...string) string { + for _, name := range names { + value := strings.TrimSpace(r.Header.Get(name)) + if value != "" { + return value + } + } + return "" +} + +func splitGroups(raw string) []string { + raw = strings.TrimSpace(raw) + if raw == "" { + return nil + } + return strings.FieldsFunc(raw, func(r rune) bool { + return r == ',' || r == ';' + }) +} + func buildBackupRecords(backups []longhorn.Backup) []api.BackupRecord { records := make([]api.BackupRecord, 0, len(backups)) latestName := "" diff --git a/internal/server/server_test.go b/internal/server/server_test.go index e172460..131b34c 100644 --- a/internal/server/server_test.go +++ b/internal/server/server_test.go @@ -120,6 +120,26 @@ func TestProtectedInventoryAllowsMaintenanceGroup(t *testing.T) { } } +func TestProtectedInventoryAllowsForwardedHeaders(t *testing.T) { + srv := &Server{ + cfg: &config.Config{AuthRequired: true, AllowedGroups: []string{"admin", "maintenance"}, BackupDriver: "longhorn", BackupMaxAge: 24 * time.Hour}, + client: &fakeKubeClient{pvcs: []k8s.PVCSummary{{Namespace: "apps", Name: "data", VolumeName: "pv-apps-data", Phase: "Bound"}}}, + longhorn: &fakeLonghornClient{backups: []longhorn.Backup{{Name: "backup-1", Created: "2026-04-12T00:00:00Z", State: "Completed", URL: "s3://bucket/backup-1"}}}, + metrics: newTelemetry(), + } + srv.handler = http.HandlerFunc(srv.route) + + req := httptest.NewRequest(http.MethodGet, "/v1/inventory", nil) + req.Header.Set("X-Forwarded-User", "brad") + req.Header.Set("X-Forwarded-Groups", "/ops;/maintenance") + res := httptest.NewRecorder() + srv.Handler().ServeHTTP(res, req) + + if res.Code != http.StatusOK { + t.Fatalf("expected 200, got %d: %s", res.Code, res.Body.String()) + } +} + func TestRestoreRejectsExistingTargetPVC(t *testing.T) { srv := &Server{ cfg: &config.Config{AuthRequired: false, BackupDriver: "longhorn"},