Files
easyai-ai-gateway/apps/api/internal/httpapi/identity_runtime.go
T
chengcheng 5c679ff13f feat(identity): 接入认证中心多租户登录
支持 Manifest V2 动态 tid 验证、Tenant Context 同步和租户内 JIT 投影,并保留 Manifest V1 与旧 Session 兼容。\n\n增加 tenantHint、租户切换、普通注册关闭及 application/principal/tenant 两级 SSF 撤销;迁移、定向安全测试和本地双租户跨仓 E2E 已通过。\n\nrelease_required=true;未执行 Release、Staging 或真实链路。
2026-07-28 17:28:35 +08:00

115 lines
3.8 KiB
Go

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)
}
type tenantContextReader interface {
Get(context.Context, string, string) (identity.TenantContext, bool, 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
TenantContext tenantContextReader
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,
TenantContext: runtime.TenantContext,
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)
}