package httpapi import ( "context" "net/url" "strings" "github.com/easyai/easyai-ai-gateway/apps/api/internal/auth" "github.com/easyai/easyai-ai-gateway/apps/api/internal/identity" "github.com/easyai/easyai-ai-gateway/apps/api/internal/oidcsession" ssfreceiver "github.com/easyai/easyai-ai-gateway/apps/api/internal/securityevents" ) type oidcTokenVerifier interface { Verify(context.Context, string) (*auth.User, error) } // identityRequestRuntime is an immutable request-level snapshot. A handler that // starts with one runtime keeps using it even when an administrator activates a // new revision while that request is in flight. type identityRequestRuntime struct { Revision identity.Revision Verifier oidcTokenVerifier PublicClient oidcPublicClient Sessions oidcSessionManager SessionCipher *oidcsession.Cipher SecurityEvents *ssfreceiver.ConnectionManager CookieSecure bool BrowserEnabled bool } func (s *Server) currentIdentityRuntime() *identityRequestRuntime { if s.identityRuntime != nil { runtime := s.identityRuntime.Current() if runtime == nil { return nil } return &identityRequestRuntime{ Revision: runtime.Revision, Verifier: runtime.Verifier, PublicClient: runtime.PublicClient, Sessions: runtime.Sessions, SessionCipher: runtime.SessionCipher, SecurityEvents: runtime.SecurityEvents, CookieSecure: runtime.CookieSecure, BrowserEnabled: runtime.PublicClient != nil, } } // Compatibility path for focused HTTP tests. NewServer never uses these // static fields after identity revisions are enabled. if s.identityTestRevision.ID == "" && s.identityTestRevision.Issuer == "" && s.oidcClient == nil && s.oidcSessions == nil && s.oidcSessionCipher == nil && s.securityEventManager == nil && !s.identityTestBrowserEnabled { return nil } revision := s.identityTestRevision if revision.State == "" { revision.State = identity.RevisionActive } webBaseURL := revision.WebBaseURL if webBaseURL == "" { webBaseURL = s.cfg.WebBaseURL } if webBaseURL == "" { webBaseURL = s.cfg.CORSAllowedOrigin } revision.WebBaseURL = webBaseURL if revision.PublicBaseURL == "" { revision.PublicBaseURL = s.cfg.PublicBaseURL } if revision.SessionAbsoluteSeconds <= 0 { revision.SessionAbsoluteSeconds = 28800 } var verifier oidcTokenVerifier if s.auth != nil { verifier = s.auth.OIDCVerifier } return &identityRequestRuntime{ Revision: revision, Verifier: verifier, PublicClient: s.oidcClient, Sessions: s.oidcSessions, SessionCipher: s.oidcSessionCipher, SecurityEvents: s.securityEventManager, CookieSecure: s.identityTestCookieSecure || strings.HasPrefix(strings.ToLower(revision.PublicBaseURL), "https://"), BrowserEnabled: s.identityTestBrowserEnabled || s.oidcClient != nil && s.oidcSessions != nil && s.oidcSessionCipher != nil, } } func (s *Server) currentSecurityEventManager() *ssfreceiver.ConnectionManager { if s.identityRuntime != nil { return s.identityRuntime.SecurityEventManager() } if runtime := s.currentIdentityRuntime(); runtime != nil { return runtime.SecurityEvents } return nil } func (s *Server) oidcCookieSecure() bool { if runtime := s.currentIdentityRuntime(); runtime != nil { return runtime.CookieSecure } return false } func originMatchesBaseURL(origin, baseURL string) bool { originURL, err := url.Parse(strings.TrimSpace(origin)) if err != nil || originURL.User != nil || originURL.Path != "" || originURL.RawQuery != "" || originURL.Fragment != "" { return false } base, err := url.Parse(strings.TrimSpace(baseURL)) if err != nil { return false } return strings.EqualFold(originURL.Scheme, base.Scheme) && strings.EqualFold(originURL.Host, base.Host) }