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:
2026-07-17 12:05:00 +08:00
parent 96bbd3a2f6
commit a9e23cb237
14 changed files with 797 additions and 152 deletions
@@ -31,11 +31,12 @@ type securityEventConnectionResponse struct {
func (s *Server) getSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
ensureSecurityEventTraceID(w, r)
w.Header().Set("Cache-Control", "no-store")
if s.securityEventManager == nil {
manager := s.currentSecurityEventManager()
if manager == nil {
writeJSON(w, http.StatusOK, map[string]any{"connected": false, "lifecycleStatus": "disconnected", "prerequisites": s.securityEventPrerequisites()})
return
}
connection, err := s.securityEventManager.Get(r.Context())
connection, err := manager.Get(r.Context())
if errors.Is(err, store.ErrSecurityEventConnectionNotFound) {
writeJSON(w, http.StatusOK, map[string]any{"connected": false, "lifecycleStatus": "disconnected", "prerequisites": s.securityEventPrerequisites()})
return
@@ -50,7 +51,8 @@ func (s *Server) getSecurityEventConnection(w http.ResponseWriter, r *http.Reque
func (s *Server) putSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
if s.securityEventManager == nil {
manager := s.currentSecurityEventManager()
if manager == nil {
writeError(w, http.StatusConflict, "OIDC must be configured before connecting security events", "security_event_prerequisite_missing")
return
}
@@ -79,12 +81,12 @@ func (s *Server) putSecurityEventConnection(w http.ResponseWriter, r *http.Reque
if s.replaySecurityEventOperation(w, r, "connect", idempotencyKey, requestHash) {
return
}
if current, err := s.securityEventManager.Get(r.Context()); err == nil && !matchConnectionVersion(w, r, current.Version) {
if current, err := manager.Get(r.Context()); err == nil && !matchConnectionVersion(w, r, current.Version) {
return
}
connection, err := s.securityEventManager.Connect(r.Context(), request.TransmitterIssuer, request.ManagementClientID, secret, idempotencyKey)
connection, err := manager.Connect(r.Context(), request.TransmitterIssuer, request.ManagementClientID, secret, idempotencyKey)
if err != nil {
s.writeSecurityEventConnectionError(w, r, "connect", traceID, err)
s.writeSecurityEventConnectionError(w, r, manager, "connect", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "connect", idempotencyKey, requestHash, traceID, connection)
@@ -92,13 +94,14 @@ func (s *Server) putSecurityEventConnection(w http.ResponseWriter, r *http.Reque
func (s *Server) verifySecurityEventConnection(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, "verify", traceID)
manager := s.currentSecurityEventManager()
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, manager, "verify", traceID)
if !ok {
return
}
connection, err := s.securityEventManager.Verify(r.Context())
connection, err := manager.Verify(r.Context())
if err != nil {
s.writeSecurityEventConnectionError(w, r, "verify", traceID, err)
s.writeSecurityEventConnectionError(w, r, manager, "verify", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "verify", idempotencyKey, requestHash, traceID, connection)
@@ -106,13 +109,14 @@ func (s *Server) verifySecurityEventConnection(w http.ResponseWriter, r *http.Re
func (s *Server) rotateSecurityEventConnectionCredential(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, "rotate", traceID)
manager := s.currentSecurityEventManager()
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, manager, "rotate", traceID)
if !ok {
return
}
connection, err := s.securityEventManager.RotateCredential(r.Context())
connection, err := manager.RotateCredential(r.Context())
if err != nil {
s.writeSecurityEventConnectionError(w, r, "rotate", traceID, err)
s.writeSecurityEventConnectionError(w, r, manager, "rotate", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "rotate", idempotencyKey, requestHash, traceID, connection)
@@ -120,25 +124,34 @@ func (s *Server) rotateSecurityEventConnectionCredential(w http.ResponseWriter,
func (s *Server) deleteSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, "disconnect", traceID)
manager := s.currentSecurityEventManager()
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, manager, "disconnect", traceID)
if !ok {
return
}
connection, err := s.securityEventManager.Disconnect(r.Context())
connection, err := manager.Disconnect(r.Context())
if err != nil {
s.writeSecurityEventConnectionError(w, r, "disconnect", traceID, err)
s.writeSecurityEventConnectionError(w, r, manager, "disconnect", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "disconnect", idempotencyKey, requestHash, traceID, connection)
}
func (s *Server) securityEventPrerequisites() map[string]any {
runtime := s.currentIdentityRuntime()
if runtime == nil {
return map[string]any{
"oidcConfigured": false, "introspectionClientConfigured": false,
"publicBaseUrlConfigured": false, "managementClientId": "", "credentialInputSupported": false,
}
}
revision := runtime.Revision
return map[string]any{
"oidcConfigured": s.cfg.OIDCEnabled && s.cfg.OIDCIssuer != "" && s.cfg.OIDCTenantID != "",
"introspectionClientConfigured": s.cfg.OIDCIntrospectionClientID != "" && s.cfg.OIDCIntrospectionClientSecret != "",
"publicBaseUrlConfigured": s.cfg.PublicBaseURL != "",
"managementClientId": s.cfg.OIDCIntrospectionClientID,
"credentialInputSupported": true,
"oidcConfigured": revision.Issuer != "" && revision.TenantID != "",
"introspectionClientConfigured": revision.MachineClientID != "" && revision.MachineCredentialRef != "",
"publicBaseUrlConfigured": revision.PublicBaseURL != "",
"managementClientId": revision.MachineClientID,
"credentialInputSupported": false,
}
}
@@ -151,8 +164,8 @@ func requiredConnectionIdempotencyKey(w http.ResponseWriter, r *http.Request) (s
return value, true
}
func (s *Server) beginSecurityEventOperation(w http.ResponseWriter, r *http.Request, operation, traceID string) (string, string, bool) {
if s.securityEventManager == nil {
func (s *Server) beginSecurityEventOperation(w http.ResponseWriter, r *http.Request, manager *ssfreceiver.ConnectionManager, operation, traceID string) (string, string, bool) {
if manager == nil {
writeError(w, http.StatusNotFound, "security event connection does not exist", "security_event_connection_not_found")
return "", "", false
}
@@ -164,9 +177,9 @@ func (s *Server) beginSecurityEventOperation(w http.ResponseWriter, r *http.Requ
if s.replaySecurityEventOperation(w, r, operation, idempotencyKey, requestHash) {
return "", "", false
}
connection, err := s.securityEventManager.Get(r.Context())
connection, err := manager.Get(r.Context())
if err != nil {
s.writeSecurityEventConnectionError(w, r, operation, traceID, err)
s.writeSecurityEventConnectionError(w, r, manager, operation, traceID, err)
return "", "", false
}
return idempotencyKey, requestHash, matchConnectionVersion(w, r, connection.Version)
@@ -247,11 +260,11 @@ func matchConnectionVersion(w http.ResponseWriter, r *http.Request, expected int
return true
}
func (s *Server) writeSecurityEventConnectionError(w http.ResponseWriter, r *http.Request, operation, traceID string, err error) {
func (s *Server) writeSecurityEventConnectionError(w http.ResponseWriter, r *http.Request, manager *ssfreceiver.ConnectionManager, operation, traceID string, err error) {
status, message, code := securityEventConnectionErrorProjection(err)
connection := ssfreceiver.ConnectionView{}
if s.securityEventManager != nil {
connection, _ = s.securityEventManager.Get(r.Context())
if manager != nil {
connection, _ = manager.Get(r.Context())
}
errorCategory := code
if connection.LastErrorCategory != nil && *connection.LastErrorCategory != "" {