easyai-ai-gateway/apps/api/internal/identityruntime/manager.go
chengcheng a9e23cb237 feat(identity): 实现统一认证运行时热切换
从 Active Revision 构造并验证 OIDC、BFF Session、Introspection 与 SSF Runtime,在数据库激活成功后原子替换内存引用。请求链路使用不可变快照,失败保留当前运行时,本地管理登录不受影响。\n\n验证:go test ./apps/api/...;go vet ./apps/api/...
2026-07-17 12:05:00 +08:00

152 lines
4.8 KiB
Go

package identityruntime
import (
"context"
"errors"
"sync"
"sync/atomic"
"time"
"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"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/securityevents"
)
type Repository interface {
IdentityConfigurationRevision(context.Context, string) (identity.Revision, error)
ActiveIdentityConfigurationRevision(context.Context) (identity.Revision, error)
MarkIdentityRevisionValidated(context.Context, string, int64, string, string) (identity.Revision, error)
MarkIdentityRevisionFailed(context.Context, string, int64, string, string, string) (identity.Revision, error)
ActivateIdentityRevision(context.Context, string, int64) (identity.Revision, bool, error)
DisableActiveIdentityRevision(context.Context, int64) (identity.Revision, error)
}
type Builder interface {
Build(context.Context, identity.Revision) (*Runtime, error)
}
type Runtime struct {
Revision identity.Revision
Verifier *auth.OIDCVerifier
PublicClient *auth.OIDCPublicClient
Sessions *oidcsession.Service
SessionCipher *oidcsession.Cipher
SecurityEvents *securityevents.ConnectionManager
CookieSecure bool
close func()
}
func (runtime *Runtime) Close() {
if runtime != nil && runtime.close != nil {
runtime.close()
}
}
type Manager struct {
repository Repository
builder Builder
operation sync.Mutex
current atomic.Pointer[Runtime]
}
func NewManager(repository Repository, builder Builder) *Manager {
return &Manager{repository: repository, builder: builder}
}
func (manager *Manager) Current() *Runtime {
return manager.current.Load()
}
func (manager *Manager) LoadActive(ctx context.Context) error {
manager.operation.Lock()
defer manager.operation.Unlock()
revision, err := manager.repository.ActiveIdentityConfigurationRevision(ctx)
if errors.Is(err, identity.ErrRevisionNotFound) {
return nil
}
if err != nil {
return err
}
runtime, err := manager.builder.Build(ctx, revision)
if err != nil {
return err
}
runtime.Revision = revision
manager.current.Store(runtime)
return nil
}
func (manager *Manager) Validate(ctx context.Context, id string, expectedVersion int64, traceID, auditID string) (identity.Revision, error) {
manager.operation.Lock()
defer manager.operation.Unlock()
revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
if err != nil {
return identity.Revision{}, err
}
if revision.Version != expectedVersion || revision.State != identity.RevisionDraft && revision.State != identity.RevisionSuperseded {
return identity.Revision{}, identity.ErrRevisionConflict
}
candidate, buildErr := manager.builder.Build(ctx, revision)
if buildErr != nil {
_, _ = manager.repository.MarkIdentityRevisionFailed(ctx, id, expectedVersion, "validation_failed", traceID, auditID)
return identity.Revision{}, buildErr
}
candidate.Close()
return manager.repository.MarkIdentityRevisionValidated(ctx, id, expectedVersion, traceID, auditID)
}
func (manager *Manager) Activate(ctx context.Context, id string, expectedVersion int64) (identity.Revision, error) {
manager.operation.Lock()
defer manager.operation.Unlock()
revision, err := manager.repository.IdentityConfigurationRevision(ctx, id)
if err != nil {
return identity.Revision{}, err
}
if revision.Version != expectedVersion || revision.State != identity.RevisionValidated {
return identity.Revision{}, identity.ErrRevisionConflict
}
candidate, err := manager.builder.Build(ctx, revision)
if err != nil {
return identity.Revision{}, err
}
activated, _, err := manager.repository.ActivateIdentityRevision(ctx, id, expectedVersion)
if err != nil {
candidate.Close()
return identity.Revision{}, err
}
candidate.Revision = activated
old := manager.current.Swap(candidate)
retireRuntime(old)
return activated, nil
}
func (manager *Manager) Disable(ctx context.Context, expectedVersion int64) (identity.Revision, error) {
manager.operation.Lock()
defer manager.operation.Unlock()
disabled, err := manager.repository.DisableActiveIdentityRevision(ctx, expectedVersion)
if err != nil {
return identity.Revision{}, err
}
old := manager.current.Swap(nil)
retireRuntime(old)
return disabled, nil
}
func (manager *Manager) PrepareSecurityEvents(ctx context.Context, revision identity.Revision, secret []byte) error {
preparer, ok := manager.builder.(interface {
PrepareSecurityEvents(context.Context, identity.Revision, []byte) error
})
if !ok {
return errors.New("security event runtime preparation is unavailable")
}
return preparer.PrepareSecurityEvents(ctx, revision, secret)
}
func retireRuntime(runtime *Runtime) {
if runtime == nil || runtime.close == nil {
return
}
time.AfterFunc(30*time.Second, runtime.Close)
}