修复 credentials_saved 状态无法恢复、配对与激活并发冲突,以及 SSF 和身份 Secret 生命周期不完整的问题。新增持久化协调器、取消与清理状态机、事务级并发门禁、受控 SSF 凭据交接、禁用后的延迟 Secret 清理,并对生产环境统一认证及 Discovery 端点强制 HTTPS。 验证:go test ./...;go test -race ./internal/auth ./internal/identity ./internal/identityruntime ./internal/securityevents ./internal/httpapi ./internal/store -count=1;go vet ./...;真实 PostgreSQL 并发及清理成功/冲突回滚测试;pnpm openapi。
109 lines
3.6 KiB
Go
109 lines
3.6 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)
|
|
}
|
|
|
|
// 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)
|
|
}
|