fix(oidc): 同步认证中心用户资料到本地用户
从已验证的 OIDC Claim 提取用户名、显示名称、邮箱、手机号和头像,并覆盖单租户、多租户及平台用户的 JIT 创建与重复登录同步。\n\n保留 metadata.manualProfile 标记下的人工资料,限制字段长度且仅接收已验证联系方式与 HTTPS 头像。已通过 auth、httpapi、store 测试及临时 PostgreSQL 集成验证。
This commit is contained in:
@@ -39,7 +39,9 @@ func TestResolveOrProvisionOIDCMultiTenantUserIsIdempotentIsolatedAndReusable(t
|
||||
input := func(tenantID, name, slug string) ResolveOrProvisionOIDCUserInput {
|
||||
return ResolveOrProvisionOIDCUserInput{
|
||||
Issuer: issuer, ApplicationID: applicationID, Subject: subject, Username: "shared-subject",
|
||||
Roles: []string{"basic"}, ContextType: "tenant",
|
||||
DisplayName: "Shared User", Email: "shared@example.test", Phone: "+8613800000000",
|
||||
AvatarURL: "https://static.example.test/shared.png",
|
||||
Roles: []string{"basic"}, ContextType: "tenant",
|
||||
TenantID: tenantID, TenantMode: "multi_tenant",
|
||||
TenantName: name, TenantSlug: slug, TenantMetadataStatus: "synced",
|
||||
TenantMetadataVersion: "v1", TenantMetadataETag: `"v1"`,
|
||||
@@ -69,7 +71,9 @@ func TestResolveOrProvisionOIDCMultiTenantUserIsIdempotentIsolatedAndReusable(t
|
||||
if userA == nil {
|
||||
userA = result.User
|
||||
}
|
||||
if result.User.GatewayUserID != userA.GatewayUserID || result.User.GatewayTenantID != userA.GatewayTenantID {
|
||||
if result.User.GatewayUserID != userA.GatewayUserID || result.User.GatewayTenantID != userA.GatewayTenantID ||
|
||||
result.User.DisplayName != "Shared User" || result.User.Email != "shared@example.test" ||
|
||||
result.User.Phone != "+8613800000000" || result.User.AvatarURL != "https://static.example.test/shared.png" {
|
||||
t.Fatalf("concurrent tenant A projection diverged: first=%#v current=%#v", userA, result.User)
|
||||
}
|
||||
if result.Created {
|
||||
@@ -177,6 +181,10 @@ func TestResolveOrProvisionOIDCUserLifecycleAndConcurrency(t *testing.T) {
|
||||
Issuer: "https://auth.test.example/issuer/shared",
|
||||
Subject: subject,
|
||||
Username: "jit-user-" + suffix,
|
||||
DisplayName: "JIT User",
|
||||
Email: "jit-user@example.test",
|
||||
Phone: "+8613900000000",
|
||||
AvatarURL: "https://static.example.test/jit-user.png",
|
||||
Roles: []string{"basic"},
|
||||
ContextType: "tenant",
|
||||
TenantID: "auth-center-test-tenant",
|
||||
@@ -214,7 +222,9 @@ WHERE target_gateway_user_id IN (
|
||||
t.Fatalf("concurrent resolve %d: %v", index, err)
|
||||
}
|
||||
result := results[index]
|
||||
if result.User == nil || result.User.GatewayUserID == "" {
|
||||
if result.User == nil || result.User.GatewayUserID == "" ||
|
||||
result.User.DisplayName != input.DisplayName || result.User.Email != input.Email ||
|
||||
result.User.Phone != input.Phone || result.User.AvatarURL != input.AvatarURL {
|
||||
t.Fatalf("concurrent resolve %d returned no local user: %+v", index, result)
|
||||
}
|
||||
if firstID == "" {
|
||||
@@ -262,12 +272,18 @@ WHERE id = $1::uuid`, auditID).Scan(&auditProjection); err != nil {
|
||||
UPDATE gateway_users
|
||||
SET display_name = 'Manual Display Name',
|
||||
email = 'manual-profile@example.test',
|
||||
phone = '+8613000000000',
|
||||
avatar_url = 'https://static.example.test/manual-profile.png',
|
||||
metadata = metadata || '{"manualProfile":true}'::jsonb
|
||||
WHERE id = $1::uuid`, firstID); err != nil {
|
||||
t.Fatalf("seed manually managed profile fields: %v", err)
|
||||
}
|
||||
|
||||
input.Username = "jit-user-renamed-" + suffix
|
||||
input.DisplayName = "Remote Display Name"
|
||||
input.Email = "remote-profile@example.test"
|
||||
input.Phone = "+8613700000000"
|
||||
input.AvatarURL = "https://static.example.test/remote-profile.png"
|
||||
input.Roles = []string{"basic", "admin"}
|
||||
input.ProvisioningEnabled = false
|
||||
repeated, err := db.ResolveOrProvisionOIDCUser(ctx, input)
|
||||
@@ -280,14 +296,16 @@ WHERE id = $1::uuid`, firstID); err != nil {
|
||||
if repeated.User.Username != input.Username || !containsOIDCTestRole(repeated.User.Roles, "admin") {
|
||||
t.Fatalf("repeat resolve did not sync token projection: %+v", repeated.User)
|
||||
}
|
||||
var displayName, email string
|
||||
var displayName, email, phone, avatarURL string
|
||||
var manualProfile bool
|
||||
if err := db.pool.QueryRow(ctx, `
|
||||
SELECT COALESCE(display_name, ''), COALESCE(email, ''), COALESCE((metadata->>'manualProfile')::boolean, false)
|
||||
FROM gateway_users WHERE id = $1::uuid`, firstID).Scan(&displayName, &email, &manualProfile); err != nil {
|
||||
SELECT COALESCE(display_name, ''), COALESCE(email, ''), COALESCE(phone, ''), COALESCE(avatar_url, ''),
|
||||
COALESCE((metadata->>'manualProfile')::boolean, false)
|
||||
FROM gateway_users WHERE id = $1::uuid`, firstID).Scan(&displayName, &email, &phone, &avatarURL, &manualProfile); err != nil {
|
||||
t.Fatalf("read manually managed profile fields: %v", err)
|
||||
}
|
||||
if displayName != "Manual Display Name" || email != "manual-profile@example.test" || !manualProfile {
|
||||
if displayName != "Manual Display Name" || email != "manual-profile@example.test" ||
|
||||
phone != "+8613000000000" || avatarURL != "https://static.example.test/manual-profile.png" || !manualProfile {
|
||||
t.Fatalf("repeat resolve overwrote manually managed profile fields")
|
||||
}
|
||||
|
||||
@@ -352,6 +370,10 @@ WHERE source='oidc_v2_platform'
|
||||
ApplicationID: applicationID,
|
||||
Subject: subject,
|
||||
Username: "platform-user-" + suffix,
|
||||
DisplayName: "Platform User",
|
||||
Email: "platform-user@example.test",
|
||||
Phone: "+8613600000000",
|
||||
AvatarURL: "https://static.example.test/platform-user.png",
|
||||
Roles: []string{"admin"},
|
||||
ContextType: "platform",
|
||||
TenantMode: "multi_tenant",
|
||||
@@ -367,6 +389,10 @@ WHERE source='oidc_v2_platform'
|
||||
result.User.TenantID != "" ||
|
||||
result.User.TenantKey != oidcPlatformTenantKey ||
|
||||
result.User.GatewayTenantID == "" ||
|
||||
result.User.DisplayName != "Platform User" ||
|
||||
result.User.Email != "platform-user@example.test" ||
|
||||
result.User.Phone != "+8613600000000" ||
|
||||
result.User.AvatarURL != "https://static.example.test/platform-user.png" ||
|
||||
result.AuditID == "" {
|
||||
t.Fatalf("platform projection=%#v", result)
|
||||
}
|
||||
@@ -407,6 +433,10 @@ WHERE source='oidc_v2_platform'
|
||||
ApplicationID: applicationID,
|
||||
Subject: subject,
|
||||
Username: "platform-user-renamed-" + suffix,
|
||||
DisplayName: "Platform User Renamed",
|
||||
Email: "platform-user-renamed@example.test",
|
||||
Phone: "+8613500000000",
|
||||
AvatarURL: "https://static.example.test/platform-user-renamed.png",
|
||||
Roles: []string{"viewer"},
|
||||
ContextType: "platform",
|
||||
TenantMode: "multi_tenant",
|
||||
@@ -415,6 +445,10 @@ WHERE source='oidc_v2_platform'
|
||||
)
|
||||
if err != nil || repeated.Created ||
|
||||
repeated.User.GatewayUserID != result.User.GatewayUserID ||
|
||||
repeated.User.DisplayName != "Platform User Renamed" ||
|
||||
repeated.User.Email != "platform-user-renamed@example.test" ||
|
||||
repeated.User.Phone != "+8613500000000" ||
|
||||
repeated.User.AvatarURL != "https://static.example.test/platform-user-renamed.png" ||
|
||||
!reflect.DeepEqual(repeated.User.Roles, []string{"viewer"}) {
|
||||
t.Fatalf("repeated platform projection=%#v err=%v", repeated, err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user