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:
2026-07-17 18:31:12 +08:00
parent cdfca61304
commit a312ad880d
55 changed files with 9225 additions and 419 deletions
+25 -13
View File
@@ -36,7 +36,10 @@ type Server struct {
identityRuntime *identityruntime.Manager
identityPairing *identity.PairingService
identityManagementMu sync.Mutex
securityEventManagementMu sync.Mutex
identityPairingWorkers sync.Map
identityCleanupWorkers sync.Map
identityRestoredPairings sync.Map
identityTestRevision identity.Revision
identityTestCookieSecure bool
identityTestBrowserEnabled bool
@@ -79,6 +82,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
if err != nil {
panic("invalid identity SecretStore: " + err.Error())
}
go ssfreceiver.RunSecurityEventRetirementWorker(ctx, db)
go ssfreceiver.RunIdentitySecretCleanupWorker(ctx, db, secretStore)
runtimeBuilder := identityruntime.NewRuntimeBuilder(ctx, db, secretStore, identityruntime.RuntimeBuilderConfig{
AppEnv: cfg.AppEnv, JWKSCacheTTL: 5 * time.Minute,
HeartbeatInterval: time.Duration(cfg.IdentitySecurityEventHeartbeatIntervalSeconds) * time.Second,
@@ -90,15 +95,10 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
logger.Error("load active identity runtime failed; local management login remains available", "error_category", "identity_runtime_load_failed")
}
server.identityPairing = identity.NewPairingService(db, secretStore, func(baseURL string) (identity.OnboardingRemote, error) {
return identity.NewOnboardingClient(baseURL, nil)
}, server.identityRuntime)
if pending, pendingErr := db.PendingIdentityPairingExchanges(ctx); pendingErr == nil {
for _, pairing := range pending {
server.startIdentityPairingWorker(pairing.ID)
}
} else if logger != nil {
logger.Warn("pending identity pairings could not be resumed", "error_category", "pairing_resume_failed")
}
return identity.NewOnboardingClient(baseURL, nil, cfg.AppEnv)
}, server.identityRuntime, cfg.AppEnv)
server.reconcileCanonicalIdentityPairing()
go server.runIdentityPairingCoordinator()
server.auth.OIDCVerifierProvider = func() *auth.OIDCVerifier {
if runtime := server.identityRuntime.Current(); runtime != nil {
return runtime.Verifier
@@ -114,8 +114,7 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
return user, oidcSessionRequestError(resolveErr)
}
server.auth.LegacyJWTEnabledProvider = func() bool {
runtime := server.identityRuntime.Current()
return runtime == nil || runtime.Revision.LegacyJWTEnabled
return server.identityRuntime.LegacyJWTEnabled()
}
server.auth.LocalAPIKeyVerifier = db.VerifyLocalAPIKey
server.runner.StartAsyncQueueWorker(ctx)
@@ -210,6 +209,8 @@ func NewServerWithContext(ctx context.Context, cfg config.Config, db *store.Stor
mux.Handle("GET /api/admin/system/identity/configuration", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getIdentityConfiguration)))
mux.Handle("POST /api/admin/system/identity/pairings", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.startIdentityPairing)))
mux.Handle("GET /api/admin/system/identity/pairings/{pairingID}", server.requireAdmin(auth.PermissionPower, http.HandlerFunc(server.getIdentityPairing)))
mux.Handle("POST /api/admin/system/identity/pairings/{pairingID}/cancel", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.cancelIdentityPairing)))
mux.Handle("POST /api/admin/system/identity/pairings/{pairingID}/retire-conflicting-security-event", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.retireIdentityPairingSecurityEventConflict)))
mux.Handle("PATCH /api/admin/system/identity/revisions/{revisionID}", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.updateIdentityDraftPolicy)))
mux.Handle("POST /api/admin/system/identity/revisions/{revisionID}/validate", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.validateIdentityRevision)))
mux.Handle("POST /api/admin/system/identity/revisions/{revisionID}/activate", server.requireAdmin(auth.PermissionManager, http.HandlerFunc(server.activateIdentityRevision)))
@@ -349,7 +350,7 @@ func (s *Server) requireAdmin(permission auth.Permission, next http.Handler) htt
func (s *Server) cors(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" && originAllowed(origin, s.cfg.CORSAllowedOrigin) {
if origin != "" && s.corsOriginAllowed(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
w.Header().Set("Access-Control-Allow-Credentials", "true")
@@ -364,10 +365,21 @@ func (s *Server) cors(next http.Handler) http.Handler {
})
}
func (s *Server) corsOriginAllowed(origin string) bool {
if originAllowed(origin, s.cfg.CORSAllowedOrigin) {
return true
}
if s.identityRuntime != nil {
return originMatchesBaseURL(origin, s.identityRuntime.TrustedWebBaseURL())
}
runtime := s.currentIdentityRuntime()
return runtime != nil && originMatchesBaseURL(origin, runtime.Revision.WebBaseURL)
}
func originAllowed(origin string, allowed string) bool {
for _, item := range strings.Split(allowed, ",") {
item = strings.TrimSpace(item)
if item == "*" || strings.EqualFold(origin, item) {
if item != "*" && strings.EqualFold(origin, item) {
return true
}
}