从 Active Revision 构造并验证 OIDC、BFF Session、Introspection 与 SSF Runtime,在数据库激活成功后原子替换内存引用。请求链路使用不可变快照,失败保留当前运行时,本地管理登录不受影响。\n\n验证:go test ./apps/api/...;go vet ./apps/api/...
152 lines
4.8 KiB
Go
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)
|
|
}
|