fix(identity): 完善统一认证配对恢复与安全退役
修复 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。
This commit is contained in:
@@ -0,0 +1,79 @@
|
||||
package securityevents
|
||||
|
||||
import (
|
||||
"context"
|
||||
"time"
|
||||
|
||||
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultSecurityEventRetirementDuration = 6 * time.Minute
|
||||
securityEventRetirementWorkerInterval = time.Minute
|
||||
)
|
||||
|
||||
type SecurityEventRetirementRepository interface {
|
||||
SecurityEventConnection(context.Context) (store.SecurityEventConnection, error)
|
||||
FinalizeRetiringSecurityEventConnection(context.Context, string, int64) error
|
||||
}
|
||||
|
||||
// RunSecurityEventRetirementWorker owns the local end of SSF retirement at the
|
||||
// server lifecycle. It intentionally does not share an Active identity
|
||||
// Runtime's context, because that Runtime is closed before the RFC 8935 overlap
|
||||
// period expires. Store-level generation CAS makes one worker per replica safe.
|
||||
func RunSecurityEventRetirementWorker(ctx context.Context, repository SecurityEventRetirementRepository) {
|
||||
runSecurityEventRetirementWorker(
|
||||
ctx,
|
||||
repository,
|
||||
defaultSecurityEventRetirementDuration,
|
||||
securityEventRetirementWorkerInterval,
|
||||
)
|
||||
}
|
||||
|
||||
func runSecurityEventRetirementWorker(
|
||||
ctx context.Context,
|
||||
repository SecurityEventRetirementRepository,
|
||||
retirementDuration time.Duration,
|
||||
interval time.Duration,
|
||||
) {
|
||||
if repository == nil {
|
||||
return
|
||||
}
|
||||
if retirementDuration <= 0 {
|
||||
retirementDuration = defaultSecurityEventRetirementDuration
|
||||
}
|
||||
if interval <= 0 {
|
||||
interval = securityEventRetirementWorkerInterval
|
||||
}
|
||||
ticker := time.NewTicker(interval)
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
finalizeDueSecurityEventRetirement(ctx, repository, retirementDuration)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func finalizeDueSecurityEventRetirement(
|
||||
ctx context.Context,
|
||||
repository SecurityEventRetirementRepository,
|
||||
retirementDuration time.Duration,
|
||||
) {
|
||||
connection, err := repository.SecurityEventConnection(ctx)
|
||||
if err != nil || connection.LifecycleStatus != "retiring" {
|
||||
return
|
||||
}
|
||||
if retirementDuration <= 0 {
|
||||
retirementDuration = defaultSecurityEventRetirementDuration
|
||||
}
|
||||
if time.Since(connection.UpdatedAt) < retirementDuration {
|
||||
return
|
||||
}
|
||||
// Finalization atomically queues every Secret reference and removes only the
|
||||
// exact retiring generation. A concurrent worker or changed generation is a
|
||||
// benign CAS miss and will be observed on the next loop.
|
||||
_ = repository.FinalizeRetiringSecurityEventConnection(ctx, connection.ConnectionID, connection.Version)
|
||||
}
|
||||
Reference in New Issue
Block a user