diff --git a/.changeset/gentle-months-refuse.md b/.changeset/gentle-months-refuse.md new file mode 100644 index 00000000000..98b7762f8f7 --- /dev/null +++ b/.changeset/gentle-months-refuse.md @@ -0,0 +1,11 @@ +--- +"@wso2is/admin.alternative-login-identifier.v1": patch +"@wso2is/admin.claims.v1": patch +"@wso2is/admin.core.v1": patch +"@wso2is/forms": patch +"@wso2is/console": patch +"@wso2is/core": patch +"@wso2is/i18n": patch +--- + +Add support for claim-wise uniqueness validation diff --git a/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 b/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 index 13b27acb6f5..eb3b282153e 100644 --- a/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 +++ b/apps/console/java/org.wso2.identity.apps.console.server.feature/resources/deployment.config.json.j2 @@ -1801,6 +1801,9 @@ {% if console.ui.is_marketing_consent_banner_enabled is defined %} "isMarketingConsentBannerEnabled": {{ console.ui.is_marketing_consent_banner_enabled }}, {% endif %} + {% if identity_mgt.user_claim_update.uniqueness.enable is defined %} + "isClaimUniquenessValidationEnabled": {{ identity_mgt.user_claim_update.uniqueness.enable }}, + {% endif %} {% if oauth.hash_tokens_and_secrets is defined %} "isClientSecretHashEnabled": {{ oauth.hash_tokens_and_secrets }}, {% endif %} diff --git a/apps/console/src/extensions/i18n/models/extensions.ts b/apps/console/src/extensions/i18n/models/extensions.ts index 7ac238d71d1..512267c6f94 100755 --- a/apps/console/src/extensions/i18n/models/extensions.ts +++ b/apps/console/src/extensions/i18n/models/extensions.ts @@ -2738,6 +2738,13 @@ export interface Extensions { }; claimUpdateNotification: { error: NotificationItem; + success: NotificationItem; + }; + claimUpdateConfirmation: { + header: string; + message: string; + content: string; + assertionHint: string; }; }; pageTitle: string; diff --git a/apps/console/src/extensions/i18n/resources/en-US/extensions.ts b/apps/console/src/extensions/i18n/resources/en-US/extensions.ts index 4423596725f..b8c1f03aace 100755 --- a/apps/console/src/extensions/i18n/resources/en-US/extensions.ts +++ b/apps/console/src/extensions/i18n/resources/en-US/extensions.ts @@ -3038,7 +3038,20 @@ export const extensions: Extensions = { error: { description: "Error updating the attribute as an unique attribute. Please try again.", message: "Error updating claim" + }, + success: { + description: "Successfully updated the {{claimName}} uniqueness validation scope.", + message: "Update successful" } + }, + claimUpdateConfirmation: { + header: "Before you proceed", + message: "Uniqueness validation for the selected attribute(s) will be applied across user stores.", + content: "This setting ensures values of selected attribute(s) remain unique across the " + + "organization for new users, which is required for alternate login identifiers to work. " + + "However, it does not guarantee uniqueness for attribute(s) of existing users that remain " + + "unchanged.", + assertionHint: "I understand and wish to proceed" } }, pageTitle: "Account Login", diff --git a/apps/console/src/public/deployment.config.json b/apps/console/src/public/deployment.config.json index acd2d775443..831f821ca3f 100644 --- a/apps/console/src/public/deployment.config.json +++ b/apps/console/src/public/deployment.config.json @@ -1234,6 +1234,7 @@ "enabled": true } }, + "isClaimUniquenessValidationEnabled": false, "isClientSecretHashEnabled": false, "isCookieConsentBannerEnabled": true, "isCustomClaimMappingEnabled": true, diff --git a/features/admin.alternative-login-identifier.v1/pages/alternative-login-identifier-page.tsx b/features/admin.alternative-login-identifier.v1/pages/alternative-login-identifier-page.tsx index b17558e19bc..a5d0ce3353a 100644 --- a/features/admin.alternative-login-identifier.v1/pages/alternative-login-identifier-page.tsx +++ b/features/admin.alternative-login-identifier.v1/pages/alternative-login-identifier-page.tsx @@ -37,10 +37,16 @@ import { import { getUsernameConfiguration } from "@wso2is/admin.users.v1/utils/user-management-utils"; import { useValidationConfigData } from "@wso2is/admin.validation.v1/api"; import { IdentityAppsError } from "@wso2is/core/errors"; -import { AlertLevels, Claim, ClaimsGetParams, IdentifiableComponentInterface, Property } from "@wso2is/core/models"; +import { + AlertLevels, + Claim, + ClaimsGetParams, + IdentifiableComponentInterface, + UniquenessScope +} from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { Field, Form } from "@wso2is/form"; -import { ContentLoader, EmphasizedSegment, Message, PageLayout } from "@wso2is/react-components"; +import { ConfirmationModal, ContentLoader, EmphasizedSegment, Message, PageLayout } from "@wso2is/react-components"; import { AxiosError } from "axios"; import isEmpty from "lodash-es/isEmpty"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; @@ -88,6 +94,8 @@ const AlternativeLoginIdentifierInterface: FunctionComponent(false); + const [ showConfirmationModal, setShowConfirmationModal ] = useState(false); + const [ pendingFormValues, setPendingFormValues ] = useState(null); const { data: validationData @@ -319,7 +327,9 @@ const AlternativeLoginIdentifierInterface: FunctionComponent { - updateClaims(checkedClaims); + if (checkedClaims.length > 0) { + updateClaims(checkedClaims); + } handleUpdateSuccess(); loadConnectorDetails(); }) @@ -332,30 +342,28 @@ const AlternativeLoginIdentifierInterface: FunctionComponent { - let isClaimUpdate: boolean = false; - let updatedClaimProperties: Property[] = [ ...claim.properties ]; - const isUniqueIndex: number = claim?.properties?.findIndex((property: Property) => property.key === "isUnique"); - - if (checkedClaims?.includes(claim.claimURI)) { - if (isUniqueIndex !== -1 && claim.properties[isUniqueIndex].value === "false") { - isClaimUpdate = true; - updatedClaimProperties[isUniqueIndex].value = "true"; - } else if (isUniqueIndex === -1) { - isClaimUpdate = true; - updatedClaimProperties.push({ key: "isUnique", value: "true" }); - } - } else if (isUniqueIndex !== -1) { - isClaimUpdate = true; - updatedClaimProperties = updatedClaimProperties.filter((property: Property) => - property.key !== "isUnique"); - } - - return { isClaimUpdate, updatedClaimProperties }; + /** + * Updates the uniqueness scope of a claim if it's selected and needs to be updated to ACROSS_USERSTORES. + * Does not modify claims that are not selected. + */ + const updateClaimUniquenessScope = (claim: Claim, checkedClaims: string[]) => { + const isSelected: boolean = checkedClaims?.includes(claim.claimURI); + + const shouldUpdateClaim: boolean = isSelected && + (claim.uniquenessScope !== UniquenessScope.ACROSS_USERSTORES); + + return { + isClaimUpdate: shouldUpdateClaim, + updatedClaim: shouldUpdateClaim + ? { + ...claim, + uniquenessScope: UniquenessScope.ACROSS_USERSTORES + } + : claim + }; }; - // Define a function to update and dispatch alerts + // Define a function to update and dispatch alerts. const updateClaimAndAlert = (claim: Claim) => { const claimId: string = claim?.id; @@ -365,6 +373,18 @@ const AlternativeLoginIdentifierInterface: FunctionComponent { + dispatch(addAlert({ + description: t( + "extensions:manage.accountLogin.alternativeLoginIdentifierPage." + + "claimUpdateNotification.success.description", + { claimName: claim.displayName } + ), + level: AlertLevels.SUCCESS, + message: t( + "extensions:manage.accountLogin.alternativeLoginIdentifierPage." + + "claimUpdateNotification.success.message" + ) + })); getClaims(); }) .catch((error: IdentityAppsError) => { @@ -380,10 +400,12 @@ const AlternativeLoginIdentifierInterface: FunctionComponent { for (const claim of availableClaims) { - const { isClaimUpdate, updatedClaimProperties } = updateClaimProperties(claim, checkedClaims); - const updatedClaim: Claim = { ...claim, properties: updatedClaimProperties }; + const { isClaimUpdate, updatedClaim } = updateClaimUniquenessScope(claim, checkedClaims); if (isClaimUpdate) { updateClaimAndAlert(updatedClaim); @@ -392,10 +414,9 @@ const AlternativeLoginIdentifierInterface: FunctionComponent { - + const getProcessedCheckedClaims = (values: AlternativeLoginIdentifierFormInterface): string[] => { const processedFormValues: AlternativeLoginIdentifierFormInterface = { ...values }; let checkedClaims: string[] = availableClaims .filter((claim: Claim) => @@ -403,14 +424,64 @@ const AlternativeLoginIdentifierInterface: FunctionComponent claim?.claimURI); - // Remove the email attribute from the allowed attributes list when email username type is enabled + // Remove the email attribute from the allowed attributes list when email username type is enabled. if (!isAlphanumericUsername) { checkedClaims = checkedClaims.filter((item: string) => item !== ClaimManagementConstants.EMAIL_CLAIM_URI); } + + return checkedClaims; + }; + + /** + * Checks if any claims need uniqueness scope update. + */ + const shouldUpdateUniquenessScope = (checkedClaims: string[]): boolean => { + return checkedClaims.some((claimURI: string) => { + const claim: Claim = availableClaims.find((c: Claim) => c.claimURI === claimURI); + + return claim.uniquenessScope !== UniquenessScope.ACROSS_USERSTORES; + }); + }; + + /** + * Processes form submission and updates connector and claims. + */ + const processFormSubmission = ( + formValues: AlternativeLoginIdentifierFormInterface, + hasUserConsent: boolean = false + ): void => { + const checkedClaims: string[] = getProcessedCheckedClaims(formValues); const updatedConnectorData: any = getUpdatedConfigurations(checkedClaims); + const requiresUniquenessScopeUpdate: boolean = shouldUpdateUniquenessScope(checkedClaims); + + // Show confirmation modal if uniqueness scope update is required and no consent received yet. + if (!hasUserConsent && requiresUniquenessScopeUpdate) { + setPendingFormValues(formValues); + setShowConfirmationModal(true); + + return; + } updateConnector(updatedConnectorData, checkedClaims); + }; + + /** + * Handles the initial form submission. + */ + const handleSubmit = (values: AlternativeLoginIdentifierFormInterface): void => { + processFormSubmission(values, false); + }; + + /** + * Handles the form submission after user consents to uniqueness scope update. + */ + const handleConsentedSubmit = (): void => { + if (!pendingFormValues) { + return; + } + processFormSubmission(pendingFormValues, true); + setShowConfirmationModal(false); }; useEffect(() => { @@ -434,7 +505,7 @@ const AlternativeLoginIdentifierInterface: FunctionComponent { if (validationData) { @@ -576,6 +647,38 @@ const AlternativeLoginIdentifierInterface: FunctionComponent ) } + { + setShowConfirmationModal(false); + } } + type="warning" + open={ showConfirmationModal } + assertion={ t("common:confirm") } + assertionHint={ t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." + + "claimUpdateConfirmation.assertionHint") } + assertionType="checkbox" + primaryAction={ t("common:confirm") } + secondaryAction={ t("common:cancel") } + onSecondaryActionClick={ (): void => { + setShowConfirmationModal(false); + } } + onPrimaryActionClick={ handleConsentedSubmit } + closeOnDimmerClick={ false } + > + + { t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." + + "claimUpdateConfirmation.header") } + + + { t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." + + "claimUpdateConfirmation.message") } + + + { t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." + + "claimUpdateConfirmation.content") } + + ); }; diff --git a/features/admin.claims.v1/components/edit/local-claim/edit-additional-properties-local-claims.tsx b/features/admin.claims.v1/components/edit/local-claim/edit-additional-properties-local-claims.tsx index 1af01382841..064b1b13a1f 100644 --- a/features/admin.claims.v1/components/edit/local-claim/edit-additional-properties-local-claims.tsx +++ b/features/admin.claims.v1/components/edit/local-claim/edit-additional-properties-local-claims.tsx @@ -1,5 +1,5 @@ /** - * Copyright (c) 2020, WSO2 LLC. (https://www.wso2.com). + * Copyright (c) 2020-2024, WSO2 LLC. (https://www.wso2.com). * * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except @@ -17,19 +17,25 @@ */ import { Show, useRequiredScopes } from "@wso2is/access-control"; -import { AppState, FeatureConfigInterface } from "@wso2is/admin.core.v1"; +import { AppConstants, AppState, FeatureConfigInterface, history } from "@wso2is/admin.core.v1"; +import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; import { IdentityAppsError } from "@wso2is/core/errors"; import { AlertLevels, Claim, Property, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; import { DynamicField , KeyValue, useTrigger } from "@wso2is/forms"; -import { EmphasizedSegment, PrimaryButton } from "@wso2is/react-components"; +import { + EmphasizedSegment, + Link, + PrimaryButton, + TAB_URL_HASH_FRAGMENT +} from "@wso2is/react-components"; import React, { FunctionComponent, ReactElement, useMemo, useState } from "react"; -import { useTranslation } from "react-i18next"; +import { Trans, useTranslation } from "react-i18next"; import { useDispatch, useSelector } from "react-redux"; import { Dispatch } from "redux"; import { Grid } from "semantic-ui-react"; import { updateAClaim } from "../../../api"; -import { ClaimManagementConstants } from "../../../constants"; +import { ClaimManagementConstants, ClaimTabIDs } from "../../../constants"; /** * Prop types for `EditAdditionalPropertiesLocalClaims` component @@ -71,6 +77,8 @@ export const EditAdditionalPropertiesLocalClaims: featureConfig?.attributeDialects?.scopes?.update ); + const { UIConfig } = useUIConfig(); + const isReadOnly: boolean = !hasAttributeUpdatePermissions; const filteredClaimProperties: Property[] = useMemo(() => { @@ -106,6 +114,46 @@ export const EditAdditionalPropertiesLocalClaims: "valueRequiredErrorMessage" ) } requiredField={ true } + keyValidation={ (key: string) => { + if (ClaimManagementConstants.RESTRICTED_PROPERTY_KEYS.includes(key)) { + return false; + } + + return true; + } } + keyValidationWarningMessage={ + UIConfig?.isClaimUniquenessValidationEnabled ? ( + + The 'isUnique' property is deprecated. Please use + { + history.push({ + hash: `#${TAB_URL_HASH_FRAGMENT}${ClaimTabIDs.GENERAL}`, + pathname: AppConstants.getPaths() + .get("LOCAL_CLAIMS_EDIT") + .replace(":id", claim.id) + }); + } } + > Uniqueness Validation + option to configure attribute uniqueness. + + ) : ( + + The 'isUnique' property is deprecated. + + ) + } update={ (data: KeyValue[]) => { const claimData: Claim = { ...claim }; diff --git a/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx b/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx index fbd3332d500..abe07009b41 100644 --- a/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx +++ b/features/admin.claims.v1/components/edit/local-claim/edit-basic-details-local-claims.tsx @@ -18,6 +18,7 @@ import { Show, useRequiredScopes } from "@wso2is/access-control"; import { AppConstants, AppState, FeatureConfigInterface, history } from "@wso2is/admin.core.v1"; +import useUIConfig from "@wso2is/admin.core.v1/hooks/use-ui-configs"; import { attributeConfig } from "@wso2is/admin.extensions.v1"; import { SCIMConfigs } from "@wso2is/admin.extensions.v1/configs/scim"; import { @@ -37,7 +38,8 @@ import { Claim, ExternalClaim, ProfileSchemaInterface, - TestableComponentInterface + TestableComponentInterface, + UniquenessScope } from "@wso2is/core/models"; import { Property } from "@wso2is/core/src/models"; import { addAlert, setProfileSchemaRequestLoadingStatus, setSCIMSchemas } from "@wso2is/core/store"; @@ -137,6 +139,8 @@ export const EditBasicDetailsLocalClaims: FunctionComponent ) } + { + claim && UIConfig?.isClaimUniquenessValidationEnabled + && !hideSpecialClaims + && !READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) && ( + + ) + } { !READONLY_CLAIM_CONFIGS.includes(claim?.claimURI) && mappingChecked ? ( !hideSpecialClaims && diff --git a/features/admin.claims.v1/constants/claim-management-constants.ts b/features/admin.claims.v1/constants/claim-management-constants.ts index 7143357b714..1d672d2084c 100644 --- a/features/admin.claims.v1/constants/claim-management-constants.ts +++ b/features/admin.claims.v1/constants/claim-management-constants.ts @@ -208,6 +208,16 @@ export class ClaimManagementConstants { public static readonly SYSTEM_CLAIM_PROPERTY_NAME: string = "isSystemClaim"; + /** + * Claim property name for uniqueness validation scope. + */ + public static readonly UNIQUENESS_SCOPE_PROPERTY_NAME: string = "uniquenessScope"; + + /** + * List of restricted property keys that cannot be used in claim properties. + */ + public static readonly RESTRICTED_PROPERTY_KEYS: string[] = [ "isUnique" ]; + /** * The error code that is returned when there is no item in the list */ @@ -219,3 +229,14 @@ export class ClaimManagementConstants { public static readonly REGEX_FIELD_MAX_LENGTH: number = 255; public static readonly REGEX_FIELD_MIN_LENGTH: number = 3; } + +/** + * Unique identifiers for claim edit tabs. + * + * @readonly + */ +export enum ClaimTabIDs { + GENERAL = "general", + ATTRIBUTE_MAPPINGS = "attribute-mappings", + ADDITIONAL_PROPERTIES = "additional-properties" +} diff --git a/features/admin.claims.v1/pages/local-claims-edit.tsx b/features/admin.claims.v1/pages/local-claims-edit.tsx index 2eb7ffdac65..e7e84cd65dc 100644 --- a/features/admin.claims.v1/pages/local-claims-edit.tsx +++ b/features/admin.claims.v1/pages/local-claims-edit.tsx @@ -23,7 +23,7 @@ import { PRIMARY_USERSTORE } from "@wso2is/admin.userstores.v1/constants"; import { UserStoreBasicData } from "@wso2is/admin.userstores.v1/models"; import { AlertLevels, Claim, TestableComponentInterface } from "@wso2is/core/models"; import { addAlert } from "@wso2is/core/store"; -import { AnimatedAvatar, ResourceTab, TabPageLayout } from "@wso2is/react-components"; +import { AnimatedAvatar, ResourceTab, ResourceTabPaneInterface, TabPageLayout } from "@wso2is/react-components"; import { AxiosResponse } from "axios"; import React, { FunctionComponent, ReactElement, useEffect, useState } from "react"; import { useTranslation } from "react-i18next"; @@ -37,6 +37,7 @@ import { EditBasicDetailsLocalClaims, EditMappedAttributesLocalClaims } from "../components"; +import { ClaimTabIDs } from "../constants/claim-management-constants"; /** * Props for the Local Claims edit page. @@ -72,6 +73,7 @@ const LocalClaimsEditPage: FunctionComponent = ( const [ claim, setClaim ] = useState(null); const [ isLocalClaimDetailsRequestLoading, setIsLocalClaimDetailsRequestLoading ] = useState(false); const [ userStores, setUserStores ] = useState([]); + const defaultActiveIndex: string = ClaimTabIDs.GENERAL; const dispatch: Dispatch = useDispatch(); @@ -136,11 +138,10 @@ const LocalClaimsEditPage: FunctionComponent = ( /** * Contains the data of the panes. */ - const panes: { - menuItem: string; - render: () => JSX.Element; - }[] = [ + const panes: ResourceTabPaneInterface[] = [ { + componentId: "general", + "data-tabid": ClaimTabIDs.GENERAL, menuItem: t("claims:local.pageLayout.edit.tabs.general"), render: () => ( @@ -153,6 +154,8 @@ const LocalClaimsEditPage: FunctionComponent = ( ) }, { + componentId: "attribute-mappings", + "data-tabid": ClaimTabIDs.ATTRIBUTE_MAPPINGS, menuItem: t("claims:local.pageLayout.edit.tabs.mappedAttributes"), render: () => ( @@ -166,6 +169,8 @@ const LocalClaimsEditPage: FunctionComponent = ( ) }, { + componentId: "additional-properties", + "data-tabid": ClaimTabIDs.ADDITIONAL_PROPERTIES, menuItem: t("claims:local.pageLayout.edit.tabs.additionalProperties"), render: () => ( @@ -182,11 +187,10 @@ const LocalClaimsEditPage: FunctionComponent = ( /** * Contains the data of the basic panes. */ - const basicPanes: { - menuItem: string; - render: () => JSX.Element; - }[] = [ + const basicPanes: ResourceTabPaneInterface[] = [ { + componentId: "general", + "data-tabid": ClaimTabIDs.GENERAL, menuItem: t("claims:local.pageLayout.edit.tabs.general"), render: () => ( @@ -199,6 +203,8 @@ const LocalClaimsEditPage: FunctionComponent = ( ) }, { + componentId: "attribute-mappings", + "data-tabid": ClaimTabIDs.ATTRIBUTE_MAPPINGS, menuItem: t("claims:local.pageLayout.edit.tabs.mappedAttributes"), render: () => ( @@ -260,12 +266,16 @@ const LocalClaimsEditPage: FunctionComponent = ( ) : userStores?.length >= 1 ? ( ) : ( boolean; + /** + * Warning message to show when key validation fails + * Can be string or JSX content + */ + keyValidationWarningMessage?: string | ReactElement; } /** @@ -128,6 +137,8 @@ export const DynamicField: FunctionComponent = ( requiredField, duplicateKeyErrorMsg, readOnly, + keyValidation, + keyValidationWarningMessage, [ "data-testid" ]: testId } = props; @@ -138,6 +149,7 @@ export const DynamicField: FunctionComponent = ( const [ updateMapIndex, setUpdateMapIndex ] = useState(null); const [ showAddErrorMsg, setAddShowErrorMsg ] = useState(false); const [ showEditErrorMsg, setShowEditErrorMsg ] = useState(false); + const [ showKeyValidationWarningMsg, setShowKeyValidationWarningMsg ] = useState(false); const initRender = useRef(true); @@ -203,9 +215,22 @@ export const DynamicField: FunctionComponent = ( ) } + { + showKeyValidationWarningMsg && ( + + { keyValidationWarningMessage } + + ) + } ) => { + if (keyValidation && !keyValidation(values.get("key").toString())) { + setShowKeyValidationWarningMsg(true); + + return; + } + setShowKeyValidationWarningMsg(false); if (!showAddErrorMsg) { const tempFields: Map = new Map(fields); const newIndex: number = tempFields.size > 0 diff --git a/modules/i18n/src/models/namespaces/claims-ns.ts b/modules/i18n/src/models/namespaces/claims-ns.ts index af4f68138f1..617a1f5891c 100644 --- a/modules/i18n/src/models/namespaces/claims-ns.ts +++ b/modules/i18n/src/models/namespaces/claims-ns.ts @@ -439,6 +439,10 @@ export interface ClaimsNS { }; additionalProperties: { hint: string; + isUniqueDeprecationMessage: { + uniquenessDisabled: string; + uniquenessEnabled: string; + }; key: string; value: string; keyRequiredErrorMessage: string; @@ -466,6 +470,15 @@ export interface ClaimsNS { invalidName: string; }; }; + uniquenessScope: { + label: string; + options: { + acrossUserstores: string; + none: string; + withinUserstore: string; + }; + }; + uniquenessScopeHint: string; nameHint: string; description: { label: string; diff --git a/modules/i18n/src/translations/en-US/portals/claims.ts b/modules/i18n/src/translations/en-US/portals/claims.ts index f89848b16cd..7709da66b1f 100644 --- a/modules/i18n/src/translations/en-US/portals/claims.ts +++ b/modules/i18n/src/translations/en-US/portals/claims.ts @@ -394,6 +394,11 @@ export const claims: ClaimsNS = { local: { additionalProperties: { hint: "Use when writing an extension using current attributes", + isUniqueDeprecationMessage: { + uniquenessDisabled: "The 'isUnique' property is deprecated.", + uniquenessEnabled: "The 'isUnique' property is deprecated. Please use " + + "<1>Uniqueness Validation option to configure attribute uniqueness." + }, key: "Name", keyRequiredErrorMessage: "Enter a name", value: "Value", @@ -494,7 +499,16 @@ export const claims: ClaimsNS = { "you need to disable account verification for your organization.", supportedByDefault: { label: "Display this attribute on the user's profile" - } + }, + uniquenessScope: { + label: "Uniqueness Validation", + options: { + acrossUserstores: "Across User Stores", + none: "None", + withinUserstore: "Within User Store" + } + }, + uniquenessScopeHint: "Select the scope to validate the uniqueness of the attribute value." }, mappedAttributes: { enableForUserStore: "Enable for this user store",