From f14d65263ea554fee824bbfa3debf36e20ced7fa Mon Sep 17 00:00:00 2001 From: Michel Osswald Date: Sat, 30 May 2026 09:47:09 +0200 Subject: [PATCH] fix: optimize login scope deduplication Summary This optimizes login scope resolution by deduplicating scopes in one pass. Before this, resolveLoginScopes in internal/auth/oidc.go rescanned the growing resolved slice for every requested scope, which made repeated custom scopes cost more work and made the merge path harder to reason about. Now one seen-map tracks both base scopes and custom scopes as the canonical path. Why This gives kontext-cli a cheaper runtime path for OIDC login scope assembly: custom scope input -> resolveLoginScopes -> deduplicated scope list in stable order This PR does not broaden behavior beyond the optimization scope. What changed Optimized resolveLoginScopes in internal/auth/oidc.go Removed repeated linear scans during custom scope deduplication Preserved default scope selection and custom scope order Updated tests for repeated custom scopes and default-scope overlap Verification go test ./internal/auth -count=1 go test ./internal/guard/judge -count=1 go test ./internal/guard/judge -count=5 go test ./... -count=1 -p 1 go vet ./... git diff --check Notes The required go test ./... -count=1 run still fails on fresh main in internal/guard/judge with two llama-server startup timing tests under package-parallel load. The auth change does not touch that package, and the judge package passes when rerun directly and when the suite is serialized with -p 1. --- internal/auth/oidc.go | 22 ++++++++++------------ internal/auth/oidc_test.go | 17 +++++++++++++++++ 2 files changed, 27 insertions(+), 12 deletions(-) diff --git a/internal/auth/oidc.go b/internal/auth/oidc.go index 2c6124b..db1ef61 100644 --- a/internal/auth/oidc.go +++ b/internal/auth/oidc.go @@ -203,24 +203,22 @@ func resolveLoginScopes(scopes []string) []string { baseScopes = identityLoginScopes } - resolved := append([]string(nil), baseScopes...) + resolved := make([]string, 0, len(baseScopes)+len(scopes)) + seen := make(map[string]struct{}, len(baseScopes)+len(scopes)) + for _, scope := range baseScopes { + resolved = append(resolved, scope) + seen[scope] = struct{}{} + } for _, scope := range scopes { - if !hasScope(resolved, scope) { - resolved = append(resolved, scope) + if _, ok := seen[scope]; ok { + continue } + resolved = append(resolved, scope) + seen[scope] = struct{}{} } return resolved } -func hasScope(scopes []string, scope string) bool { - for _, existing := range scopes { - if existing == scope { - return true - } - } - return false -} - func applyTokenExtraEmailFallback(session *Session, token *oauth2.Token) { if session.User.Email != "" { return diff --git a/internal/auth/oidc_test.go b/internal/auth/oidc_test.go index 2e1eee7..cc3a49f 100644 --- a/internal/auth/oidc_test.go +++ b/internal/auth/oidc_test.go @@ -67,6 +67,23 @@ func TestResolveLoginScopesDeduplicatesDefaultScopes(t *testing.T) { } } +func TestResolveLoginScopesDeduplicatesRepeatedCustomScopes(t *testing.T) { + t.Parallel() + + input := []string{"gateway:access", "openid", "gateway:access", "profile", "audit:read"} + got := resolveLoginScopes(input) + want := []string{ + "openid", + "email", + "profile", + "gateway:access", + "audit:read", + } + if !reflect.DeepEqual(got, want) { + t.Fatalf("resolveLoginScopes(%#v) = %#v, want %#v", input, got, want) + } +} + func TestDecodeJWTClaims(t *testing.T) { t.Parallel()