Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for claim-wise uniqueness validation #7106

Merged
11 changes: 11 additions & 0 deletions .changeset/gentle-months-refuse.md
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
Expand Up @@ -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 %}
Expand Down
7 changes: 7 additions & 0 deletions apps/console/src/extensions/i18n/models/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2737,6 +2737,13 @@ export interface Extensions {
};
claimUpdateNotification: {
error: NotificationItem;
success: NotificationItem;
};
claimUpdateConfirmation: {
header: string;
message: string;
content: string;
assertionHint: string;
};
};
pageTitle: string;
Expand Down
13 changes: 13 additions & 0 deletions apps/console/src/extensions/i18n/resources/en-US/extensions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3037,7 +3037,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",
Expand Down
1 change: 1 addition & 0 deletions apps/console/src/public/deployment.config.json
Original file line number Diff line number Diff line change
Expand Up @@ -1234,6 +1234,7 @@
"enabled": true
}
},
"isClaimUniquenessValidationEnabled": false,
"isClientSecretHashEnabled": false,
"isCookieConsentBannerEnabled": true,
"isCustomClaimMappingEnabled": true,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -88,6 +94,8 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
ClaimManagementConstants.MOBILE_CLAIM_URI
];
const [ isAlphanumericUsername, setIsAlphanumericUsername ] = useState<boolean>(false);
const [ showConfirmationModal, setShowConfirmationModal ] = useState<boolean>(false);
const [ pendingFormValues, setPendingFormValues ] = useState<AlternativeLoginIdentifierFormInterface>(null);

const {
data: validationData
Expand Down Expand Up @@ -319,7 +327,9 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
setIsLoading(true);
updateGovernanceConnector(data, categoryId, connectorId)
.then(() => {
updateClaims(checkedClaims);
if (checkedClaims.length > 0) {
updateClaims(checkedClaims);
}
handleUpdateSuccess();
loadConnectorDetails();
})
Expand All @@ -332,30 +342,28 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
});
};

// Define a function to update claim properties with `isUnique` property.
const updateClaimProperties =(claim: Claim, checkedClaims: string[]) => {
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;
Expand All @@ -365,6 +373,18 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde

return updateAClaim(claimId, claim)
.then(() => {
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) => {
Expand All @@ -380,10 +400,12 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
});
};

/**
* Updates the uniqueness scope of claims to ACROSS_USERSTORES.
*/
const updateClaims = (checkedClaims : string[]) => {
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);
Expand All @@ -392,25 +414,74 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
};

/**
* Handle form submit click.
* Gets the processed checked claims from form values.
*/
const handleSubmit = (values: AlternativeLoginIdentifierFormInterface) => {

const getProcessedCheckedClaims = (values: AlternativeLoginIdentifierFormInterface): string[] => {
const processedFormValues: AlternativeLoginIdentifierFormInterface = { ...values };
let checkedClaims: string[] = availableClaims
.filter((claim: Claim) =>
processedFormValues[claim?.displayName?.toLowerCase()] !== undefined
? processedFormValues[claim?.displayName?.toLowerCase()] : false)
.map((claim: Claim) => 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(() => {
Expand All @@ -434,7 +505,7 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
]);

/**
* Get username type
* Get username type.
*/
useEffect(() => {
if (validationData) {
Expand Down Expand Up @@ -576,6 +647,38 @@ const AlternativeLoginIdentifierInterface: FunctionComponent<AlternativeLoginIde
<ContentLoader />
)
}
<ConfirmationModal
data-componentid={ `${componentId}-confirmation-modal` }
onClose={ (): void => {
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 }
>
<ConfirmationModal.Header>
{ t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." +
"claimUpdateConfirmation.header") }
</ConfirmationModal.Header>
<ConfirmationModal.Message attached warning>
{ t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." +
"claimUpdateConfirmation.message") }
</ConfirmationModal.Message>
<ConfirmationModal.Content>
{ t("extensions:manage.accountLogin.alternativeLoginIdentifierPage." +
"claimUpdateConfirmation.content") }
</ConfirmationModal.Content>
</ConfirmationModal>
</>
);
};
Expand Down
Loading
Loading