feat(identity): 实现统一认证运行时热切换
从 Active Revision 构造并验证 OIDC、BFF Session、Introspection 与 SSF Runtime,在数据库激活成功后原子替换内存引用。请求链路使用不可变快照,失败保留当前运行时,本地管理登录不受影响。\n\n验证:go test ./apps/api/...;go vet ./apps/api/...
This commit is contained in:
@@ -0,0 +1,151 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user