fix: 修复 OIDC 用户预配与跨标签页登录态
增加受控 JIT 本地用户投影,并使用 HttpOnly Cookie 在新标签页恢复认证中心登录态。补充错误语义、安全校验、自动化测试与回滚配置。
This commit is contained in:
@@ -0,0 +1,90 @@
|
||||
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/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
|
||||
}
|
||||
if s.oidcUserResolver == nil {
|
||||
s.writeOIDCUserResolutionError(w, r, errors.New("OIDC user resolver is unavailable"))
|
||||
return
|
||||
}
|
||||
|
||||
result, err := s.oidcUserResolver.ResolveOrProvisionOIDCUser(r.Context(), store.ResolveOrProvisionOIDCUserInput{
|
||||
Issuer: s.cfg.OIDCIssuer,
|
||||
Subject: user.ID,
|
||||
Username: user.Username,
|
||||
Roles: user.Roles,
|
||||
TenantID: user.TenantID,
|
||||
GatewayTenantKey: s.cfg.OIDCGatewayTenantKey,
|
||||
ProvisioningEnabled: s.cfg.OIDCJITProvisioningEnabled,
|
||||
RequestIP: limitAuditText(requestIP(r), 128),
|
||||
UserAgent: limitAuditText(r.UserAgent(), 512),
|
||||
})
|
||||
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) 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
|
||||
}
|
||||
Reference in New Issue
Block a user