auth: accept oauth2-proxy forwarded headers

This commit is contained in:
Brad Stein 2026-04-12 11:36:22 -03:00
parent 09ab3ec889
commit 98f1166685
3 changed files with 47 additions and 4 deletions

View File

@ -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.

View File

@ -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 := ""

View File

@ -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"},