Skip to content

Commit

Permalink
Merge pull request #7106 from AfraHussaindeen/attribute-uniqueness-fe…
Browse files Browse the repository at this point in the history
…ature

Add support for claim-wise uniqueness validation
  • Loading branch information
AfraHussaindeen authored Dec 12, 2024
2 parents 43c9351 + 32fec6a commit 59a7b7c
Show file tree
Hide file tree
Showing 17 changed files with 372 additions and 50 deletions.
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 @@ -2738,6 +2738,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 @@ -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",
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

0 comments on commit 59a7b7c

Please sign in to comment.