From fd05c049d34a7f8f5f3cbd3a8648d4650887ee15 Mon Sep 17 00:00:00 2001 From: kyy Date: Wed, 17 Jun 2026 10:50:37 +0900 Subject: [PATCH] =?UTF-8?q?tenants=20=EB=AA=85=EC=B9=AD=20=EB=B0=8F=20prof?= =?UTF-8?q?ile=20=EC=A0=84=ED=99=94=EB=B2=88=ED=98=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/handler/auth_handler.go | 5 ++- .../handler/auth_handler_consent_test.go | 10 ++--- .../auth_handler_dynamic_claims_test.go | 41 ++++++++++++------- .../internal/handler/client_tenant_access.go | 19 ++++++--- .../handler/client_tenant_access_test.go | 8 ++-- backend/internal/handler/dev_handler_test.go | 8 ++-- .../clients/ClientGeneralPage.claims.test.tsx | 24 +++++------ .../features/clients/ClientGeneralPage.tsx | 29 ++++++++----- devfront/src/locales/en.toml | 6 ++- devfront/src/locales/ko.toml | 6 ++- docs/rp-iam-integration-guide.md | 3 ++ 11 files changed, 98 insertions(+), 61 deletions(-) diff --git a/backend/internal/handler/auth_handler.go b/backend/internal/handler/auth_handler.go index dae3e471..a54e91a6 100644 --- a/backend/internal/handler/auth_handler.go +++ b/backend/internal/handler/auth_handler.go @@ -1111,6 +1111,9 @@ func buildOidcClaimsFromTraits(traits map[string]any, scopes []string, tenantID if len(emails) > 0 { profile["emails"] = emails } + if phone := getString("phone_number"); phone != "" { + profile["phones"] = []string{phone} + } if len(profile) > 0 { claims["profile"] = profile } @@ -1364,7 +1367,7 @@ func (h *AuthHandler) withHanmacFamilyTenantClaims(ctx context.Context, claims m func tenantClaimScopeRequested(scopes []string) bool { for _, scope := range scopes { - if strings.EqualFold(strings.TrimSpace(scope), "tenant") { + if strings.EqualFold(strings.TrimSpace(scope), "tenants") || strings.EqualFold(strings.TrimSpace(scope), "tenant") { return true } } diff --git a/backend/internal/handler/auth_handler_consent_test.go b/backend/internal/handler/auth_handler_consent_test.go index dcb368b6..b0b7eb43 100644 --- a/backend/internal/handler/auth_handler_consent_test.go +++ b/backend/internal/handler/auth_handler_consent_test.go @@ -203,7 +203,7 @@ func TestGetConsentRequest_AddsMandatoryTenantScope(t *testing.T) { "allowed_tenants": []string{"tenant-allow"}, "structured_scopes": []map[string]any{ {"name": "openid", "mandatory": true}, - {"name": "tenant", "mandatory": true, "locked": true}, + {"name": "tenants", "mandatory": true, "locked": true}, {"name": "profile", "mandatory": false}, }, }, @@ -262,9 +262,9 @@ func TestGetConsentRequest_AddsMandatoryTenantScope(t *testing.T) { var body map[string]any json.NewDecoder(resp.Body).Decode(&body) - assert.Equal(t, []any{"openid", "tenant", "profile"}, body["requested_scope"]) + assert.Equal(t, []any{"openid", "tenants", "profile"}, body["requested_scope"]) scopeDetails := body["scope_details"].(map[string]any) - tenantDetail := scopeDetails["tenant"].(map[string]any) + tenantDetail := scopeDetails["tenants"].(map[string]any) assert.Equal(t, true, tenantDetail["mandatory"]) } @@ -448,7 +448,7 @@ func TestAcceptConsentRequest_EnforcesMandatoryTenantScope(t *testing.T) { "allowed_tenants": []string{"tenant-abc"}, "structured_scopes": []map[string]any{ {"name": "openid", "mandatory": true}, - {"name": "tenant", "mandatory": true, "locked": true}, + {"name": "tenants", "mandatory": true, "locked": true}, {"name": "profile", "mandatory": false}, }, }, @@ -511,5 +511,5 @@ func TestAcceptConsentRequest_EnforcesMandatoryTenantScope(t *testing.T) { resp, err := app.Test(req) assert.NoError(t, err) assert.Equal(t, http.StatusOK, resp.StatusCode) - assert.Equal(t, []string{"openid", "tenant", "profile"}, capturedGrantScopes) + assert.Equal(t, []string{"openid", "tenants", "profile"}, capturedGrantScopes) } diff --git a/backend/internal/handler/auth_handler_dynamic_claims_test.go b/backend/internal/handler/auth_handler_dynamic_claims_test.go index f634f568..900ab75e 100644 --- a/backend/internal/handler/auth_handler_dynamic_claims_test.go +++ b/backend/internal/handler/auth_handler_dynamic_claims_test.go @@ -70,9 +70,10 @@ func TestWithRefreshTokenExpiryClaim_UsesHydraRefreshTokenTTL(t *testing.T) { func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { traits := map[string]any{ - "email": "user@baron.com", - "name": "홍길동", - "tenant_id": "primary-tenant-999", // Added primary tenant + "email": "user@baron.com", + "name": "홍길동", + "phone_number": "+821012345678", + "tenant_id": "primary-tenant-999", // Added primary tenant "tenant-1": map[string]any{ "department": "개발팀", "grade": "선임", @@ -91,6 +92,8 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { assert.Equal(t, "primary-tenant-999", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) + profile := claims["profile"].(map[string]any) + assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) assert.Contains(t, claims["joined_tenants"], "tenant-1") @@ -105,6 +108,8 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { assert.Equal(t, "tenant-1", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) + profile := claims["profile"].(map[string]any) + assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) assert.Contains(t, claims["joined_tenants"], "tenant-1") @@ -119,6 +124,8 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { assert.Equal(t, "tenant-2", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) + profile := claims["profile"].(map[string]any) + assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) assert.Contains(t, claims["joined_tenants"], "primary-tenant-999") @@ -131,17 +138,21 @@ func TestBuildOidcClaimsFromTraits_DynamicClaims(t *testing.T) { assert.Equal(t, "tenant-3", claims["tenant_id"]) assert.Nil(t, claims["department"]) assert.Nil(t, claims["grade"]) + profile := claims["profile"].(map[string]any) + assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.Nil(t, claims["tenants"]) assert.Contains(t, claims["joined_tenants"], "tenant-1") assert.Contains(t, claims["joined_tenants"], "primary-tenant-999") }) - t.Run("Tenant scope includes detailed tenant metadata", func(t *testing.T) { - claims := buildOidcClaimsFromTraits(traits, []string{"openid", "profile", "tenant"}, "tenant-1") + t.Run("Tenants scope includes detailed tenant metadata", func(t *testing.T) { + claims := buildOidcClaimsFromTraits(traits, []string{"openid", "profile", "tenants"}, "tenant-1") assert.Equal(t, "tenant-1", claims["tenant_id"]) assert.Equal(t, "개발팀", claims["department"]) assert.Equal(t, "선임", claims["grade"]) + profile := claims["profile"].(map[string]any) + assert.Equal(t, []string{"+821012345678"}, profile["phones"]) assert.NotNil(t, claims["tenants"]) assert.Contains(t, claims["joined_tenants"], "tenant-1") assert.Contains(t, claims["joined_tenants"], "tenant-2") @@ -190,7 +201,7 @@ func TestAcceptConsentRequest_DynamicClaims(t *testing.T) { if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-dynamic" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-dynamic", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "subject": "user-123", "client": map[string]any{ "client_id": "client-app", @@ -260,7 +271,7 @@ func TestAcceptConsentRequest_DynamicClaims(t *testing.T) { reqBody, _ := json.Marshal(map[string]any{ "consent_challenge": "challenge-dynamic", - "grant_scope": []string{"openid", "profile", "tenant"}, + "grant_scope": []string{"openid", "profile", "tenants"}, }) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody)) req.Header.Set("Content-Type", "application/json") @@ -290,7 +301,7 @@ func TestAcceptConsentRequest_UsesRepresentativeTenantIDInsteadOfClientTenantCon if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-representative-tenant" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-representative-tenant", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "subject": "user-representative", "client": map[string]any{ "client_id": "client-app", @@ -367,7 +378,7 @@ func TestAcceptConsentRequest_IncludesHanmacFamilyTenantClaimDetails(t *testing. if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-hanmac-tenant-claim" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-hanmac-tenant-claim", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "subject": "user-hanmac", "client": map[string]any{ "client_id": "hanmac-rp", @@ -462,7 +473,7 @@ func TestAcceptConsentRequest_IncludesHanmacFamilyTenantClaimDetails(t *testing. reqBody, _ := json.Marshal(map[string]any{ "consent_challenge": "challenge-hanmac-tenant-claim", - "grant_scope": []string{"openid", "profile", "tenant"}, + "grant_scope": []string{"openid", "profile", "tenants"}, }) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody)) req.Header.Set("Content-Type", "application/json") @@ -574,7 +585,7 @@ func TestAcceptConsentRequest_DoesNotEmitLegacyProfileArray(t *testing.T) { if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-rp-profile" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-rp-profile", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "subject": "user-123", "client": map[string]any{ "client_id": "client-app", @@ -666,7 +677,7 @@ func TestGetConsentRequest_Skip_DynamicClaims(t *testing.T) { if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-skip-dynamic" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-skip-dynamic", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "skip": true, "subject": "user-456", "client": map[string]any{ @@ -861,7 +872,7 @@ func TestAcceptConsentRequest_AppliesConfiguredIDTokenClaims(t *testing.T) { if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-configured-claims" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-configured-claims", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "subject": "user-789", "client": map[string]any{ "client_id": "client-configured-claims", @@ -973,7 +984,7 @@ func TestAcceptConsentRequest_UsesUpdatedRPUserMetadataForRPClaims(t *testing.T) if r.URL.Path == "/oauth2/auth/requests/consent" && r.URL.Query().Get("consent_challenge") == "challenge-rp-user-claims" { return httpJSONAny(r, http.StatusOK, map[string]any{ "challenge": "challenge-rp-user-claims", - "requested_scope": []string{"openid", "profile", "tenant"}, + "requested_scope": []string{"openid", "profile", "tenants"}, "subject": "user-rp-claims", "client": map[string]any{ "client_id": "client-rp-claims", @@ -1119,7 +1130,7 @@ func TestAcceptConsentRequest_UsesUpdatedRPUserMetadataForRPClaims(t *testing.T) reqBody, _ := json.Marshal(map[string]any{ "consent_challenge": "challenge-rp-user-claims", - "grant_scope": []string{"openid", "profile", "tenant"}, + "grant_scope": []string{"openid", "profile", "tenants"}, }) req := httptest.NewRequest(http.MethodPost, "/api/v1/auth/consent/accept", bytes.NewReader(reqBody)) req.Header.Set("Content-Type", "application/json") diff --git a/backend/internal/handler/client_tenant_access.go b/backend/internal/handler/client_tenant_access.go index a5f9bbe3..7d2fb95c 100644 --- a/backend/internal/handler/client_tenant_access.go +++ b/backend/internal/handler/client_tenant_access.go @@ -463,7 +463,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { out := make([]string, 0, len(combined)) appendIfPresent := func(scope string) { - scope = strings.TrimSpace(scope) + scope = canonicalConsentScopeName(scope) if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { return } @@ -471,7 +471,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { return } for _, candidate := range combined { - if strings.TrimSpace(candidate) != scope { + if canonicalConsentScopeName(candidate) != scope { continue } seen[scope] = struct{}{} @@ -481,10 +481,10 @@ func normalizeScopesInConsentOrder(scopes []string) []string { } appendIfPresent("openid") - appendIfPresent("tenant") + appendIfPresent("tenants") for _, scope := range combined { - scope = strings.TrimSpace(scope) + scope = canonicalConsentScopeName(scope) if scope == "" || isLegacyRefreshTokenScopeAlias(scope) { continue } @@ -501,7 +501,7 @@ func normalizeScopesInConsentOrder(scopes []string) []string { func requiredClientScopes(client domain.HydraClient) []string { required := make([]string, 0, 4) if clientTenantAccessRestricted(client.Metadata) { - required = append(required, "tenant") + required = append(required, "tenants") } if client.Metadata == nil { @@ -535,3 +535,12 @@ func requiredClientScopes(client domain.HydraClient) []string { return normalizeScopesInConsentOrder(required) } + +func canonicalConsentScopeName(scope string) string { + switch strings.ToLower(strings.TrimSpace(scope)) { + case "tenant": + return "tenants" + default: + return strings.TrimSpace(scope) + } +} diff --git a/backend/internal/handler/client_tenant_access_test.go b/backend/internal/handler/client_tenant_access_test.go index 661cd631..af7ebf42 100644 --- a/backend/internal/handler/client_tenant_access_test.go +++ b/backend/internal/handler/client_tenant_access_test.go @@ -121,20 +121,20 @@ func TestCreateClient_RejectsTenantAccessWithoutAllowedTenants(t *testing.T) { assert.False(t, hydraCalled) } -func TestMergeRequestedScopesWithClientRequirements_AddsTenantScope(t *testing.T) { +func TestMergeRequestedScopesWithClientRequirements_AddsTenantsScope(t *testing.T) { client := domain.HydraClient{ Metadata: map[string]any{ "tenant_access_restricted": true, "structured_scopes": []map[string]any{ {"name": "openid", "mandatory": true}, - {"name": "tenant", "mandatory": true, "locked": true}, + {"name": "tenants", "mandatory": true, "locked": true}, {"name": "profile", "mandatory": false}, }, }, } merged := mergeRequestedScopesWithClientRequirements(client, []string{"openid", "profile"}) - assert.Equal(t, []string{"openid", "tenant", "profile"}, merged) + assert.Equal(t, []string{"openid", "tenants", "profile"}, merged) } func TestMergeRequestedScopesWithClientRequirements_StripsRefreshTokenScopeAliases(t *testing.T) { @@ -154,7 +154,7 @@ func TestMergeRequestedScopesWithClientRequirements_StripsRefreshTokenScopeAlias []string{"openid", "offline", "profile", "offline_access"}, ) - assert.Equal(t, []string{"openid", "tenant", "profile", "offline_access", "email"}, merged) + assert.Equal(t, []string{"openid", "tenants", "profile", "offline_access", "email"}, merged) } func TestBuildHydraAuthorizationURL_StripsRefreshTokenScopeAliases(t *testing.T) { diff --git a/backend/internal/handler/dev_handler_test.go b/backend/internal/handler/dev_handler_test.go index fd67469a..a769b41d 100644 --- a/backend/internal/handler/dev_handler_test.go +++ b/backend/internal/handler/dev_handler_test.go @@ -864,7 +864,7 @@ func TestUpdateClient_AuditDetailsIncludeGeneralSettingChanges(t *testing.T) { }, "grant_types": []string{"authorization_code", "refresh_token"}, "response_types": []string{"code"}, - "scope": "openid profile email tenant", + "scope": "openid profile email tenants", "token_endpoint_auth_method": "private_key_jwt", "metadata": map[string]any{ "status": "active", @@ -905,7 +905,7 @@ func TestUpdateClient_AuditDetailsIncludeGeneralSettingChanges(t *testing.T) { body, _ := json.Marshal(map[string]any{ "name": "App One Updated", - "scopes": []string{"openid", "profile", "email", "tenant"}, + "scopes": []string{"openid", "profile", "email", "tenants"}, "metadata": map[string]any{ "tenant_access_restricted": true, "allowed_tenants": []string{"tenant-1", "tenant-2"}, @@ -3009,7 +3009,7 @@ func TestUpdateClient_RevokesExistingConsentsWhenTenantPolicyChanges(t *testing. RedirectURIs: []string{"https://rp.example.com/callback"}, GrantTypes: []string{"authorization_code", "refresh_token"}, ResponseTypes: []string{"code"}, - Scope: "openid tenant profile email", + Scope: "openid tenants profile email", TokenEndpointAuthMethod: "none", Metadata: map[string]any{ "tenant_access_restricted": true, @@ -3093,7 +3093,7 @@ func TestUpdateClient_DoesNotRevokeConsentsWhenTenantPolicyUnchanged(t *testing. RedirectURIs: []string{"https://rp.example.com/callback"}, GrantTypes: []string{"authorization_code", "refresh_token"}, ResponseTypes: []string{"code"}, - Scope: "openid tenant profile email", + Scope: "openid tenants profile email", TokenEndpointAuthMethod: "none", Metadata: map[string]any{ "tenant_access_restricted": true, diff --git a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx index 2fa975a5..359c4d3a 100644 --- a/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.claims.test.tsx @@ -91,22 +91,22 @@ function makeClientDetail( if (includeTenantScope) { structuredScopes.push({ id: "2", - name: "tenant", + name: "tenants", description: "Tenant access", mandatory: tenantScopeMandatory, locked: tenantAccessRestricted, }); } - return { - client: { + return { + client: { id: "client-claims", name: "Claims App", type: "private", status: "active", redirectUris: ["https://rp.example.com/callback"], scopes: includeTenantScope - ? ["openid", "tenant", "profile"] + ? ["openid", "tenants", "profile"] : ["openid", "profile"], tokenEndpointAuthMethod: "client_secret_basic", metadata: { @@ -334,7 +334,7 @@ describe("ClientGeneralPage RP claims", () => { ); }); - it("preserves tenant scope mandatory state when tenant access restriction is off", async () => { + it("preserves tenants scope mandatory state when tenant access restriction is off", async () => { fetchClientMock.mockResolvedValue( makeClientDetail("old_claim", { includeTenantScope: true, @@ -354,8 +354,8 @@ describe("ClientGeneralPage RP claims", () => { const tenantScopeRow = Array.from(container.querySelectorAll("tr")).find( (row) => - Array.from(row.querySelectorAll("input")).some( - (input) => (input as HTMLInputElement).value === "tenant", + Array.from(row.querySelectorAll("input")).some( + (input) => (input as HTMLInputElement).value === "tenants", ), ); @@ -389,11 +389,11 @@ describe("ClientGeneralPage RP claims", () => { expect(updateClientMock).toHaveBeenCalledWith( "client-claims", expect.objectContaining({ - metadata: expect.objectContaining({ - tenant_access_restricted: false, - structured_scopes: expect.arrayContaining([ - expect.objectContaining({ - name: "tenant", + metadata: expect.objectContaining({ + tenant_access_restricted: false, + structured_scopes: expect.arrayContaining([ + expect.objectContaining({ + name: "tenants", mandatory: false, locked: false, }), diff --git a/devfront/src/features/clients/ClientGeneralPage.tsx b/devfront/src/features/clients/ClientGeneralPage.tsx index 3ab2cd64..e4de8e18 100644 --- a/devfront/src/features/clients/ClientGeneralPage.tsx +++ b/devfront/src/features/clients/ClientGeneralPage.tsx @@ -650,8 +650,8 @@ function ClientGeneralPage() { }, { id: "2", - name: "tenant", - description: t("msg.dev.clients.scopes.tenant", "소속 테넌트 정보 접근"), + name: "tenants", + description: t("msg.dev.clients.scopes.tenants", "소속 테넌트 정보 접근"), mandatory: false, }, { @@ -675,14 +675,14 @@ function ClientGeneralPage() { ); const tenantScopeDescription = t( - "msg.dev.clients.scopes.tenant", + "msg.dev.clients.scopes.tenants", "소속 테넌트 정보 접근", ); const buildTenantScope = useCallback( (id: string): ScopeItem => ({ id, - name: "tenant", + name: "tenants", description: tenantScopeDescription, mandatory: true, locked: true, @@ -693,12 +693,15 @@ function ClientGeneralPage() { const normalizeScopesForTenantAccess = useCallback( (nextScopes: ScopeItem[], restricted: boolean): ScopeItem[] => { const normalized = nextScopes.map((scope) => { - if (scope.name.trim() !== "tenant") { + const scopeName = scope.name.trim(); + if (scopeName !== "tenants" && scopeName !== "tenant") { return scope; } + const canonicalName = "tenants"; if (restricted) { return { ...scope, + name: canonicalName, description: scope.description || tenantScopeDescription, mandatory: true, locked: true, @@ -706,6 +709,7 @@ function ClientGeneralPage() { } return { ...scope, + name: canonicalName, description: scope.description || tenantScopeDescription, locked: false, }; @@ -713,20 +717,23 @@ function ClientGeneralPage() { if ( restricted && - !normalized.some((scope) => scope.name.trim() === "tenant") + !normalized.some( + (scope) => scope.name.trim() === "tenants" || scope.name.trim() === "tenant", + ) ) { - normalized.push(buildTenantScope(`tenant-${Date.now()}`)); + normalized.push(buildTenantScope(`tenants-${Date.now()}`)); } const openidScopes = normalized.filter( (scope) => scope.name.trim() === "openid", ); const tenantScopes = normalized.filter( - (scope) => scope.name.trim() === "tenant", + (scope) => + scope.name.trim() === "tenants" || scope.name.trim() === "tenant", ); const remainingScopes = normalized.filter((scope) => { const name = scope.name.trim(); - return name !== "openid" && name !== "tenant"; + return name !== "openid" && name !== "tenants" && name !== "tenant"; }); return [...openidScopes, ...tenantScopes, ...remainingScopes]; @@ -762,7 +769,7 @@ function ClientGeneralPage() { }, { id: "standard-tenant", - name: "tenant", + name: "tenants", description: tenantScopeDescription, source: "standard", }, @@ -2389,7 +2396,7 @@ function ClientGeneralPage() {

{t( "ui.dev.clients.general.tenant_access.hint", - "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.", + "제한을 켜면 tenants 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다.", )}

diff --git a/devfront/src/locales/en.toml b/devfront/src/locales/en.toml index 4911ef5c..751463a4 100644 --- a/devfront/src/locales/en.toml +++ b/devfront/src/locales/en.toml @@ -518,6 +518,8 @@ description = "Manage OIDC applications, authentication methods, redirect URIs, email = "Email" openid = "Openid" profile = "Profile" +tenant = "Tenant access" +tenants = "Tenants access" [msg.dev.dashboard] access_denied = "The dashboard is available only to users with developer access." @@ -1622,7 +1624,7 @@ description = "Scope Description" mandatory = "Mandatory" name = "Scope Name" delete = "Delete" -tenant = "Tenant" +tenants = "Tenants" [ui.dev.clients.general.tenant_access] title = "Tenant access restriction" @@ -1633,7 +1635,7 @@ search_placeholder = "Search by tenant name or slug" selected_title = "Allowed tenants" selected_empty = "No tenants selected yet." empty = "No tenants match your search." -hint = "Turning this on adds the tenant scope automatically and requires at least one allowed tenant." +hint = "Turning this on adds the tenants scope automatically and requires at least one allowed tenant." autocomplete_hint = "Type a tenant name to see autocomplete suggestions. Click one to add it to the allowed list." validation_required = "Select at least one allowed tenant when tenant access restriction is enabled." picker_title = "Select tenant" diff --git a/devfront/src/locales/ko.toml b/devfront/src/locales/ko.toml index 53e30423..72576b64 100644 --- a/devfront/src/locales/ko.toml +++ b/devfront/src/locales/ko.toml @@ -518,6 +518,8 @@ description = "OIDC 앱, 인증 방식, 리다이렉트 URI, 비밀키 재발행 email = "이메일 주소 접근" openid = "OIDC 인증 필수 스코프" profile = "기본 프로필 정보 접근" +tenant = "테넌트 접근" +tenants = "테넌트 접근" [msg.dev.dashboard] access_denied = "대시보드는 개발자 권한이 있어야 볼 수 있습니다." @@ -1621,7 +1623,7 @@ description = "설명" mandatory = "필수" name = "스코프 이름" delete = "삭제" -tenant = "테넌트" +tenants = "테넌트들" [ui.dev.clients.general.tenant_access] title = "테넌트 접근 제한" @@ -1632,7 +1634,7 @@ search_placeholder = "테넌트 이름 또는 슬러그로 검색" selected_title = "허용 테넌트" selected_empty = "아직 선택된 테넌트가 없습니다." empty = "검색 결과가 없습니다." -hint = "제한을 켜면 tenant 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다." +hint = "제한을 켜면 tenants 스코프가 자동으로 포함되며, 허용 테넌트를 하나 이상 선택해야 합니다." autocomplete_hint = "테넌트 이름을 입력하면 자동 완성 후보가 나타납니다. 클릭하면 허용 목록에 추가됩니다." validation_required = "테넌트 접근 제한을 사용할 경우 허용 테넌트를 하나 이상 선택해야 합니다." picker_title = "테넌트 선택" diff --git a/docs/rp-iam-integration-guide.md b/docs/rp-iam-integration-guide.md index 6c712e25..5574aaa9 100644 --- a/docs/rp-iam-integration-guide.md +++ b/docs/rp-iam-integration-guide.md @@ -241,6 +241,9 @@ Baron은 기본적으로 대표소속 tenant와 전체 소속 tenant 목록을 "emails": [ "hanmac-user@example.com" ], + "phones": [ + "+821012345678" + ], "names": { "name": "한맥 사용자" }