easyai-ai-gateway/apps/api/internal/httpapi/identity_configuration_handlers.go
chengcheng cdfca61304 docs(identity): 发布统一认证管理契约
为配对、策略、验证、激活、回滚、禁用和公开运行时接口补充 OpenAPI 注解,并将通用 Identity 类型加入生成范围。生成契约不包含 Exchange Token、Machine Secret 或 Secret 引用。\n\n验证:pnpm openapi;OpenAPI 敏感字段扫描通过
2026-07-17 12:31:11 +08:00

616 lines
24 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package httpapi
import (
"crypto/sha256"
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
"strconv"
"strings"
"sync"
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/identity"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
)
type identityConfigurationView struct {
Active *identity.Revision `json:"active"`
Draft *identity.Revision `json:"draft"`
Previous *identity.Revision `json:"previous"`
Pairing *identity.PairingExchange `json:"pairing,omitempty"`
Runtime identityRuntimeStatus `json:"runtime"`
}
type identityRuntimeStatus struct {
Enabled bool `json:"enabled"`
RevisionID string `json:"revisionId,omitempty"`
Login string `json:"login"`
JIT string `json:"jit"`
TokenIntrospection string `json:"tokenIntrospection"`
SessionRevocation string `json:"sessionRevocation"`
LastTraceID string `json:"lastTraceId,omitempty"`
LastAuditID string `json:"lastAuditId,omitempty"`
LastErrorCategory string `json:"lastErrorCategory,omitempty"`
}
type publicIdentityConfiguration struct {
Enabled bool `json:"enabled"`
OIDCLogin bool `json:"oidcLogin"`
LoginURL string `json:"loginUrl,omitempty"`
LogoutURL string `json:"logoutUrl,omitempty"`
Status string `json:"status"`
}
type identityPolicyPatch struct {
LocalTenantKey *string `json:"localTenantKey"`
RolePrefix *string `json:"rolePrefix"`
JITEnabled *bool `json:"jitEnabled"`
LegacyJWTEnabled *bool `json:"legacyJwtEnabled"`
SessionIdleSeconds *int `json:"sessionIdleSeconds"`
SessionAbsoluteSeconds *int `json:"sessionAbsoluteSeconds"`
SessionRefreshSeconds *int `json:"sessionRefreshSeconds"`
}
type storedIdentityResponse struct {
Status int `json:"status"`
Body json.RawMessage `json:"body"`
ETag string `json:"etag,omitempty"`
AuditID string `json:"auditId,omitempty"`
}
type identityWriteOperation struct {
operation, key, requestHash string
release func()
once sync.Once
}
func (operation *identityWriteOperation) close() {
if operation != nil {
operation.once.Do(operation.release)
}
}
// getPublicIdentityConfiguration godoc
// @Summary 获取公开统一认证状态
// @Description 返回 Web 运行时需要的启用状态和非敏感登录、退出入口,不返回 Client、Secret 或内部标识。
// @Tags identity
// @Produce json
// @Success 200 {object} publicIdentityConfiguration
// @Router /api/v1/public/identity [get]
func (s *Server) getPublicIdentityConfiguration(w http.ResponseWriter, _ *http.Request) {
w.Header().Set("Cache-Control", "no-store")
runtime := s.currentIdentityRuntime()
if runtime == nil || runtime.Revision.State != identity.RevisionActive {
writeJSON(w, http.StatusOK, publicIdentityConfiguration{Status: "disabled"})
return
}
view := publicIdentityConfiguration{Enabled: true, Status: "active"}
if oidcRuntimeReady(runtime) {
view.OIDCLogin = true
view.LoginURL = "/api/v1/auth/oidc/login"
view.LogoutURL = "/api/v1/auth/oidc/logout"
}
writeJSON(w, http.StatusOK, view)
}
// getIdentityConfiguration godoc
// @Summary 获取统一认证配置
// @Description 返回 Active、Draft、Previous Revision、配对进度和脱敏运行时健康状态。
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Success 200 {object} identityConfigurationView
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 503 {object} ErrorEnvelope
// @Router /api/admin/system/identity/configuration [get]
func (s *Server) getIdentityConfiguration(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
view := identityConfigurationView{Runtime: s.identityRuntimeStatus(r)}
if revision, err := s.store.ActiveIdentityConfigurationRevision(r.Context()); err == nil {
view.Active = &revision
} else if !errors.Is(err, identity.ErrRevisionNotFound) {
writeError(w, http.StatusServiceUnavailable, "统一认证配置暂时不可用", "IDENTITY_CONFIGURATION_UNAVAILABLE")
return
}
if revision, err := s.store.LatestInactiveIdentityConfigurationRevision(r.Context()); err == nil {
view.Draft = &revision
if pairing, pairingErr := s.store.IdentityPairingExchangeForRevision(r.Context(), revision.ID); pairingErr == nil {
view.Pairing = &pairing
}
} else if !errors.Is(err, identity.ErrRevisionNotFound) {
writeError(w, http.StatusServiceUnavailable, "统一认证草稿暂时不可用", "IDENTITY_CONFIGURATION_UNAVAILABLE")
return
}
if revision, err := s.store.LatestSupersededIdentityConfigurationRevision(r.Context()); err == nil {
view.Previous = &revision
} else if !errors.Is(err, identity.ErrRevisionNotFound) {
writeError(w, http.StatusServiceUnavailable, "统一认证历史版本暂时不可用", "IDENTITY_CONFIGURATION_UNAVAILABLE")
return
}
writeJSON(w, http.StatusOK, view)
}
// startIdentityPairing godoc
// @Summary 启动统一认证配对
// @Description 使用一次性接入码创建 Draft 和异步 Exchange。接入码仅允许出现在请求 Body。
// @Tags identity
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "幂等键"
// @Param If-Match header string true "首次配对固定为 W/\"0\""
// @Param body body identity.PairingInput true "配对参数"
// @Success 202 {object} identity.PairingExchange
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 428 {object} ErrorEnvelope
// @Router /api/admin/system/identity/pairings [post]
func (s *Server) startIdentityPairing(w http.ResponseWriter, r *http.Request) {
var input identity.PairingInput
if !decodeIdentityRequest(w, r, &input) {
return
}
operation, ok := s.beginIdentityWrite(w, r, "pairing.start", 0, input)
if !ok {
return
}
defer operation.close()
traceID := ensureIdentityTraceID(w, r)
pairing, err := s.identityPairing.Start(r.Context(), input, traceID)
if err != nil {
s.writeIdentityError(w, r, "pairing.start", "", traceID, err)
return
}
auditID := s.recordIdentityConfigurationAudit(r, "pairing.start", pairing.RevisionID, "accepted", traceID, "")
pairing.AuthCenterAuditID = ""
s.completeIdentityWrite(w, r, operation, http.StatusAccepted, pairing, pairing.Version, auditID)
s.startIdentityPairingWorker(pairing.ID)
}
// getIdentityPairing godoc
// @Summary 查询统一认证配对状态
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Param pairingID path string true "配对 ID"
// @Success 200 {object} identity.PairingExchange
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 404 {object} ErrorEnvelope
// @Router /api/admin/system/identity/pairings/{pairingID} [get]
func (s *Server) getIdentityPairing(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Cache-Control", "no-store")
pairing, err := s.store.IdentityPairingExchange(r.Context(), r.PathValue("pairingID"))
if err != nil {
s.writeIdentityError(w, r, "pairing.get", r.PathValue("pairingID"), ensureIdentityTraceID(w, r), err)
return
}
w.Header().Set("ETag", identityETag(pairing.Version))
writeJSON(w, http.StatusOK, pairing)
}
// updateIdentityDraftPolicy godoc
// @Summary 修改统一认证 Draft 策略
// @Tags identity
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param revisionID path string true "Revision ID"
// @Param Idempotency-Key header string true "幂等键"
// @Param If-Match header string true "当前 Revision ETag"
// @Param body body identityPolicyPatch true "Gateway 本地策略"
// @Success 200 {object} identity.Revision
// @Failure 400 {object} ErrorEnvelope
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 412 {object} ErrorEnvelope
// @Failure 428 {object} ErrorEnvelope
// @Router /api/admin/system/identity/revisions/{revisionID} [patch]
func (s *Server) updateIdentityDraftPolicy(w http.ResponseWriter, r *http.Request) {
var patch identityPolicyPatch
if !decodeIdentityRequest(w, r, &patch) {
return
}
expectedVersion, ok := requiredIdentityVersion(w, r)
if !ok {
return
}
operation, ok := s.beginIdentityWriteWithVersion(w, r, "revision.policy", expectedVersion, patch)
if !ok {
return
}
defer operation.close()
revision, err := s.store.IdentityConfigurationRevision(r.Context(), r.PathValue("revisionID"))
if err != nil || revision.Version != expectedVersion {
s.writeIdentityError(w, r, "revision.policy", r.PathValue("revisionID"), ensureIdentityTraceID(w, r), firstIdentityError(err, identity.ErrRevisionConflict))
return
}
policy := identity.RevisionPolicy{
LocalTenantKey: revision.LocalTenantKey, RolePrefix: revision.RolePrefix, JITEnabled: revision.JITEnabled,
LegacyJWTEnabled: revision.LegacyJWTEnabled, SessionIdleSeconds: revision.SessionIdleSeconds,
SessionAbsoluteSeconds: revision.SessionAbsoluteSeconds, SessionRefreshSeconds: revision.SessionRefreshSeconds,
}
applyIdentityPolicyPatch(&policy, patch)
updated, err := s.store.UpdateIdentityRevisionPolicy(r.Context(), revision.ID, expectedVersion, policy)
if err != nil {
s.writeIdentityError(w, r, "revision.policy", revision.ID, ensureIdentityTraceID(w, r), err)
return
}
traceID := ensureIdentityTraceID(w, r)
auditID := s.recordIdentityConfigurationAudit(r, "revision.policy", revision.ID, "success", traceID, "")
s.completeIdentityWrite(w, r, operation, http.StatusOK, updated, updated.Version, auditID)
}
// validateIdentityRevision godoc
// @Summary 验证统一认证 Revision
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Param revisionID path string true "Revision ID"
// @Param Idempotency-Key header string true "幂等键"
// @Param If-Match header string true "当前 Revision ETag"
// @Success 200 {object} identity.Revision
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 412 {object} ErrorEnvelope
// @Failure 428 {object} ErrorEnvelope
// @Failure 502 {object} ErrorEnvelope
// @Router /api/admin/system/identity/revisions/{revisionID}/validate [post]
func (s *Server) validateIdentityRevision(w http.ResponseWriter, r *http.Request) {
s.runIdentityRevisionAction(w, r, "revision.validate", func(expected int64, traceID, auditID string) (identity.Revision, error) {
return s.identityRuntime.Validate(r.Context(), r.PathValue("revisionID"), expected, traceID, auditID)
})
}
// activateIdentityRevision godoc
// @Summary 激活统一认证 Revision
// @Description 在完整候选 Runtime 验证成功且本地 Break-glass Manager 可用后原子热切换。
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Param revisionID path string true "Revision ID"
// @Param Idempotency-Key header string true "幂等键"
// @Param If-Match header string true "当前 Revision ETag"
// @Success 200 {object} identity.Revision
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 412 {object} ErrorEnvelope
// @Failure 428 {object} ErrorEnvelope
// @Router /api/admin/system/identity/revisions/{revisionID}/activate [post]
func (s *Server) activateIdentityRevision(w http.ResponseWriter, r *http.Request) {
s.runIdentityRevisionAction(w, r, "revision.activate", func(expected int64, traceID, auditID string) (identity.Revision, error) {
return s.identityRuntime.Activate(r.Context(), r.PathValue("revisionID"), expected, traceID, auditID)
})
}
// rollbackIdentityRevision godoc
// @Summary 回滚统一认证 Revision
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Param revisionID path string true "Revision ID"
// @Param Idempotency-Key header string true "幂等键"
// @Param If-Match header string true "当前 Revision ETag"
// @Success 200 {object} identity.Revision
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 412 {object} ErrorEnvelope
// @Failure 428 {object} ErrorEnvelope
// @Router /api/admin/system/identity/revisions/{revisionID}/rollback [post]
func (s *Server) rollbackIdentityRevision(w http.ResponseWriter, r *http.Request) {
s.runIdentityRevisionAction(w, r, "revision.rollback", func(expected int64, traceID, auditID string) (identity.Revision, error) {
return s.identityRuntime.Rollback(r.Context(), r.PathValue("revisionID"), expected, traceID, auditID)
})
}
// disableIdentityConfiguration godoc
// @Summary 禁用统一认证
// @Description 保留 Superseded Revision 供回滚,清理 BFF Session并继续允许本地管理登录。
// @Tags identity
// @Produce json
// @Security BearerAuth
// @Param Idempotency-Key header string true "幂等键"
// @Param If-Match header string true "当前 Active Revision ETag"
// @Success 200 {object} identity.Revision
// @Failure 401 {object} ErrorEnvelope
// @Failure 403 {object} ErrorEnvelope
// @Failure 409 {object} ErrorEnvelope
// @Failure 412 {object} ErrorEnvelope
// @Failure 428 {object} ErrorEnvelope
// @Router /api/admin/system/identity/disable [post]
func (s *Server) disableIdentityConfiguration(w http.ResponseWriter, r *http.Request) {
expectedVersion, ok := requiredIdentityVersion(w, r)
if !ok {
return
}
operation, ok := s.beginIdentityWriteWithVersion(w, r, "revision.disable", expectedVersion, struct{}{})
if !ok {
return
}
defer operation.close()
traceID := ensureIdentityTraceID(w, r)
auditID := s.recordIdentityConfigurationAudit(r, "revision.disable", "active", "requested", traceID, "")
disabled, err := s.identityRuntime.Disable(r.Context(), expectedVersion, traceID, auditID)
if err != nil {
s.writeIdentityError(w, r, "revision.disable", "active", traceID, err)
return
}
s.completeIdentityWrite(w, r, operation, http.StatusOK, disabled, disabled.Version, auditID)
}
func (s *Server) runIdentityRevisionAction(w http.ResponseWriter, r *http.Request, action string, run func(int64, string, string) (identity.Revision, error)) {
expectedVersion, ok := requiredIdentityVersion(w, r)
if !ok {
return
}
operation, ok := s.beginIdentityWriteWithVersion(w, r, action, expectedVersion, struct{}{})
if !ok {
return
}
defer operation.close()
traceID := ensureIdentityTraceID(w, r)
auditID := s.recordIdentityConfigurationAudit(r, action, r.PathValue("revisionID"), "requested", traceID, "")
revision, err := run(expectedVersion, traceID, auditID)
if err != nil {
s.writeIdentityError(w, r, action, r.PathValue("revisionID"), traceID, err)
return
}
s.completeIdentityWrite(w, r, operation, http.StatusOK, revision, revision.Version, auditID)
}
func (s *Server) identityRuntimeStatus(r *http.Request) identityRuntimeStatus {
runtime := s.currentIdentityRuntime()
if runtime == nil {
return identityRuntimeStatus{Login: "disabled", JIT: "disabled", TokenIntrospection: "disabled", SessionRevocation: "disabled"}
}
revision := runtime.Revision
status := identityRuntimeStatus{
Enabled: true, RevisionID: revision.ID, Login: capabilityHealth(runtime.PublicClient != nil),
JIT: capabilityHealth(revision.JITEnabled), TokenIntrospection: capabilityHealth(revision.TokenIntrospection),
SessionRevocation: capabilityHealth(revision.SessionRevocation), LastTraceID: revision.LastTraceID,
LastAuditID: revision.LastAuditID, LastErrorCategory: revision.LastErrorCategory,
}
if revision.SessionRevocation && runtime.SecurityEvents != nil {
if connection, err := runtime.SecurityEvents.Get(r.Context()); err == nil && connection.LifecycleStatus != "active" {
status.SessionRevocation = connection.HealthMode
}
}
return status
}
func capabilityHealth(enabled bool) string {
if enabled {
return "healthy"
}
return "disabled"
}
func decodeIdentityRequest(w http.ResponseWriter, r *http.Request, output any) bool {
r.Body = http.MaxBytesReader(w, r.Body, 32*1024)
decoder := json.NewDecoder(r.Body)
decoder.DisallowUnknownFields()
if err := decoder.Decode(output); err != nil {
writeError(w, http.StatusBadRequest, "统一认证请求格式无效", "INVALID_REQUEST")
return false
}
if err := decoder.Decode(&struct{}{}); !errors.Is(err, io.EOF) {
writeError(w, http.StatusBadRequest, "统一认证请求格式无效", "INVALID_REQUEST")
return false
}
return true
}
func (s *Server) beginIdentityWrite(w http.ResponseWriter, r *http.Request, operation string, version int64, request any) (*identityWriteOperation, bool) {
if parsed, ok := requiredIdentityVersion(w, r); !ok || parsed != version {
if ok {
writeError(w, http.StatusPreconditionFailed, "统一认证配置版本已变化", "IDENTITY_VERSION_CONFLICT")
}
return nil, false
}
return s.beginIdentityWriteWithVersion(w, r, operation, version, request)
}
func (s *Server) beginIdentityWriteWithVersion(w http.ResponseWriter, r *http.Request, operation string, version int64, request any) (*identityWriteOperation, bool) {
key := strings.TrimSpace(r.Header.Get("Idempotency-Key"))
if key == "" || len(key) > 255 {
writeError(w, http.StatusBadRequest, "Idempotency-Key is required", "IDEMPOTENCY_KEY_REQUIRED")
return nil, false
}
encoded, _ := json.Marshal(request)
digest := sha256.Sum256(append([]byte(fmt.Sprintf("%s\x00%d\x00", operation, version)), encoded...))
requestHash := fmt.Sprintf("%x", digest[:])
s.identityManagementMu.Lock()
write := &identityWriteOperation{operation: operation, key: key, requestHash: requestHash, release: s.identityManagementMu.Unlock}
if s.store == nil {
return write, true
}
recorded, err := s.store.IdentityManagementRequest(r.Context(), operation, key)
if errors.Is(err, store.ErrIdentityManagementRequestNotFound) {
return write, true
}
if err != nil {
write.close()
writeError(w, http.StatusServiceUnavailable, "幂等状态暂时不可用", "IDENTITY_IDEMPOTENCY_UNAVAILABLE")
return nil, false
}
if recorded.RequestHash != requestHash {
write.close()
writeError(w, http.StatusConflict, "Idempotency-Key 已用于其他请求", "IDEMPOTENCY_KEY_REUSED")
return nil, false
}
var response storedIdentityResponse
if json.Unmarshal(recorded.Response, &response) != nil {
write.close()
writeError(w, http.StatusServiceUnavailable, "幂等响应暂时不可用", "IDENTITY_IDEMPOTENCY_UNAVAILABLE")
return nil, false
}
write.close()
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Idempotent-Replayed", "true")
if response.ETag != "" {
w.Header().Set("ETag", response.ETag)
}
if response.AuditID != "" {
w.Header().Set("X-Audit-Id", response.AuditID)
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(response.Status)
_, _ = w.Write(response.Body)
return nil, false
}
func (s *Server) completeIdentityWrite(w http.ResponseWriter, r *http.Request, operation *identityWriteOperation, status int, payload any, version int64, auditID string) {
body, _ := json.Marshal(payload)
stored, _ := json.Marshal(storedIdentityResponse{Status: status, Body: body, ETag: identityETag(version), AuditID: auditID})
if s.store != nil {
if err := s.store.RecordIdentityManagementRequest(r.Context(), store.IdentityManagementRequest{
Operation: operation.operation, Key: operation.key, RequestHash: operation.requestHash, Response: stored,
}); err != nil && s.logger != nil {
s.logger.ErrorContext(r.Context(), "identity idempotency response could not be recorded", "operation", operation.operation, "error_category", "idempotency_store_failed")
}
}
operation.close()
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("ETag", identityETag(version))
if auditID != "" {
w.Header().Set("X-Audit-Id", auditID)
}
writeJSON(w, status, payload)
}
func requiredIdentityVersion(w http.ResponseWriter, r *http.Request) (int64, bool) {
value := strings.Trim(strings.TrimPrefix(strings.TrimSpace(r.Header.Get("If-Match")), "W/"), `"`)
version, err := strconv.ParseInt(value, 10, 64)
if err != nil || version < 0 {
writeError(w, http.StatusPreconditionRequired, "If-Match is required", "IF_MATCH_REQUIRED")
return 0, false
}
return version, true
}
func identityETag(version int64) string { return fmt.Sprintf(`W/"%d"`, version) }
func applyIdentityPolicyPatch(policy *identity.RevisionPolicy, patch identityPolicyPatch) {
if patch.LocalTenantKey != nil {
policy.LocalTenantKey = strings.TrimSpace(*patch.LocalTenantKey)
}
if patch.RolePrefix != nil {
policy.RolePrefix = strings.TrimSpace(*patch.RolePrefix)
}
if patch.JITEnabled != nil {
policy.JITEnabled = *patch.JITEnabled
}
if patch.LegacyJWTEnabled != nil {
policy.LegacyJWTEnabled = *patch.LegacyJWTEnabled
}
if patch.SessionIdleSeconds != nil {
policy.SessionIdleSeconds = *patch.SessionIdleSeconds
}
if patch.SessionAbsoluteSeconds != nil {
policy.SessionAbsoluteSeconds = *patch.SessionAbsoluteSeconds
}
if patch.SessionRefreshSeconds != nil {
policy.SessionRefreshSeconds = *patch.SessionRefreshSeconds
}
}
func firstIdentityError(actual, fallback error) error {
if actual != nil {
return actual
}
return fallback
}
func ensureIdentityTraceID(w http.ResponseWriter, r *http.Request) string {
return ensureSecurityEventTraceID(w, r)
}
func (s *Server) writeIdentityError(w http.ResponseWriter, r *http.Request, action, targetID, traceID string, err error) {
status, message, code := identityErrorProjection(err)
s.recordIdentityConfigurationAudit(r, action, targetID, "failure", traceID, code)
writeError(w, status, message, code)
}
func identityErrorProjection(err error) (int, string, string) {
switch {
case errors.Is(err, identity.ErrRevisionNotFound):
return http.StatusNotFound, "统一认证配置不存在", "IDENTITY_CONFIGURATION_NOT_FOUND"
case errors.Is(err, identity.ErrRevisionConflict):
return http.StatusPreconditionFailed, "统一认证配置版本或状态已变化", "IDENTITY_VERSION_CONFLICT"
case errors.Is(err, identity.ErrBreakGlassRequired):
return http.StatusConflict, "请先保留至少一个可用的本地应急管理员凭据", "BREAK_GLASS_MANAGER_REQUIRED"
case errors.Is(err, identity.ErrLocalTenantInvalid):
return http.StatusConflict, "本地租户映射无效", "IDENTITY_LOCAL_TENANT_INVALID"
case err != nil && (strings.Contains(err.Error(), "invalid") || strings.Contains(err.Error(), "required")):
return http.StatusBadRequest, "统一认证配置无效", "IDENTITY_CONFIGURATION_INVALID"
default:
return http.StatusBadGateway, "认证中心或统一认证服务暂时不可用", "IDENTITY_SERVICE_UNAVAILABLE"
}
}
func (s *Server) recordIdentityConfigurationAudit(r *http.Request, action, targetID, outcome, traceID, errorCategory string) string {
if s.store == nil {
return ""
}
actor, _ := auth.UserFromContext(r.Context())
input := store.AuditLogInput{
Category: "identity", Action: "identity." + action, TargetType: "identity_configuration_revision",
TargetID: firstNonEmptyText(targetID, "pending"), RequestIP: limitAuditText(requestIP(r), 128),
UserAgent: limitAuditText(r.UserAgent(), 512), Metadata: map[string]any{
"outcome": outcome, "traceId": traceID, "errorCategory": errorCategory,
},
}
if actor != nil {
input.ActorGatewayUserID = uuidText(firstNonEmptyText(actor.GatewayUserID, actor.ID))
input.ActorUserID, input.ActorUsername, input.ActorSource, input.ActorRoles = actor.ID, actor.Username, actor.Source, actor.Roles
}
audit, err := s.store.RecordAuditLog(r.Context(), input)
if err != nil {
if s.logger != nil {
s.logger.WarnContext(r.Context(), "record identity audit failed", "action", action, "error_category", "audit_store_failed", "trace_id", traceID)
}
return ""
}
return audit.ID
}
func (s *Server) startIdentityPairingWorker(pairingID string) {
if _, loaded := s.identityPairingWorkers.LoadOrStore(pairingID, struct{}{}); loaded {
return
}
go func() {
defer s.identityPairingWorkers.Delete(pairingID)
delay := time.Second
for {
pairing, err := s.identityPairing.Continue(s.ctx, pairingID)
if err == nil {
if pairing.Status == identity.PairingCompleted || pairing.Status == identity.PairingFailed || pairing.Status == identity.PairingExpired {
return
}
delay = time.Second
} else {
if s.logger != nil {
s.logger.Warn("identity pairing step failed and will retry", "pairing_id", pairingID, "error_category", "pairing_step_failed")
}
if delay < 15*time.Second {
delay *= 2
}
}
select {
case <-s.ctx.Done():
return
case <-time.After(delay):
}
}
}()
}