fix(oidc): 同步认证中心用户资料到本地用户
从已验证的 OIDC Claim 提取用户名、显示名称、邮箱、手机号和头像,并覆盖单租户、多租户及平台用户的 JIT 创建与重复登录同步。\n\n保留 metadata.manualProfile 标记下的人工资料,限制字段长度且仅接收已验证联系方式与 HTTPS 头像。已通过 auth、httpapi、store 测试及临时 PostgreSQL 集成验证。
This commit is contained in:
@@ -26,6 +26,10 @@ type ResolveOrProvisionOIDCUserInput struct {
|
||||
ApplicationID string
|
||||
Subject string
|
||||
Username string
|
||||
DisplayName string
|
||||
Email string
|
||||
Phone string
|
||||
AvatarURL string
|
||||
Roles []string
|
||||
ContextType string
|
||||
TenantID string
|
||||
@@ -122,16 +126,22 @@ func (s *Store) ResolveOrProvisionOIDCUser(ctx context.Context, input ResolveOrP
|
||||
|
||||
createdUser, err := scanUser(tx.QueryRow(ctx, `
|
||||
INSERT INTO gateway_users (
|
||||
user_key, source, external_user_id, username, gateway_tenant_id, tenant_id, tenant_key,
|
||||
user_key, source, external_user_id, username, display_name, email, phone, avatar_url,
|
||||
gateway_tenant_id, tenant_id, tenant_key,
|
||||
default_user_group_id, roles, auth_profile, metadata, status, last_login_at, synced_at, source_updated_at
|
||||
)
|
||||
VALUES ($1, 'oidc', $2, $3, $4::uuid, $5, $6, $7::uuid, $8::jsonb, '{}'::jsonb, $9::jsonb,
|
||||
VALUES ($1, 'oidc', $2, $3, NULLIF($4, ''), NULLIF($5, ''), NULLIF($6, ''), NULLIF($7, ''),
|
||||
$8::uuid, $9, $10, $11::uuid, $12::jsonb, '{}'::jsonb, $13::jsonb,
|
||||
'active', now(), now(), now())
|
||||
ON CONFLICT DO NOTHING
|
||||
RETURNING `+userColumns,
|
||||
userKey,
|
||||
input.Subject,
|
||||
username,
|
||||
input.DisplayName,
|
||||
input.Email,
|
||||
input.Phone,
|
||||
input.AvatarURL,
|
||||
tenantID,
|
||||
input.TenantID,
|
||||
input.GatewayTenantKey,
|
||||
@@ -244,7 +254,11 @@ func (s *Store) syncExistingOIDCUser(ctx context.Context, tx pgx.Tx, projection
|
||||
updated, err := scanUser(tx.QueryRow(ctx, `
|
||||
UPDATE gateway_users
|
||||
SET username = COALESCE(NULLIF($2, ''), username),
|
||||
roles = $3::jsonb,
|
||||
display_name = CASE WHEN metadata->>'manualProfile' = 'true' THEN display_name ELSE COALESCE(NULLIF($3, ''), display_name) END,
|
||||
email = CASE WHEN metadata->>'manualProfile' = 'true' THEN email ELSE COALESCE(NULLIF($4, ''), email) END,
|
||||
phone = CASE WHEN metadata->>'manualProfile' = 'true' THEN phone ELSE COALESCE(NULLIF($5, ''), phone) END,
|
||||
avatar_url = CASE WHEN metadata->>'manualProfile' = 'true' THEN avatar_url ELSE COALESCE(NULLIF($6, ''), avatar_url) END,
|
||||
roles = $7::jsonb,
|
||||
last_login_at = now(),
|
||||
synced_at = now(),
|
||||
source_updated_at = now(),
|
||||
@@ -256,6 +270,10 @@ WHERE id = $1::uuid
|
||||
RETURNING `+userColumns,
|
||||
projection.user.ID,
|
||||
input.Username,
|
||||
input.DisplayName,
|
||||
input.Email,
|
||||
input.Phone,
|
||||
input.AvatarURL,
|
||||
string(rolesJSON),
|
||||
))
|
||||
if err != nil {
|
||||
@@ -343,6 +361,10 @@ 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.DisplayName = limitOIDCProfileText(input.DisplayName, 200)
|
||||
input.Email = limitOIDCProfileText(input.Email, 320)
|
||||
input.Phone = limitOIDCProfileText(input.Phone, 64)
|
||||
input.AvatarURL = limitOIDCProfileText(input.AvatarURL, 2048)
|
||||
input.ContextType = strings.TrimSpace(input.ContextType)
|
||||
input.TenantID = strings.TrimSpace(input.TenantID)
|
||||
input.ApplicationID = strings.TrimSpace(input.ApplicationID)
|
||||
@@ -359,6 +381,15 @@ func normalizeOIDCUserInput(input ResolveOrProvisionOIDCUserInput) ResolveOrProv
|
||||
return input
|
||||
}
|
||||
|
||||
func limitOIDCProfileText(value string, limit int) string {
|
||||
value = strings.TrimSpace(value)
|
||||
runes := []rune(value)
|
||||
if len(runes) > limit {
|
||||
return string(runes[:limit])
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
func normalizeOIDCRoles(roles []string) []string {
|
||||
result := make([]string, 0, len(roles))
|
||||
seen := make(map[string]struct{}, len(roles))
|
||||
@@ -390,6 +421,10 @@ func authUserFromOIDCProjection(user GatewayUser, userGroupKey string) *auth.Use
|
||||
return &auth.User{
|
||||
ID: user.ExternalUserID,
|
||||
Username: user.Username,
|
||||
DisplayName: user.DisplayName,
|
||||
Email: user.Email,
|
||||
Phone: user.Phone,
|
||||
AvatarURL: user.AvatarURL,
|
||||
Roles: user.Roles,
|
||||
ContextType: "tenant",
|
||||
TenantID: user.TenantID,
|
||||
|
||||
Reference in New Issue
Block a user