fix(identity): 支持平台用户显式登录 AI Gateway

修复多租户身份配置将所有人类登录都强制解释为租户上下文的问题。Web 现在提供平台与租户两个受控入口,API 严格绑定 context_type、tid、issuer、application 和 subject,并为平台用户建立独立本地投影与可刷新会话。\n\n风险:新增会话身份列保持旧会话可读,新会话一律使用严格约束;未改变租户数据隔离和 API Key 行为。\n\n验证:Go 全量测试与 go vet 通过;PostgreSQL 平台投影、会话和安全事件集成测试通过;前端 lint、141 项测试与生产 build 通过;OpenAPI 生成和迁移安全测试通过。
This commit is contained in:
2026-07-31 14:24:40 +08:00
parent 88c971564a
commit c0296dbf06
29 changed files with 1061 additions and 84 deletions
+38 -1
View File
@@ -11,6 +11,7 @@ import (
"time"
"github.com/easyai/easyai-ai-gateway/apps/api/internal/auth"
"github.com/google/uuid"
"github.com/jackc/pgx/v5"
)
@@ -26,6 +27,7 @@ type ResolveOrProvisionOIDCUserInput struct {
Subject string
Username string
Roles []string
ContextType string
TenantID string
TenantMode string
TenantName string
@@ -58,8 +60,16 @@ type oidcUserProjection struct {
func (s *Store) ResolveOrProvisionOIDCUser(ctx context.Context, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
input = normalizeOIDCUserInput(input)
if input.TenantMode == "multi_tenant" {
switch oidcProjectionKind(input) {
case "platform":
return s.resolveOrProvisionOIDCPlatformUser(ctx, input)
case "multi_tenant":
return s.resolveOrProvisionOIDCMultiTenantUser(ctx, input)
case "single_tenant":
// Continue through the fixed local Tenant projection below.
default:
return ResolveOrProvisionOIDCUserResult{},
errors.New("invalid OIDC user context")
}
if input.Issuer == "" || input.Subject == "" || input.TenantID == "" {
return ResolveOrProvisionOIDCUserResult{}, errors.New("invalid OIDC user projection input")
@@ -187,6 +197,31 @@ RETURNING `+userColumns,
}, nil
}
func oidcProjectionKind(input ResolveOrProvisionOIDCUserInput) string {
switch input.ContextType {
case "platform":
if input.TenantMode == "multi_tenant" &&
uuid.Validate(input.ApplicationID) == nil &&
input.TenantID == "" {
return "platform"
}
case "tenant":
if input.TenantID == "" {
return ""
}
switch input.TenantMode {
case "multi_tenant":
if uuid.Validate(input.ApplicationID) == nil &&
uuid.Validate(input.TenantID) == nil {
return "multi_tenant"
}
case "single_tenant":
return "single_tenant"
}
}
return ""
}
func (s *Store) syncExistingOIDCUser(ctx context.Context, tx pgx.Tx, projection oidcUserProjection, input ResolveOrProvisionOIDCUserInput) (ResolveOrProvisionOIDCUserResult, error) {
if projection.userDeleted || projection.user.Status != "active" {
return ResolveOrProvisionOIDCUserResult{}, ErrOIDCUserDisabled
@@ -308,6 +343,7 @@ func normalizeOIDCUserInput(input ResolveOrProvisionOIDCUserInput) ResolveOrProv
input.Issuer = strings.TrimRight(strings.TrimSpace(input.Issuer), "/")
input.Subject = strings.TrimSpace(input.Subject)
input.Username = strings.TrimSpace(input.Username)
input.ContextType = strings.TrimSpace(input.ContextType)
input.TenantID = strings.TrimSpace(input.TenantID)
input.ApplicationID = strings.TrimSpace(input.ApplicationID)
input.TenantMode = strings.TrimSpace(input.TenantMode)
@@ -355,6 +391,7 @@ func authUserFromOIDCProjection(user GatewayUser, userGroupKey string) *auth.Use
ID: user.ExternalUserID,
Username: user.Username,
Roles: user.Roles,
ContextType: "tenant",
TenantID: user.TenantID,
GatewayTenantID: user.GatewayTenantID,
TenantKey: user.TenantKey,