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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user