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
@@ -78,10 +78,28 @@ func (s *Server) putSecurityEventConnection(w http.ResponseWriter, r *http.Reque
defer clear(secret)
secretDigest := sha256.Sum256(secret)
requestHash := securityEventOperationHash("connect", request.TransmitterIssuer+"\x00"+request.ManagementClientID+"\x00"+fmt.Sprintf("%x", secretDigest[:]))
s.securityEventManagementMu.Lock()
defer s.securityEventManagementMu.Unlock()
if s.replaySecurityEventOperation(w, r, "connect", idempotencyKey, requestHash) {
return
}
if current, err := manager.Get(r.Context()); err == nil && !matchConnectionVersion(w, r, current.Version) {
current, currentErr := manager.Get(r.Context())
switch {
case currentErr == nil:
if !matchConnectionVersion(w, r, current.Version) {
return
}
case errors.Is(currentErr, store.ErrSecurityEventConnectionNotFound):
current = ssfreceiver.ConnectionView{}
if !matchConnectionVersion(w, r, 0) {
return
}
default:
writeError(w, http.StatusServiceUnavailable, "security event connection state is unavailable", "security_event_state_unavailable")
return
}
auditID, ok := s.requireSecurityEventConnectionAudit(w, r, "connect", traceID, current)
if !ok {
return
}
connection, err := manager.Connect(r.Context(), request.TransmitterIssuer, request.ManagementClientID, secret, idempotencyKey)
@@ -89,13 +107,15 @@ func (s *Server) putSecurityEventConnection(w http.ResponseWriter, r *http.Reque
s.writeSecurityEventConnectionError(w, r, manager, "connect", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "connect", idempotencyKey, requestHash, traceID, connection)
s.writeSecurityEventOperation(w, r, "connect", idempotencyKey, requestHash, traceID, auditID, connection)
}
func (s *Server) verifySecurityEventConnection(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
s.securityEventManagementMu.Lock()
defer s.securityEventManagementMu.Unlock()
manager := s.currentSecurityEventManager()
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, manager, "verify", traceID)
idempotencyKey, requestHash, auditID, ok := s.beginSecurityEventOperation(w, r, manager, "verify", traceID)
if !ok {
return
}
@@ -104,13 +124,15 @@ func (s *Server) verifySecurityEventConnection(w http.ResponseWriter, r *http.Re
s.writeSecurityEventConnectionError(w, r, manager, "verify", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "verify", idempotencyKey, requestHash, traceID, connection)
s.writeSecurityEventOperation(w, r, "verify", idempotencyKey, requestHash, traceID, auditID, connection)
}
func (s *Server) rotateSecurityEventConnectionCredential(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
s.securityEventManagementMu.Lock()
defer s.securityEventManagementMu.Unlock()
manager := s.currentSecurityEventManager()
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, manager, "rotate", traceID)
idempotencyKey, requestHash, auditID, ok := s.beginSecurityEventOperation(w, r, manager, "rotate", traceID)
if !ok {
return
}
@@ -119,13 +141,15 @@ func (s *Server) rotateSecurityEventConnectionCredential(w http.ResponseWriter,
s.writeSecurityEventConnectionError(w, r, manager, "rotate", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "rotate", idempotencyKey, requestHash, traceID, connection)
s.writeSecurityEventOperation(w, r, "rotate", idempotencyKey, requestHash, traceID, auditID, connection)
}
func (s *Server) deleteSecurityEventConnection(w http.ResponseWriter, r *http.Request) {
traceID := ensureSecurityEventTraceID(w, r)
s.securityEventManagementMu.Lock()
defer s.securityEventManagementMu.Unlock()
manager := s.currentSecurityEventManager()
idempotencyKey, requestHash, ok := s.beginSecurityEventOperation(w, r, manager, "disconnect", traceID)
idempotencyKey, requestHash, auditID, ok := s.beginSecurityEventOperation(w, r, manager, "disconnect", traceID)
if !ok {
return
}
@@ -134,7 +158,7 @@ func (s *Server) deleteSecurityEventConnection(w http.ResponseWriter, r *http.Re
s.writeSecurityEventConnectionError(w, r, manager, "disconnect", traceID, err)
return
}
s.writeSecurityEventOperation(w, r, "disconnect", idempotencyKey, requestHash, traceID, connection)
s.writeSecurityEventOperation(w, r, "disconnect", idempotencyKey, requestHash, traceID, auditID, connection)
}
func (s *Server) securityEventPrerequisites() map[string]any {
@@ -164,25 +188,32 @@ func requiredConnectionIdempotencyKey(w http.ResponseWriter, r *http.Request) (s
return value, true
}
func (s *Server) beginSecurityEventOperation(w http.ResponseWriter, r *http.Request, manager *ssfreceiver.ConnectionManager, operation, traceID string) (string, string, bool) {
func (s *Server) beginSecurityEventOperation(w http.ResponseWriter, r *http.Request, manager *ssfreceiver.ConnectionManager, operation, traceID string) (string, string, string, bool) {
if manager == nil {
writeError(w, http.StatusNotFound, "security event connection does not exist", "security_event_connection_not_found")
return "", "", false
return "", "", "", false
}
idempotencyKey, ok := requiredConnectionIdempotencyKey(w, r)
if !ok {
return "", "", false
return "", "", "", false
}
requestHash := securityEventOperationHash(operation, normalizedConnectionETag(r.Header.Get("If-Match")))
if s.replaySecurityEventOperation(w, r, operation, idempotencyKey, requestHash) {
return "", "", false
return "", "", "", false
}
connection, err := manager.Get(r.Context())
if err != nil {
s.writeSecurityEventConnectionError(w, r, manager, operation, traceID, err)
return "", "", false
return "", "", "", false
}
return idempotencyKey, requestHash, matchConnectionVersion(w, r, connection.Version)
if !matchConnectionVersion(w, r, connection.Version) {
return "", "", "", false
}
auditID, ok := s.requireSecurityEventConnectionAudit(w, r, operation, traceID, connection)
if !ok {
return "", "", "", false
}
return idempotencyKey, requestHash, auditID, true
}
func (s *Server) replaySecurityEventOperation(w http.ResponseWriter, r *http.Request, operation, idempotencyKey, requestHash string) bool {
@@ -216,8 +247,7 @@ func (s *Server) replaySecurityEventOperation(w http.ResponseWriter, r *http.Req
return true
}
func (s *Server) writeSecurityEventOperation(w http.ResponseWriter, r *http.Request, operation, idempotencyKey, requestHash, traceID string, connection ssfreceiver.ConnectionView) {
auditID := s.recordSecurityEventConnectionAudit(r, operation, "success", "", traceID, connection)
func (s *Server) writeSecurityEventOperation(w http.ResponseWriter, r *http.Request, operation, idempotencyKey, requestHash, traceID, auditID string, connection ssfreceiver.ConnectionView) {
if auditID != "" {
w.Header().Set("X-Audit-Id", auditID)
}
@@ -328,6 +358,16 @@ func (s *Server) recordSecurityEventConnectionAudit(r *http.Request, operation,
return audit.ID
}
func (s *Server) requireSecurityEventConnectionAudit(w http.ResponseWriter, r *http.Request, operation, traceID string, connection ssfreceiver.ConnectionView) (string, bool) {
auditID := s.recordSecurityEventConnectionAudit(r, operation, "requested", "", traceID, connection)
if auditID == "" {
writeError(w, http.StatusServiceUnavailable, "security event audit is unavailable; operation was not executed", "security_event_audit_unavailable")
return "", false
}
w.Header().Set("X-Audit-Id", auditID)
return auditID, true
}
func securityEventConnectionAuditInput(r *http.Request, actor *auth.User, operation, outcome, errorCategory, traceID string, connection ssfreceiver.ConnectionView) store.AuditLogInput {
input := store.AuditLogInput{
Category: "identity", Action: "identity.security_event_connection." + operation,