easyai-ai-gateway/apps/api/internal/httpapi/oidc_user_middleware.go
chengcheng a9e23cb237 feat(identity): 实现统一认证运行时热切换
从 Active Revision 构造并验证 OIDC、BFF Session、Introspection 与 SSF Runtime,在数据库激活成功后原子替换内存引用。请求链路使用不可变快照,失败保留当前运行时,本地管理登录不受影响。\n\n验证:go test ./apps/api/...;go vet ./apps/api/...
2026-07-17 12:05:00 +08:00

102 lines
4.0 KiB
Go

package httpapi
import (
"context"
"errors"
"net/http"
"strings"
"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"
)
const (
errorCodeGatewayUserNotProvisioned = "GATEWAY_USER_NOT_PROVISIONED"
errorCodeGatewayUserDisabled = "GATEWAY_USER_DISABLED"
errorCodeGatewayTenantUnavailable = "GATEWAY_TENANT_UNAVAILABLE"
errorCodeGatewayProvisioningFailed = "GATEWAY_USER_PROVISIONING_FAILED"
)
type oidcUserResolver interface {
ResolveOrProvisionOIDCUser(context.Context, store.ResolveOrProvisionOIDCUserInput) (store.ResolveOrProvisionOIDCUserResult, error)
}
func (s *Server) requireUser(permission auth.Permission, next http.Handler) http.Handler {
return s.auth.Require(permission, s.resolveGatewayUser(next))
}
func (s *Server) resolveGatewayUser(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
user, ok := auth.UserFromContext(r.Context())
if !ok || user == nil || !strings.EqualFold(strings.TrimSpace(user.Source), "oidc") {
next.ServeHTTP(w, r)
return
}
result, err := s.resolveOIDCUserProjection(r.Context(), r, user)
if err != nil {
s.writeOIDCUserResolutionError(w, r, err)
return
}
if result.User == nil || strings.TrimSpace(result.User.GatewayUserID) == "" {
s.writeOIDCUserResolutionError(w, r, errors.New("OIDC user resolver returned no local user"))
return
}
if result.Created {
s.logger.InfoContext(r.Context(), "OIDC gateway user provisioned",
"gatewayUserId", result.User.GatewayUserID,
"auditId", result.AuditID,
)
}
next.ServeHTTP(w, r.WithContext(auth.WithUser(r.Context(), result.User)))
})
}
func (s *Server) resolveOIDCUserProjection(ctx context.Context, r *http.Request, user *auth.User) (store.ResolveOrProvisionOIDCUserResult, error) {
runtime := s.currentIdentityRuntime()
if runtime == nil {
return store.ResolveOrProvisionOIDCUserResult{}, errors.New("active identity runtime is unavailable")
}
return s.resolveOIDCUserProjectionForRevision(ctx, r, user, runtime.Revision)
}
func (s *Server) resolveOIDCUserProjectionForRevision(ctx context.Context, r *http.Request, user *auth.User, revision identity.Revision) (store.ResolveOrProvisionOIDCUserResult, error) {
if s.oidcUserResolver == nil {
return store.ResolveOrProvisionOIDCUserResult{}, errors.New("OIDC user resolver is unavailable")
}
return s.oidcUserResolver.ResolveOrProvisionOIDCUser(ctx, store.ResolveOrProvisionOIDCUserInput{
Issuer: revision.Issuer,
Subject: user.ID,
Username: user.Username,
Roles: user.Roles,
TenantID: user.TenantID,
GatewayTenantKey: revision.LocalTenantKey,
ProvisioningEnabled: revision.JITEnabled,
RequestIP: limitAuditText(requestIP(r), 128),
UserAgent: limitAuditText(r.UserAgent(), 512),
})
}
func (s *Server) writeOIDCUserResolutionError(w http.ResponseWriter, r *http.Request, err error) {
switch {
case errors.Is(err, store.ErrOIDCUserNotProvisioned):
writeError(w, http.StatusForbidden, "该账号尚未开通 EasyAI Gateway", errorCodeGatewayUserNotProvisioned)
case errors.Is(err, store.ErrOIDCUserDisabled):
writeError(w, http.StatusForbidden, "该 Gateway 账号已停用,请联系管理员", errorCodeGatewayUserDisabled)
case errors.Is(err, store.ErrOIDCTenantUnavailable):
writeError(w, http.StatusServiceUnavailable, "Gateway 租户尚未就绪,请联系管理员", errorCodeGatewayTenantUnavailable)
default:
s.logger.ErrorContext(r.Context(), "resolve OIDC gateway user failed", "error", err, "path", r.URL.Path)
writeError(w, http.StatusServiceUnavailable, "Gateway 账号初始化失败,请稍后重试", errorCodeGatewayProvisioningFailed)
}
}
func limitAuditText(value string, limit int) string {
value = strings.TrimSpace(value)
runes := []rune(value)
if limit > 0 && len(runes) > limit {
return string(runes[:limit])
}
return value
}