From a6f63250b0952373d0b28a2db57a5cbc7bf3bf56 Mon Sep 17 00:00:00 2001 From: lashinie Date: Fri, 19 Apr 2024 14:49:32 +0530 Subject: [PATCH 01/54] support multiple mobile numbers per user --- .../recovery/IdentityRecoveryConstants.java | 21 +- .../MobileNumberVerificationHandler.java | 251 +++++++++++++----- .../signup/UserSelfRegistrationManager.java | 53 +++- 3 files changed, 246 insertions(+), 79 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index b29bc5bee5..c0c9bf07c4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -121,6 +121,8 @@ public class IdentityRecoveryConstants { public static final String USER_ROLES_CLAIM = "http://wso2.org/claims/roles"; public static final String EMAIL_ADDRESS_CLAIM = "http://wso2.org/claims/emailaddress"; public static final String MOBILE_NUMBER_CLAIM = "http://wso2.org/claims/mobile"; + public static final String MOBILE_NUMBERS_CLAIM = "http://wso2.org/claims/mobileNumbers"; + public static final String VERIFIED_MOBILE_NUMBERS_CLAIM = "http://wso2.org/claims/verifiedMobileNumbers"; public static final String DEFAULT_CHALLENGE_QUESTION_SEPARATOR = "!"; public static final String ACCOUNT_STATE_CLAIM_URI = "http://wso2.org/claims/identity/accountState"; public static final String PENDING_SELF_REGISTRATION = "PENDING_SR"; @@ -129,6 +131,12 @@ public class IdentityRecoveryConstants { public static final String PENDING_EMAIL_VERIFICATION = "PENDING_EV"; public static final String ACCOUNT_STATE_UNLOCKED = "UNLOCKED"; + /* + This constant will be used to identify that user is verifying a mobile number which needs to be added to + http://wso2.org/claims/verifiedMobileNumbers claim and not to http://wso2.org/claims/mobile. + */ + public static final String MOBILE_NUMBER_VERIFICATION_ONLY = "verificationOnly"; + public static final String PASSWORD_RESET_FAIL_ATTEMPTS_CLAIM = "http://wso2" + ".org/claims/identity/failedPasswordRecoveryAttempts"; public static final String SIGN_UP_ROLE_SEPARATOR = ","; @@ -434,6 +442,12 @@ public enum ErrorMessages { // UEV - User Email Verification. ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND("UEV-10001", "Email address not found for email verification"), + // UMV - User Mobile Verification. + ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED("UMV-10001", " Verified mobile numbers claim cannot be" + + " updated as mobile number verification on update is disabled."), + ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS("UMV-10002", "Unable to verify " + + "multiple mobile numbers simultaneously."), + INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request") , // Idle User Account Identification related Error messages. @@ -958,6 +972,11 @@ public enum SkipMobileNumberVerificationOnUpdateStates { /* State maintained to skip triggering an SMS OTP verification, when the mobile number was updated by user during the SMS OTP flow at the first login where the mobile number is not previously set. At the moment mobile number was already verified during the SMS OTP verification. So no need to verify it again. */ - SKIP_ON_SMS_OTP_FLOW + SKIP_ON_SMS_OTP_FLOW, + + /* State maintained to skip triggering an SMS OTP verification, when the mobile number to be updated is included + in the verifiedMobileNumbers claim, which has been already verified. + */ + SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 4e3b7e7790..7230cf22ef 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -22,11 +22,11 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.base.IdentityRuntimeException; import org.wso2.carbon.identity.core.bean.context.MessageContext; import org.wso2.carbon.identity.core.handler.InitConfig; +import org.wso2.carbon.identity.event.IdentityEventClientException; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.IdentityEventException; import org.wso2.carbon.identity.event.event.Event; @@ -50,8 +50,12 @@ import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; +import java.util.ArrayList; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; +import java.util.LinkedList; +import java.util.List; /** * This event handler is used to send a verification SMS when a claim update event to update the mobile number @@ -95,11 +99,15 @@ public void handleEvent(Event event) throws IdentityEventException { } /* We need to empty 'MOBILE_NUMBER_PENDING_VALUE_CLAIM' because having a value in that claim implies a verification is pending. But verification is not enabled anymore. */ - if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)) { + if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) || + claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { invalidatePendingMobileVerification(user, userStoreManager, claims); } - claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); - return; + if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getCode(), IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getMessage()); + } } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { @@ -132,7 +140,7 @@ public int getPriority(MessageContext messageContext) { /** * Store verification details in the recovery data store and initiate notification. * - * @param user User. + * @param user User. * @param verificationPendingMobileNumber Updated mobile number that is pending verification. * @throws IdentityEventException */ @@ -163,9 +171,9 @@ private void initNotificationForMobileNumberVerificationOnUpdate(User user, Stri /** * Trigger the SMS notification. * - * @param user User. - * @param code SMS OTP. - * @param props Other properties. + * @param user User. + * @param code SMS OTP. + * @param props Other properties. * @param verificationPendingMobileNumber Mobile number to which the SMS should be sent. * @throws IdentityRecoveryException */ @@ -210,9 +218,9 @@ private void triggerNotification(User user, String code, Property[] props, Strin /** * Form User object from username, tenant domain, and user store domain. * - * @param userName UserName. - * @param tenantDomain Tenant Domain. - * @param userStoreDomain User Domain. + * @param userName UserName. + * @param tenantDomain Tenant Domain. + * @param userStoreDomain User Domain. * @return User. */ private User getUser(String userName, String tenantDomain, String userStoreDomain) { @@ -228,27 +236,31 @@ private User getUser(String userName, String tenantDomain, String userStoreDomai * If the mobile claim is updated, set it to the 'MOBILE_NUMBER_PENDING_VALUE_CLAIM' claim. * Set thread local state to skip sending verification notification in inapplicable claim update scenarios. * - * @param claims Map of claims to be updated. - * @param userStoreManager User store manager. - * @param user User. + * @param claims Map of claims to be updated. + * @param userStoreManager User store manager. + * @param user User. * @throws IdentityEventException */ private void preSetUserClaimOnMobileNumberUpdate(Map claims, UserStoreManager userStoreManager, User user) throws IdentityEventException { - if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals - (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { + if (MapUtils.isEmpty(claims)) { // Not required to handle in this handler. + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); return; } - /* - Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the - OTP. Therefore, the mobile number is already verified & no need to verify it again. - */ - if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString().equals + if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM) && + claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)) { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getCode(), IdentityRecoveryConstants. + ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getMessage()); + } + + if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { - invalidatePendingMobileVerification(user, userStoreManager, claims); + // Not required to handle in this handler. return; } @@ -256,64 +268,159 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); } - if (MapUtils.isEmpty(claims)) { - // Not required to handle in this handler. - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; - } + /* + Handle mobileNumbers and verifyMobileNumbers claims. + */ + List exisitingVerifiedNumbersList = getExistingClaimList(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListFromString(claims.get(IdentityRecoveryConstants. + VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; + + List exisitingAllNumbersList = getExistingClaimList(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? + getListFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : exisitingAllNumbersList; - String mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + /* + Finds the verification pending mobile number and remove it from the verified numbers list in the payload. + */ + String mobileNumber = null; + if (updatedVerifiedNumbersList != null) { + mobileNumber = getVerificationPendingMobileNumber(exisitingVerifiedNumbersList, + updatedVerifiedNumbersList); + updatedVerifiedNumbersList.remove(mobileNumber); + } - if (StringUtils.isNotBlank(mobileNumber) && - isVerificationPendingMobileClaimConfigAvailable(user.getTenantDomain())) { - String existingMobileNumber; - String username = user.getUserName(); - try { - existingMobileNumber = userStoreManager.getUserClaimValue(username, IdentityRecoveryConstants. - MOBILE_NUMBER_CLAIM, null); - } catch (UserStoreException e) { - String error = String.format("Error occurred while retrieving existing mobile number for user: %s in " + - "domain: %s and user store: %s", username, user.getTenantDomain(), user.getUserStoreDomain()); - throw new IdentityEventException(error, e); + /* + Finds the removed numbers from the existing mobile numbers list and remove them from the verified numbers list. + As verified numbers list should not contain numbers that are not in the mobile numbers list. + */ + if (!updatedAllNumbersList.isEmpty()) { + for (String number : exisitingAllNumbersList) { + if (!updatedAllNumbersList.contains(number) && updatedVerifiedNumbersList != null) { + updatedVerifiedNumbersList.remove(number); + } } + } - if (StringUtils.equals(mobileNumber, existingMobileNumber)) { - if (log.isDebugEnabled()) { - log.debug(String.format("The mobile number to be updated: %s is same as the existing mobile " + - "number for user: %s in domain: %s and user store: %s. Hence an SMS OTP verification " + - "will not be triggered.", mobileNumber, username, user.getTenantDomain(), - user.getUserStoreDomain())); - } - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); + if (mobileNumber == null) { + mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + + /* + Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the + OTP. Therefore, the mobile number is already verified & no need to verify it again. + */ + if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString(). + equals(Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate()) && mobileNumber != null) { invalidatePendingMobileVerification(user, userStoreManager, claims); return; } /* - When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyMobile' - temporary claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to - check for 'verifyMobile' claim. - */ - if (Utils.isUseVerifyClaimEnabled() && !isVerifyMobileClaimAvailable(claims)) { + Mobile numbers in verifiedMobileNumbers claim are already verified. No need to verify again. + */ + if (exisitingVerifiedNumbersList.contains(mobileNumber)) { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString()); invalidatePendingMobileVerification(user, userStoreManager, claims); return; } + + if (StringUtils.isNotBlank(mobileNumber)) { + String existingMobileNumber; + String username = user.getUserName(); + try { + existingMobileNumber = userStoreManager.getUserClaimValue(username, IdentityRecoveryConstants. + MOBILE_NUMBER_CLAIM, null); + } catch (UserStoreException e) { + String error = String.format("Error occurred while retrieving existing mobile number for user: %s in " + + "domain: %s and user store: %s", username, user.getTenantDomain(), user.getUserStoreDomain()); + throw new IdentityEventException(error, e); + } + + if (StringUtils.equals(mobileNumber, existingMobileNumber)) { + if (log.isDebugEnabled()) { + log.debug(String.format("The mobile number to be updated: %s is same as the existing mobile " + + "number for user: %s in domain: %s and user store: %s. Hence an SMS OTP " + + "verification will not be triggered.", mobileNumber, username, + user.getTenantDomain(), user.getUserStoreDomain())); + } + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); + invalidatePendingMobileVerification(user, userStoreManager, claims); + return; + } + /* + When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyMobile' + temporary claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to + check for 'verifyMobile' claim. + */ + if (Utils.isUseVerifyClaimEnabled() && !isVerifyMobileClaimAvailable(claims)) { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + invalidatePendingMobileVerification(user, userStoreManager, claims); + return; + } + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + } else { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + } + } + + if (isVerificationPendingMobileClaimConfigAvailable(user.getTenantDomain())) { claims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, mobileNumber); - claims.remove(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); - } else { - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); } + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, String.join(",", updatedAllNumbersList)); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(",", + updatedVerifiedNumbersList)); + + } + + private List getListFromString(String str) { + + return str != null ? new LinkedList<>(Arrays.asList(str.split(","))) : new ArrayList<>(); + } + + private List getExistingClaimList(UserStoreManager userStoreManager, User user, String claimURI) throws + IdentityEventException { + + List existingClaimList; + try { + existingClaimList = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null) != null ? + new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, + null).split(","))) : new ArrayList<>(); + } catch (UserStoreException e) { + throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + + " for user: " + user.toFullQualifiedUsername(), e); + } + return existingClaimList; + } + + private String getVerificationPendingMobileNumber(List existingVerifiedNumbersList, + List updatedVerifiedNumbersList) throws + IdentityEventException { + + String mobileNumber = null; + for (String verificationPendingNumber : updatedVerifiedNumbersList) { + if (!existingVerifiedNumbersList.contains(verificationPendingNumber)) { + if (mobileNumber == null) { + mobileNumber = verificationPendingNumber; + } else { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getCode(), IdentityRecoveryConstants. + ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getMessage()); + } + } + } + return mobileNumber; } /** * Initiate notification sending process if the thread local is not set to skip verification process. * - * @param user User. - * @param userStoreManager User store manager. + * @param user User. + * @param userStoreManager User store manager. * @throws IdentityEventException */ private void postSetUserClaimOnMobileNumberUpdate(User user, UserStoreManager userStoreManager) throws @@ -325,11 +432,13 @@ private void postSetUserClaimOnMobileNumberUpdate(User user, UserStoreManager us if (!IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (skipMobileNumVerificationOnUpdateState) && !IdentityRecoveryConstants. SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString().equals - (skipMobileNumVerificationOnUpdateState) && !IdentityRecoveryConstants + (skipMobileNumVerificationOnUpdateState) && !IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString().equals (skipMobileNumVerificationOnUpdateState) && !IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString().equals - (skipMobileNumVerificationOnUpdateState)) { + (skipMobileNumVerificationOnUpdateState) && !IdentityRecoveryConstants. + SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString(). + equals(skipMobileNumVerificationOnUpdateState)) { String verificationPendingMobileNumClaim = getVerificationPendingMobileNumValue(userStoreManager, user); @@ -345,8 +454,8 @@ private void postSetUserClaimOnMobileNumberUpdate(User user, UserStoreManager us /** * Get the 'http://wso2.org/claims/identity/mobileNumber.pendingValue' claim value. * - * @param userStoreManager User store manager. - * @param user User. + * @param userStoreManager User store manager. + * @param user User. * @return Claim value. * @throws IdentityEventException */ @@ -379,7 +488,7 @@ private String getVerificationPendingMobileNumValue(UserStoreManager userStoreMa /** * Check whether mobile verification on update feature is enabled via connector configuration. * - * @param userTenantDomain Tenant domain of the user. + * @param userTenantDomain Tenant domain of the user. * @return True if the feature is enabled, false otherwise. * @throws IdentityEventException */ @@ -392,13 +501,13 @@ private boolean isMobileVerificationOnUpdateEnabled(String userTenantDomain) thr /** * Invalidate pending mobile number verification. * - * @param user User. - * @param userStoreManager User store manager. - * @param claims User claims. + * @param user User. + * @param userStoreManager User store manager. + * @param claims User claims. * @throws IdentityEventException */ private void invalidatePendingMobileVerification(User user, UserStoreManager userStoreManager, - Map claims ) throws IdentityEventException { + Map claims) throws IdentityEventException { if (isVerificationPendingMobileClaimConfigAvailable(user.getTenantDomain()) && StringUtils.isNotBlank(getVerificationPendingMobileNumValue(userStoreManager, user))) { @@ -417,7 +526,7 @@ private void invalidatePendingMobileVerification(User user, UserStoreManager use /** * Check if the claims contain the temporary claim 'verifyMobile' and it is set to true. * - * @param claims User claims. + * @param claims User claims. * @return True if 'verifyMobile' claim is available as true, false otherwise. */ private boolean isVerifyMobileClaimAvailable(Map claims) { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index ec1825784a..0c68ae6e32 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -98,16 +98,15 @@ import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; -import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.time.Instant; import java.util.ArrayList; +import java.util.Arrays; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; -import java.util.Optional; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -832,7 +831,7 @@ private boolean isMobileVerificationEnabledForPrivilegedUsers(String tenantDomai /** * Introspects self registration confirmation code details without invalidating it. - * Does not triggering notification events or update user claims. + * Does not trigger notification events or update user claims. * * @param skipExpiredCodeValidation Skip confirmation code validation against expiration. * @param code Confirmation code. @@ -844,7 +843,7 @@ private UserRecoveryData introspectSelfRegistrationCode(String code, boolean ski UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); - // If the code is validated, the load method will return data. Otherwise method will throw exceptions. + // If the code is validated, the load method will return data. Otherwise, method will throw exceptions. UserRecoveryData recoveryData; if (!skipExpiredCodeValidation) { recoveryData = userRecoveryDataStore.load(code); @@ -927,10 +926,50 @@ public void confirmVerificationCodeMe(String code, Map propertie if (RecoverySteps.VERIFY_MOBILE_NUMBER.equals(recoveryData.getRecoveryStep())) { String pendingMobileNumberClaimValue = recoveryData.getRemainingSetIds(); if (StringUtils.isNotBlank(pendingMobileNumberClaimValue)) { + /* + Verifying whether user is trying to add a mobile number to http://wso2.org/claims/verifedMobileNumbers + claim. + */ + if (Boolean.parseBoolean(properties.get(IdentityRecoveryConstants.MOBILE_NUMBER_VERIFICATION_ONLY))) { + try { + String existingVerifiedMobileNumbers = userStoreManager.getUserClaimValue(user.getUserName(), + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, null); + List existingVerifiedMobileNumbersList = new ArrayList<>(); + if (StringUtils.isNotBlank(existingVerifiedMobileNumbers)) { + existingVerifiedMobileNumbersList.addAll(Arrays.asList(existingVerifiedMobileNumbers.split( + ","))); + } + if (!existingVerifiedMobileNumbersList.contains(pendingMobileNumberClaimValue)) { + existingVerifiedMobileNumbersList.add(pendingMobileNumberClaimValue); + userClaims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(",", existingVerifiedMobileNumbersList)); + } + + /* + VerifiedMobileNumbers is a subset of mobileNumbers. Hence adding the verified number to + mobileNumbers claim as well. + */ + String allMobileNumbers = userStoreManager.getUserClaimValue(user.getUserName(), + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, null); + List allMobileNumbersList = new ArrayList<>(); + if (StringUtils.isNotBlank(allMobileNumbers)) { + allMobileNumbersList.addAll(Arrays.asList(allMobileNumbers.split(","))); + } + if (!allMobileNumbersList.contains(pendingMobileNumberClaimValue)) { + allMobileNumbersList.add(pendingMobileNumberClaimValue); + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(",", allMobileNumbersList)); + } + } catch (UserStoreException e) {; + log.error("Error while retrieving verified mobile numbers for user : " + user.getUserName(), + e); + } + } else { + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileNumberClaimValue); + userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); + } userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); - userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileNumberClaimValue); - userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); } } From d97eaabc03bab48d403c84f9a2cb01f1aa809938 Mon Sep 17 00:00:00 2001 From: lashinie Date: Fri, 19 Apr 2024 16:24:23 +0530 Subject: [PATCH 02/54] support multiple mobile numbers per user --- .../recovery/handler/MobileNumberVerificationHandler.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 7230cf22ef..8c9e75756d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -103,11 +103,13 @@ public void handleEvent(Event event) throws IdentityEventException { claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { invalidatePendingMobileVerification(user, userStoreManager, claims); } + claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getCode(), IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getMessage()); } + return; } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { @@ -260,7 +262,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { - // Not required to handle in this handler. + invalidatePendingMobileVerification(user, userStoreManager, claims); return; } From 3968a6a84fe1820ca6994ff0df2da809148af8f3 Mon Sep 17 00:00:00 2001 From: lashinie Date: Wed, 24 Apr 2024 09:10:18 +0530 Subject: [PATCH 03/54] add method descriptions --- .../MobileNumberVerificationHandler.java | 45 ++++++++++++++----- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 8c9e75756d..d94cf9d44d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -273,16 +273,17 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use /* Handle mobileNumbers and verifyMobileNumbers claims. */ - List exisitingVerifiedNumbersList = getExistingClaimList(userStoreManager, user, + List exisitingVerifiedNumbersList = getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. - VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListFromString(claims.get(IdentityRecoveryConstants. + VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants. VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - List exisitingAllNumbersList = getExistingClaimList(userStoreManager, user, + List exisitingAllNumbersList = getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? - getListFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : exisitingAllNumbersList; + getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : + exisitingAllNumbersList; /* Finds the verification pending mobile number and remove it from the verified numbers list in the payload. @@ -312,7 +313,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use /* Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the OTP. Therefore, the mobile number is already verified & no need to verify it again. - */ + */ if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString(). equals(Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate()) && mobileNumber != null) { invalidatePendingMobileVerification(user, userStoreManager, claims); @@ -379,26 +380,48 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } - private List getListFromString(String str) { + /** + * Convert comma separated list of mobile numbers to a list. + * + * @param mobileNumbers Comma separated list of mobile numbers. + * @return List of mobile numbers. + */ + private List getListOfMobileNumbersFromString(String mobileNumbers) { - return str != null ? new LinkedList<>(Arrays.asList(str.split(","))) : new ArrayList<>(); + return mobileNumbers != null ? new LinkedList<>(Arrays.asList(mobileNumbers.split(","))) : + new ArrayList<>(); } - private List getExistingClaimList(UserStoreManager userStoreManager, User user, String claimURI) throws + /** + * Get the existing claim value of the given claim URI. + * + * @param userStoreManager User store manager. + * @param user User. + * @param claimURI Claim URI. + * @return List of existing claim values. + */ + private List getExistingClaimValue(UserStoreManager userStoreManager, User user, String claimURI) throws IdentityEventException { - List existingClaimList; + List existingClaimValue; try { - existingClaimList = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null) != null ? + existingClaimValue = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null) != null ? new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null).split(","))) : new ArrayList<>(); } catch (UserStoreException e) { throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + " for user: " + user.toFullQualifiedUsername(), e); } - return existingClaimList; + return existingClaimValue; } + /** + * Get the mobile number that is pending verification. + * + * @param existingVerifiedNumbersList List of existing verified mobile numbers. + * @param updatedVerifiedNumbersList List of updated verified mobile numbers. + * @return Mobile number that is pending verification. + */ private String getVerificationPendingMobileNumber(List existingVerifiedNumbersList, List updatedVerifiedNumbersList) throws IdentityEventException { From f52b4615a7bdea5954f3856084cef43545dc1535 Mon Sep 17 00:00:00 2001 From: lashinie Date: Mon, 29 Apr 2024 14:25:24 +0530 Subject: [PATCH 04/54] fix validation when mobile number update on verification is turned off --- .../recovery/handler/MobileNumberVerificationHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index d94cf9d44d..f2d08063b4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -104,7 +104,7 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingMobileVerification(user, userStoreManager, claims); } claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); - if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { + if (!claims.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM).isEmpty()) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getCode(), IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getMessage()); From 5e090681edb12bf96e5fc879ebd451fbe90c13d4 Mon Sep 17 00:00:00 2001 From: lashinie Date: Mon, 20 May 2024 15:53:08 +0530 Subject: [PATCH 05/54] fix validation when mobile number update on verification is turned off --- .../recovery/handler/MobileNumberVerificationHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index f2d08063b4..d94cf9d44d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -104,7 +104,7 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingMobileVerification(user, userStoreManager, claims); } claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); - if (!claims.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM).isEmpty()) { + if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getCode(), IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getMessage()); From 28119457515d91513fce33622a545becf0a64b2f Mon Sep 17 00:00:00 2001 From: lashinie Date: Wed, 29 May 2024 08:51:25 +0530 Subject: [PATCH 06/54] add multiple email addresses and verified email addresses per user support --- .../recovery/IdentityRecoveryConstants.java | 23 +- .../handler/UserEmailVerificationHandler.java | 232 ++++++++++++++---- .../signup/UserSelfRegistrationManager.java | 37 ++- .../carbon/identity/recovery/util/Utils.java | 30 ++- 4 files changed, 259 insertions(+), 63 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 01191ed205..1bad8fecb7 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -121,6 +121,8 @@ public class IdentityRecoveryConstants { public static final String USER_ROLES_CLAIM = "http://wso2.org/claims/roles"; public static final String EMAIL_ADDRESS_CLAIM = "http://wso2.org/claims/emailaddress"; public static final String MOBILE_NUMBER_CLAIM = "http://wso2.org/claims/mobile"; + public static final String EMAIL_ADDRESSES_CLAIM = "http://wso2.org/claims/emailAddresses"; + public static final String VERIFIED_EMAIL_ADDRESSES_CLAIM = "http://wso2.org/claims/verifiedEmailAddresses"; public static final String DEFAULT_CHALLENGE_QUESTION_SEPARATOR = "!"; public static final String ACCOUNT_STATE_CLAIM_URI = "http://wso2.org/claims/identity/accountState"; public static final String PENDING_SELF_REGISTRATION = "PENDING_SR"; @@ -210,6 +212,9 @@ public class IdentityRecoveryConstants { public static final String ACCOUNT_STATUS_DISABLED = "password.recovery.failed.account.disabled"; public static final String IGNORE_IF_TEMPLATE_NOT_FOUND = "ignoreIfTemplateNotFound"; + public static final String SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER = + "SupportMultipleEmailsAndMobileNumberPerUser.Enabled"; + private IdentityRecoveryConstants() { } @@ -440,9 +445,16 @@ public enum ErrorMessages { // UEV - User Email Verification. ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND("UEV-10001", "Email address not found for email verification"), + ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED("UEV-10002", "Email verification is not enabled"), + ERROR_CODE_VERIFY_MULTIPLE_EMAILS("UEV-10003", "Unable to verify multiple email addresses " + + "simultaneously"), + ERROR_CODE_SUPPORT_MULTIPLE_EMAILS_NOT_ENABLED("UEV-10004", "Support for multiple email addresses " + + "per user is not enabled"), + ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS("UEV-10005", "Cannot initiate verification for email" + + " address claim as support for multiple email addresses per user is enabled."), + + INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request"), - INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request") - , // Idle User Account Identification related Error messages. ERROR_RETRIEVING_ASSOCIATED_USER("UMM-65005", "Error retrieving the associated user for the user: %s in the tenant %s."); @@ -858,7 +870,12 @@ public enum SkipEmailVerificationOnUpdateStates { /* State maintained to skip triggering an email verification, when the email address was updated by user during the Email OTP flow at the first login where the email address is not previously set. At the moment email address was already verified during the email OTP verification. So no need to verify it again. */ - SKIP_ON_EMAIL_OTP_FLOW + SKIP_ON_EMAIL_OTP_FLOW, + + /* State maintained to skip triggering an SMS OTP verification, when the email address to be updated is included + in the verifiedEmailAddresses claim, which has been already verified. + */ + SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES } /** diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 221cc523ea..b498a6a7f0 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -50,14 +50,17 @@ import org.wso2.carbon.user.core.UserStoreManager; import java.security.SecureRandom; +import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; +import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Random; import java.util.UUID; +import java.util.stream.Collectors; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.*; public class UserEmailVerificationHandler extends AbstractEventHandler { @@ -89,6 +92,9 @@ public void handleEvent(Event event) throws IdentityEventException { Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); + boolean supportMultipleEmails = Boolean.parseBoolean(IdentityUtil + .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean enable = false; if (IdentityEventConstants.Event.PRE_ADD_USER.equals(eventName) || IdentityEventConstants.Event.POST_ADD_USER.equals(eventName)) { @@ -96,8 +102,7 @@ public void handleEvent(Event event) throws IdentityEventException { .ENABLE_EMAIL_VERIFICATION, user.getTenantDomain())); } else if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName) || IdentityEventConstants.Event.POST_SET_USER_CLAIMS.equals(eventName)) { - enable = Boolean.parseBoolean(Utils.getConnectorConfig(IdentityRecoveryConstants.ConnectorConfig - .ENABLE_EMAIL_VERIFICATION_ON_UPDATE, user.getTenantDomain())); + enable = isEmailVerificationOnUpdateEnabled(user.getTenantDomain()); if (!enable) { /* We need to empty 'EMAIL_ADDRESS_PENDING_VALUE_CLAIM' because having a value in that claim implies a verification is pending. But verification is not enabled anymore. */ @@ -109,7 +114,22 @@ public void handleEvent(Event event) throws IdentityEventException { } invalidatePendingEmailVerification(user, userStoreManager, claims); } + + if (supportMultipleEmails && claims.containsKey( + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) { + throw new IdentityEventClientException(ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED.getCode(), + ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED.getMessage()); + } claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); + } else if (supportMultipleEmails) { + List verifiedEmails = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && + !verifiedEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { + throw new IdentityEventClientException(ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS + .getCode(), ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS.getMessage()); + + } } } @@ -137,7 +157,8 @@ public void handleEvent(Event event) throws IdentityEventException { if (claims == null || claims.isEmpty()) { // Not required to handle in this handler. return; - } else if (claims.containsKey(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM) && Boolean.parseBoolean(claims.get(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM))) { + } else if (claims.containsKey(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM) && + Boolean.parseBoolean(claims.get(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM))) { if (!claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) || StringUtils.isBlank(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { throw new IdentityEventClientException(ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND.getCode(), @@ -148,8 +169,10 @@ public void handleEvent(Event event) throws IdentityEventException { claim.setValue(claims.get(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM)); Utils.setEmailVerifyTemporaryClaim(claim); claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); - Utils.publishRecoveryEvent(eventProperties, IdentityEventConstants.Event.PRE_VERIFY_EMAIL_CLAIM, null); - } else if (claims.containsKey(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM) && Boolean.parseBoolean(claims.get(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM))) { + Utils.publishRecoveryEvent(eventProperties, IdentityEventConstants.Event.PRE_VERIFY_EMAIL_CLAIM, + null); + } else if (claims.containsKey(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM) && + Boolean.parseBoolean(claims.get(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM))) { Claim claim = new Claim(); claim.setClaimUri(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM); claim.setValue(claims.get(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM)); @@ -247,6 +270,12 @@ public void handleEvent(Event event) throws IdentityEventException { } } + private boolean isEmailVerificationOnUpdateEnabled(String tenantDomain) throws IdentityEventException { + + return Boolean.parseBoolean(Utils.getConnectorConfig(IdentityRecoveryConstants.ConnectorConfig + .ENABLE_EMAIL_VERIFICATION_ON_UPDATE, tenantDomain)); + } + @Override public void init(InitConfig configuration) throws IdentityRuntimeException { @@ -297,7 +326,8 @@ protected void initNotification(User user, Enum recoveryScenario, Enum recoveryS /** * This method sets a random value for the credentials, if the ask password flow is enabled. - * @param credentials Credentials object + * + * @param credentials Credentials object */ private void setRandomValueForCredentials(Object credentials) { @@ -469,19 +499,15 @@ protected User getUser(Map eventProperties, UserStoreManager userStoreManager) { private void preSetUserClaimsOnEmailUpdate(Map claims, UserStoreManager userStoreManager, User user) throws IdentityEventException { - if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals - (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { - // Not required to handle in this handler. + if (MapUtils.isEmpty(claims)) { + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants. + SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); return; } - /* - Within the Email OTP flow, the email address is updated in the user profile after successfully verifying the - OTP. Therefore, the email is already verified & no need to verify it again. - */ - if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW.toString().equals + if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { - invalidatePendingEmailVerification(user, userStoreManager, claims); + // Not required to handle in this handler. return; } @@ -496,49 +522,159 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } - String emailAddress = claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); + boolean supportMultipleEmails = Boolean.parseBoolean(IdentityUtil + .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); - if (StringUtils.isNotBlank(emailAddress)) { + String emailAddress = null; + List existingVerifiedEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - String existingEmail; - String username = user.getUserName(); - try { - existingEmail = userStoreManager.getUserClaimValue(username, - IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, null); - } catch (UserStoreException e) { - String error = String.format("Error occurred while retrieving existing email address for user: %s in " + - "domain : %s", username, user.getTenantDomain()); - throw new IdentityEventException(error, e); + // Handle email addresses and verified email addresses claims. + if (supportMultipleEmails) { + + List updatedVerifiedEmailAddresses = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_EMAIL_ADDRESSES_CLAIM) ? getListOfEmailAddressesFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) : existingVerifiedEmailAddresses; + + List existingAllEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + List updatedAllEmailAddresses = claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM) ? + getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : + existingAllEmailAddresses; + + // Find the verification pending email address and remove it from verified email addresses list in the payload. + if (updatedVerifiedEmailAddresses != null) { + emailAddress = getVerificationPendingEmailAddress(existingVerifiedEmailAddresses, + updatedVerifiedEmailAddresses); + updatedVerifiedEmailAddresses.remove(emailAddress); } - if (emailAddress.equals(existingEmail)) { + /* + Find the removed numbers from the existing email addresses list and remove them from the verified email + addresses list, as verified email addresses list should not contain email addresses that are not in the email + addresses list. + */ + if (!updatedAllEmailAddresses.isEmpty()) { + for (String existingEmailAddress : existingAllEmailAddresses) { + if (!updatedAllEmailAddresses.contains(existingEmailAddress) && + updatedVerifiedEmailAddresses != null) { + updatedVerifiedEmailAddresses.remove(existingEmailAddress); + } + } + } + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( + updatedVerifiedEmailAddresses, ",")); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, StringUtils.join( + updatedAllEmailAddresses, ",")); + } else { + // email addresses and verified email addresses should not be updated when support for multiple email + // addresses is disabled. + claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + + emailAddress = claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); + } + if (emailAddress != null) { + + /* + Within the Email OTP flow, the email address is updated in the user profile after successfully verifying the + OTP. Therefore, the email is already verified & no need to verify it again. + */ + if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW.toString().equals + (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { + invalidatePendingEmailVerification(user, userStoreManager, claims); + return; + } + + if (existingVerifiedEmailAddresses != null && existingVerifiedEmailAddresses.contains(emailAddress)) { if (log.isDebugEnabled()) { log.debug(String.format("The email address to be updated: %s is same as the existing email " + "address for user: %s in domain %s. Hence an email verification will not be " + - "triggered.", emailAddress, username, user.getTenantDomain())); + "triggered.", emailAddress, user.getUserName(), user.getTenantDomain())); } Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString()); + .SkipEmailVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString()); invalidatePendingEmailVerification(user, userStoreManager, claims); return; + } else { + String existingEmail; + existingEmail = getEmailClaimValue(user, userStoreManager); + + if (emailAddress.equals(existingEmail)) { + if (log.isDebugEnabled()) { + log.debug(String.format("The email address to be updated: %s is already verified and contains" + + " in the verified email addresses list. Hence an email verification will not be " + + "triggered.", emailAddress)); + } + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString()); + invalidatePendingEmailVerification(user, userStoreManager, claims); + + if (supportMultipleEmails) { + if (existingVerifiedEmailAddresses!= null && + !existingVerifiedEmailAddresses.contains(emailAddress)) { + existingVerifiedEmailAddresses.add(emailAddress); + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( + existingVerifiedEmailAddresses, ",")); + } + } + return; + } } - /* - When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyEmail' temporary - claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to check for - 'verifyEmail' claim. - */ + + /* + When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyEmail' temporary + claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to check for + 'verifyEmail' claim. + */ if (Utils.isUseVerifyClaimEnabled() && !isVerifyEmailClaimAvailable(claims)) { Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); invalidatePendingEmailVerification(user, userStoreManager, claims); return; } - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); - } else { - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); } + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); + } + + /** + * Get the email address that is pending verification. + * + * @param existingVerifiedEmailAddresses List of existing verified email addresses. + * @param updatedVerifiedEmailAddresses List of updated verified email addresses. + * @return email address that is pending verification. + */ + private String getVerificationPendingEmailAddress(List existingVerifiedEmailAddresses, + List updatedVerifiedEmailAddresses) throws + IdentityEventException { + + String emailAddress = null; + for (String verificationPendingEmailAddress : updatedVerifiedEmailAddresses) { + if (existingVerifiedEmailAddresses.stream().noneMatch(email -> + email.trim().equalsIgnoreCase(verificationPendingEmailAddress.trim()))) { + if (emailAddress == null) { + emailAddress = verificationPendingEmailAddress; + } else { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_VERIFY_MULTIPLE_EMAILS.getCode(), IdentityRecoveryConstants. + ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_EMAILS.getMessage()); + } + } + } + return emailAddress; + } + + /** + * Convert comma separated list of email addresses to a list. + * + * @param emails Comma separated list of mobile numbers. + * @return List of email addresses. + */ + private List getListOfEmailAddressesFromString(String emails) { + + return emails != null ? new LinkedList<>(Arrays.asList(emails.split(","))).stream().map(String::trim) + .collect(Collectors.toList()) : new ArrayList<>(); } private void postSetUserClaimsOnEmailUpdate(User user, UserStoreManager userStoreManager) throws @@ -549,11 +685,13 @@ private void postSetUserClaimsOnEmailUpdate(User user, UserStoreManager userStor if (!IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (skipEmailVerificationOnUpdateState) && !IdentityRecoveryConstants. SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString().equals - (skipEmailVerificationOnUpdateState) && !IdentityRecoveryConstants + (skipEmailVerificationOnUpdateState) && !IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString().equals (skipEmailVerificationOnUpdateState) && !IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW.toString().equals - (skipEmailVerificationOnUpdateState)) { + (skipEmailVerificationOnUpdateState) && !IdentityRecoveryConstants. + SkipEmailVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString().equals( + skipEmailVerificationOnUpdateState)) { String pendingVerificationEmailClaimValue = getPendingVerificationEmailValue(userStoreManager, user); @@ -599,13 +737,13 @@ private String getPendingVerificationEmailValue(UserStoreManager userStoreManage /** * Invalidate pending email verification. * - * @param user User. - * @param userStoreManager User store manager. - * @param claims User claims. + * @param user User. + * @param userStoreManager User store manager. + * @param claims User claims. * @throws IdentityEventException */ private void invalidatePendingEmailVerification(User user, UserStoreManager userStoreManager, - Map claims ) throws IdentityEventException { + Map claims) throws IdentityEventException { if (StringUtils.isNotBlank(getPendingVerificationEmailValue(userStoreManager, user))) { claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, StringUtils.EMPTY); @@ -623,7 +761,7 @@ private void invalidatePendingEmailVerification(User user, UserStoreManager user /** * Check if the claims contain the temporary claim 'verifyEmail' and it is set to true. * - * @param claims User claims. + * @param claims User claims. * @return True if 'verifyEmail' claim is available as true, false otherwise. */ private boolean isVerifyEmailClaimAvailable(Map claims) { @@ -645,7 +783,7 @@ private boolean isVerifyEmailClaimAvailable(Map claims) { * @throws IdentityEventException IdentityEventException. */ private void sendNotificationToExistingEmailOnEmailUpdate(User user, UserStoreManager userStoreManager, - String newEmailAddress, String templateType) throws IdentityEventException { + String newEmailAddress, String templateType) throws IdentityEventException { boolean enable = Boolean.parseBoolean(Utils.getConnectorConfig(IdentityRecoveryConstants.ConnectorConfig .ENABLE_NOTIFICATION_ON_EMAIL_UPDATE, user.getTenantDomain())); if (!enable) { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index ec1825784a..44b1bed291 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -101,13 +101,7 @@ import java.net.URISyntaxException; import java.text.SimpleDateFormat; import java.time.Instant; -import java.util.ArrayList; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.Optional; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -759,12 +753,39 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi HashMap userClaims = getClaimsListToUpdate(user, verifiedChannelType, externallyVerifiedClaim, recoveryData.getRecoveryScenario().toString()); + boolean supportMultipleEmailsAndMobileNumbers = Boolean.parseBoolean(IdentityUtil + .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + if (RecoverySteps.VERIFY_EMAIL.equals(recoveryData.getRecoveryStep())) { String pendingEmailClaimValue = recoveryData.getRemainingSetIds(); if (StringUtils.isNotBlank(pendingEmailClaimValue)) { eventProperties.put(IdentityEventConstants.EventProperty.VERIFIED_EMAIL, pendingEmailClaimValue); userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, StringUtils.EMPTY); - userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, pendingEmailClaimValue); //todo?? + if (supportMultipleEmailsAndMobileNumbers) { + try { + List verifiedEmails = Utils.getExistingClaimValue( + (org.wso2.carbon.user.core.UserStoreManager) eventProperties.get( + IdentityEventConstants.EventProperty.USER_STORE_MANAGER), user, + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + verifiedEmails.add(pendingEmailClaimValue); + userClaims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( + verifiedEmails, ",")); + + List allEmails = Utils.getExistingClaimValue( + (org.wso2.carbon.user.core.UserStoreManager) eventProperties.get( + IdentityEventConstants.EventProperty.USER_STORE_MANAGER), user, + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + if (!allEmails.contains(pendingEmailClaimValue)) { + allEmails.add(pendingEmailClaimValue); + userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, StringUtils.join( + allEmails, ",")) ; + } + } catch (IdentityEventException e) { + log.error("Error occurred while obtaining claim for the user "); + } + } else { + userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, pendingEmailClaimValue); + } // Todo passes when email address is properly set here. Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index 9948fcf49e..1b8ee1241e 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -82,11 +82,7 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.UUID; +import java.util.*; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; @@ -1676,4 +1672,28 @@ private static String convertFailureReasonsToString(List getExistingClaimValue(org.wso2.carbon.user.core.UserStoreManager userStoreManager, + User user, String claimURI) throws IdentityEventException { + + List existingClaimValue; + try { + existingClaimValue = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null) != null ? + new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, + null).split(","))) : new ArrayList<>(); + } catch (org.wso2.carbon.user.core.UserStoreException e) { + throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + + " for user: " + user.toFullQualifiedUsername(), e); + } + return existingClaimValue; + } + } From 63e822d1b527aca4307b419c990b1461eba4a1de Mon Sep 17 00:00:00 2001 From: lashinie Date: Wed, 29 May 2024 09:36:49 +0530 Subject: [PATCH 07/54] fix the logic for when verification on update is disabled --- .../recovery/handler/UserEmailVerificationHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index b498a6a7f0..1c5cb1ddae 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -115,8 +115,7 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingEmailVerification(user, userStoreManager, claims); } - if (supportMultipleEmails && claims.containsKey( - IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) { + if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) { throw new IdentityEventClientException(ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED.getCode(), ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED.getMessage()); } From b5ab7c7d762d42f9f3a2fe5b9d841adb94c13762 Mon Sep 17 00:00:00 2001 From: lashinie Date: Wed, 29 May 2024 10:01:22 +0530 Subject: [PATCH 08/54] fix imports --- .../org/wso2/carbon/identity/recovery/util/Utils.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index 1b8ee1241e..72fa75d74d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -82,7 +82,13 @@ import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.security.SecureRandom; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.UUID; import java.util.concurrent.TimeUnit; import java.util.regex.Pattern; From d63a79b367d5c055c415b8076f20a3ca7cb09fe8 Mon Sep 17 00:00:00 2001 From: lashinie Date: Thu, 30 May 2024 13:16:52 +0530 Subject: [PATCH 09/54] improvements for support multiple mobile numbers and email address config --- .../recovery/IdentityRecoveryConstants.java | 6 +- .../MobileNumberVerificationHandler.java | 150 +++++++++++------- .../handler/UserEmailVerificationHandler.java | 6 +- .../signup/UserSelfRegistrationManager.java | 9 +- 4 files changed, 107 insertions(+), 64 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index de09742e37..df027abc4e 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -466,9 +466,11 @@ public enum ErrorMessages { " updated as mobile number verification on update is disabled."), ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS("UMV-10002", "Unable to verify " + "multiple mobile numbers simultaneously."), + ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED("UEV-10003", "Support for multiple mobile " + + "numbers per user is not enabled"), + ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER("UMV-10004", "Cannot initiate " + + "verification for mobile number claim as support for multiple mobile numbers per user is enabled."), - INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request") - , INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request"), // Idle User Account Identification related Error messages. diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index d94cf9d44d..3d70f226e9 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -26,6 +26,7 @@ import org.wso2.carbon.identity.base.IdentityRuntimeException; import org.wso2.carbon.identity.core.bean.context.MessageContext; import org.wso2.carbon.identity.core.handler.InitConfig; +import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventClientException; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.IdentityEventException; @@ -50,12 +51,8 @@ import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.HashMap; -import java.util.Map; -import java.util.LinkedList; -import java.util.List; +import java.util.*; +import java.util.stream.Collectors; /** * This event handler is used to send a verification SMS when a claim update event to update the mobile number @@ -89,6 +86,9 @@ public void handleEvent(Event event) throws IdentityEventException { Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); + boolean supportMultipleMobileNumbers = Boolean.parseBoolean(IdentityUtil.getProperty(IdentityRecoveryConstants + .SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean enable = isMobileVerificationOnUpdateEnabled(user.getTenantDomain()); if (!enable) { @@ -112,6 +112,34 @@ public void handleEvent(Event event) throws IdentityEventException { return; } + List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + List mobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + if (supportMultipleMobileNumbers) { + if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && + !verifiedMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER.getCode(), IdentityRecoveryConstants + .ErrorMessages.ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER.getMessage()); + } + } else { + /* Check whether the mobile numbers and verified mobile numbers claims have been updated when support for + multiple mobile numbers is disabled. */ + List updatedVerifiedNumbers = StringUtils.isNotBlank(claims.get(IdentityRecoveryConstants + .VERIFIED_MOBILE_NUMBERS_CLAIM)) ? getListOfMobileNumbersFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : new ArrayList<>(); + List updatedAllNumbers = StringUtils.isNotBlank(claims.get(IdentityRecoveryConstants + .MOBILE_NUMBERS_CLAIM)) ? getListOfMobileNumbersFromString(claims.get( + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : new ArrayList<>(); + if (!(new HashSet<>(updatedVerifiedNumbers).equals(new HashSet<>(verifiedMobileNumbers))) || + !(new HashSet<>(updatedAllNumbers).equals(new HashSet<>(mobileNumbers)))) { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED.getCode(), IdentityRecoveryConstants + .ErrorMessages.ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED.getMessage()); + } + } + if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { preSetUserClaimOnMobileNumberUpdate(claims, userStoreManager, user); claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); @@ -253,13 +281,6 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use return; } - if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM) && - claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)) { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getCode(), IdentityRecoveryConstants. - ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getMessage()); - } - if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { invalidatePendingMobileVerification(user, userStoreManager, claims); @@ -270,45 +291,54 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); } - /* - Handle mobileNumbers and verifyMobileNumbers claims. - */ + boolean supportMultipleMobileNumbers = Boolean.parseBoolean(IdentityUtil.getProperty(IdentityRecoveryConstants + .SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + + String mobileNumber = null; List exisitingVerifiedNumbersList = getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. - VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants. - VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - List exisitingAllNumbersList = getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); - List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? - getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : - exisitingAllNumbersList; + if (supportMultipleMobileNumbers) { + List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - /* - Finds the verification pending mobile number and remove it from the verified numbers list in the payload. - */ - String mobileNumber = null; - if (updatedVerifiedNumbersList != null) { - mobileNumber = getVerificationPendingMobileNumber(exisitingVerifiedNumbersList, - updatedVerifiedNumbersList); - updatedVerifiedNumbersList.remove(mobileNumber); - } + List exisitingAllNumbersList = getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? + getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : + exisitingAllNumbersList; + /* + Finds the verification pending mobile number and remove it from the verified numbers list in the payload. + */ + if (updatedVerifiedNumbersList != null) { + mobileNumber = getVerificationPendingMobileNumber(exisitingVerifiedNumbersList, + updatedVerifiedNumbersList); + updatedVerifiedNumbersList.remove(mobileNumber); + } - /* - Finds the removed numbers from the existing mobile numbers list and remove them from the verified numbers list. - As verified numbers list should not contain numbers that are not in the mobile numbers list. - */ - if (!updatedAllNumbersList.isEmpty()) { - for (String number : exisitingAllNumbersList) { - if (!updatedAllNumbersList.contains(number) && updatedVerifiedNumbersList != null) { - updatedVerifiedNumbersList.remove(number); + /* + Finds the removed numbers from the existing mobile numbers list and remove them from the verified numbers + list. As verified numbers list should not contain numbers that are not in the mobile numbers list. + */ + if (!updatedAllNumbersList.isEmpty()) { + for (String number : exisitingAllNumbersList) { + if (!updatedAllNumbersList.contains(number) && updatedVerifiedNumbersList != null) { + updatedVerifiedNumbersList.remove(number); + } } } - } + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, String.join(",", updatedAllNumbersList)); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(",", + updatedVerifiedNumbersList)); + } else { + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - if (mobileNumber == null) { mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + } + + if (mobileNumber != null) { /* Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the @@ -322,14 +352,14 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use /* Mobile numbers in verifiedMobileNumbers claim are already verified. No need to verify again. */ - if (exisitingVerifiedNumbersList.contains(mobileNumber)) { + if (exisitingVerifiedNumbersList != null && exisitingVerifiedNumbersList.contains(mobileNumber)) { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString()); invalidatePendingMobileVerification(user, userStoreManager, claims); return; } - if (StringUtils.isNotBlank(mobileNumber)) { + else { String existingMobileNumber; String username = user.getUserName(); try { @@ -351,6 +381,15 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); invalidatePendingMobileVerification(user, userStoreManager, claims); + + if (supportMultipleMobileNumbers) { + if (exisitingVerifiedNumbersList != null && !exisitingVerifiedNumbersList.contains( + mobileNumber)) { + exisitingVerifiedNumbersList.add(mobileNumber); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(",", + exisitingVerifiedNumbersList)); + } + } return; } /* @@ -365,19 +404,15 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use return; } claims.remove(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); - } else { - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); } + } else { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); } if (isVerificationPendingMobileClaimConfigAvailable(user.getTenantDomain())) { claims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, mobileNumber); } - claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, String.join(",", updatedAllNumbersList)); - claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(",", - updatedVerifiedNumbersList)); - } /** @@ -388,8 +423,8 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use */ private List getListOfMobileNumbersFromString(String mobileNumbers) { - return mobileNumbers != null ? new LinkedList<>(Arrays.asList(mobileNumbers.split(","))) : - new ArrayList<>(); + return StringUtils.isBlank(mobileNumbers) ? new ArrayList<>() : new LinkedList<>(Arrays.asList( + mobileNumbers.split(","))).stream().map(String::trim).collect(Collectors.toList()); } /** @@ -405,9 +440,10 @@ private List getExistingClaimValue(UserStoreManager userStoreManager, Us List existingClaimValue; try { - existingClaimValue = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null) != null ? - new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, - null).split(","))) : new ArrayList<>(); + existingClaimValue = StringUtils.isNotBlank(userStoreManager.getUserClaimValue(user.getUserName(), + claimURI, null)) ? new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue( + user.getUserName(), claimURI, null).split(","))).stream().map(String::trim) + .collect(Collectors.toList()) : new ArrayList<>(); } catch (UserStoreException e) { throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + " for user: " + user.toFullQualifiedUsername(), e); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 1c5cb1ddae..50fbeceb56 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -566,8 +566,10 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, StringUtils.join( updatedAllEmailAddresses, ",")); } else { - // email addresses and verified email addresses should not be updated when support for multiple email - // addresses is disabled. + /* + email addresses and verified email addresses should not be updated when support for multiple email + addresses is disabled. + */ claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 3eae11fe93..833c20a422 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -950,6 +950,9 @@ public void confirmVerificationCodeMe(String code, Map propertie UserStoreManager userStoreManager = getUserStoreManager(user); HashMap userClaims = new HashMap<>(); + boolean supportMultipleEmailsAndMobileNumbers = Boolean.parseBoolean(IdentityUtil + .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + if (RecoverySteps.VERIFY_MOBILE_NUMBER.equals(recoveryData.getRecoveryStep())) { String pendingMobileNumberClaimValue = recoveryData.getRemainingSetIds(); if (StringUtils.isNotBlank(pendingMobileNumberClaimValue)) { @@ -957,7 +960,7 @@ public void confirmVerificationCodeMe(String code, Map propertie Verifying whether user is trying to add a mobile number to http://wso2.org/claims/verifedMobileNumbers claim. */ - if (Boolean.parseBoolean(properties.get(IdentityRecoveryConstants.MOBILE_NUMBER_VERIFICATION_ONLY))) { + if (supportMultipleEmailsAndMobileNumbers) { try { String existingVerifiedMobileNumbers = userStoreManager.getUserClaimValue(user.getUserName(), IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, null); @@ -973,7 +976,7 @@ public void confirmVerificationCodeMe(String code, Map propertie } /* - VerifiedMobileNumbers is a subset of mobileNumbers. Hence adding the verified number to + VerifiedMobileNumbers is a subset of mobileNumbers. Hence, adding the verified number to mobileNumbers claim as well. */ String allMobileNumbers = userStoreManager.getUserClaimValue(user.getUserName(), @@ -988,7 +991,7 @@ public void confirmVerificationCodeMe(String code, Map propertie String.join(",", allMobileNumbersList)); } } catch (UserStoreException e) {; - log.error("Error while retrieving verified mobile numbers for user : " + user.getUserName(), + log.error("Error while retrieving mobile numbers claims for user : " + user.getUserName(), e); } } else { From a32a3756740167a10fad954dabac940518bdd584 Mon Sep 17 00:00:00 2001 From: lashinie Date: Thu, 30 May 2024 14:47:17 +0530 Subject: [PATCH 10/54] drop email addresses and mobile numbers claim when multiple mobile and email per user config is disabled --- .../recovery/IdentityRecoveryConstants.java | 11 ++--- .../MobileNumberVerificationHandler.java | 43 ++++++++----------- .../handler/UserEmailVerificationHandler.java | 17 +++++--- 3 files changed, 34 insertions(+), 37 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index df027abc4e..8a758deac8 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -133,12 +133,6 @@ public class IdentityRecoveryConstants { public static final String PENDING_EMAIL_VERIFICATION = "PENDING_EV"; public static final String ACCOUNT_STATE_UNLOCKED = "UNLOCKED"; - /* - This constant will be used to identify that user is verifying a mobile number which needs to be added to - http://wso2.org/claims/verifiedMobileNumbers claim and not to http://wso2.org/claims/mobile. - */ - public static final String MOBILE_NUMBER_VERIFICATION_ONLY = "verificationOnly"; - public static final String PASSWORD_RESET_FAIL_ATTEMPTS_CLAIM = "http://wso2" + ".org/claims/identity/failedPasswordRecoveryAttempts"; public static final String SIGN_UP_ROLE_SEPARATOR = ","; @@ -220,6 +214,9 @@ public class IdentityRecoveryConstants { public static final String ACCOUNT_STATUS_DISABLED = "password.recovery.failed.account.disabled"; public static final String IGNORE_IF_TEMPLATE_NOT_FOUND = "ignoreIfTemplateNotFound"; + /* + This config enables the support to store multiple mobile numbers and email addresses per user. + */ public static final String SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER = "SupportMultipleEmailsAndMobileNumberPerUser.Enabled"; @@ -890,7 +887,7 @@ public enum SkipEmailVerificationOnUpdateStates { address was already verified during the email OTP verification. So no need to verify it again. */ SKIP_ON_EMAIL_OTP_FLOW, - /* State maintained to skip triggering an SMS OTP verification, when the email address to be updated is included + /* State maintained to skip triggering an email verification, when the email address to be updated is included in the verifiedEmailAddresses claim, which has been already verified. */ SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 3d70f226e9..fc70b810bd 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -51,7 +51,13 @@ import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; -import java.util.*; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.HashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; /** @@ -104,40 +110,27 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingMobileVerification(user, userStoreManager, claims); } claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); - if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getCode(), IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED.getMessage()); - } + claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); return; } - List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - List mobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); if (supportMultipleMobileNumbers) { - if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && - !verifiedMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { + + List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + String updatedMobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + if (StringUtils.isNotBlank(updatedMobileNumber) && !verifiedMobileNumbers.contains(updatedMobileNumber)) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER.getCode(), IdentityRecoveryConstants .ErrorMessages.ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER.getMessage()); } } else { - /* Check whether the mobile numbers and verified mobile numbers claims have been updated when support for - multiple mobile numbers is disabled. */ - List updatedVerifiedNumbers = StringUtils.isNotBlank(claims.get(IdentityRecoveryConstants - .VERIFIED_MOBILE_NUMBERS_CLAIM)) ? getListOfMobileNumbersFromString(claims.get( - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : new ArrayList<>(); - List updatedAllNumbers = StringUtils.isNotBlank(claims.get(IdentityRecoveryConstants - .MOBILE_NUMBERS_CLAIM)) ? getListOfMobileNumbersFromString(claims.get( - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : new ArrayList<>(); - if (!(new HashSet<>(updatedVerifiedNumbers).equals(new HashSet<>(verifiedMobileNumbers))) || - !(new HashSet<>(updatedAllNumbers).equals(new HashSet<>(mobileNumbers)))) { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED.getCode(), IdentityRecoveryConstants - .ErrorMessages.ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED.getMessage()); + // Multiple mobile numbers per user support is disabled. + if (log.isDebugEnabled()) { + log.debug("Supporting multiple mobile numbers per user is disabled."); } + + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 50fbeceb56..de9b916c1a 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -96,13 +96,16 @@ public void handleEvent(Event event) throws IdentityEventException { .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); boolean enable = false; + if (IdentityEventConstants.Event.PRE_ADD_USER.equals(eventName) || IdentityEventConstants.Event.POST_ADD_USER.equals(eventName)) { enable = Boolean.parseBoolean(Utils.getConnectorConfig(IdentityRecoveryConstants.ConnectorConfig .ENABLE_EMAIL_VERIFICATION, user.getTenantDomain())); } else if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName) || IdentityEventConstants.Event.POST_SET_USER_CLAIMS.equals(eventName)) { + enable = isEmailVerificationOnUpdateEnabled(user.getTenantDomain()); + if (!enable) { /* We need to empty 'EMAIL_ADDRESS_PENDING_VALUE_CLAIM' because having a value in that claim implies a verification is pending. But verification is not enabled anymore. */ @@ -115,12 +118,10 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingEmailVerification(user, userStoreManager, claims); } - if (claims.containsKey(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) { - throw new IdentityEventClientException(ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED.getCode(), - ERROR_CODE_EMAIL_VERIFICATION_NOT_ENABLED.getMessage()); - } + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); - } else if (supportMultipleEmails) { + } + if (supportMultipleEmails) { List verifiedEmails = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && @@ -129,6 +130,12 @@ public void handleEvent(Event event) throws IdentityEventException { .getCode(), ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS.getMessage()); } + } else { + // Supporting multiple email addresses per user is disabled. + if (log.isDebugEnabled()) { + log.debug("Supporting multiple email addresses per user is disabled."); + } + claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); } } From 5cc690937c7f8cd428f2a4cdbb62cf218b231080 Mon Sep 17 00:00:00 2001 From: lashinie Date: Thu, 30 May 2024 20:33:39 +0530 Subject: [PATCH 11/54] logic improvements --- .../recovery/IdentityRecoveryConstants.java | 22 +++-- .../MobileNumberVerificationHandler.java | 88 +++++++++---------- .../handler/UserEmailVerificationHandler.java | 61 ++++++++----- .../signup/UserSelfRegistrationManager.java | 7 +- .../carbon/identity/recovery/util/Utils.java | 4 +- 5 files changed, 101 insertions(+), 81 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 8a758deac8..60170cd759 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -455,8 +455,12 @@ public enum ErrorMessages { "simultaneously"), ERROR_CODE_SUPPORT_MULTIPLE_EMAILS_NOT_ENABLED("UEV-10004", "Support for multiple email addresses " + "per user is not enabled"), - ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS("UEV-10005", "Cannot initiate verification for email" + - " address claim as support for multiple email addresses per user is enabled."), + ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST("UEV-10005", "As multiple " + + "email addresses support is enabled, primary email address should be included in the email " + + "addresses list."), + ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST("UEV-10006", "As multiple " + + "email addresses support and email verification is enabled, primary email address should be included " + + "in the verified email addresses list."), // UMV - User Mobile Verification. ERROR_CODE_MOBILE_VERIFICATION_NOT_ENABLED("UMV-10001", " Verified mobile numbers claim cannot be" + @@ -465,8 +469,12 @@ public enum ErrorMessages { "multiple mobile numbers simultaneously."), ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED("UEV-10003", "Support for multiple mobile " + "numbers per user is not enabled"), - ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER("UMV-10004", "Cannot initiate " + - "verification for mobile number claim as support for multiple mobile numbers per user is enabled."), + ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST("UMV-10004", "As multiple " + + "mobile numbers support is enabled, primary mobile number should be included in the mobile " + + "numbers list."), + ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST("UMV-10005", "As multiple " + + "mobile numbers support and mobile verification is enabled, primary mobile number should be included " + + "in the verified mobile numbers list."), INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request"), @@ -888,8 +896,7 @@ public enum SkipEmailVerificationOnUpdateStates { SKIP_ON_EMAIL_OTP_FLOW, /* State maintained to skip triggering an email verification, when the email address to be updated is included - in the verifiedEmailAddresses claim, which has been already verified. - */ + in the verifiedEmailAddresses claim, which has been already verified. */ SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES } @@ -1000,8 +1007,7 @@ public enum SkipMobileNumberVerificationOnUpdateStates { SKIP_ON_SMS_OTP_FLOW, /* State maintained to skip triggering an SMS OTP verification, when the mobile number to be updated is included - in the verifiedMobileNumbers claim, which has been already verified. - */ + in the verifiedMobileNumbers claim, which has been already verified. */ SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index fc70b810bd..7a68b95c70 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -54,7 +54,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -111,26 +110,45 @@ public void handleEvent(Event event) throws IdentityEventException { } claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - return; - } - - if (supportMultipleMobileNumbers) { - List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - String updatedMobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); - if (StringUtils.isNotBlank(updatedMobileNumber) && !verifiedMobileNumbers.contains(updatedMobileNumber)) { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER.getCode(), IdentityRecoveryConstants - .ErrorMessages.ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_MOBILE_NUMBER.getMessage()); + if (supportMultipleMobileNumbers) { + List allMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && + !allMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode(), + IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getMessage()); + } + } else { + // Multiple mobile numbers per user support is disabled. + if (log.isDebugEnabled()) { + log.debug("Supporting multiple mobile numbers per user is disabled."); + } + claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } + return; } else { - // Multiple mobile numbers per user support is disabled. - if (log.isDebugEnabled()) { - log.debug("Supporting multiple mobile numbers per user is disabled."); + if (supportMultipleMobileNumbers) { + List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && + !verifiedMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { + throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode(), + IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getMessage()); + } + } else { + // Multiple mobile numbers per user support is disabled. + if (log.isDebugEnabled()) { + log.debug("Supporting multiple mobile numbers per user is disabled."); + } + claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } - - claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { @@ -288,15 +306,15 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use .SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); String mobileNumber = null; - List exisitingVerifiedNumbersList = getExistingClaimValue(userStoreManager, user, + List exisitingVerifiedNumbersList = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); if (supportMultipleMobileNumbers) { List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - List exisitingAllNumbersList = getExistingClaimValue(userStoreManager, user, + List exisitingAllNumbersList = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : @@ -345,14 +363,12 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use /* Mobile numbers in verifiedMobileNumbers claim are already verified. No need to verify again. */ - if (exisitingVerifiedNumbersList != null && exisitingVerifiedNumbersList.contains(mobileNumber)) { + if (exisitingVerifiedNumbersList != null && exisitingVerifiedNumbersList.contains(mobileNumber)) { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString()); invalidatePendingMobileVerification(user, userStoreManager, claims); return; - } - - else { + } else { String existingMobileNumber; String username = user.getUserName(); try { @@ -420,30 +436,6 @@ private List getListOfMobileNumbersFromString(String mobileNumbers) { mobileNumbers.split(","))).stream().map(String::trim).collect(Collectors.toList()); } - /** - * Get the existing claim value of the given claim URI. - * - * @param userStoreManager User store manager. - * @param user User. - * @param claimURI Claim URI. - * @return List of existing claim values. - */ - private List getExistingClaimValue(UserStoreManager userStoreManager, User user, String claimURI) throws - IdentityEventException { - - List existingClaimValue; - try { - existingClaimValue = StringUtils.isNotBlank(userStoreManager.getUserClaimValue(user.getUserName(), - claimURI, null)) ? new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue( - user.getUserName(), claimURI, null).split(","))).stream().map(String::trim) - .collect(Collectors.toList()) : new ArrayList<>(); - } catch (UserStoreException e) { - throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + - " for user: " + user.toFullQualifiedUsername(), e); - } - return existingClaimValue; - } - /** * Get the mobile number that is pending verification. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index de9b916c1a..128bb0fe09 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -60,7 +60,9 @@ import java.util.UUID; import java.util.stream.Collectors; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.*; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND; public class UserEmailVerificationHandler extends AbstractEventHandler { @@ -118,24 +120,41 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingEmailVerification(user, userStoreManager, claims); } - claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); - } - if (supportMultipleEmails) { - List verifiedEmails = Utils.getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && - !verifiedEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { - throw new IdentityEventClientException(ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS - .getCode(), ERROR_CODE_CANNOT_INITIATE_VERIFICATION_FOR_EMAIL_ADDRESS.getMessage()); - + if (supportMultipleEmails) { + List allEmails = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && + !allEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { + throw new IdentityEventClientException(ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST + .getCode(), ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST.getMessage()); + } + } else { + // Supporting multiple email addresses per user is disabled. + if (log.isDebugEnabled()) { + log.debug("Supporting multiple email addresses per user is disabled."); + } + claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); } + claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); } else { - // Supporting multiple email addresses per user is disabled. - if (log.isDebugEnabled()) { - log.debug("Supporting multiple email addresses per user is disabled."); + if (supportMultipleEmails) { + List verifiedEmails = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && + !verifiedEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { + throw new IdentityEventClientException( + ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getCode(), + ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getMessage()); + } + } else { + // Multiple email addresses per user support is disabled. + if (log.isDebugEnabled()) { + log.debug("Supporting multiple mobile email addresses per user is disabled."); + } + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); } - claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); } } @@ -630,11 +649,11 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } } - /* - When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyEmail' temporary - claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to check for - 'verifyEmail' claim. - */ + /* + When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyEmail' temporary + claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to check for + 'verifyEmail' claim. + */ if (Utils.isUseVerifyClaimEnabled() && !isVerifyEmailClaimAvailable(claims)) { Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 833c20a422..3d61e1bd71 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -710,7 +710,8 @@ public UserRecoveryData introspectUserSelfRegistration(boolean skipExpiredCodeVa } private UserRecoveryData validateSelfRegistrationCode(String code, String verifiedChannelType, - String verifiedChannelClaim, Map properties, boolean skipExpiredCodeValidation) + String verifiedChannelClaim, Map properties, + boolean skipExpiredCodeValidation) throws IdentityRecoveryException { Utils.unsetThreadLocalToSkipSendingEmailVerificationOnUpdate(); @@ -786,7 +787,9 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi allEmails, ",")) ; } } catch (IdentityEventException e) { - log.error("Error occurred while obtaining claim for the user "); + log.error("Error occurred while obtaining claim for the user : " + user.getUserName()); + throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + + "value for the user : " + user.getUserName(), e); } } else { userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, pendingEmailClaimValue); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index 72fa75d74d..d0959e9ad5 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -1692,7 +1692,8 @@ public static List getExistingClaimValue(org.wso2.carbon.user.core.UserS List existingClaimValue; try { - existingClaimValue = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null) != null ? + existingClaimValue = StringUtils.isNotBlank(userStoreManager.getUserClaimValue(user.getUserName(), + claimURI, null)) ? new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null).split(","))) : new ArrayList<>(); } catch (org.wso2.carbon.user.core.UserStoreException e) { @@ -1701,5 +1702,4 @@ public static List getExistingClaimValue(org.wso2.carbon.user.core.UserS } return existingClaimValue; } - } From 4c19b27a9d6597c21633732ad79c15d1fa86a7ed Mon Sep 17 00:00:00 2001 From: lashinie Date: Mon, 3 Jun 2024 11:25:58 +0530 Subject: [PATCH 12/54] add supportMultiEmailsAndMobileNumbers config as a governance config --- .../recovery/IdentityRecoveryConstants.java | 9 +++---- .../connector/UserClaimUpdateConfigImpl.java | 26 ++++++++++++++++++- .../MobileNumberVerificationHandler.java | 6 ++--- .../handler/UserEmailVerificationHandler.java | 6 ++--- .../signup/UserSelfRegistrationManager.java | 8 +++--- .../carbon/identity/recovery/util/Utils.java | 17 ++++++++++++ .../UserClaimUpdateConfigImplTest.java | 8 +++++- 7 files changed, 60 insertions(+), 20 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 60170cd759..987b2e409c 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -214,12 +214,6 @@ public class IdentityRecoveryConstants { public static final String ACCOUNT_STATUS_DISABLED = "password.recovery.failed.account.disabled"; public static final String IGNORE_IF_TEMPLATE_NOT_FOUND = "ignoreIfTemplateNotFound"; - /* - This config enables the support to store multiple mobile numbers and email addresses per user. - */ - public static final String SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER = - "SupportMultipleEmailsAndMobileNumberPerUser.Enabled"; - private IdentityRecoveryConstants() { } @@ -693,6 +687,9 @@ public static class ConnectorConfig { public static final String ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER = "UserClaimUpdate.MobileNumber." + "EnableVerificationByPrivilegedUser"; public static final String USE_VERIFY_CLAIM_ON_UPDATE = "UserClaimUpdate.UseVerifyClaim"; + // This config enables the support to store multiple mobile numbers and email addresses per user. + public static final String SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER = + "UserClaimUpdate.SupportMultiEmailsAndMobileNumbers.Enable"; public static final String ASK_PASSWORD_EXPIRY_TIME = "EmailVerification.AskPassword.ExpiryTime"; public static final String ASK_PASSWORD_TEMP_PASSWORD_GENERATOR = "EmailVerification.AskPassword.PasswordGenerator"; public static final String ASK_PASSWORD_DISABLE_RANDOM_VALUE_FOR_CREDENTIALS = "EmailVerification.AskPassword" + diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java index ca899dc8ce..c26fe95b1d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java @@ -56,6 +56,7 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static final String DEFAULT_MOBILE_NUM_VERIFICATION_ON_UPDATE_SMS_OTP_EXPIRY_TIME = "5"; private static final String DEFAULT_ENABLE_VALUE_FOR_MOBILE_NUMBER_VERIFICATION_ON_UPDATE = "false"; private static final String DEFAULT_MOBILE_NUM_VERIFICATION_BY_PRIVILEGED_USERS = "false"; + private static final String DEFAULT_SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS = "true"; private static final String USER_CLAIM_UPDATE_ELEMENT = "UserClaimUpdate"; private static final String ENABLE_ELEMENT = "Enable"; private static final String SEND_OTP_IN_EMAIL_ELEMENT = "SendOTPInEmail"; @@ -71,6 +72,7 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static final String VERIFICATION_ON_UPDATE_ELEMENT = "VerificationOnUpdate"; private static final String NOTIFICATION_ON_UPDATE_ELEMENT = "NotificationOnUpdate"; private static final String ENABLE_MOBILE_VERIFICATION_PRIVILEGED_USER = "EnableVerificationByPrivilegedUser"; + private static final String SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_ELEMENT = "SupportMultiEmailsAndMobileNumbers"; private static String enableEmailVerificationOnUpdateProperty = null; private static String enableSendOTPInEmailProperty = null; private static String useUppercaseCharactersInOTPProperty = null; @@ -82,6 +84,7 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static String enableMobileNumVerificationOnUpdateProperty = null; private static String mobileNumVerificationOnUpdateCodeExpiryProperty = null; private static String mobileNumVerificationByPrivilegedUsersProperty = null; + private static String supportMultiEmailsAndMobileNumbersProperty = null; @Override public String getName() { @@ -139,6 +142,8 @@ public Map getPropertyNameMapping() { "Enable mobile number verification by privileged users"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, "Mobile number verification on update SMS OTP expiry time"); + nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, + "Support multiple emails and mobile numbers per user"); return nameMapping; } @@ -171,6 +176,8 @@ public Map getPropertyDescriptionMapping() { "Validity time of the mobile number confirmation OTP in minutes."); descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, "Allow privileged users to initiate mobile number verification on update."); + descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, + "Allow users to add multiple email addresses and mobile numbers to their account."); return descriptionMapping; } @@ -189,6 +196,7 @@ public String[] getPropertyNames() { properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE); properties.add(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME); properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER); + properties.add(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER); return properties.toArray(new String[0]); } @@ -206,6 +214,7 @@ public Properties getDefaultPropertyValues(String tenantDomain) { String enableMobileNumVerificationOnUpdate = DEFAULT_ENABLE_VALUE_FOR_MOBILE_NUMBER_VERIFICATION_ON_UPDATE; String mobileNumVerificationOnUpdateCodeExpiry = DEFAULT_MOBILE_NUM_VERIFICATION_ON_UPDATE_SMS_OTP_EXPIRY_TIME; String mobileNumVerificationByPrivilegedUsers = DEFAULT_MOBILE_NUM_VERIFICATION_BY_PRIVILEGED_USERS; + String supportMultiEmailsAndMobileNumbers = DEFAULT_SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS; loadConfigurations(); @@ -242,6 +251,9 @@ public Properties getDefaultPropertyValues(String tenantDomain) { if (StringUtils.isNotBlank(mobileNumVerificationByPrivilegedUsersProperty)) { mobileNumVerificationByPrivilegedUsers = mobileNumVerificationByPrivilegedUsersProperty; } + if (StringUtils.isNotBlank(supportMultiEmailsAndMobileNumbersProperty)) { + supportMultiEmailsAndMobileNumbers = supportMultiEmailsAndMobileNumbersProperty; + } Properties properties = new Properties(); properties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION_ON_UPDATE, @@ -266,6 +278,8 @@ public Properties getDefaultPropertyValues(String tenantDomain) { mobileNumVerificationOnUpdateCodeExpiry); properties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, mobileNumVerificationByPrivilegedUsers); + properties.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, + supportMultiEmailsAndMobileNumbers); return properties; } @@ -292,6 +306,7 @@ private void loadConfigurations() { // Read configuration values defined in identity.xml. OMElement userClaimUpdate = IdentityConfigParser.getInstance().getConfigElement(USER_CLAIM_UPDATE_ELEMENT); + OMElement supportMultiEmailsAndMobileNumbers = null; Iterator claims = null; OMElement otpConfigs = null; if (userClaimUpdate != null) { @@ -299,6 +314,9 @@ private void loadConfigurations() { .IDENTITY_DEFAULT_NAMESPACE, CLAIM_ELEMENT)); otpConfigs = userClaimUpdate.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_ELEMENT)); + supportMultiEmailsAndMobileNumbers = userClaimUpdate.getFirstChildWithName( + new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, + SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_ELEMENT)); } if (claims != null) { @@ -362,6 +380,10 @@ private void loadConfigurations() { otpLengthProperty = otpConfigs.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_LENGTH_ELEMENT)).getText(); } + if (supportMultiEmailsAndMobileNumbers != null) { + supportMultiEmailsAndMobileNumbersProperty = supportMultiEmailsAndMobileNumbers.getFirstChildWithName( + new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, ENABLE_ELEMENT)).getText(); + } } @Override @@ -399,7 +421,9 @@ public Map getMetaData() { meta.put(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue())); + meta.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); + return meta; } - } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 7a68b95c70..2cf01490fb 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -91,8 +91,7 @@ public void handleEvent(Event event) throws IdentityEventException { Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); - boolean supportMultipleMobileNumbers = Boolean.parseBoolean(IdentityUtil.getProperty(IdentityRecoveryConstants - .SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); boolean enable = isMobileVerificationOnUpdateEnabled(user.getTenantDomain()); @@ -302,8 +301,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); } - boolean supportMultipleMobileNumbers = Boolean.parseBoolean(IdentityUtil.getProperty(IdentityRecoveryConstants - .SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); String mobileNumber = null; List exisitingVerifiedNumbersList = Utils.getExistingClaimValue(userStoreManager, user, diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 128bb0fe09..9bddda4630 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -94,8 +94,7 @@ public void handleEvent(Event event) throws IdentityEventException { Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); - boolean supportMultipleEmails = Boolean.parseBoolean(IdentityUtil - .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); boolean enable = false; @@ -547,8 +546,7 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } - boolean supportMultipleEmails = Boolean.parseBoolean(IdentityUtil - .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); String emailAddress = null; List existingVerifiedEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 3d61e1bd71..23658ec9fb 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -759,8 +759,8 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi HashMap userClaims = getClaimsListToUpdate(user, verifiedChannelType, externallyVerifiedClaim, recoveryData.getRecoveryScenario().toString()); - boolean supportMultipleEmailsAndMobileNumbers = Boolean.parseBoolean(IdentityUtil - .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user + .getTenantDomain()); if (RecoverySteps.VERIFY_EMAIL.equals(recoveryData.getRecoveryStep())) { String pendingEmailClaimValue = recoveryData.getRemainingSetIds(); @@ -953,8 +953,8 @@ public void confirmVerificationCodeMe(String code, Map propertie UserStoreManager userStoreManager = getUserStoreManager(user); HashMap userClaims = new HashMap<>(); - boolean supportMultipleEmailsAndMobileNumbers = Boolean.parseBoolean(IdentityUtil - .getProperty(IdentityRecoveryConstants.SUPPORT_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); + boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user + .getTenantDomain()); if (RecoverySteps.VERIFY_MOBILE_NUMBER.equals(recoveryData.getRecoveryStep())) { String pendingMobileNumberClaimValue = recoveryData.getRemainingSetIds(); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index d0959e9ad5..e8862711bf 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -1335,6 +1335,23 @@ public static boolean isUseVerifyClaimEnabled() { (IdentityRecoveryConstants.ConnectorConfig.USE_VERIFY_CLAIM_ON_UPDATE)); } + /** + * Check whether the supporting multiple email addresses and mobile numbers per user is enabled. + * + * @return True if the config is set to true, false otherwise. + */ + public static boolean isMultiEmailsAndMobileNumbersPerUserEnabled(String tenantDomain) { + + try { + return Boolean.parseBoolean(getConnectorConfig(IdentityRecoveryConstants.ConnectorConfig + .SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, tenantDomain)); + } catch (IdentityEventException e) { + log.error("Error while getting connector configurations support multi emails and mobile numbers per" + + " user.", e); + return true; + } + } + /** * Trigger recovery event. * diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java index 4383641b4d..2814f8f973 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java @@ -137,6 +137,8 @@ public void testGetPropertyNameMapping() { "Mobile number verification on update SMS OTP expiry time"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, "Enable mobile number verification by privileged users"); + nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, + "Support multiple emails and mobile numbers per user"); Map nameMapping = userClaimUpdateConfig.getPropertyNameMapping(); assertEquals(nameMapping, nameMappingExpected, "Maps are not equal."); } @@ -171,6 +173,8 @@ public void testGetPropertyDescriptionMapping() { "Validity time of the mobile number confirmation OTP in minutes."); descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, "Allow privileged users to initiate mobile number verification on update."); + descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, + "Allow users to add multiple email addresses and mobile numbers to their account."); Map descriptionMapping = userClaimUpdateConfig.getPropertyDescriptionMapping(); assertEquals(descriptionMapping, descriptionMappingExpected, "Maps are not equal."); } @@ -189,6 +193,7 @@ public void testGetPropertyNames() { propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_NOTIFICATION_ON_EMAIL_UPDATE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME); + propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER); String[] propertiesArrayExpected = propertiesExpected.toArray(new String[0]); String[] properties = userClaimUpdateConfig.getPropertyNames(); @@ -244,7 +249,8 @@ public void testGetDefaultProperties() throws IdentityGovernanceException { EMAIL_VERIFICATION_ON_UPDATE_OTP_LENGTH, IdentityRecoveryConstants.ConnectorConfig .EMAIL_VERIFICATION_ON_UPDATE_EXPIRY_TIME, IdentityRecoveryConstants.ConnectorConfig .ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE, IdentityRecoveryConstants.ConnectorConfig - .MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME,"testproperty"}; + .MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, IdentityRecoveryConstants.ConnectorConfig + .SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, "testproperty"}; IdentityConfigParser mockConfigParser = mock(IdentityConfigParser.class); mockedIdentityConfigParser.when(IdentityConfigParser::getInstance).thenReturn(mockConfigParser); From 88e24a90a363d54207d5c959878fa9fee9f604cc Mon Sep 17 00:00:00 2001 From: lashinie Date: Tue, 4 Jun 2024 11:57:31 +0530 Subject: [PATCH 13/54] code refactoring --- .../handler/MobileNumberVerificationHandler.java | 10 +++++----- .../handler/UserEmailVerificationHandler.java | 13 ++++++------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 2cf01490fb..c8bf26022f 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -354,7 +354,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use OTP. Therefore, the mobile number is already verified & no need to verify it again. */ if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString(). - equals(Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate()) && mobileNumber != null) { + equals(Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { invalidatePendingMobileVerification(user, userStoreManager, claims); return; } @@ -430,7 +430,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use */ private List getListOfMobileNumbersFromString(String mobileNumbers) { - return StringUtils.isBlank(mobileNumbers) ? new ArrayList<>() : new LinkedList<>(Arrays.asList( + return StringUtils.isBlank(mobileNumbers) ? new ArrayList<>() : new ArrayList<>(Arrays.asList( mobileNumbers.split(","))).stream().map(String::trim).collect(Collectors.toList()); } @@ -451,9 +451,9 @@ private String getVerificationPendingMobileNumber(List existingVerifiedN if (mobileNumber == null) { mobileNumber = verificationPendingNumber; } else { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getCode(), IdentityRecoveryConstants. - ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getMessage()); + throw new IdentityEventClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFY_MULTIPLE_MOBILE_NUMBERS.getMessage()); } } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 9bddda4630..4992fd3b44 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -119,6 +119,9 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingEmailVerification(user, userStoreManager, claims); } + // Drop the verified email addresses claim as verification on update is not enabled. + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + if (supportMultipleEmails) { List allEmails = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); @@ -133,7 +136,6 @@ public void handleEvent(Event event) throws IdentityEventException { log.debug("Supporting multiple email addresses per user is disabled."); } claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); - claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); } claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); } else { @@ -523,12 +525,6 @@ protected User getUser(Map eventProperties, UserStoreManager userStoreManager) { private void preSetUserClaimsOnEmailUpdate(Map claims, UserStoreManager userStoreManager, User user) throws IdentityEventException { - if (MapUtils.isEmpty(claims)) { - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants. - SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; - } - if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { // Not required to handle in this handler. @@ -659,6 +655,9 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); + } else { + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); } claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); } From 4b619b70d59c68e2b1040285b42781074885eebc Mon Sep 17 00:00:00 2001 From: lashinie Date: Tue, 4 Jun 2024 11:57:31 +0530 Subject: [PATCH 14/54] code refactoring --- .../recovery/handler/MobileNumberVerificationHandler.java | 1 + .../recovery/handler/UserEmailVerificationHandler.java | 7 +++++++ 2 files changed, 8 insertions(+) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index c8bf26022f..a9b26b554b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -415,6 +415,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } else { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; } if (isVerificationPendingMobileClaimConfigAvailable(user.getTenantDomain())) { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 4992fd3b44..42781ced4d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -525,6 +525,12 @@ protected User getUser(Map eventProperties, UserStoreManager userStoreManager) { private void preSetUserClaimsOnEmailUpdate(Map claims, UserStoreManager userStoreManager, User user) throws IdentityEventException { + if (MapUtils.isEmpty(claims)) { + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants. + SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } + if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { // Not required to handle in this handler. @@ -658,6 +664,7 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } else { Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; } claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); } From b24c93c3551c6251aa4ef6b8b5cdd1f203eea309 Mon Sep 17 00:00:00 2001 From: lashinie Date: Tue, 4 Jun 2024 12:09:15 +0530 Subject: [PATCH 15/54] code refactoring --- .../recovery/handler/UserEmailVerificationHandler.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 42781ced4d..16decec1d2 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -660,13 +660,12 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore invalidatePendingEmailVerification(user, userStoreManager, claims); return; } + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); } else { Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; } - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); } /** From 0afcb52a0ae3ae3da7c84ec83165b72137c8c51a Mon Sep 17 00:00:00 2001 From: lashinie Date: Thu, 6 Jun 2024 15:17:03 +0530 Subject: [PATCH 16/54] address comments --- .../recovery/IdentityRecoveryConstants.java | 38 ++++----- .../connector/UserClaimUpdateConfigImpl.java | 34 ++++---- .../MobileNumberVerificationHandler.java | 75 ++++++++++-------- .../handler/UserEmailVerificationHandler.java | 79 +++++++++++-------- .../signup/UserSelfRegistrationManager.java | 12 ++- .../carbon/identity/recovery/util/Utils.java | 8 +- .../UserClaimUpdateConfigImplTest.java | 9 ++- 7 files changed, 135 insertions(+), 120 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 987b2e409c..32b6b76168 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -1,17 +1,19 @@ /* - * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2016-2024, WSO2 LLC. (http://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package org.wso2.carbon.identity.recovery; @@ -449,10 +451,10 @@ public enum ErrorMessages { "simultaneously"), ERROR_CODE_SUPPORT_MULTIPLE_EMAILS_NOT_ENABLED("UEV-10004", "Support for multiple email addresses " + "per user is not enabled"), - ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST("UEV-10005", "As multiple " + + ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST("UEV-10005", "As multiple " + "email addresses support is enabled, primary email address should be included in the email " + "addresses list."), - ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST("UEV-10006", "As multiple " + + ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST("UEV-10006", "As multiple " + "email addresses support and email verification is enabled, primary email address should be included " + "in the verified email addresses list."), @@ -463,12 +465,12 @@ public enum ErrorMessages { "multiple mobile numbers simultaneously."), ERROR_CODE_SUPPORT_MULTIPLE_MOBILE_NUMBERS_NOT_ENABLED("UEV-10003", "Support for multiple mobile " + "numbers per user is not enabled"), - ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST("UMV-10004", "As multiple " + - "mobile numbers support is enabled, primary mobile number should be included in the mobile " + - "numbers list."), - ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST("UMV-10005", "As multiple " + - "mobile numbers support and mobile verification is enabled, primary mobile number should be included " + - "in the verified mobile numbers list."), + ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST("UMV-10004", + "As multiple mobile numbers support is enabled, primary mobile number should be included in " + + "the mobile numbers list."), + ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST("UMV-10005", "As " + + "multiple mobile numbers support and mobile verification is enabled, primary mobile number should be " + + "included in the verified mobile numbers list."), INVALID_PASSWORD_RECOVERY_REQUEST("APR-10000", "Invalid Password Recovery Request"), @@ -689,7 +691,7 @@ public static class ConnectorConfig { public static final String USE_VERIFY_CLAIM_ON_UPDATE = "UserClaimUpdate.UseVerifyClaim"; // This config enables the support to store multiple mobile numbers and email addresses per user. public static final String SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER = - "UserClaimUpdate.SupportMultiEmailsAndMobileNumbers.Enable"; + "UserClaimUpdate.EnableMultipleEmailsAndMobileNumbers"; public static final String ASK_PASSWORD_EXPIRY_TIME = "EmailVerification.AskPassword.ExpiryTime"; public static final String ASK_PASSWORD_TEMP_PASSWORD_GENERATOR = "EmailVerification.AskPassword.PasswordGenerator"; public static final String ASK_PASSWORD_DISABLE_RANDOM_VALUE_FOR_CREDENTIALS = "EmailVerification.AskPassword" + diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java index c26fe95b1d..0268d43078 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java @@ -1,18 +1,21 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020-2024, WSO2 LLC. (http://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations und + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ + package org.wso2.carbon.identity.recovery.connector; import org.apache.axiom.om.OMElement; @@ -72,7 +75,7 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static final String VERIFICATION_ON_UPDATE_ELEMENT = "VerificationOnUpdate"; private static final String NOTIFICATION_ON_UPDATE_ELEMENT = "NotificationOnUpdate"; private static final String ENABLE_MOBILE_VERIFICATION_PRIVILEGED_USER = "EnableVerificationByPrivilegedUser"; - private static final String SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_ELEMENT = "SupportMultiEmailsAndMobileNumbers"; + private static final String ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT = "EnableMultipleEmailsAndMobileNumbers"; private static String enableEmailVerificationOnUpdateProperty = null; private static String enableSendOTPInEmailProperty = null; private static String useUppercaseCharactersInOTPProperty = null; @@ -306,7 +309,6 @@ private void loadConfigurations() { // Read configuration values defined in identity.xml. OMElement userClaimUpdate = IdentityConfigParser.getInstance().getConfigElement(USER_CLAIM_UPDATE_ELEMENT); - OMElement supportMultiEmailsAndMobileNumbers = null; Iterator claims = null; OMElement otpConfigs = null; if (userClaimUpdate != null) { @@ -314,9 +316,9 @@ private void loadConfigurations() { .IDENTITY_DEFAULT_NAMESPACE, CLAIM_ELEMENT)); otpConfigs = userClaimUpdate.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_ELEMENT)); - supportMultiEmailsAndMobileNumbers = userClaimUpdate.getFirstChildWithName( + supportMultiEmailsAndMobileNumbersProperty = userClaimUpdate.getFirstChildWithName( new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, - SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_ELEMENT)); + ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT)).getText(); } if (claims != null) { @@ -335,7 +337,7 @@ private void loadConfigurations() { (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, VERIFICATION_CODE_ELEMENT)); if (verificationCode != null) { emailVerificationOnUpdateCodeExpiryProperty = verificationCode.getFirstChildWithName(new - QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, EXPIRY_TIME_ELEMENT)) + QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, EXPIRY_TIME_ELEMENT)) .getText(); } } @@ -361,7 +363,7 @@ private void loadConfigurations() { (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, VERIFICATION_CODE_ELEMENT)); if (verificationCode != null) { mobileNumVerificationOnUpdateCodeExpiryProperty = verificationCode.getFirstChildWithName( - new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, EXPIRY_TIME_ELEMENT)) + new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, EXPIRY_TIME_ELEMENT)) .getText(); } } @@ -380,10 +382,6 @@ private void loadConfigurations() { otpLengthProperty = otpConfigs.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_LENGTH_ELEMENT)).getText(); } - if (supportMultiEmailsAndMobileNumbers != null) { - supportMultiEmailsAndMobileNumbersProperty = supportMultiEmailsAndMobileNumbers.getFirstChildWithName( - new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, ENABLE_ELEMENT)).getText(); - } } @Override diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index a9b26b554b..ec676299f6 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -1,7 +1,7 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020-2024, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at @@ -11,7 +11,7 @@ * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ @@ -22,11 +22,11 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.base.IdentityRuntimeException; import org.wso2.carbon.identity.core.bean.context.MessageContext; import org.wso2.carbon.identity.core.handler.InitConfig; -import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventClientException; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.IdentityEventException; @@ -54,9 +54,10 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.stream.Collectors; /** @@ -116,15 +117,13 @@ public void handleEvent(Event event) throws IdentityEventException { if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && !allMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode(), + ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode(), IdentityRecoveryConstants.ErrorMessages - .ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getMessage()); + .ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getMessage()); } } else { // Multiple mobile numbers per user support is disabled. - if (log.isDebugEnabled()) { - log.debug("Supporting multiple mobile numbers per user is disabled."); - } + log.debug("Supporting multiple mobile numbers per user is disabled."); claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } @@ -136,15 +135,13 @@ public void handleEvent(Event event) throws IdentityEventException { if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && !verifiedMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode(), + ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode(), IdentityRecoveryConstants.ErrorMessages - .ERROR_CODE_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getMessage()); + .ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getMessage()); } } else { // Multiple mobile numbers per user support is disabled. - if (log.isDebugEnabled()) { - log.debug("Supporting multiple mobile numbers per user is disabled."); - } + log.debug("Supporting multiple mobile numbers per user is disabled."); claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } @@ -302,21 +299,23 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); String mobileNumber = null; + List exisitingVerifiedNumbersList = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; + + List exisitingAllNumbersList = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? + getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : + exisitingAllNumbersList; if (supportMultipleMobileNumbers) { - List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. - VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - - List exisitingAllNumbersList = Utils.getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); - List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? - getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : - exisitingAllNumbersList; /* Finds the verification pending mobile number and remove it from the verified numbers list in the payload. */ @@ -337,9 +336,10 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } } } - claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, String.join(",", updatedAllNumbersList)); - claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(",", - updatedVerifiedNumbersList)); + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedAllNumbersList)); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); } else { claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); @@ -390,11 +390,15 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use invalidatePendingMobileVerification(user, userStoreManager, claims); if (supportMultipleMobileNumbers) { - if (exisitingVerifiedNumbersList != null && !exisitingVerifiedNumbersList.contains( - mobileNumber)) { - exisitingVerifiedNumbersList.add(mobileNumber); - claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(",", - exisitingVerifiedNumbersList)); + if (!updatedVerifiedNumbersList.contains(mobileNumber)) { + updatedVerifiedNumbersList.add(mobileNumber); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); + } + if (!updatedAllNumbersList.contains(mobileNumber)) { + updatedAllNumbersList.add(mobileNumber); + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedAllNumbersList)); } } return; @@ -431,8 +435,9 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use */ private List getListOfMobileNumbersFromString(String mobileNumbers) { + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); return StringUtils.isBlank(mobileNumbers) ? new ArrayList<>() : new ArrayList<>(Arrays.asList( - mobileNumbers.split(","))).stream().map(String::trim).collect(Collectors.toList()); + mobileNumbers.split(multiAttributeSeparator))).stream().map(String::trim).collect(Collectors.toList()); } /** @@ -446,9 +451,11 @@ private String getVerificationPendingMobileNumber(List existingVerifiedN List updatedVerifiedNumbersList) throws IdentityEventException { + Set existingVerifiedNumbersSet = new HashSet<>(existingVerifiedNumbersList); String mobileNumber = null; + for (String verificationPendingNumber : updatedVerifiedNumbersList) { - if (!existingVerifiedNumbersList.contains(verificationPendingNumber)) { + if (!existingVerifiedNumbersSet.contains(verificationPendingNumber)) { if (mobileNumber == null) { mobileNumber = verificationPendingNumber; } else { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 16decec1d2..de9cd2f541 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -1,17 +1,19 @@ /* - * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2016-2024, WSO2 LLC. (http://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations und + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package org.wso2.carbon.identity.recovery.handler; @@ -21,6 +23,7 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.base.IdentityRuntimeException; import org.wso2.carbon.identity.core.bean.context.MessageContext; @@ -60,8 +63,8 @@ import java.util.UUID; import java.util.stream.Collectors; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND; public class UserEmailVerificationHandler extends AbstractEventHandler { @@ -127,8 +130,8 @@ public void handleEvent(Event event) throws IdentityEventException { IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && !allEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { - throw new IdentityEventClientException(ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST - .getCode(), ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST.getMessage()); + throw new IdentityEventClientException(ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST + .getCode(), ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST.getMessage()); } } else { // Supporting multiple email addresses per user is disabled. @@ -145,8 +148,8 @@ public void handleEvent(Event event) throws IdentityEventException { if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && !verifiedEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { throw new IdentityEventClientException( - ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getCode(), - ERROR_CODE_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getMessage()); + ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getCode(), + ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getMessage()); } } else { // Multiple email addresses per user support is disabled. @@ -549,24 +552,25 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); String emailAddress = null; + List existingVerifiedEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + List existingAllEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + + List updatedVerifiedEmailAddresses = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_EMAIL_ADDRESSES_CLAIM) ? getListOfEmailAddressesFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) : existingVerifiedEmailAddresses; + List updatedAllEmailAddresses = claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM) ? + getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : + existingAllEmailAddresses; // Handle email addresses and verified email addresses claims. if (supportMultipleEmails) { - List updatedVerifiedEmailAddresses = claims.containsKey(IdentityRecoveryConstants. - VERIFIED_EMAIL_ADDRESSES_CLAIM) ? getListOfEmailAddressesFromString(claims.get( - IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) : existingVerifiedEmailAddresses; - - List existingAllEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, - IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); - List updatedAllEmailAddresses = claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM) ? - getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : - existingAllEmailAddresses; - // Find the verification pending email address and remove it from verified email addresses list in the payload. if (updatedVerifiedEmailAddresses != null) { emailAddress = getVerificationPendingEmailAddress(existingVerifiedEmailAddresses, @@ -587,10 +591,10 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } } } - claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( - updatedVerifiedEmailAddresses, ",")); - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, StringUtils.join( - updatedAllEmailAddresses, ",")); + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); } else { /* email addresses and verified email addresses should not be updated when support for multiple email @@ -638,11 +642,15 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore invalidatePendingEmailVerification(user, userStoreManager, claims); if (supportMultipleEmails) { - if (existingVerifiedEmailAddresses!= null && - !existingVerifiedEmailAddresses.contains(emailAddress)) { - existingVerifiedEmailAddresses.add(emailAddress); - claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( - existingVerifiedEmailAddresses, ",")); + if (!updatedVerifiedEmailAddresses.contains(emailAddress)) { + updatedVerifiedEmailAddresses.add(emailAddress); + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); + } + if (!updatedAllEmailAddresses.contains(emailAddress)) { + updatedAllEmailAddresses.add(emailAddress); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); } } return; @@ -703,8 +711,9 @@ private String getVerificationPendingEmailAddress(List existingVerifiedE */ private List getListOfEmailAddressesFromString(String emails) { - return emails != null ? new LinkedList<>(Arrays.asList(emails.split(","))).stream().map(String::trim) - .collect(Collectors.toList()) : new ArrayList<>(); + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); + return emails != null ? new LinkedList<>(Arrays.asList(emails.split(multiAttributeSeparator))).stream() + .map(String::trim).collect(Collectors.toList()) : new ArrayList<>(); } private void postSetUserClaimsOnEmailUpdate(User user, UserStoreManager userStoreManager) throws diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 23658ec9fb..1cae62b104 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -1,12 +1,12 @@ /* - * Copyright (c) 2016, WSO2 LLC. (https://www.wso2.com) All Rights Reserved. + * Copyright (c) 2016-2024, WSO2 LLC. (http://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 * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an @@ -770,16 +770,14 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi if (supportMultipleEmailsAndMobileNumbers) { try { List verifiedEmails = Utils.getExistingClaimValue( - (org.wso2.carbon.user.core.UserStoreManager) eventProperties.get( - IdentityEventConstants.EventProperty.USER_STORE_MANAGER), user, + (org.wso2.carbon.user.core.UserStoreManager) userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); verifiedEmails.add(pendingEmailClaimValue); userClaims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( verifiedEmails, ",")); List allEmails = Utils.getExistingClaimValue( - (org.wso2.carbon.user.core.UserStoreManager) eventProperties.get( - IdentityEventConstants.EventProperty.USER_STORE_MANAGER), user, + (org.wso2.carbon.user.core.UserStoreManager) userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); if (!allEmails.contains(pendingEmailClaimValue)) { allEmails.add(pendingEmailClaimValue); @@ -1002,7 +1000,7 @@ public void confirmVerificationCodeMe(String code, Map propertie userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); } userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index e8862711bf..7e4aaf7170 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -1,8 +1,8 @@ /* - * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2016-2024, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, - * Version 2.0 (the "License"); you may not use this file except + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * @@ -11,7 +11,7 @@ * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java index 2814f8f973..08383d690d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java @@ -1,20 +1,21 @@ /* - * Copyright (c) 2020, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2020-2024, WSO2 LLC. (http://www.wso2.com). * - * WSO2 Inc. licenses this file to you under the Apache License, + * WSO2 LLC. licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file except * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the + * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ + package org.wso2.carbon.identity.recovery.connector; import org.apache.axiom.om.OMElement; From caa6430858d99d6060e8a0c73c683a541aee0ed7 Mon Sep 17 00:00:00 2001 From: lashinie Date: Tue, 4 Jun 2024 11:57:31 +0530 Subject: [PATCH 17/54] address comments --- .../java/org/wso2/carbon/identity/recovery/util/Utils.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index 7e4aaf7170..77aeeff273 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -27,6 +27,7 @@ import org.json.JSONObject; import org.wso2.carbon.CarbonConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.Property; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.auth.attribute.handler.exception.AuthAttributeHandlerClientException; @@ -1709,10 +1710,11 @@ public static List getExistingClaimValue(org.wso2.carbon.user.core.UserS List existingClaimValue; try { + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); existingClaimValue = StringUtils.isNotBlank(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null)) ? new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, - null).split(","))) : new ArrayList<>(); + null).split(multiAttributeSeparator))) : new ArrayList<>(); } catch (org.wso2.carbon.user.core.UserStoreException e) { throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + " for user: " + user.toFullQualifiedUsername(), e); From ad929248affb72d68991095accb54013b0fdd6ce Mon Sep 17 00:00:00 2001 From: lashinie Date: Thu, 6 Jun 2024 17:24:51 +0530 Subject: [PATCH 18/54] update unit tests --- .../recovery/connector/UserClaimUpdateConfigImpl.java | 8 ++++++-- .../recovery/connector/UserClaimUpdateConfigImplTest.java | 7 ++++++- 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java index 0268d43078..5d66d4c8f5 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java @@ -311,14 +311,15 @@ private void loadConfigurations() { OMElement userClaimUpdate = IdentityConfigParser.getInstance().getConfigElement(USER_CLAIM_UPDATE_ELEMENT); Iterator claims = null; OMElement otpConfigs = null; + OMElement supportMultiEmailsAndMobileNumbers = null; if (userClaimUpdate != null) { claims = userClaimUpdate.getChildrenWithName(new QName(IdentityCoreConstants .IDENTITY_DEFAULT_NAMESPACE, CLAIM_ELEMENT)); otpConfigs = userClaimUpdate.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_ELEMENT)); - supportMultiEmailsAndMobileNumbersProperty = userClaimUpdate.getFirstChildWithName( + supportMultiEmailsAndMobileNumbers = userClaimUpdate.getFirstChildWithName( new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, - ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT)).getText(); + ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT)); } if (claims != null) { @@ -382,6 +383,9 @@ private void loadConfigurations() { otpLengthProperty = otpConfigs.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_LENGTH_ELEMENT)).getText(); } + if (supportMultiEmailsAndMobileNumbers != null) { + supportMultiEmailsAndMobileNumbersProperty = supportMultiEmailsAndMobileNumbers.getText(); + } } @Override diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java index 08383d690d..f4ba4f959e 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java @@ -62,9 +62,11 @@ public class UserClaimUpdateConfigImplTest { private static final String VERIFICATION_CODE_ELEMENT = "VerificationCode"; private static final String EXPIRY_TIME_ELEMENT = "ExpiryTime"; private static final String VERIFICATION_ON_UPDATE_ELEMENT = "VerificationOnUpdate"; + private static final String ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT = + "EnableMultipleEmailsAndMobileNumbers"; private MockedStatic mockedIdentityConfigParser; - @BeforeTest + @BeforeMethod public void init() { userClaimUpdateConfig = new UserClaimUpdateConfigImpl(); @@ -195,6 +197,7 @@ public void testGetPropertyNames() { propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER); + propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER); String[] propertiesArrayExpected = propertiesExpected.toArray(new String[0]); String[] properties = userClaimUpdateConfig.getPropertyNames(); @@ -226,6 +229,8 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { VERIFICATION_CODE_ELEMENT))).thenReturn(mockOMElement); when(mockOMElement.getFirstChildWithName(new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, EXPIRY_TIME_ELEMENT))).thenReturn(mockOMElement); + when(mockOMElement.getFirstChildWithName(new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, + USER_CLAIM_UPDATE_ELEMENT))).thenReturn(mockOMElement); Properties defaultPropertyValues = userClaimUpdateConfig.getDefaultPropertyValues(TENANT_DOMAIN); assertNotNull(defaultPropertyValues.getProperty(IdentityRecoveryConstants.ConnectorConfig From a633496c71920886c4865b36e437b3c28fc2c5fb Mon Sep 17 00:00:00 2001 From: Lashini Jayasekara <30428591+lashinijay@users.noreply.github.com> Date: Thu, 6 Jun 2024 17:26:50 +0530 Subject: [PATCH 19/54] Update components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java --- .../recovery/connector/UserClaimUpdateConfigImplTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java index f4ba4f959e..4e57464165 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java @@ -66,7 +66,7 @@ public class UserClaimUpdateConfigImplTest { "EnableMultipleEmailsAndMobileNumbers"; private MockedStatic mockedIdentityConfigParser; - @BeforeMethod + @BeforeTest public void init() { userClaimUpdateConfig = new UserClaimUpdateConfigImpl(); From d8f0d29a8872f2ccadab2cc7b8a685a50e62d5d6 Mon Sep 17 00:00:00 2001 From: lashinie Date: Mon, 8 Jul 2024 16:08:31 +0530 Subject: [PATCH 20/54] fix deleting primary email address and mobile number flow --- .../recovery/handler/MobileNumberVerificationHandler.java | 2 ++ .../identity/recovery/handler/UserEmailVerificationHandler.java | 2 ++ 2 files changed, 4 insertions(+) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index ec676299f6..b3140b10b4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -115,6 +115,7 @@ public void handleEvent(Event event) throws IdentityEventException { List allMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && + !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty() && !allMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode(), @@ -133,6 +134,7 @@ public void handleEvent(Event event) throws IdentityEventException { List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && + !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty() && !verifiedMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode(), diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index de9cd2f541..76a0b2ac8f 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -129,6 +129,7 @@ public void handleEvent(Event event) throws IdentityEventException { List allEmails = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && + !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty() && !allEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { throw new IdentityEventClientException(ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST .getCode(), ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST.getMessage()); @@ -146,6 +147,7 @@ public void handleEvent(Event event) throws IdentityEventException { List verifiedEmails = Utils.getExistingClaimValue(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && + !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty() && !verifiedEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { throw new IdentityEventClientException( ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getCode(), From 852bc401e08f8a1ee12d8c7af97f4befd8d2ed8f Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 12 Sep 2024 09:52:36 +0530 Subject: [PATCH 21/54] Add mobile number verification handler tests. --- .../recovery/IdentityRecoveryConstants.java | 3 + .../MobileNumberVerificationHandler.java | 3 + .../MobileNumberVerificationHandlerTest.java | 498 ++++++++++++++++++ 3 files changed, 504 insertions(+) create mode 100644 components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java index 3e8f0f371e..91961a28b0 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/IdentityRecoveryConstants.java @@ -219,6 +219,9 @@ public class IdentityRecoveryConstants { public static final String ACCOUNT_STATUS_DISABLED = "password.recovery.failed.account.disabled"; public static final String IGNORE_IF_TEMPLATE_NOT_FOUND = "ignoreIfTemplateNotFound"; + public static final String TRUE = "true"; + public static final String FALSE = "false"; + private IdentityRecoveryConstants() { } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index b3140b10b4..8547fccb1f 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -91,6 +91,9 @@ public void handleEvent(Event event) throws IdentityEventException { getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME)); Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); + if (claims == null) { + claims = new HashMap<>(); + } boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java new file mode 100644 index 0000000000..ef2c8a8f3b --- /dev/null +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -0,0 +1,498 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.recovery.handler; + +import org.apache.commons.lang.StringUtils; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.event.IdentityEventClientException; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.event.Event; +import org.testng.annotations.Test; +import org.testng.Assert; +import org.wso2.carbon.identity.event.IdentityEventConstants; +import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; +import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; +import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; +import org.wso2.carbon.identity.recovery.util.Utils; +import org.wso2.carbon.user.api.Claim; +import org.wso2.carbon.user.api.ClaimManager; +import org.wso2.carbon.user.core.tenant.TenantManager; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.user.core.UserStoreManager; +import org.wso2.carbon.user.core.config.RealmConfiguration; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.user.api.UserStoreException; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +/** + * Unit test cases for MobileNumberVerificationHandler. + */ +public class MobileNumberVerificationHandlerTest { + + @InjectMocks + private MobileNumberVerificationHandler mobileNumberVerificationHandler; + + @Mock + private UserStoreManager userStoreManager; + + @Mock + private RealmConfiguration realmConfiguration; + + @Mock + private ClaimManager claimManager; + + @Mock + private RealmService realmService; + + @Mock + private UserRealm userRealm; + + @Mock + private TenantManager tenantManager; + + @Mock + private JDBCRecoveryDataStore jdbcRecoveryDataStore; + + @Mock + private UserRecoveryDataStore userRecoveryDataStore; + + @Mock + private IdentityEventService identityEventService; + + @Mock + private IdentityRecoveryServiceDataHolder serviceDataHolder; + + private MockedStatic mockedJDBCRecoveryDataStore; + private MockedStatic mockedUtils; + private MockedStatic mockedIdentityRecoveryServiceDataHolder; + private MockedStatic mockedFrameworkUtils; + + private static final String username = "testuser"; + private static final String tenantDomain = "test.com"; + private static final int tenantId = 5; + private static final String userStoreDomain = "TESTING"; + private static final String existingNumber1 = "0777777777"; + private static final String existingNumber2 = "0711111111"; + private static final String newMobileNumber = "0722222222"; + + @BeforeMethod + public void setUp() throws UserStoreException { + + MockitoAnnotations.openMocks(this); + mobileNumberVerificationHandler = new MobileNumberVerificationHandler(); + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedUtils = mockStatic(Utils.class); + mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + mockedFrameworkUtils = mockStatic(FrameworkUtils.class); + + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); + mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) + .thenReturn(serviceDataHolder); + mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + + when(serviceDataHolder.getRealmService()).thenReturn(realmService); + when(serviceDataHolder.getIdentityEventService()).thenReturn(identityEventService); + when(realmService.getTenantManager()).thenReturn(tenantManager); + when(tenantManager.getTenantId(tenantDomain)).thenReturn(tenantId); + when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); + when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); + when(userRealm.getClaimManager()).thenReturn(claimManager); + when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); + when(realmConfiguration.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME)) + .thenReturn(userStoreDomain); + } + + @AfterMethod + public void tearDown() { + + mockedJDBCRecoveryDataStore.close(); + mockedUtils.close(); + mockedIdentityRecoveryServiceDataHolder.close(); + mockedFrameworkUtils.close(); + } + + @Test + public void testGetNames() { + + Assert.assertEquals(mobileNumberVerificationHandler.getName(), "userMobileVerification"); + Assert.assertEquals(mobileNumberVerificationHandler.getFriendlyName(), "User Mobile Number Verification"); + } + + @Test(description = "Verification disabled, Multi-attribute disabled, Change primary mobile") + public void testHandleEventVerificationDisabledMultiAttributeDisabled() + throws UserStoreException, IdentityEventException, IdentityRecoveryException { + + Event event = + getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, IdentityRecoveryConstants.FALSE, + existingNumber1); + + mockVerificationPendingMobileNumber(); + mockUtilMethods(false, false, false); + + mobileNumberVerificationHandler.handleEvent(event); + verify(userRecoveryDataStore).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), + eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + } + + @Test(description = "Verification disabled, Multi-attribute enabled, Change primary mobile") + public void testHandleEventVerificationDisabledMultiAttributeEnabled() + throws UserStoreException, IdentityEventException, IdentityRecoveryException { + + Event event = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, IdentityRecoveryConstants.FALSE, + existingNumber1); + + mockVerificationPendingMobileNumber(); + mockUtilMethods(false, true, false); + + // Mobile number is not included in all mobile numbers list. + List allMobileNumbers = Arrays.asList(existingNumber1, existingNumber2); + mockExistingNumbersList(allMobileNumbers); + + try { + mobileNumberVerificationHandler.handleEvent(event); + } catch (IdentityEventClientException e) { + Assert.assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode()); + } + } + + @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Change primary mobile") + public void testHandleEventVerificationEnabledMultiAttributeDisabledPreSet() + throws UserStoreException, IdentityEventException, IdentityRecoveryException { + + mockVerificationPendingMobileNumber(); + mockUtilMethods(true, false, false); + + // Case 1: Claims null. + Event event1 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS); + mobileNumberVerificationHandler.handleEvent(event1); + mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( + eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + + /* + * Case 2: Thread local set to skip. + * Expected: Invalidation should be triggered. + */ + Event event2 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + existingNumber1); + mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) + .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_CONFIRM.toString()); + mockVerificationPendingMobileNumber(); + + mobileNumberVerificationHandler.handleEvent(event2); + verify(userRecoveryDataStore, atLeastOnce()).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), + eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + + /* + * Case 3: Thread local set to some value. + * Expected: Invalidation should be triggered. + */ + Event event3 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + existingNumber1); + mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) + .thenReturn("test"); + + mobileNumberVerificationHandler.handleEvent(event3); + mockedUtils.verify(Utils::unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate); + + /* + * Case 4: Claims not null, new mobile number is sent to be verified. + * Expected: New mobile number should be added to the mobileNumber.pendingValue claim. + */ + String newVerifiedMobileNumbersList = existingNumber1 + "," + newMobileNumber; + String newMobileNumbersList = existingNumber1 + "," + existingNumber2; + Event event4 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbersList, newMobileNumbersList); + + // Mock existing verified mobile numbers. + List exisitingVerifiedNumbersList = Arrays.asList(existingNumber1, existingNumber2); + mockedUtils.when(() -> Utils.getExistingClaimValue(any(), any(), + eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM))) + .thenReturn(exisitingVerifiedNumbersList); + + List existingAllMobileNumbers = Collections.singletonList(existingNumber1); + mockExistingNumbersList(existingAllMobileNumbers); + + mobileNumberVerificationHandler.handleEvent(event4); + Map userClaimsEvent2 = getUserClaimsFromEvent(event4); + Assert.assertEquals(userClaimsEvent2.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + newMobileNumber); + + /* + * Case 5: Claims not null, new mobile number is same as the existing mobile number. + * Expected: Pending mobile verification should be invalidated. + */ + mockExistingPrimaryMobileNumber(newMobileNumber); + Event event5 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbersList, newMobileNumbersList); + + mobileNumberVerificationHandler.handleEvent(event5); + Map userClaimsEvent3 = getUserClaimsFromEvent(event5); + + verify(userRecoveryDataStore, atLeastOnce()).invalidate(any(), + eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + Assert.assertEquals(userClaimsEvent3.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + ""); + } + + @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute enabled, Change primary mobile") + public void testHandleEventVerificationEnabledMultiAttributeEnabledPreSet() + throws UserStoreException, IdentityEventException, IdentityRecoveryException { + + mockVerificationPendingMobileNumber(); + mockUtilMethods(true, true, false); + + /* + * Case 1: Try to update primary mobile number which is not in the verified mobile numbers list. + * Expected: IdentityEventClientException + */ + String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; + String newMobileNumbers = existingNumber1 + "," + existingNumber2; + Event event1 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers, newMobileNumbers); + + List exisitingVerifiedNumbersList = new ArrayList<>(Arrays.asList(existingNumber1)); + mockExistingVerifiedNumbersList(exisitingVerifiedNumbersList); + + List existingAllMobileNumbers = new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber)); + mockExistingNumbersList(existingAllMobileNumbers); + + try { + mobileNumberVerificationHandler.handleEvent(event1); + } catch (IdentityEventClientException e) { + Assert.assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode()); + } + + /* + * Case 2: Try to update primary mobile number which is in the verified mobile numbers list. + * Expected: Thread local should be set to skip sending SMS OTP verification. + */ + List existingVerifiedNumbersList1 = new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber)); + mockExistingVerifiedNumbersList(existingVerifiedNumbersList1); + + mobileNumberVerificationHandler.handleEvent(event1); + mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( + eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + + /* + * Case 3: Try to update verified numbers list with a new mobile number, which is not in the all mobile numbers. + */ + String newVerifiedMobileNumbers3 = existingNumber1 + "," + newMobileNumber; + Event event3 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers3, null, null); + + List existingVerifiedNumbersList3 = new ArrayList<>(Arrays.asList(existingNumber1)); + mockExistingVerifiedNumbersList(existingVerifiedNumbersList3); + + List existingAllMobileNumbers3 = new ArrayList<>(Arrays.asList(existingNumber1)); + mockExistingNumbersList(existingAllMobileNumbers3); + + mobileNumberVerificationHandler.handleEvent(event3); + Map userClaimsCase3 = getUserClaimsFromEvent(event3); + Assert.assertEquals(userClaimsCase3.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + newMobileNumber); + + /* + * Case 4: Try to update verified numbers list with the new mobile number, which is not in the all + * mobile numbers. + * Set the new mobile number as existing primary mobile number. + * Expected: Verification skip thread local should be set. + * Expected: The new mobile number should be added to the mobileNumbers claim. + */ + Event event4 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers3, null, null); + + mockExistingPrimaryMobileNumber(newMobileNumber); + mobileNumberVerificationHandler.handleEvent(event4); + Map userClaimsCase4 = getUserClaimsFromEvent(event4); + Assert.assertTrue(StringUtils.contains(userClaimsCase4.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), + newMobileNumber)); + Assert.assertTrue(StringUtils.contains( + userClaimsCase4.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM), + newMobileNumber)); + mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( + eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_EXISTING_MOBILE_NUM.toString()))); + + } + + @Test(description = "POST_SET_USER_CLAIMS: Verification enabled, Multi-attribute enabled") + public void testHandleEventPostSet() throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + Event event = getEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS); + mockUtilMethods(true, true, false); + + /* + Case 1: skipSendingSmsOtpVerificationOnUpdate set to skip. + Expected: Invalidation should not be triggered. + */ + mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) + .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_EXISTING_MOBILE_NUM.toString()); + mobileNumberVerificationHandler.handleEvent(event); + verify(userRecoveryDataStore, never()).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), + eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + + /* + Case 2: skipSendingSmsOtpVerificationOnUpdate set to null. + Expected: Notification event should be triggered. + */ + mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) + .thenReturn(null); + mockVerificationPendingMobileNumber(); + mobileNumberVerificationHandler.handleEvent(event); + verify(identityEventService).handleEvent(any()); + } + + private void mockExistingPrimaryMobileNumber(String mobileNumber) throws UserStoreException { + + when(userStoreManager.getUserClaimValue(anyString(), + eq(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM), isNull())).thenReturn(mobileNumber); + } + + private void mockExistingNumbersList(List existingAllMobileNumbers) { + + mockedUtils.when(() -> Utils.getExistingClaimValue(any(), any(), + eq(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM))) + .thenReturn(existingAllMobileNumbers); + } + + private void mockExistingVerifiedNumbersList(List exisitingVerifiedNumbersList) { + + mockedUtils.when(() -> Utils.getExistingClaimValue(any(), any(), + eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM))) + .thenReturn(exisitingVerifiedNumbersList); + } + + + @SuppressWarnings("unchecked") + private static Map getUserClaimsFromEvent(Event event2) { + + Map eventProperties = event2.getEventProperties(); + return (Map) eventProperties.get(IdentityEventConstants.EventProperty.USER_CLAIMS); + } + + private void mockUtilMethods(boolean mobileVerificationEnabled, boolean multiAttributeEnabled, + boolean useVerifyClaimEnabled) { + + mockedUtils.when(() -> + Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(anyString())).thenReturn(multiAttributeEnabled); + mockedUtils.when(Utils::isUseVerifyClaimEnabled).thenReturn(useVerifyClaimEnabled); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE), + anyString())) + .thenReturn(String.valueOf(mobileVerificationEnabled)); + } + + private void mockVerificationPendingMobileNumber() throws UserStoreException { + + // Verification pending mobile number claim config. + Claim pendingMobileNumberClaim = new Claim(); + pendingMobileNumberClaim.setClaimUri(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM); + when(claimManager.getClaim(eq(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM))) + .thenReturn(pendingMobileNumberClaim); + + Map pendingMobileNumberClaimMap = new HashMap<>(); + pendingMobileNumberClaimMap.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, existingNumber2); + when(userStoreManager.getUserClaimValues(eq(username), + eq(new String[]{IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM}), isNull())) + .thenReturn(pendingMobileNumberClaimMap); + } + + private Event getEvent(String eventType) { + + return getEvent(eventType, null, null, null, + null); + } + + private Event getEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim) { + + String mobileNumberClaim = existingNumber1 + "," + existingNumber2; + return getEvent(eventType, verifyMobileClaim, verifiedMobileNumbersClaim, mobileNumberClaim , newMobileNumber); + } + + private Event getEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim, + String mobileNumbersClaim) { + + return getEvent(eventType, verifyMobileClaim, verifiedMobileNumbersClaim, mobileNumbersClaim, newMobileNumber); + } + + private Event getEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim, + String mobileNumbersClaim, String mobileNumber) { + + Map eventProperties = getEventProperties(); + Map claims = new HashMap<>(); + if (mobileNumber != null && !mobileNumber.isEmpty()) { + claims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, mobileNumber); + } + if (verifyMobileClaim != null) { + claims.put(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM, verifyMobileClaim); + } + if (mobileNumbersClaim != null) { + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, mobileNumbersClaim); + } + if (verifiedMobileNumbersClaim != null) { + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, verifiedMobileNumbersClaim); + } + eventProperties.put(IdentityEventConstants.EventProperty.USER_CLAIMS, claims); + return new Event(eventType, eventProperties); + } + + private Map getEventProperties() { + + Map eventProperties = new HashMap<>(); + eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, username); + eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); + return eventProperties; + } +} From 8478c0728a4b1446d0072fc7f9dd21fc3df39abf Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 12 Sep 2024 09:52:46 +0530 Subject: [PATCH 22/54] Add mobile number verification handler tests. --- .../src/test/resources/testng.xml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml index 7c8f7107ff..0f054a3596 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml @@ -30,4 +30,9 @@ + + + + + From 233f288ecb475fa5a6ec788ff40297db062167d0 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 12 Sep 2024 11:31:36 +0530 Subject: [PATCH 23/54] Remove governance config and get it from IdentityUtil. --- .../connector/UserClaimUpdateConfigImpl.java | 23 ------------------- .../carbon/identity/recovery/util/Utils.java | 10 ++------ .../UserClaimUpdateConfigImplTest.java | 8 +------ 3 files changed, 3 insertions(+), 38 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java index 5d66d4c8f5..0165bbec94 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java @@ -59,7 +59,6 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static final String DEFAULT_MOBILE_NUM_VERIFICATION_ON_UPDATE_SMS_OTP_EXPIRY_TIME = "5"; private static final String DEFAULT_ENABLE_VALUE_FOR_MOBILE_NUMBER_VERIFICATION_ON_UPDATE = "false"; private static final String DEFAULT_MOBILE_NUM_VERIFICATION_BY_PRIVILEGED_USERS = "false"; - private static final String DEFAULT_SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS = "true"; private static final String USER_CLAIM_UPDATE_ELEMENT = "UserClaimUpdate"; private static final String ENABLE_ELEMENT = "Enable"; private static final String SEND_OTP_IN_EMAIL_ELEMENT = "SendOTPInEmail"; @@ -87,7 +86,6 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static String enableMobileNumVerificationOnUpdateProperty = null; private static String mobileNumVerificationOnUpdateCodeExpiryProperty = null; private static String mobileNumVerificationByPrivilegedUsersProperty = null; - private static String supportMultiEmailsAndMobileNumbersProperty = null; @Override public String getName() { @@ -145,8 +143,6 @@ public Map getPropertyNameMapping() { "Enable mobile number verification by privileged users"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, "Mobile number verification on update SMS OTP expiry time"); - nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, - "Support multiple emails and mobile numbers per user"); return nameMapping; } @@ -179,8 +175,6 @@ public Map getPropertyDescriptionMapping() { "Validity time of the mobile number confirmation OTP in minutes."); descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, "Allow privileged users to initiate mobile number verification on update."); - descriptionMapping.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, - "Allow users to add multiple email addresses and mobile numbers to their account."); return descriptionMapping; } @@ -199,7 +193,6 @@ public String[] getPropertyNames() { properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE); properties.add(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME); properties.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER); - properties.add(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER); return properties.toArray(new String[0]); } @@ -217,7 +210,6 @@ public Properties getDefaultPropertyValues(String tenantDomain) { String enableMobileNumVerificationOnUpdate = DEFAULT_ENABLE_VALUE_FOR_MOBILE_NUMBER_VERIFICATION_ON_UPDATE; String mobileNumVerificationOnUpdateCodeExpiry = DEFAULT_MOBILE_NUM_VERIFICATION_ON_UPDATE_SMS_OTP_EXPIRY_TIME; String mobileNumVerificationByPrivilegedUsers = DEFAULT_MOBILE_NUM_VERIFICATION_BY_PRIVILEGED_USERS; - String supportMultiEmailsAndMobileNumbers = DEFAULT_SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS; loadConfigurations(); @@ -254,9 +246,6 @@ public Properties getDefaultPropertyValues(String tenantDomain) { if (StringUtils.isNotBlank(mobileNumVerificationByPrivilegedUsersProperty)) { mobileNumVerificationByPrivilegedUsers = mobileNumVerificationByPrivilegedUsersProperty; } - if (StringUtils.isNotBlank(supportMultiEmailsAndMobileNumbersProperty)) { - supportMultiEmailsAndMobileNumbers = supportMultiEmailsAndMobileNumbersProperty; - } Properties properties = new Properties(); properties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION_ON_UPDATE, @@ -281,8 +270,6 @@ public Properties getDefaultPropertyValues(String tenantDomain) { mobileNumVerificationOnUpdateCodeExpiry); properties.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, mobileNumVerificationByPrivilegedUsers); - properties.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, - supportMultiEmailsAndMobileNumbers); return properties; } @@ -311,15 +298,11 @@ private void loadConfigurations() { OMElement userClaimUpdate = IdentityConfigParser.getInstance().getConfigElement(USER_CLAIM_UPDATE_ELEMENT); Iterator claims = null; OMElement otpConfigs = null; - OMElement supportMultiEmailsAndMobileNumbers = null; if (userClaimUpdate != null) { claims = userClaimUpdate.getChildrenWithName(new QName(IdentityCoreConstants .IDENTITY_DEFAULT_NAMESPACE, CLAIM_ELEMENT)); otpConfigs = userClaimUpdate.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_ELEMENT)); - supportMultiEmailsAndMobileNumbers = userClaimUpdate.getFirstChildWithName( - new QName(IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, - ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT)); } if (claims != null) { @@ -383,9 +366,6 @@ private void loadConfigurations() { otpLengthProperty = otpConfigs.getFirstChildWithName(new QName (IdentityCoreConstants.IDENTITY_DEFAULT_NAMESPACE, OTP_LENGTH_ELEMENT)).getText(); } - if (supportMultiEmailsAndMobileNumbers != null) { - supportMultiEmailsAndMobileNumbersProperty = supportMultiEmailsAndMobileNumbers.getText(); - } } @Override @@ -423,9 +403,6 @@ public Map getMetaData() { meta.put(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue())); - meta.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, - getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); - return meta; } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index b41a50e968..ce386d2661 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -1356,14 +1356,8 @@ public static boolean isUseVerifyClaimEnabled() { */ public static boolean isMultiEmailsAndMobileNumbersPerUserEnabled(String tenantDomain) { - try { - return Boolean.parseBoolean(getConnectorConfig(IdentityRecoveryConstants.ConnectorConfig - .SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, tenantDomain)); - } catch (IdentityEventException e) { - log.error("Error while getting connector configurations support multi emails and mobile numbers per" + - " user.", e); - return true; - } + return Boolean.parseBoolean(IdentityUtil.getProperty( + IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); } /** diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java index 4e57464165..ed38fc30f0 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java @@ -140,8 +140,6 @@ public void testGetPropertyNameMapping() { "Mobile number verification on update SMS OTP expiry time"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, "Enable mobile number verification by privileged users"); - nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, - "Support multiple emails and mobile numbers per user"); Map nameMapping = userClaimUpdateConfig.getPropertyNameMapping(); assertEquals(nameMapping, nameMappingExpected, "Maps are not equal."); } @@ -176,8 +174,6 @@ public void testGetPropertyDescriptionMapping() { "Validity time of the mobile number confirmation OTP in minutes."); descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER, "Allow privileged users to initiate mobile number verification on update."); - descriptionMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, - "Allow users to add multiple email addresses and mobile numbers to their account."); Map descriptionMapping = userClaimUpdateConfig.getPropertyDescriptionMapping(); assertEquals(descriptionMapping, descriptionMappingExpected, "Maps are not equal."); } @@ -197,7 +193,6 @@ public void testGetPropertyNames() { propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER); - propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER); String[] propertiesArrayExpected = propertiesExpected.toArray(new String[0]); String[] properties = userClaimUpdateConfig.getPropertyNames(); @@ -255,8 +250,7 @@ public void testGetDefaultProperties() throws IdentityGovernanceException { EMAIL_VERIFICATION_ON_UPDATE_OTP_LENGTH, IdentityRecoveryConstants.ConnectorConfig .EMAIL_VERIFICATION_ON_UPDATE_EXPIRY_TIME, IdentityRecoveryConstants.ConnectorConfig .ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE, IdentityRecoveryConstants.ConnectorConfig - .MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, IdentityRecoveryConstants.ConnectorConfig - .SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER, "testproperty"}; + .MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, "testproperty"}; IdentityConfigParser mockConfigParser = mock(IdentityConfigParser.class); mockedIdentityConfigParser.when(IdentityConfigParser::getInstance).thenReturn(mockConfigParser); From 6fbbd8c66508e3010890619e585487c0a8c18e8f Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 12 Sep 2024 14:41:00 +0530 Subject: [PATCH 24/54] Change utils methods and add unit tests for tests. --- .../MobileNumberVerificationHandler.java | 12 +- .../carbon/identity/recovery/util/Utils.java | 31 ++--- .../MobileNumberVerificationHandlerTest.java | 32 +++-- .../identity/recovery/util/UtilsTest.java | 112 +++++++++++++----- 4 files changed, 130 insertions(+), 57 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 8547fccb1f..519923b6b5 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -95,7 +95,7 @@ public void handleEvent(Event event) throws IdentityEventException { claims = new HashMap<>(); } - boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); + boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); boolean enable = isMobileVerificationOnUpdateEnabled(user.getTenantDomain()); @@ -115,7 +115,7 @@ public void handleEvent(Event event) throws IdentityEventException { claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); if (supportMultipleMobileNumbers) { - List allMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + List allMobileNumbers = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty() && @@ -134,7 +134,7 @@ public void handleEvent(Event event) throws IdentityEventException { return; } else { if (supportMultipleMobileNumbers) { - List verifiedMobileNumbers = Utils.getExistingClaimValue(userStoreManager, user, + List verifiedMobileNumbers = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty() && @@ -303,18 +303,18 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); } - boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); + boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); String mobileNumber = null; - List exisitingVerifiedNumbersList = Utils.getExistingClaimValue(userStoreManager, user, + List exisitingVerifiedNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - List exisitingAllNumbersList = Utils.getExistingClaimValue(userStoreManager, user, + List exisitingAllNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index ce386d2661..6d97548a42 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -1354,7 +1354,7 @@ public static boolean isUseVerifyClaimEnabled() { * * @return True if the config is set to true, false otherwise. */ - public static boolean isMultiEmailsAndMobileNumbersPerUserEnabled(String tenantDomain) { + public static boolean isMultiEmailsAndMobileNumbersPerUserEnabled() { return Boolean.parseBoolean(IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER)); @@ -1751,27 +1751,28 @@ public static String getUserClaim(User user, String userClaim) throws IdentityRe } /** - * Get the existing multi-valued claims such as emailAddresses and verifiedEmailAddresses of the given claim URI. + * Retrieves the existing multi-valued claims for a given claim URI. * * @param userStoreManager User store manager. - * @param user User. - * @param claimURI Claim URI. + * @param user User object. + * @param claimURI Claim URI to retrieve. * @return List of existing claim values. + * @throws IdentityEventException If an error occurs while retrieving the claim value. */ - public static List getExistingClaimValue(org.wso2.carbon.user.core.UserStoreManager userStoreManager, - User user, String claimURI) throws IdentityEventException { + public static List getMultiValuedClaim(UserStoreManager userStoreManager, User user, String claimURI) + throws IdentityEventException { - List existingClaimValue; try { - String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); - existingClaimValue = StringUtils.isNotBlank(userStoreManager.getUserClaimValue(user.getUserName(), - claimURI, null)) ? - new LinkedList<>(Arrays.asList(userStoreManager.getUserClaimValue(user.getUserName(), claimURI, - null).split(multiAttributeSeparator))) : new ArrayList<>(); - } catch (org.wso2.carbon.user.core.UserStoreException e) { - throw new IdentityEventException("Error occurred while retrieving claim value of " + claimURI + + String claimValue = userStoreManager.getUserClaimValue(user.getUserName(), claimURI, null); + if (StringUtils.isBlank(claimValue)) { + return new ArrayList<>(); + } + + String separator = FrameworkUtils.getMultiAttributeSeparator(); + return new ArrayList<>(Arrays.asList(claimValue.split(separator))); + } catch (UserStoreException e) { + throw new IdentityEventException("Error retrieving claim " + claimURI + " for user: " + user.toFullQualifiedUsername(), e); } - return existingClaimValue; } } diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index ef2c8a8f3b..b6f46bf4ef 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -121,7 +121,7 @@ public class MobileNumberVerificationHandlerTest { private static final String newMobileNumber = "0722222222"; @BeforeMethod - public void setUp() throws UserStoreException { + public void setUpMethod() throws UserStoreException { MockitoAnnotations.openMocks(this); mobileNumberVerificationHandler = new MobileNumberVerificationHandler(); @@ -253,7 +253,7 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabledPreSet() // Mock existing verified mobile numbers. List exisitingVerifiedNumbersList = Arrays.asList(existingNumber1, existingNumber2); - mockedUtils.when(() -> Utils.getExistingClaimValue(any(), any(), + mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM))) .thenReturn(exisitingVerifiedNumbersList); @@ -315,10 +315,14 @@ public void testHandleEventVerificationEnabledMultiAttributeEnabledPreSet() * Case 2: Try to update primary mobile number which is in the verified mobile numbers list. * Expected: Thread local should be set to skip sending SMS OTP verification. */ - List existingVerifiedNumbersList1 = new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber)); - mockExistingVerifiedNumbersList(existingVerifiedNumbersList1); + String newVerifiedMobileNumbers2 = existingNumber1 + "," + newMobileNumber; + String newMobileNumbers2 = existingNumber1 + "," + newMobileNumber; + Event event2 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers2, newMobileNumbers2, newMobileNumber); + List existingVerifiedNumbersList2 = new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber)); + mockExistingVerifiedNumbersList(existingVerifiedNumbersList2); - mobileNumberVerificationHandler.handleEvent(event1); + mobileNumberVerificationHandler.handleEvent(event2); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); @@ -391,6 +395,16 @@ public void testHandleEventPostSet() throws IdentityEventException, IdentityReco mockVerificationPendingMobileNumber(); mobileNumberVerificationHandler.handleEvent(event); verify(identityEventService).handleEvent(any()); + + // Case 3: Handle exception thrown from userStoreManager.getUserClaimValues. + when(userStoreManager.getUserClaimValues(eq(username), + eq(new String[]{IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM}), isNull())) + .thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + mobileNumberVerificationHandler.handleEvent(event); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventException); + } } private void mockExistingPrimaryMobileNumber(String mobileNumber) throws UserStoreException { @@ -401,14 +415,14 @@ private void mockExistingPrimaryMobileNumber(String mobileNumber) throws UserSto private void mockExistingNumbersList(List existingAllMobileNumbers) { - mockedUtils.when(() -> Utils.getExistingClaimValue(any(), any(), + mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), eq(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM))) .thenReturn(existingAllMobileNumbers); } private void mockExistingVerifiedNumbersList(List exisitingVerifiedNumbersList) { - mockedUtils.when(() -> Utils.getExistingClaimValue(any(), any(), + mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM))) .thenReturn(exisitingVerifiedNumbersList); } @@ -424,8 +438,8 @@ private static Map getUserClaimsFromEvent(Event event2) { private void mockUtilMethods(boolean mobileVerificationEnabled, boolean multiAttributeEnabled, boolean useVerifyClaimEnabled) { - mockedUtils.when(() -> - Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(anyString())).thenReturn(multiAttributeEnabled); + mockedUtils.when( + Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(multiAttributeEnabled); mockedUtils.when(Utils::isUseVerifyClaimEnabled).thenReturn(useVerifyClaimEnabled); mockedUtils.when(() -> Utils.getConnectorConfig( eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_NUM_VERIFICATION_ON_UPDATE), diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java index a1e4c25c34..b89018caec 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java @@ -26,9 +26,11 @@ import org.testng.annotations.BeforeClass; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.IdentityEventException; import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; @@ -37,16 +39,19 @@ import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; public class UtilsTest { - private static final String TENANT_DOMAIN = "test.com"; - private static final int TENANT_ID = 123; - private static final String USER_NAME = "testUser"; - private static final String USER_STORE_DOMAIN = "TEST"; @Mock private UserStoreManager userStoreManager; @Mock @@ -56,50 +61,58 @@ public class UtilsTest { @Mock private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; - private MockedStatic mockedStaticIdentityTenantUtil; - private MockedStatic mockedStaticUserStoreManager; - private MockedStatic mockedIdentityRecoveryServiceDataHolder; - private MockedStatic mockedIdentityUtil; - - @BeforeMethod - public void setUp() { + private static MockedStatic mockedStaticIdentityTenantUtil; + private static MockedStatic mockedStaticUserStoreManager; + private static MockedStatic mockedIdentityRecoveryServiceDataHolder; + private static MockedStatic mockedStaticIdentityUtil; + private static MockedStatic mockedStaticFrameworkUtils; - MockitoAnnotations.openMocks(this); - } + private static final String TENANT_DOMAIN = "test.com"; + private static final int TENANT_ID = 123; + private static final String USER_NAME = "testUser"; + private static final String USER_STORE_DOMAIN = "TEST"; @BeforeClass - public void beforeTest() { + public static void beforeClass() { mockedStaticIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); mockedStaticUserStoreManager = mockStatic(UserStoreManager.class); mockedIdentityRecoveryServiceDataHolder = Mockito.mockStatic(IdentityRecoveryServiceDataHolder.class); - mockedIdentityUtil = mockStatic(IdentityUtil.class); + mockedStaticIdentityUtil = mockStatic(IdentityUtil.class); + mockedStaticFrameworkUtils = mockStatic(FrameworkUtils.class); } @AfterClass - public void afterTest() { + public static void afterClass() { mockedStaticIdentityTenantUtil.close(); mockedStaticUserStoreManager.close(); mockedIdentityRecoveryServiceDataHolder.close(); - mockedIdentityUtil.close(); + mockedStaticIdentityUtil.close(); + mockedStaticFrameworkUtils.close(); } - @Test(expectedExceptions = IdentityRecoveryClientException.class) - public void testCheckPasswordPatternViolationForInvalidDomain() throws Exception { + @BeforeMethod + public void setUp() throws UserStoreException { - User user = new User(); - user.setUserName(USER_NAME); - user.setTenantDomain(TENANT_DOMAIN); - user.setUserStoreDomain(USER_STORE_DOMAIN); + MockitoAnnotations.openMocks(this); + + mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) + .thenReturn(identityRecoveryServiceDataHolder); - when(IdentityTenantUtil.getTenantId(user.getTenantDomain())).thenReturn(TENANT_ID); - mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance).thenReturn( - identityRecoveryServiceDataHolder); when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); when(realmService.getTenantUserRealm(TENANT_ID)).thenReturn(userRealm); when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); - mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + + mockedStaticIdentityUtil.when(() -> IdentityTenantUtil.getTenantId(TENANT_DOMAIN)).thenReturn(TENANT_ID); + mockedStaticIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + mockedStaticFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + } + + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testCheckPasswordPatternViolationForInvalidDomain() throws Exception { + + User user = getUser(); when(userStoreManager.getSecondaryUserStoreManager(USER_STORE_DOMAIN)).thenReturn(null); try { @@ -111,4 +124,49 @@ public void testCheckPasswordPatternViolationForInvalidDomain() throws Exception throw e; } } + + @Test + public void testGetClaimFromUserStoreManager() throws Exception { + + User user = getUser(); + Map claimMap = new HashMap<>(); + claimMap.put("testClaim", "testValue"); + when(userStoreManager.getUserClaimValues(any(), any(), anyString())) + .thenReturn(claimMap); + + String result = Utils.getClaimFromUserStoreManager(user, "testClaim"); + assertEquals("testValue", result); + } + + @Test + public void testGetMultiValuedClaim() throws IdentityEventException, org.wso2.carbon.user.core.UserStoreException { + + User user = getUser(); + String claimValue = "value1,value2,value3"; + List expectedClaimList = Arrays.asList("value1", "value2", "value3"); + when(userStoreManager.getUserClaimValue(any(), anyString(), any())) + .thenReturn(claimValue); + + List result = Utils.getMultiValuedClaim(userStoreManager, user, "testClaim"); + assertEquals(expectedClaimList, result); + } + + @Test + public void testIsMultiEmailsAndMobileNumbersPerUserEnabled() { + + mockedStaticIdentityUtil.when(() -> IdentityUtil.getProperty(IdentityRecoveryConstants.ConnectorConfig + .SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER)) + .thenReturn("true"); + boolean result = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); + assertEquals(result, true); + } + + private static User getUser() { + + User user = new User(); + user.setUserName(USER_NAME); + user.setTenantDomain(TENANT_DOMAIN); + user.setUserStoreDomain(USER_STORE_DOMAIN); + return user; + } } From 8deef4a46882211065347ea51683e031f4465320 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 12 Sep 2024 14:55:32 +0530 Subject: [PATCH 25/54] Change util methods. --- .../handler/UserEmailVerificationHandler.java | 12 ++++++------ .../recovery/signup/UserSelfRegistrationManager.java | 10 ++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 80c336d40e..89f065678d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -97,7 +97,7 @@ public void handleEvent(Event event) throws IdentityEventException { Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); - boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); + boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); boolean enable = false; @@ -126,7 +126,7 @@ public void handleEvent(Event event) throws IdentityEventException { claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); if (supportMultipleEmails) { - List allEmails = Utils.getExistingClaimValue(userStoreManager, user, + List allEmails = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty() && @@ -144,7 +144,7 @@ public void handleEvent(Event event) throws IdentityEventException { claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); } else { if (supportMultipleEmails) { - List verifiedEmails = Utils.getExistingClaimValue(userStoreManager, user, + List verifiedEmails = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty() && @@ -558,14 +558,14 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } - boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user.getTenantDomain()); + boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); String emailAddress = null; - List existingVerifiedEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, + List existingVerifiedEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - List existingAllEmailAddresses = Utils.getExistingClaimValue(userStoreManager, user, + List existingAllEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); List updatedVerifiedEmailAddresses = claims.containsKey(IdentityRecoveryConstants. diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 1cae62b104..d80ca37031 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -759,8 +759,7 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi HashMap userClaims = getClaimsListToUpdate(user, verifiedChannelType, externallyVerifiedClaim, recoveryData.getRecoveryScenario().toString()); - boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user - .getTenantDomain()); + boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); if (RecoverySteps.VERIFY_EMAIL.equals(recoveryData.getRecoveryStep())) { String pendingEmailClaimValue = recoveryData.getRemainingSetIds(); @@ -769,14 +768,14 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, StringUtils.EMPTY); if (supportMultipleEmailsAndMobileNumbers) { try { - List verifiedEmails = Utils.getExistingClaimValue( + List verifiedEmails = Utils.getMultiValuedClaim( (org.wso2.carbon.user.core.UserStoreManager) userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); verifiedEmails.add(pendingEmailClaimValue); userClaims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( verifiedEmails, ",")); - List allEmails = Utils.getExistingClaimValue( + List allEmails = Utils.getMultiValuedClaim( (org.wso2.carbon.user.core.UserStoreManager) userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); if (!allEmails.contains(pendingEmailClaimValue)) { @@ -951,8 +950,7 @@ public void confirmVerificationCodeMe(String code, Map propertie UserStoreManager userStoreManager = getUserStoreManager(user); HashMap userClaims = new HashMap<>(); - boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(user - .getTenantDomain()); + boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); if (RecoverySteps.VERIFY_MOBILE_NUMBER.equals(recoveryData.getRecoveryStep())) { String pendingMobileNumberClaimValue = recoveryData.getRemainingSetIds(); From f5705ed5edcd406f04f2d5153b60a2106b4af5be Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Sat, 14 Sep 2024 14:07:55 +0530 Subject: [PATCH 26/54] Fix requiring multi attribute claims in normal flow issue in mobile verification. --- .../MobileNumberVerificationHandler.java | 156 +++++----- .../MobileNumberVerificationHandlerTest.java | 286 ++++++++---------- 2 files changed, 208 insertions(+), 234 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 519923b6b5..e75a60c0ca 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.recovery.handler; +import org.apache.commons.collections.CollectionUtils; import org.apache.commons.collections.MapUtils; import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; @@ -299,6 +300,16 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use return; } + /* + Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the + OTP. Therefore, the mobile number is already verified & no need to verify it again. + */ + if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString(). + equals(Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { + invalidatePendingMobileVerification(user, userStoreManager, claims); + return; + } + if (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate() != null) { Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); } @@ -308,23 +319,26 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use String mobileNumber = null; - List exisitingVerifiedNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - List updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. - VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; - - List exisitingAllNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); - List updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? - getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : - exisitingAllNumbersList; + List updatedVerifiedNumbersList = new ArrayList<>(); + List updatedAllNumbersList; if (supportMultipleMobileNumbers) { + List exisitingVerifiedNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + updatedVerifiedNumbersList = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_MOBILE_NUMBERS_CLAIM) ? getListOfMobileNumbersFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)) : exisitingVerifiedNumbersList; + + List exisitingAllNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + updatedAllNumbersList = claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) ? + getListOfMobileNumbersFromString(claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) : + exisitingAllNumbersList; + /* Finds the verification pending mobile number and remove it from the verified numbers list in the payload. */ - if (updatedVerifiedNumbersList != null) { + if (CollectionUtils.isNotEmpty(updatedVerifiedNumbersList)) { mobileNumber = getVerificationPendingMobileNumber(exisitingVerifiedNumbersList, updatedVerifiedNumbersList); updatedVerifiedNumbersList.remove(mobileNumber); @@ -334,98 +348,76 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Finds the removed numbers from the existing mobile numbers list and remove them from the verified numbers list. As verified numbers list should not contain numbers that are not in the mobile numbers list. */ - if (!updatedAllNumbersList.isEmpty()) { - for (String number : exisitingAllNumbersList) { - if (!updatedAllNumbersList.contains(number) && updatedVerifiedNumbersList != null) { - updatedVerifiedNumbersList.remove(number); - } - } + if (CollectionUtils.isNotEmpty(updatedAllNumbersList)) { + updatedVerifiedNumbersList.removeIf(number -> !updatedAllNumbersList.contains(number)); } + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, String.join(multiAttributeSeparator, updatedAllNumbersList)); claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); } else { + updatedAllNumbersList = new ArrayList<>(); claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); } - if (mobileNumber != null) { + if (mobileNumber == null) { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } - /* - Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the - OTP. Therefore, the mobile number is already verified & no need to verify it again. - */ - if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_SMS_OTP_FLOW.toString(). - equals(Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { - invalidatePendingMobileVerification(user, userStoreManager, claims); - return; + String existingMobileNumber; + String username = user.getUserName(); + try { + existingMobileNumber = userStoreManager.getUserClaimValue(username, IdentityRecoveryConstants. + MOBILE_NUMBER_CLAIM, null); + } catch (UserStoreException e) { + String error = String.format("Error occurred while retrieving existing mobile number for user: %s in " + + "domain: %s and user store: %s", username, user.getTenantDomain(), user.getUserStoreDomain()); + throw new IdentityEventException(error, e); + } + + if (StringUtils.equals(mobileNumber, existingMobileNumber)) { + if (log.isDebugEnabled()) { + log.debug(String.format("The mobile number to be updated: %s is same as the existing mobile " + + "number for user: %s in domain: %s and user store: %s. Hence an SMS OTP " + + "verification will not be triggered.", mobileNumber, username, + user.getTenantDomain(), user.getUserStoreDomain())); } - /* - Mobile numbers in verifiedMobileNumbers claim are already verified. No need to verify again. - */ - if (exisitingVerifiedNumbersList != null && exisitingVerifiedNumbersList.contains(mobileNumber)) { - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString()); - invalidatePendingMobileVerification(user, userStoreManager, claims); - return; - } else { - String existingMobileNumber; - String username = user.getUserName(); - try { - existingMobileNumber = userStoreManager.getUserClaimValue(username, IdentityRecoveryConstants. - MOBILE_NUMBER_CLAIM, null); - } catch (UserStoreException e) { - String error = String.format("Error occurred while retrieving existing mobile number for user: %s in " + - "domain: %s and user store: %s", username, user.getTenantDomain(), user.getUserStoreDomain()); - throw new IdentityEventException(error, e); - } + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); + invalidatePendingMobileVerification(user, userStoreManager, claims); - if (StringUtils.equals(mobileNumber, existingMobileNumber)) { - if (log.isDebugEnabled()) { - log.debug(String.format("The mobile number to be updated: %s is same as the existing mobile " + - "number for user: %s in domain: %s and user store: %s. Hence an SMS OTP " + - "verification will not be triggered.", mobileNumber, username, - user.getTenantDomain(), user.getUserStoreDomain())); - } - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); - invalidatePendingMobileVerification(user, userStoreManager, claims); - - if (supportMultipleMobileNumbers) { - if (!updatedVerifiedNumbersList.contains(mobileNumber)) { - updatedVerifiedNumbersList.add(mobileNumber); - claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); - } - if (!updatedAllNumbersList.contains(mobileNumber)) { - updatedAllNumbersList.add(mobileNumber); - claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, updatedAllNumbersList)); - } - } - return; + if (supportMultipleMobileNumbers) { + if (!updatedVerifiedNumbersList.contains(mobileNumber)) { + updatedVerifiedNumbersList.add(mobileNumber); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); } - /* - When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyMobile' - temporary claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to - check for 'verifyMobile' claim. - */ - if (Utils.isUseVerifyClaimEnabled() && !isVerifyMobileClaimAvailable(claims)) { - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - invalidatePendingMobileVerification(user, userStoreManager, claims); - return; + if (!updatedAllNumbersList.contains(mobileNumber)) { + updatedAllNumbersList.add(mobileNumber); + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedAllNumbersList)); } - claims.remove(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); } - } else { + return; + } + /* + When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyMobile' + temporary claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to + check for 'verifyMobile' claim. + */ + if (Utils.isUseVerifyClaimEnabled() && !isVerifyMobileClaimAvailable(claims)) { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + invalidatePendingMobileVerification(user, userStoreManager, claims); return; } + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); if (isVerificationPendingMobileClaimConfigAvailable(user.getTenantDomain())) { claims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, mobileNumber); diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index b6f46bf4ef..b35b95b510 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -95,9 +95,6 @@ public class MobileNumberVerificationHandlerTest { @Mock private TenantManager tenantManager; - @Mock - private JDBCRecoveryDataStore jdbcRecoveryDataStore; - @Mock private UserRecoveryDataStore userRecoveryDataStore; @@ -168,28 +165,32 @@ public void testHandleEventVerificationDisabledMultiAttributeDisabled() throws UserStoreException, IdentityEventException, IdentityRecoveryException { Event event = - getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, IdentityRecoveryConstants.FALSE, - existingNumber1); - + createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, + null, null, newMobileNumber); mockVerificationPendingMobileNumber(); mockUtilMethods(false, false, false); mobileNumberVerificationHandler.handleEvent(event); + + // Expectation: Any pending mobile verification should be invalidated. verify(userRecoveryDataStore).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); } @Test(description = "Verification disabled, Multi-attribute enabled, Change primary mobile") public void testHandleEventVerificationDisabledMultiAttributeEnabled() throws UserStoreException, IdentityEventException, IdentityRecoveryException { - Event event = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, IdentityRecoveryConstants.FALSE, - existingNumber1); + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, + null, null, newMobileNumber); mockVerificationPendingMobileNumber(); mockUtilMethods(false, true, false); - // Mobile number is not included in all mobile numbers list. + // New primary mobile number is not included in all mobile numbers list. List allMobileNumbers = Arrays.asList(existingNumber1, existingNumber2); mockExistingNumbersList(allMobileNumbers); @@ -201,178 +202,182 @@ public void testHandleEventVerificationDisabledMultiAttributeEnabled() } } - @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Change primary mobile") - public void testHandleEventVerificationEnabledMultiAttributeDisabledPreSet() - throws UserStoreException, IdentityEventException, IdentityRecoveryException { + @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Claims null") + public void testHandleEventVerificationEnabledMultiAttributeDisabledThreadLocal() throws Exception { + /* + Case 1: Claims null, skipSendingSmsOtpVerificationOnUpdate set to skip. + */ mockVerificationPendingMobileNumber(); mockUtilMethods(true, false, false); + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + null, null, null); - // Case 1: Claims null. - Event event1 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS); - mobileNumberVerificationHandler.handleEvent(event1); + mobileNumberVerificationHandler.handleEvent(event); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); /* - * Case 2: Thread local set to skip. - * Expected: Invalidation should be triggered. + Case 2: skipSendingSmsOtpVerificationOnUpdate set to skip. */ - Event event2 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - existingNumber1); + // Case 2.1: skipSendingSmsOtpVerificationOnUpdate set to SKIP_ON_CONFIRM. + mockVerificationPendingMobileNumber(); + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + null, null, newMobileNumber); mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_CONFIRM.toString()); - mockVerificationPendingMobileNumber(); mobileNumberVerificationHandler.handleEvent(event2); - verify(userRecoveryDataStore, atLeastOnce()).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), - eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + Map userClaims2 = getUserClaimsFromEvent(event2); + Assert.assertEquals(userClaims2.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); + + // Case 2.2: skipSendingSmsOtpVerificationOnUpdate set to SKIP_ON_SMS_OTP_FLOW. + mockVerificationPendingMobileNumber(); + Event event2_1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + null, null, newMobileNumber); + mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) + .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_SMS_OTP_FLOW.toString()); + + mobileNumberVerificationHandler.handleEvent(event2_1); + Map userClaims2_1 = getUserClaimsFromEvent(event2_1); + Assert.assertEquals(userClaims2_1.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); /* - * Case 3: Thread local set to some value. - * Expected: Invalidation should be triggered. + Case 3: skipSendingSmsOtpVerificationOnUpdate set to some value. */ - Event event3 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - existingNumber1); + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + null, null, newMobileNumber); mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) .thenReturn("test"); mobileNumberVerificationHandler.handleEvent(event3); mockedUtils.verify(Utils::unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate); + } - /* - * Case 4: Claims not null, new mobile number is sent to be verified. - * Expected: New mobile number should be added to the mobileNumber.pendingValue claim. - */ - String newVerifiedMobileNumbersList = existingNumber1 + "," + newMobileNumber; - String newMobileNumbersList = existingNumber1 + "," + existingNumber2; - Event event4 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbersList, newMobileNumbersList); + @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Change primary mobile") + public void testHandleEventVerificationEnabledMultiAttributeDisabled() throws Exception { - // Mock existing verified mobile numbers. - List exisitingVerifiedNumbersList = Arrays.asList(existingNumber1, existingNumber2); - mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), - eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM))) - .thenReturn(exisitingVerifiedNumbersList); + mockVerificationPendingMobileNumber(); + mockUtilMethods(true, false, false); + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + null, null, newMobileNumber); - List existingAllMobileNumbers = Collections.singletonList(existingNumber1); - mockExistingNumbersList(existingAllMobileNumbers); + mobileNumberVerificationHandler.handleEvent(event); - mobileNumberVerificationHandler.handleEvent(event4); - Map userClaimsEvent2 = getUserClaimsFromEvent(event4); - Assert.assertEquals(userClaimsEvent2.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), newMobileNumber); /* - * Case 5: Claims not null, new mobile number is same as the existing mobile number. - * Expected: Pending mobile verification should be invalidated. + Case 2: New mobile number is same as existing primary mobile number. */ mockExistingPrimaryMobileNumber(newMobileNumber); - Event event5 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbersList, newMobileNumbersList); - - mobileNumberVerificationHandler.handleEvent(event5); - Map userClaimsEvent3 = getUserClaimsFromEvent(event5); - - verify(userRecoveryDataStore, atLeastOnce()).invalidate(any(), - eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); - Assert.assertEquals(userClaimsEvent3.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), - ""); - } + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + null, null, newMobileNumber); - @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute enabled, Change primary mobile") - public void testHandleEventVerificationEnabledMultiAttributeEnabledPreSet() - throws UserStoreException, IdentityEventException, IdentityRecoveryException { + mobileNumberVerificationHandler.handleEvent(event2); - mockVerificationPendingMobileNumber(); - mockUtilMethods(true, true, false); + Map userClaims2 = getUserClaimsFromEvent(event2); + Assert.assertEquals(userClaims2.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); /* - * Case 1: Try to update primary mobile number which is not in the verified mobile numbers list. - * Expected: IdentityEventClientException + Case 3: Enable userVerify and send verifyMobileClaim as false. */ - String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; - String newMobileNumbers = existingNumber1 + "," + existingNumber2; - Event event1 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbers, newMobileNumbers); + mockExistingPrimaryMobileNumber(existingNumber1); + mockedUtils.when(Utils::isUseVerifyClaimEnabled).thenReturn(true); + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newMobileNumber); + + mobileNumberVerificationHandler.handleEvent(event3); + mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( + IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_INAPPLICABLE_CLAIMS.toString())); + } - List exisitingVerifiedNumbersList = new ArrayList<>(Arrays.asList(existingNumber1)); - mockExistingVerifiedNumbersList(exisitingVerifiedNumbersList); + @Test(description = "Verification enabled, Multi-attribute enabled, Update primary mobile not in verified list") + public void testUpdatePrimaryMobileNotInVerifiedList() throws Exception { - List existingAllMobileNumbers = new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber)); - mockExistingNumbersList(existingAllMobileNumbers); + mockUtilMethods(true, true, false); + String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; + String newMobileNumbers = existingNumber1 + "," + newMobileNumber; + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + newVerifiedMobileNumbers, newMobileNumbers, newMobileNumber); + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber))); try { - mobileNumberVerificationHandler.handleEvent(event1); + mobileNumberVerificationHandler.handleEvent(event); + Assert.fail("Expected IdentityEventClientException was not thrown"); } catch (IdentityEventClientException e) { - Assert.assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode()); + Assert.assertEquals(e.getErrorCode(), + IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode()); } + } - /* - * Case 2: Try to update primary mobile number which is in the verified mobile numbers list. - * Expected: Thread local should be set to skip sending SMS OTP verification. - */ - String newVerifiedMobileNumbers2 = existingNumber1 + "," + newMobileNumber; - String newMobileNumbers2 = existingNumber1 + "," + newMobileNumber; - Event event2 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbers2, newMobileNumbers2, newMobileNumber); - List existingVerifiedNumbersList2 = new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber)); - mockExistingVerifiedNumbersList(existingVerifiedNumbersList2); + @Test(description = "Verification enabled, Multi-attribute enabled, Update primary mobile in verified list") + public void testUpdatePrimaryMobileInVerifiedList() throws Exception { + + mockVerificationPendingMobileNumber(); + mockUtilMethods(true, true, false); + String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; + String newMobileNumbers = existingNumber1 + "," + newMobileNumber; + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, + IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers, newMobileNumbers, newMobileNumber); + mockExistingVerifiedNumbersList(Arrays.asList(existingNumber1, newMobileNumber)); + + + mobileNumberVerificationHandler.handleEvent(event); - mobileNumberVerificationHandler.handleEvent(event2); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( - eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates - .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + } - /* - * Case 3: Try to update verified numbers list with a new mobile number, which is not in the all mobile numbers. - */ - String newVerifiedMobileNumbers3 = existingNumber1 + "," + newMobileNumber; - Event event3 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbers3, null, null); + @Test(description = "Verification enabled, Multi-attribute enabled, Add new mobile to verified list") + public void testAddNewMobileToVerifiedList() throws Exception { - List existingVerifiedNumbersList3 = new ArrayList<>(Arrays.asList(existingNumber1)); - mockExistingVerifiedNumbersList(existingVerifiedNumbersList3); + mockVerificationPendingMobileNumber(); + mockUtilMethods(true, true, false); + String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, + IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers, null, null); + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); - List existingAllMobileNumbers3 = new ArrayList<>(Arrays.asList(existingNumber1)); - mockExistingNumbersList(existingAllMobileNumbers3); + mobileNumberVerificationHandler.handleEvent(event); - mobileNumberVerificationHandler.handleEvent(event3); - Map userClaimsCase3 = getUserClaimsFromEvent(event3); - Assert.assertEquals(userClaimsCase3.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), newMobileNumber); - /* - * Case 4: Try to update verified numbers list with the new mobile number, which is not in the all - * mobile numbers. - * Set the new mobile number as existing primary mobile number. - * Expected: Verification skip thread local should be set. - * Expected: The new mobile number should be added to the mobileNumbers claim. - */ - Event event4 = getEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbers3, null, null); - - mockExistingPrimaryMobileNumber(newMobileNumber); - mobileNumberVerificationHandler.handleEvent(event4); - Map userClaimsCase4 = getUserClaimsFromEvent(event4); - Assert.assertTrue(StringUtils.contains(userClaimsCase4.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), - newMobileNumber)); - Assert.assertTrue(StringUtils.contains( - userClaimsCase4.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM), - newMobileNumber)); - mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( - eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates - .SKIP_ON_EXISTING_MOBILE_NUM.toString()))); + // Case 2: Add multiple numbers to new verified list at once. + String newVerifiedMobileNumbers2 = existingNumber1 + "," + newMobileNumber; + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, + IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers2, null, null); + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList())); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + try { + mobileNumberVerificationHandler.handleEvent(event2); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventClientException); + } } @Test(description = "POST_SET_USER_CLAIMS: Verification enabled, Multi-attribute enabled") public void testHandleEventPostSet() throws IdentityEventException, IdentityRecoveryException, UserStoreException { - Event event = getEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS); + Event event = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, null, + null, null, null); mockUtilMethods(true, true, false); /* @@ -462,28 +467,14 @@ private void mockVerificationPendingMobileNumber() throws UserStoreException { .thenReturn(pendingMobileNumberClaimMap); } - private Event getEvent(String eventType) { - - return getEvent(eventType, null, null, null, - null); - } - - private Event getEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim) { - - String mobileNumberClaim = existingNumber1 + "," + existingNumber2; - return getEvent(eventType, verifyMobileClaim, verifiedMobileNumbersClaim, mobileNumberClaim , newMobileNumber); - } - - private Event getEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim, - String mobileNumbersClaim) { - - return getEvent(eventType, verifyMobileClaim, verifiedMobileNumbersClaim, mobileNumbersClaim, newMobileNumber); - } - - private Event getEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim, + private Event createEvent(String eventType, String verifyMobileClaim, String verifiedMobileNumbersClaim, String mobileNumbersClaim, String mobileNumber) { - Map eventProperties = getEventProperties(); + Map eventProperties = new HashMap<>(); + eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, username); + eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); + Map claims = new HashMap<>(); if (mobileNumber != null && !mobileNumber.isEmpty()) { claims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, mobileNumber); @@ -500,13 +491,4 @@ private Event getEvent(String eventType, String verifyMobileClaim, String verifi eventProperties.put(IdentityEventConstants.EventProperty.USER_CLAIMS, claims); return new Event(eventType, eventProperties); } - - private Map getEventProperties() { - - Map eventProperties = new HashMap<>(); - eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, username); - eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); - eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); - return eventProperties; - } } From e6bbcc6aa950368664463c1e4782a51bdaa18ff7 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Sat, 14 Sep 2024 18:23:21 +0530 Subject: [PATCH 27/54] Change logic to automatically add primary to all numbers list when verification disabled. --- .../MobileNumberVerificationHandler.java | 22 ++++++++++++------- .../MobileNumberVerificationHandlerTest.java | 16 ++++++-------- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index e75a60c0ca..bbd143b09c 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -116,15 +116,21 @@ public void handleEvent(Event event) throws IdentityEventException { claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); if (supportMultipleMobileNumbers) { - List allMobileNumbers = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && - !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty() && - !allMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode(), - IdentityRecoveryConstants.ErrorMessages - .ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getMessage()); + !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty()) { + String mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + List exisitingAllNumbersList = Utils.getMultiValuedClaim(userStoreManager, user, + IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + List updatedAllNumbersList = + claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM) + ? getListOfMobileNumbersFromString( + claims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM)) + : exisitingAllNumbersList; + if (!updatedAllNumbersList.contains(mobileNumber)) { + updatedAllNumbersList.add(mobileNumber); + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(FrameworkUtils.getMultiAttributeSeparator(), updatedAllNumbersList)); + } } } else { // Multiple mobile numbers per user support is disabled. diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index b35b95b510..e1040d90ea 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -186,20 +186,18 @@ public void testHandleEventVerificationDisabledMultiAttributeEnabled() Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, null, null, newMobileNumber); - mockVerificationPendingMobileNumber(); mockUtilMethods(false, true, false); - // New primary mobile number is not included in all mobile numbers list. - List allMobileNumbers = Arrays.asList(existingNumber1, existingNumber2); + // New primary mobile number is not included in existing all mobile numbers list. + List allMobileNumbers = new ArrayList<>(Arrays.asList(existingNumber1, existingNumber2)); mockExistingNumbersList(allMobileNumbers); - try { - mobileNumberVerificationHandler.handleEvent(event); - } catch (IdentityEventClientException e) { - Assert.assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_MOBILE_NUMBERS_LIST.getCode()); - } + // Expectation: New mobile number should be added to the mobile numbers claim. + mobileNumberVerificationHandler.handleEvent(event); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertTrue(StringUtils.contains( + userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), newMobileNumber)); } @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Claims null") From 5041a4a551f28acca6d10a25ca8e25a8e50e7e6d Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Sun, 15 Sep 2024 22:52:50 +0530 Subject: [PATCH 28/54] Change email update logic and refactor code - add few todos. --- .../MobileNumberVerificationHandler.java | 5 +- .../handler/UserEmailVerificationHandler.java | 183 ++++++++++-------- .../MobileNumberVerificationHandlerTest.java | 4 +- 3 files changed, 107 insertions(+), 85 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index bbd143b09c..2c7618a954 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -301,11 +301,12 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } if (IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString().equals - (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { + (Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate())) { invalidatePendingMobileVerification(user, userStoreManager, claims); return; } - + + // TODO: Check why this was moved to the bottom before? This was not triggered due to that. /* Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the OTP. Therefore, the mobile number is already verified & no need to verify it again. diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 89f065678d..31aa2023d0 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -96,6 +96,9 @@ public void handleEvent(Event event) throws IdentityEventException { Map claims = (Map) eventProperties.get(IdentityEventConstants.EventProperty .USER_CLAIMS); + if (claims == null) { + claims = new HashMap<>(); + } boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); @@ -122,17 +125,27 @@ public void handleEvent(Event event) throws IdentityEventException { invalidatePendingEmailVerification(user, userStoreManager, claims); } - // Drop the verified email addresses claim as verification on update is not enabled. - claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - if (supportMultipleEmails) { - List allEmails = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + // Drop the verified email addresses claim as verification on update is not enabled. + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && - !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty() && - !allEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { - throw new IdentityEventClientException(ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST - .getCode(), ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST.getMessage()); + !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty()) { + + String email = claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); + List existingAllEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + + List updatedAllEmailAddresses = claims.containsKey( + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM) + ? getListOfEmailAddressesFromString( + claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) + : existingAllEmailAddresses; + if (!updatedAllEmailAddresses.contains(email)) { + updatedAllEmailAddresses.add(email); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, + String.join(FrameworkUtils.getMultiAttributeSeparator(), updatedAllEmailAddresses)); + } } } else { // Supporting multiple email addresses per user is disabled. @@ -547,6 +560,17 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } + // TODO: Check why this was moved to the bottom before? This was not triggered due to that. + /* + Within the Email OTP flow, the email address is updated in the user profile after successfully verifying the + OTP. Therefore, the email is already verified & no need to verify it again. + */ + if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW.toString().equals + (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { + invalidatePendingEmailVerification(user, userStoreManager, claims); + return; + } + if (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate() != null) { Utils.unsetThreadLocalToSkipSendingEmailVerificationOnUpdate(); } @@ -562,21 +586,22 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); String emailAddress = null; - - List existingVerifiedEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - List existingAllEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); - - List updatedVerifiedEmailAddresses = claims.containsKey(IdentityRecoveryConstants. - VERIFIED_EMAIL_ADDRESSES_CLAIM) ? getListOfEmailAddressesFromString(claims.get( - IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) : existingVerifiedEmailAddresses; - List updatedAllEmailAddresses = claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM) ? - getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : - existingAllEmailAddresses; + List updatedVerifiedEmailAddresses = new ArrayList<>(); + List updatedAllEmailAddresses = new ArrayList<>(); // Handle email addresses and verified email addresses claims. if (supportMultipleEmails) { + List existingVerifiedEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + List existingAllEmailAddresses = Utils.getMultiValuedClaim(userStoreManager, user, + IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + + updatedVerifiedEmailAddresses = claims.containsKey(IdentityRecoveryConstants. + VERIFIED_EMAIL_ADDRESSES_CLAIM) ? getListOfEmailAddressesFromString(claims.get( + IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM)) : existingVerifiedEmailAddresses; + updatedAllEmailAddresses = claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM) ? + getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : + existingAllEmailAddresses; // Find the verification pending email address and remove it from verified email addresses list in the payload. if (updatedVerifiedEmailAddresses != null) { @@ -585,6 +610,23 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore updatedVerifiedEmailAddresses.remove(emailAddress); } + /* + TODO: Check if this can be removed, this is not triggered. But if we can skip throwing an error when + the primary mobile number is not in the existing verified list, instead go with verification process, + then, this can be useful. + */ + if (existingVerifiedEmailAddresses.contains(emailAddress)) { + if (log.isDebugEnabled()) { + log.debug(String.format("The email address to be updated: %s is same as the existing email " + + "address for user: %s in domain %s. Hence an email verification will not be " + + "triggered.", emailAddress, user.getUserName(), user.getTenantDomain())); + } + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString()); + invalidatePendingEmailVerification(user, userStoreManager, claims); + return; + } + /* Find the removed numbers from the existing email addresses list and remove them from the verified email addresses list, as verified email addresses list should not contain email addresses that are not in the email @@ -612,75 +654,54 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore emailAddress = claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); } - if (emailAddress != null) { - - /* - Within the Email OTP flow, the email address is updated in the user profile after successfully verifying the - OTP. Therefore, the email is already verified & no need to verify it again. - */ - if (IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW.toString().equals - (Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate())) { - invalidatePendingEmailVerification(user, userStoreManager, claims); - return; - } - if (existingVerifiedEmailAddresses != null && existingVerifiedEmailAddresses.contains(emailAddress)) { - if (log.isDebugEnabled()) { - log.debug(String.format("The email address to be updated: %s is same as the existing email " + - "address for user: %s in domain %s. Hence an email verification will not be " + - "triggered.", emailAddress, user.getUserName(), user.getTenantDomain())); - } - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString()); - invalidatePendingEmailVerification(user, userStoreManager, claims); - return; - } else { - String existingEmail; - existingEmail = getEmailClaimValue(user, userStoreManager); + if (emailAddress == null) { + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } - if (emailAddress.equals(existingEmail)) { - if (log.isDebugEnabled()) { - log.debug(String.format("The email address to be updated: %s is already verified and contains" + - " in the verified email addresses list. Hence an email verification will not be " + - "triggered.", emailAddress)); - } - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString()); - invalidatePendingEmailVerification(user, userStoreManager, claims); + String existingEmail; + existingEmail = getEmailClaimValue(user, userStoreManager); - if (supportMultipleEmails) { - if (!updatedVerifiedEmailAddresses.contains(emailAddress)) { - updatedVerifiedEmailAddresses.add(emailAddress); - claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, - StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); - } - if (!updatedAllEmailAddresses.contains(emailAddress)) { - updatedAllEmailAddresses.add(emailAddress); - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, - StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); - } - } - return; + if (emailAddress.equals(existingEmail)) { + if (log.isDebugEnabled()) { + log.debug(String.format("The email address to be updated: %s is already verified and contains" + + " in the verified email addresses list. Hence an email verification will not be " + + "triggered.", emailAddress)); + } + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString()); + invalidatePendingEmailVerification(user, userStoreManager, claims); + + if (supportMultipleEmails) { + if (!updatedVerifiedEmailAddresses.contains(emailAddress)) { + updatedVerifiedEmailAddresses.add(emailAddress); + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); + } + if (!updatedAllEmailAddresses.contains(emailAddress)) { + updatedAllEmailAddresses.add(emailAddress); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); } } + return; + } - /* - When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyEmail' temporary - claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to check for - 'verifyEmail' claim. - */ - if (Utils.isUseVerifyClaimEnabled() && !isVerifyEmailClaimAvailable(claims)) { - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - invalidatePendingEmailVerification(user, userStoreManager, claims); - return; - } - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); - claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); - } else { + /* + When 'UseVerifyClaim' is enabled, the verification should happen only if the 'verifyEmail' temporary + claim exists as 'true' in the claim list. If 'UseVerifyClaim' is disabled, no need to check for + 'verifyEmail' claim. + */ + if (Utils.isUseVerifyClaimEnabled() && !isVerifyEmailClaimAvailable(claims)) { Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + invalidatePendingEmailVerification(user, userStoreManager, claims); + return; } + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, emailAddress); + claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); } /** diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index e1040d90ea..469188295e 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -208,10 +208,10 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabledThreadLocal( */ mockVerificationPendingMobileNumber(); mockUtilMethods(true, false, false); - Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, + Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, null, null, null); - mobileNumberVerificationHandler.handleEvent(event); + mobileNumberVerificationHandler.handleEvent(event1); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); From 05dc2de32ad3f3177a818f3ee272767355253c3b Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Wed, 18 Sep 2024 10:06:59 +0530 Subject: [PATCH 29/54] Add user email verification handler test --- .../MobileNumberVerificationHandlerTest.java | 3 +- .../UserEmailVerificationHandlerTest.java | 444 ++++++++++++++++++ .../src/test/resources/testng.xml | 1 + 3 files changed, 447 insertions(+), 1 deletion(-) create mode 100644 components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index 469188295e..d4104ca633 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -335,7 +335,8 @@ public void testUpdatePrimaryMobileInVerifiedList() throws Exception { mobileNumberVerificationHandler.handleEvent(event); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( - eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); } @Test(description = "Verification enabled, Multi-attribute enabled, Add new mobile to verified list") diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java new file mode 100644 index 0000000000..85ad442e71 --- /dev/null +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -0,0 +1,444 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.recovery.handler; + +import org.apache.commons.lang.StringUtils; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.Assert; +import org.testng.annotations.AfterClass; +import org.testng.annotations.BeforeClass; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.event.IdentityEventConstants; +import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.event.IdentityEventClientException; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.event.Event; +import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; +import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; +import org.wso2.carbon.identity.recovery.util.Utils; +import org.wso2.carbon.user.api.ClaimManager; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.user.core.UserStoreException; +import org.wso2.carbon.user.core.UserStoreManager; +import org.wso2.carbon.user.core.config.RealmConfiguration; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.user.core.tenant.TenantManager; + +import java.util.Arrays; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +public class UserEmailVerificationHandlerTest { + + @InjectMocks + private UserEmailVerificationHandler userEmailVerificationHandler; + + @Mock + private UserStoreManager userStoreManager; + + @Mock + private RealmConfiguration realmConfiguration; + + @Mock + private ClaimManager claimManager; + + @Mock + private RealmService realmService; + + @Mock + private UserRealm userRealm; + + @Mock + private TenantManager tenantManager; + + @Mock + private UserRecoveryDataStore userRecoveryDataStore; + + @Mock + private IdentityEventService identityEventService; + + @Mock + private IdentityRecoveryServiceDataHolder serviceDataHolder; + + private MockedStatic mockedJDBCRecoveryDataStore; + private MockedStatic mockedUtils; + private MockedStatic mockedIdentityRecoveryServiceDataHolder; + private MockedStatic mockedFrameworkUtils; + + private static final String tenantDomain = "test.com"; + private static final String userStoreDomainName = "TESTING"; + private static final String username = "testuser"; + private static final String existingEmail1 = "old1@abc.com"; + private static final String existingEmail2 = "old2@abc.com"; + private static final String newEmail = "new@abc.com"; + + @BeforeClass + public void init() { + + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedUtils = mockStatic(Utils.class); + mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + mockedFrameworkUtils = mockStatic(FrameworkUtils.class); + } + + @AfterClass + public void close() { + + mockedJDBCRecoveryDataStore.close(); + mockedUtils.close(); + mockedIdentityRecoveryServiceDataHolder.close(); + mockedFrameworkUtils.close(); + } + + @BeforeMethod + public void setUp() { + + MockitoAnnotations.openMocks(this); + userEmailVerificationHandler = new UserEmailVerificationHandler(); + + mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) + .thenReturn(serviceDataHolder); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); + mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + + when(serviceDataHolder.getIdentityEventService()).thenReturn(identityEventService); + when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); + when(realmConfiguration.getUserStoreProperty(eq( + UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME))).thenReturn(userStoreDomainName); + } + + @Test + public void getNames() { + + Assert.assertEquals(userEmailVerificationHandler.getName(), "userEmailVerification"); + Assert.assertEquals(userEmailVerificationHandler.getFriendlyName(), "User Email Verification"); + } + + @Test(description = "Verification - Disabled, Multi attribute - Disabled") + public void testHandleEventPreSetUserClaimsVerificationDisabledMultiDisabled() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + /* + Notification on email update is enabled. + Expected: Notification event should be triggered, pending email claim should be set to empty string. + */ + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockUtilMethods(false, false, false, + true); + mockPrimaryEmail(existingEmail2); + mockPendingVerificationEmail(existingEmail1); + + userEmailVerificationHandler.handleEvent(event); + verify(identityEventService).handleEvent(any()); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); + } + + @Test(description = "Verification - Disabled, Multi attribute - Enabled") + public void testHandleEventPreSetUserClaimsVerificationDisabledMultiEnabled() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + /* + New Email is not in the existing email address list. + Expected: New email should be added to the new email addresses claim. + */ + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockUtilMethods(false, true, false, + false); + List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + mockExistingEmailAddressesList(existingEmails); + + userEmailVerificationHandler.handleEvent(event); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertTrue(StringUtils.contains( + userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), newEmail)); + } + + @Test(description = "Verification - Enabled, Multi attribute - Disabled") + public void testHandleEventPreSetUserClaimsVerificationEnabledMultiDisabled() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + /* + New Email is not in the existing email address list. + Expected: IdentityEventClientException should be thrown. + */ + Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + mockUtilMethods(true, false, false, + false); + + userEmailVerificationHandler.handleEvent(event1); + Map userClaimsC1 = getUserClaimsFromEvent(event1); + Assert.assertEquals(userClaimsC1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), newEmail); + } + + @Test(description = "Verification - Enabled, Multi attribute - Enabled, Change primary email which is not in the " + + "verified email list") + public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC1() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + /* + Try to change the primary email, new Email is not in the existing verified email address list. + Expected: IdentityEventClientException should be thrown. + */ + Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockUtilMethods(true, true, false, + false); + List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + mockExistingEmailAddressesList(existingEmails); + + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1)); + mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); + + try { + userEmailVerificationHandler.handleEvent(event1); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventClientException); + } + } + + @Test(description = "Verification - Enabled, Multi attribute - Enabled, Change primary email which is already " + + "in the verified email list") + public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC2() throws IdentityEventException { + + /* + Try to change the primary email, new Email is in the existing verified email address list. + Expected: Thread local should be set to skip sending email verification on update. + */ + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockUtilMethods(true, true, false, + false); + List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, newEmail)); + mockExistingEmailAddressesList(existingEmails); + + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1, newEmail)); + mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); + + userEmailVerificationHandler.handleEvent(event); + mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate( + eq(IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates + .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + } + + @Test(description = "Verification - Enabled, Multi attribute - Enabled, Update verified list with new email") + public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC3() + throws IdentityEventException, UserStoreException { + + /* + Case 1:Try to update the verified email list with a new email. + Expected: IdentityEventClientException should be thrown. + */ + String newVerifiedEmails = String.format("%s,%s", existingEmail1, newEmail); + Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedEmails, null, null); + + mockUtilMethods(true, true, false, + false); + List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1)); + mockExistingEmailAddressesList(existingEmails); + + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1)); + mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); + + userEmailVerificationHandler.handleEvent(event1); + Map userClaims1 = getUserClaimsFromEvent(event1); + Assert.assertEquals(userClaims1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), newEmail); + + /* + Case 2: Update verified email list with the existing primary email which is not in the verified email list. + Expected: Email should be added to the updated verified email list. + */ + String newVerifiedEmails2 = String.format("%s,%s", existingEmail1, newEmail); + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedEmails2, null, null); + + mockUtilMethods(true, true, false, + false); + List existingEmails2 = new ArrayList<>(Arrays.asList(existingEmail1)); + mockExistingEmailAddressesList(existingEmails2); + + List existingVerifiedEmails2 = new ArrayList<>(Arrays.asList(existingEmail1)); + mockExistingVerifiedEmailAddressesList(existingVerifiedEmails2); + + mockPrimaryEmail(newEmail); + + userEmailVerificationHandler.handleEvent(event2); + Map userClaims2 = getUserClaimsFromEvent(event2); + String updatedVerifiedEmails = userClaims2.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + Assert.assertTrue(StringUtils.contains(updatedVerifiedEmails, newEmail)); + } + + @Test(description = "Verification - Enabled, Multi attribute - Enabled, Remove email from email list") + public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC4() throws IdentityEventException { + + /* + Remove an email from the existing emails list. + Expected: Removed email should be removed from the verified email list as well. + */ + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, existingEmail1, null); + + mockUtilMethods(true, true, false, + false); + List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + mockExistingEmailAddressesList(existingEmails); + + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); + + userEmailVerificationHandler.handleEvent(event); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), existingEmail1); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM), existingEmail1); + } + + @Test + public void testHandleEventPostSetUserClaims() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + Event event = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, null); + mockUtilMethods(true, true, false, + true); + mockPendingVerificationEmail(existingEmail1); + + userEmailVerificationHandler.handleEvent(event); + verify(identityEventService).handleEvent(any()); + } + + private void mockExistingEmailAddressesList(List existingEmails) { + + mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), + eq(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM))) + .thenReturn(existingEmails); + } + + private void mockExistingVerifiedEmailAddressesList(List existingVerifiedEmails) { + + mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), + eq(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM))) + .thenReturn(existingVerifiedEmails); + } + + private void mockPrimaryEmail(String primaryEmail) throws UserStoreException { + + when(userStoreManager.getUserClaimValue(anyString(), eq(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM), + any())).thenReturn(primaryEmail); + } + + private void mockPendingVerificationEmail(String pendingEmail) throws UserStoreException { + + Map pendingEmailClaim = new HashMap<>(); + pendingEmailClaim.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, pendingEmail); + when(userStoreManager.getUserClaimValues(anyString(), + eq(new String[]{IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM}), + any())).thenReturn(pendingEmailClaim); + } + + private void mockUtilMethods(boolean emailVerificationEnabled, boolean multiAttributeEnabled, + boolean userVerifyClaimEnabled, boolean notificationOnEmailUpdate) { + + mockedUtils.when( + Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(multiAttributeEnabled); + mockedUtils.when(Utils::isUseVerifyClaimEnabled).thenReturn(userVerifyClaimEnabled); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION_ON_UPDATE), + anyString())) + .thenReturn(String.valueOf(emailVerificationEnabled)); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_NOTIFICATION_ON_EMAIL_UPDATE), + anyString())) + .thenReturn(String.valueOf(notificationOnEmailUpdate)); + } + + @SuppressWarnings("unchecked") + private static Map getUserClaimsFromEvent(Event event2) { + + Map eventProperties = event2.getEventProperties(); + return (Map) eventProperties.get(IdentityEventConstants.EventProperty.USER_CLAIMS); + } + + private Event createEvent(String eventType, String verifyEmailClaim, String verifiedEmailsClaim, + String emailsClaim, String primaryEmail) { + + return createEvent(eventType, verifyEmailClaim, verifiedEmailsClaim, emailsClaim, primaryEmail, null, null); + } + + private Event createEvent(String eventType, String verifyEmailClaim, String verifiedEmailsClaim, + String emailsClaim, String primaryEmail, Map additionalEventProperties, + Map additionalClaims) { + + Map eventProperties = new HashMap<>(); + eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, username); + eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); + + if (additionalEventProperties != null) { + eventProperties.putAll(additionalEventProperties); + } + + Map claims = new HashMap<>(); + if (primaryEmail != null && !primaryEmail.isEmpty()) { + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, primaryEmail); + } + if (verifyEmailClaim != null) { + claims.put(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM, verifyEmailClaim); + } + if (emailsClaim != null) { + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, emailsClaim); + } + if (verifiedEmailsClaim != null) { + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, verifiedEmailsClaim); + } + + if (additionalClaims != null) { + claims.putAll(additionalClaims); + } + + eventProperties.put(IdentityEventConstants.EventProperty.USER_CLAIMS, claims); + return new Event(eventType, eventProperties); + } +} diff --git a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml index 0f054a3596..d8278d5b0d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml @@ -33,6 +33,7 @@ + From a81c82edf26566b30780758a8e96b79129444a09 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Wed, 18 Sep 2024 15:03:43 +0530 Subject: [PATCH 30/54] Add missing user email verification handler test cases. --- .../handler/UserEmailVerificationHandler.java | 13 - .../UserEmailVerificationHandlerTest.java | 301 +++++++++++++++--- 2 files changed, 254 insertions(+), 60 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 31aa2023d0..4efd9b2d0e 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -560,7 +560,6 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } - // TODO: Check why this was moved to the bottom before? This was not triggered due to that. /* Within the Email OTP flow, the email address is updated in the user profile after successfully verifying the OTP. Therefore, the email is already verified & no need to verify it again. @@ -575,13 +574,6 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore Utils.unsetThreadLocalToSkipSendingEmailVerificationOnUpdate(); } - if (MapUtils.isEmpty(claims)) { - // Not required to handle in this handler. - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants. - SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; - } - boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); @@ -610,11 +602,6 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore updatedVerifiedEmailAddresses.remove(emailAddress); } - /* - TODO: Check if this can be removed, this is not triggered. But if we can skip throwing an error when - the primary mobile number is not in the existing verified list, instead go with verification process, - then, this can be useful. - */ if (existingVerifiedEmailAddresses.contains(emailAddress)) { if (log.isDebugEnabled()) { log.debug(String.format("The email address to be updated: %s is same as the existing email " + diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java index 85ad442e71..f1a47cb1f2 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -24,11 +24,11 @@ import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; import org.testng.Assert; -import org.testng.annotations.AfterClass; -import org.testng.annotations.BeforeClass; +import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.event.IdentityEventClientException; @@ -40,14 +40,11 @@ import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; -import org.wso2.carbon.user.api.ClaimManager; -import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.Claim; import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.config.RealmConfiguration; -import org.wso2.carbon.user.core.service.RealmService; -import org.wso2.carbon.user.core.tenant.TenantManager; import java.util.Arrays; import java.util.ArrayList; @@ -56,6 +53,7 @@ import java.util.Map; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mockStatic; @@ -73,18 +71,6 @@ public class UserEmailVerificationHandlerTest { @Mock private RealmConfiguration realmConfiguration; - @Mock - private ClaimManager claimManager; - - @Mock - private RealmService realmService; - - @Mock - private UserRealm userRealm; - - @Mock - private TenantManager tenantManager; - @Mock private UserRecoveryDataStore userRecoveryDataStore; @@ -98,6 +84,7 @@ public class UserEmailVerificationHandlerTest { private MockedStatic mockedUtils; private MockedStatic mockedIdentityRecoveryServiceDataHolder; private MockedStatic mockedFrameworkUtils; + private MockedStatic mockedIdentityUtils; private static final String tenantDomain = "test.com"; private static final String userStoreDomainName = "TESTING"; @@ -106,34 +93,34 @@ public class UserEmailVerificationHandlerTest { private static final String existingEmail2 = "old2@abc.com"; private static final String newEmail = "new@abc.com"; - @BeforeClass - public void init() { - - mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); - mockedUtils = mockStatic(Utils.class); - mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); - mockedFrameworkUtils = mockStatic(FrameworkUtils.class); - } - - @AfterClass + @AfterMethod public void close() { mockedJDBCRecoveryDataStore.close(); mockedUtils.close(); mockedIdentityRecoveryServiceDataHolder.close(); mockedFrameworkUtils.close(); + mockedIdentityUtils.close(); } @BeforeMethod - public void setUp() { + public void setUp() throws NoSuchFieldException, IllegalAccessException { MockitoAnnotations.openMocks(this); + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedUtils = mockStatic(Utils.class); + mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + mockedFrameworkUtils = mockStatic(FrameworkUtils.class); + mockedIdentityUtils = mockStatic(IdentityUtil.class); + userEmailVerificationHandler = new UserEmailVerificationHandler(); mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) .thenReturn(serviceDataHolder); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + mockedIdentityUtils.when(() -> IdentityUtil.addDomainToName(eq(username), anyString())) + .thenReturn(String.format("%s/%s", username, userStoreDomainName)); when(serviceDataHolder.getIdentityEventService()).thenReturn(identityEventService); when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); @@ -150,7 +137,7 @@ public void getNames() { @Test(description = "Verification - Disabled, Multi attribute - Disabled") public void testHandleEventPreSetUserClaimsVerificationDisabledMultiDisabled() - throws IdentityEventException, IdentityRecoveryException, UserStoreException { + throws IdentityEventException, UserStoreException { /* Notification on email update is enabled. @@ -161,14 +148,30 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiDisabled() mockUtilMethods(false, false, false, true); - mockPrimaryEmail(existingEmail2); - mockPendingVerificationEmail(existingEmail1); + mockPrimaryEmail(existingEmail1); + mockPendingVerificationEmail(existingEmail2); userEmailVerificationHandler.handleEvent(event); verify(identityEventService).handleEvent(any()); Map userClaims = getUserClaimsFromEvent(event); Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), StringUtils.EMPTY); + + // Case 2: Throw UserStoreException when getting the primary email. + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockUtilMethods(false, false, false, + true); + mockPendingVerificationEmail(existingEmail2); + when(userStoreManager.getUserClaimValue(anyString(), eq(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM), + any())).thenThrow(new UserStoreException("error")); + + try { + userEmailVerificationHandler.handleEvent(event2); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventException); + } } @Test(description = "Verification - Disabled, Multi attribute - Enabled") @@ -191,24 +194,46 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiEnabled() Map userClaims = getUserClaimsFromEvent(event); Assert.assertTrue(StringUtils.contains( userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), newEmail)); + + // Case 2 : Send email addresses claim with event. + String emailsClaim = String.format("%s,%s", existingEmail1, existingEmail2); + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, emailsClaim, newEmail); + + mockUtilMethods(false, true, false, + false); + + userEmailVerificationHandler.handleEvent(event2); + Map userClaims2 = getUserClaimsFromEvent(event2); + Assert.assertTrue(StringUtils.contains( + userClaims2.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), newEmail)); } @Test(description = "Verification - Enabled, Multi attribute - Disabled") public void testHandleEventPreSetUserClaimsVerificationEnabledMultiDisabled() throws IdentityEventException, IdentityRecoveryException, UserStoreException { - /* - New Email is not in the existing email address list. - Expected: IdentityEventClientException should be thrown. - */ - Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, null, null, newEmail); mockUtilMethods(true, false, false, false); + mockPendingVerificationEmail(existingEmail1); - userEmailVerificationHandler.handleEvent(event1); - Map userClaimsC1 = getUserClaimsFromEvent(event1); + userEmailVerificationHandler.handleEvent(event); + Map userClaimsC1 = getUserClaimsFromEvent(event); Assert.assertEquals(userClaimsC1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), newEmail); + + // Case 2: Send SELF_SIGNUP_ROLE with the event. + mockPendingVerificationEmail(existingEmail1); + String[] roleList = new String[]{IdentityRecoveryConstants.SELF_SIGNUP_ROLE}; + Map additionalEventProperties = new HashMap<>(); + additionalEventProperties.put(IdentityEventConstants.EventProperty.ROLE_LIST, roleList); + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail, additionalEventProperties, null); + + userEmailVerificationHandler.handleEvent(event2); + Map userClaimsC2 = getUserClaimsFromEvent(event2); + Assert.assertFalse(userClaimsC2.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM)); } @Test(description = "Verification - Enabled, Multi attribute - Enabled, Change primary email which is not in the " + @@ -335,6 +360,49 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC4() t Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM), existingEmail1); } + @Test + public void testHandleEventThreadLocalValues() throws IdentityEventException, UserStoreException { + + mockUtilMethods(true, false, false, + false); + mockPendingVerificationEmail(existingEmail1); + + // Case 1: Thread local value = SKIP_ON_CONFIRM. + Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockedUtils.when(Utils::getThreadLocalToSkipSendingEmailVerificationOnUpdate).thenReturn( + IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM + .toString()); + + userEmailVerificationHandler.handleEvent(event1); + Map userClaims1 = getUserClaimsFromEvent(event1); + Assert.assertFalse(userClaims1.containsKey(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM)); + + // Case 2: Thread local value = SKIP_ON_CONFIRM. + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockedUtils.when(Utils::getThreadLocalToSkipSendingEmailVerificationOnUpdate).thenReturn( + IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW + .toString()); + + userEmailVerificationHandler.handleEvent(event2); + Map userClaims2 = getUserClaimsFromEvent(event2); + Assert.assertFalse(userClaims2.containsKey(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM)); + + // Case 2: Thread local value = random value. + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, newEmail); + + mockedUtils.when(Utils::getThreadLocalToSkipSendingEmailVerificationOnUpdate).thenReturn("test"); + + userEmailVerificationHandler.handleEvent(event3); + Map userClaims3 = getUserClaimsFromEvent(event3); + Assert.assertFalse(userClaims3.containsKey(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM)); + + } + @Test public void testHandleEventPostSetUserClaims() throws IdentityEventException, IdentityRecoveryException, UserStoreException { @@ -349,6 +417,143 @@ public void testHandleEventPostSetUserClaims() verify(identityEventService).handleEvent(any()); } + @Test + public void testHandleEventPreAddUserVerifyEmailClaim() throws IdentityEventException { + + /* + Case 1: Enable verifyEmail claim and not provide the email address claim value. + */ + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION, true); + Event event1 = createEvent(IdentityEventConstants.Event.PRE_ADD_USER, IdentityRecoveryConstants.TRUE, + null, null, null); + + try { + userEmailVerificationHandler.handleEvent(event1); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventClientException); + Assert.assertEquals(((IdentityEventClientException) e).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND.getCode()); + } + + /* + Case 2: Provide the email address claim value. + */ + Event event2 = createEvent(IdentityEventConstants.Event.PRE_ADD_USER, IdentityRecoveryConstants.TRUE, + null, null, newEmail); + + userEmailVerificationHandler.handleEvent(event2); + mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), + eq(IdentityEventConstants.Event.PRE_VERIFY_EMAIL_CLAIM), + any())); + } + + @Test + public void testHandleEventPreAddUserAskPasswordClaim() throws IdentityEventException { + + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION, true); + mockedIdentityUtils.when(() -> IdentityUtil.getProperty(eq(IdentityRecoveryConstants + .ConnectorConfig.ASK_PASSWORD_DISABLE_RANDOM_VALUE_FOR_CREDENTIALS))) + .thenReturn(Boolean.TRUE.toString()); + char[] password = "test1".toCharArray(); + mockedUtils.when(() -> Utils.generateRandomPassword(anyInt())) + .thenReturn(password); + + StringBuffer credentials = new StringBuffer("test1"); + Map additionalProperties = new HashMap<>(); + additionalProperties.put(IdentityEventConstants.EventProperty.CREDENTIAL, credentials); + + Map additionalClaims = new HashMap<>(); + additionalClaims.put(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM, Boolean.TRUE.toString()); + + Event event = createEvent(IdentityEventConstants.Event.PRE_ADD_USER, IdentityRecoveryConstants.FALSE, + null, null, newEmail, additionalProperties, additionalClaims); + + + userEmailVerificationHandler.handleEvent(event); + mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), + eq(IdentityEventConstants.Event.PRE_ADD_USER_WITH_ASK_PASSWORD), + any())); + } + + @Test + public void testHandleEventPostAddUserVerifyEmailClaim() throws IdentityEventException { + + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION, true); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.EMAIL_ACCOUNT_LOCK_ON_CREATION, + true); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig + .EMAIL_VERIFICATION_NOTIFICATION_INTERNALLY_MANAGE,true); + + mockedUtils.when(() -> Utils.isAccountStateClaimExisting(anyString())).thenReturn(true); + + Claim claim = new Claim(); + claim.setClaimUri(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); + claim.setValue(Boolean.TRUE.toString()); + mockedUtils.when(Utils::getEmailVerifyTemporaryClaim).thenReturn(claim); + + Event event = createEvent(IdentityEventConstants.Event.POST_ADD_USER, IdentityRecoveryConstants.TRUE, + null, null, null); + + userEmailVerificationHandler.handleEvent(event); + verify(identityEventService).handleEvent(any()); + mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), + eq(IdentityEventConstants.Event.POST_VERIFY_EMAIL_CLAIM), + any())); + } + + @Test(priority = 99) + public void testHandleEventPostAddUserAskPasswordClaimNotificationInternallyManaged() + throws IdentityEventException, IdentityRecoveryException { + + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION, true); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.EMAIL_ACCOUNT_LOCK_ON_CREATION, + true); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig + .EMAIL_VERIFICATION_NOTIFICATION_INTERNALLY_MANAGE,true); + + mockedUtils.when(() -> Utils.isAccountStateClaimExisting(anyString())).thenReturn(true); + + Claim temporaryEmailClaim = new Claim(); + temporaryEmailClaim.setClaimUri(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM); + temporaryEmailClaim.setValue(Boolean.TRUE.toString()); + mockedUtils.when(Utils::getEmailVerifyTemporaryClaim).thenReturn(temporaryEmailClaim); + + Event event = createEvent(IdentityEventConstants.Event.POST_ADD_USER, IdentityRecoveryConstants.FALSE, + null, null, null); + + userEmailVerificationHandler.handleEvent(event); + verify(userRecoveryDataStore).store(any()); + mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), + eq(IdentityEventConstants.Event.POST_ADD_USER_WITH_ASK_PASSWORD), + any())); + } + + @Test(priority = 100) + public void testHandleEventPostAddUserAskPasswordClaimNotificationExternallyManaged() + throws IdentityEventException, IdentityRecoveryException { + + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION, true); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.EMAIL_ACCOUNT_LOCK_ON_CREATION, + true); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig + .EMAIL_VERIFICATION_NOTIFICATION_INTERNALLY_MANAGE,false); + + Claim temporaryEmailClaim = new Claim(); + temporaryEmailClaim.setClaimUri(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM); + temporaryEmailClaim.setValue(Boolean.TRUE.toString()); + mockedUtils.when(Utils::getEmailVerifyTemporaryClaim).thenReturn(temporaryEmailClaim); + + Event event = createEvent(IdentityEventConstants.Event.POST_ADD_USER, IdentityRecoveryConstants.FALSE, + null, null, null); + + userEmailVerificationHandler.handleEvent(event); + + verify(userRecoveryDataStore).store(any()); + mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), + eq(IdentityEventConstants.Event.POST_ADD_USER_WITH_ASK_PASSWORD), + any())); + } + private void mockExistingEmailAddressesList(List existingEmails) { mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), @@ -384,14 +589,16 @@ private void mockUtilMethods(boolean emailVerificationEnabled, boolean multiAttr mockedUtils.when( Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(multiAttributeEnabled); mockedUtils.when(Utils::isUseVerifyClaimEnabled).thenReturn(userVerifyClaimEnabled); - mockedUtils.when(() -> Utils.getConnectorConfig( - eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION_ON_UPDATE), - anyString())) - .thenReturn(String.valueOf(emailVerificationEnabled)); - mockedUtils.when(() -> Utils.getConnectorConfig( - eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_NOTIFICATION_ON_EMAIL_UPDATE), - anyString())) - .thenReturn(String.valueOf(notificationOnEmailUpdate)); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_EMAIL_VERIFICATION_ON_UPDATE, + emailVerificationEnabled); + mockGetConnectorConfig(IdentityRecoveryConstants.ConnectorConfig.ENABLE_NOTIFICATION_ON_EMAIL_UPDATE, + notificationOnEmailUpdate); + } + + private void mockGetConnectorConfig(String connectorConfig, boolean value) { + + mockedUtils.when(() -> Utils.getConnectorConfig(eq(connectorConfig), anyString())) + .thenReturn(String.valueOf(value)); } @SuppressWarnings("unchecked") From d6a5683e5dd9a7546b0f6f9fe3bb97ed1b0b0f0a Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Wed, 18 Sep 2024 15:27:16 +0530 Subject: [PATCH 31/54] Add connector config update tests --- .../connector/UserClaimUpdateConfigImpl.java | 1 - .../UserClaimUpdateConfigImplTest.java | 9 +++++++++ .../MobileNumberVerificationHandlerTest.java | 18 ++++++++++++++++++ .../UserEmailVerificationHandlerTest.java | 4 ++-- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java index 0165bbec94..415157d405 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImpl.java @@ -74,7 +74,6 @@ public class UserClaimUpdateConfigImpl implements IdentityConnectorConfig { private static final String VERIFICATION_ON_UPDATE_ELEMENT = "VerificationOnUpdate"; private static final String NOTIFICATION_ON_UPDATE_ELEMENT = "NotificationOnUpdate"; private static final String ENABLE_MOBILE_VERIFICATION_PRIVILEGED_USER = "EnableVerificationByPrivilegedUser"; - private static final String ENABLE_MULTIPLE_EMAILS_AND_MOBILE_NUMBERS_ELEMENT = "EnableMultipleEmailsAndMobileNumbers"; private static String enableEmailVerificationOnUpdateProperty = null; private static String enableSendOTPInEmailProperty = null; private static String useUppercaseCharactersInOTPProperty = null; diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java index ed38fc30f0..9520096812 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/UserClaimUpdateConfigImplTest.java @@ -21,6 +21,7 @@ import org.apache.axiom.om.OMElement; import org.mockito.MockedStatic; import org.mockito.Mockito; +import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; @@ -29,6 +30,7 @@ import org.wso2.carbon.identity.core.util.IdentityCoreConstants; import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.application.common.model.Property; import java.util.ArrayList; import java.util.HashMap; @@ -259,4 +261,11 @@ public void testGetDefaultProperties() throws IdentityGovernanceException { assertEquals(defaultPropertyValues.size(), propertyNames.length - 1, "Maps are not equal as" + " their size differs."); } + + @Test + public void testGetMetaData() { + + Map metaData = userClaimUpdateConfig.getMetaData(); + Assert.assertEquals(metaData.size(), 10); + } } diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index d4104ca633..77a4795cda 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -370,6 +370,24 @@ public void testAddNewMobileToVerifiedList() throws Exception { } catch (Exception e) { Assert.assertTrue(e instanceof IdentityEventClientException); } + + // Case 3: Added new number is existing primary mobile number. + String newVerifiedMobileNumbers3 = existingNumber1 + "," + newMobileNumber; + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, + IdentityRecoveryConstants.FALSE, + newVerifiedMobileNumbers3, null, null); + + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + mockExistingPrimaryMobileNumber(newMobileNumber); + + mobileNumberVerificationHandler.handleEvent(event3); + + Map userClaims3 = getUserClaimsFromEvent(event3); + Assert.assertTrue( + StringUtils.contains(userClaims3.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), newMobileNumber)); + Assert.assertTrue(StringUtils.contains(userClaims3.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM), + newMobileNumber)); } @Test(description = "POST_SET_USER_CLAIMS: Verification enabled, Multi-attribute enabled") diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java index f1a47cb1f2..d83ade11c4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -501,7 +501,7 @@ public void testHandleEventPostAddUserVerifyEmailClaim() throws IdentityEventExc any())); } - @Test(priority = 99) + @Test public void testHandleEventPostAddUserAskPasswordClaimNotificationInternallyManaged() throws IdentityEventException, IdentityRecoveryException { @@ -528,7 +528,7 @@ public void testHandleEventPostAddUserAskPasswordClaimNotificationInternallyMana any())); } - @Test(priority = 100) + @Test public void testHandleEventPostAddUserAskPasswordClaimNotificationExternallyManaged() throws IdentityEventException, IdentityRecoveryException { From 2e56b759b891f23435f4b379fdde3bbc8a26ae37 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 19 Sep 2024 00:10:09 +0530 Subject: [PATCH 32/54] Rename test variables. --- .../handler/UserEmailVerificationHandler.java | 7 +- .../MobileNumberVerificationHandlerTest.java | 97 ++++++++-------- .../UserEmailVerificationHandlerTest.java | 108 +++++++++--------- 3 files changed, 106 insertions(+), 106 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 4efd9b2d0e..adc72e4ae9 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -63,9 +63,10 @@ import java.util.UUID; import java.util.stream.Collectors; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_EMAILS_LIST; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND; public class UserEmailVerificationHandler extends AbstractEventHandler { diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index 77a4795cda..3ae4195b5c 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -63,7 +63,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; -import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -109,13 +108,13 @@ public class MobileNumberVerificationHandlerTest { private MockedStatic mockedIdentityRecoveryServiceDataHolder; private MockedStatic mockedFrameworkUtils; - private static final String username = "testuser"; - private static final String tenantDomain = "test.com"; - private static final int tenantId = 5; - private static final String userStoreDomain = "TESTING"; - private static final String existingNumber1 = "0777777777"; - private static final String existingNumber2 = "0711111111"; - private static final String newMobileNumber = "0722222222"; + private static final String TEST_USERNAME = "testuser"; + private static final String TEST_TENANT_DOMAIN = "test.com"; + private static final int TEST_TENANT_ID = 5; + private static final String TEST_USER_STORE_DOMAIN = "TESTING"; + private static final String EXISTING_NUMBER_1 = "0777777777"; + private static final String EXISTING_NUMBER_2 = "0711111111"; + private static final String NEW_MOBILE_NUMBER = "0722222222"; @BeforeMethod public void setUpMethod() throws UserStoreException { @@ -135,13 +134,13 @@ public void setUpMethod() throws UserStoreException { when(serviceDataHolder.getRealmService()).thenReturn(realmService); when(serviceDataHolder.getIdentityEventService()).thenReturn(identityEventService); when(realmService.getTenantManager()).thenReturn(tenantManager); - when(tenantManager.getTenantId(tenantDomain)).thenReturn(tenantId); + when(tenantManager.getTenantId(TEST_TENANT_DOMAIN)).thenReturn(TEST_TENANT_ID); when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); when(userRealm.getClaimManager()).thenReturn(claimManager); when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); when(realmConfiguration.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME)) - .thenReturn(userStoreDomain); + .thenReturn(TEST_USER_STORE_DOMAIN); } @AfterMethod @@ -166,7 +165,7 @@ public void testHandleEventVerificationDisabledMultiAttributeDisabled() Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mockVerificationPendingMobileNumber(); mockUtilMethods(false, false, false); @@ -185,19 +184,19 @@ public void testHandleEventVerificationDisabledMultiAttributeEnabled() throws UserStoreException, IdentityEventException, IdentityRecoveryException { Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mockVerificationPendingMobileNumber(); mockUtilMethods(false, true, false); // New primary mobile number is not included in existing all mobile numbers list. - List allMobileNumbers = new ArrayList<>(Arrays.asList(existingNumber1, existingNumber2)); + List allMobileNumbers = new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1, EXISTING_NUMBER_2)); mockExistingNumbersList(allMobileNumbers); // Expectation: New mobile number should be added to the mobile numbers claim. mobileNumberVerificationHandler.handleEvent(event); Map userClaims = getUserClaimsFromEvent(event); Assert.assertTrue(StringUtils.contains( - userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), newMobileNumber)); + userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), NEW_MOBILE_NUMBER)); } @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Claims null") @@ -222,7 +221,7 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabledThreadLocal( // Case 2.1: skipSendingSmsOtpVerificationOnUpdate set to SKIP_ON_CONFIRM. mockVerificationPendingMobileNumber(); Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_CONFIRM.toString()); @@ -235,7 +234,7 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabledThreadLocal( // Case 2.2: skipSendingSmsOtpVerificationOnUpdate set to SKIP_ON_SMS_OTP_FLOW. mockVerificationPendingMobileNumber(); Event event2_1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_SMS_OTP_FLOW.toString()); @@ -249,7 +248,7 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabledThreadLocal( Case 3: skipSendingSmsOtpVerificationOnUpdate set to some value. */ Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) .thenReturn("test"); @@ -263,20 +262,20 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabled() throws Ex mockVerificationPendingMobileNumber(); mockUtilMethods(true, false, false); Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mobileNumberVerificationHandler.handleEvent(event); Map userClaims = getUserClaimsFromEvent(event); Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), - newMobileNumber); + NEW_MOBILE_NUMBER); /* Case 2: New mobile number is same as existing primary mobile number. */ - mockExistingPrimaryMobileNumber(newMobileNumber); + mockExistingPrimaryMobileNumber(NEW_MOBILE_NUMBER); Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mobileNumberVerificationHandler.handleEvent(event2); @@ -287,10 +286,10 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabled() throws Ex /* Case 3: Enable userVerify and send verifyMobileClaim as false. */ - mockExistingPrimaryMobileNumber(existingNumber1); + mockExistingPrimaryMobileNumber(EXISTING_NUMBER_1); mockedUtils.when(Utils::isUseVerifyClaimEnabled).thenReturn(true); Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newMobileNumber); + null, null, NEW_MOBILE_NUMBER); mobileNumberVerificationHandler.handleEvent(event3); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( @@ -302,12 +301,12 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabled() throws Ex public void testUpdatePrimaryMobileNotInVerifiedList() throws Exception { mockUtilMethods(true, true, false); - String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; - String newMobileNumbers = existingNumber1 + "," + newMobileNumber; + String newVerifiedMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; + String newMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - newVerifiedMobileNumbers, newMobileNumbers, newMobileNumber); - mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); - mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1, newMobileNumber))); + newVerifiedMobileNumbers, newMobileNumbers, NEW_MOBILE_NUMBER); + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1, NEW_MOBILE_NUMBER))); try { mobileNumberVerificationHandler.handleEvent(event); @@ -324,12 +323,12 @@ public void testUpdatePrimaryMobileInVerifiedList() throws Exception { mockVerificationPendingMobileNumber(); mockUtilMethods(true, true, false); - String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; - String newMobileNumbers = existingNumber1 + "," + newMobileNumber; + String newVerifiedMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; + String newMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - newVerifiedMobileNumbers, newMobileNumbers, newMobileNumber); - mockExistingVerifiedNumbersList(Arrays.asList(existingNumber1, newMobileNumber)); + newVerifiedMobileNumbers, newMobileNumbers, NEW_MOBILE_NUMBER); + mockExistingVerifiedNumbersList(Arrays.asList(EXISTING_NUMBER_1, NEW_MOBILE_NUMBER)); mobileNumberVerificationHandler.handleEvent(event); @@ -344,26 +343,26 @@ public void testAddNewMobileToVerifiedList() throws Exception { mockVerificationPendingMobileNumber(); mockUtilMethods(true, true, false); - String newVerifiedMobileNumbers = existingNumber1 + "," + newMobileNumber; + String newVerifiedMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, newVerifiedMobileNumbers, null, null); - mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); - mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); mobileNumberVerificationHandler.handleEvent(event); Map userClaims = getUserClaimsFromEvent(event); Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), - newMobileNumber); + NEW_MOBILE_NUMBER); // Case 2: Add multiple numbers to new verified list at once. - String newVerifiedMobileNumbers2 = existingNumber1 + "," + newMobileNumber; + String newVerifiedMobileNumbers2 = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, newVerifiedMobileNumbers2, null, null); mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList())); - mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); try { mobileNumberVerificationHandler.handleEvent(event2); @@ -372,22 +371,22 @@ public void testAddNewMobileToVerifiedList() throws Exception { } // Case 3: Added new number is existing primary mobile number. - String newVerifiedMobileNumbers3 = existingNumber1 + "," + newMobileNumber; + String newVerifiedMobileNumbers3 = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, newVerifiedMobileNumbers3, null, null); - mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); - mockExistingNumbersList(new ArrayList<>(Arrays.asList(existingNumber1))); - mockExistingPrimaryMobileNumber(newMobileNumber); + mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); + mockExistingPrimaryMobileNumber(NEW_MOBILE_NUMBER); mobileNumberVerificationHandler.handleEvent(event3); Map userClaims3 = getUserClaimsFromEvent(event3); Assert.assertTrue( - StringUtils.contains(userClaims3.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), newMobileNumber)); + StringUtils.contains(userClaims3.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), NEW_MOBILE_NUMBER)); Assert.assertTrue(StringUtils.contains(userClaims3.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM), - newMobileNumber)); + NEW_MOBILE_NUMBER)); } @Test(description = "POST_SET_USER_CLAIMS: Verification enabled, Multi-attribute enabled") @@ -419,7 +418,7 @@ public void testHandleEventPostSet() throws IdentityEventException, IdentityReco verify(identityEventService).handleEvent(any()); // Case 3: Handle exception thrown from userStoreManager.getUserClaimValues. - when(userStoreManager.getUserClaimValues(eq(username), + when(userStoreManager.getUserClaimValues(eq(TEST_USERNAME), eq(new String[]{IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM}), isNull())) .thenThrow(new org.wso2.carbon.user.core.UserStoreException()); try { @@ -478,8 +477,8 @@ private void mockVerificationPendingMobileNumber() throws UserStoreException { .thenReturn(pendingMobileNumberClaim); Map pendingMobileNumberClaimMap = new HashMap<>(); - pendingMobileNumberClaimMap.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, existingNumber2); - when(userStoreManager.getUserClaimValues(eq(username), + pendingMobileNumberClaimMap.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, EXISTING_NUMBER_2); + when(userStoreManager.getUserClaimValues(eq(TEST_USERNAME), eq(new String[]{IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM}), isNull())) .thenReturn(pendingMobileNumberClaimMap); } @@ -488,8 +487,8 @@ private Event createEvent(String eventType, String verifyMobileClaim, String ver String mobileNumbersClaim, String mobileNumber) { Map eventProperties = new HashMap<>(); - eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, username); - eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, TEST_USERNAME); + eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, TEST_TENANT_DOMAIN); eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); Map claims = new HashMap<>(); diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java index d83ade11c4..68acf72e4e 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -86,12 +86,12 @@ public class UserEmailVerificationHandlerTest { private MockedStatic mockedFrameworkUtils; private MockedStatic mockedIdentityUtils; - private static final String tenantDomain = "test.com"; - private static final String userStoreDomainName = "TESTING"; - private static final String username = "testuser"; - private static final String existingEmail1 = "old1@abc.com"; - private static final String existingEmail2 = "old2@abc.com"; - private static final String newEmail = "new@abc.com"; + private static final String TEST_TENANT_DOMAIN = "test.com"; + private static final String TEST_USER_STORE_DOMAIN = "TESTING"; + private static final String TEST_USERNAME = "testuser"; + private static final String EXISTING_EMAIL_1 = "old1@abc.com"; + private static final String EXISTING_EMAIL_2 = "old2@abc.com"; + private static final String NEW_EMAIL = "new@abc.com"; @AfterMethod public void close() { @@ -119,13 +119,13 @@ public void setUp() throws NoSuchFieldException, IllegalAccessException { .thenReturn(serviceDataHolder); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); - mockedIdentityUtils.when(() -> IdentityUtil.addDomainToName(eq(username), anyString())) - .thenReturn(String.format("%s/%s", username, userStoreDomainName)); + mockedIdentityUtils.when(() -> IdentityUtil.addDomainToName(eq(TEST_USERNAME), anyString())) + .thenReturn(String.format("%s/%s", TEST_USERNAME, TEST_USER_STORE_DOMAIN)); when(serviceDataHolder.getIdentityEventService()).thenReturn(identityEventService); when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); when(realmConfiguration.getUserStoreProperty(eq( - UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME))).thenReturn(userStoreDomainName); + UserCoreConstants.RealmConfig.PROPERTY_DOMAIN_NAME))).thenReturn(TEST_USER_STORE_DOMAIN); } @Test @@ -144,12 +144,12 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiDisabled() Expected: Notification event should be triggered, pending email claim should be set to empty string. */ Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockUtilMethods(false, false, false, true); - mockPrimaryEmail(existingEmail1); - mockPendingVerificationEmail(existingEmail2); + mockPrimaryEmail(EXISTING_EMAIL_1); + mockPendingVerificationEmail(EXISTING_EMAIL_2); userEmailVerificationHandler.handleEvent(event); verify(identityEventService).handleEvent(any()); @@ -159,11 +159,11 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiDisabled() // Case 2: Throw UserStoreException when getting the primary email. Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockUtilMethods(false, false, false, true); - mockPendingVerificationEmail(existingEmail2); + mockPendingVerificationEmail(EXISTING_EMAIL_2); when(userStoreManager.getUserClaimValue(anyString(), eq(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM), any())).thenThrow(new UserStoreException("error")); @@ -183,22 +183,22 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiEnabled() Expected: New email should be added to the new email addresses claim. */ Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockUtilMethods(false, true, false, false); - List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + List existingEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1, EXISTING_EMAIL_2)); mockExistingEmailAddressesList(existingEmails); userEmailVerificationHandler.handleEvent(event); Map userClaims = getUserClaimsFromEvent(event); Assert.assertTrue(StringUtils.contains( - userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), newEmail)); + userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), NEW_EMAIL)); // Case 2 : Send email addresses claim with event. - String emailsClaim = String.format("%s,%s", existingEmail1, existingEmail2); + String emailsClaim = String.format("%s,%s", EXISTING_EMAIL_1, EXISTING_EMAIL_2); Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, emailsClaim, newEmail); + null, emailsClaim, NEW_EMAIL); mockUtilMethods(false, true, false, false); @@ -206,7 +206,7 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiEnabled() userEmailVerificationHandler.handleEvent(event2); Map userClaims2 = getUserClaimsFromEvent(event2); Assert.assertTrue(StringUtils.contains( - userClaims2.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), newEmail)); + userClaims2.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), NEW_EMAIL)); } @Test(description = "Verification - Enabled, Multi attribute - Disabled") @@ -214,22 +214,22 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiDisabled() throws IdentityEventException, IdentityRecoveryException, UserStoreException { Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockUtilMethods(true, false, false, false); - mockPendingVerificationEmail(existingEmail1); + mockPendingVerificationEmail(EXISTING_EMAIL_1); userEmailVerificationHandler.handleEvent(event); Map userClaimsC1 = getUserClaimsFromEvent(event); - Assert.assertEquals(userClaimsC1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), newEmail); + Assert.assertEquals(userClaimsC1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), NEW_EMAIL); // Case 2: Send SELF_SIGNUP_ROLE with the event. - mockPendingVerificationEmail(existingEmail1); + mockPendingVerificationEmail(EXISTING_EMAIL_1); String[] roleList = new String[]{IdentityRecoveryConstants.SELF_SIGNUP_ROLE}; Map additionalEventProperties = new HashMap<>(); additionalEventProperties.put(IdentityEventConstants.EventProperty.ROLE_LIST, roleList); Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail, additionalEventProperties, null); + null, null, NEW_EMAIL, additionalEventProperties, null); userEmailVerificationHandler.handleEvent(event2); Map userClaimsC2 = getUserClaimsFromEvent(event2); @@ -246,14 +246,14 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC1() Expected: IdentityEventClientException should be thrown. */ Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockUtilMethods(true, true, false, false); - List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + List existingEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1, EXISTING_EMAIL_2)); mockExistingEmailAddressesList(existingEmails); - List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1)); + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1)); mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); try { @@ -272,14 +272,14 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC2() t Expected: Thread local should be set to skip sending email verification on update. */ Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockUtilMethods(true, true, false, false); - List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, newEmail)); + List existingEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1, NEW_EMAIL)); mockExistingEmailAddressesList(existingEmails); - List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1, newEmail)); + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1, NEW_EMAIL)); mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); userEmailVerificationHandler.handleEvent(event); @@ -296,44 +296,44 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC3() Case 1:Try to update the verified email list with a new email. Expected: IdentityEventClientException should be thrown. */ - String newVerifiedEmails = String.format("%s,%s", existingEmail1, newEmail); + String newVerifiedEmails = String.format("%s,%s", EXISTING_EMAIL_1, NEW_EMAIL); Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, newVerifiedEmails, null, null); mockUtilMethods(true, true, false, false); - List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1)); + List existingEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1)); mockExistingEmailAddressesList(existingEmails); - List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1)); + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1)); mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); userEmailVerificationHandler.handleEvent(event1); Map userClaims1 = getUserClaimsFromEvent(event1); - Assert.assertEquals(userClaims1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), newEmail); + Assert.assertEquals(userClaims1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), NEW_EMAIL); /* Case 2: Update verified email list with the existing primary email which is not in the verified email list. Expected: Email should be added to the updated verified email list. */ - String newVerifiedEmails2 = String.format("%s,%s", existingEmail1, newEmail); + String newVerifiedEmails2 = String.format("%s,%s", EXISTING_EMAIL_1, NEW_EMAIL); Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, newVerifiedEmails2, null, null); mockUtilMethods(true, true, false, false); - List existingEmails2 = new ArrayList<>(Arrays.asList(existingEmail1)); + List existingEmails2 = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1)); mockExistingEmailAddressesList(existingEmails2); - List existingVerifiedEmails2 = new ArrayList<>(Arrays.asList(existingEmail1)); + List existingVerifiedEmails2 = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1)); mockExistingVerifiedEmailAddressesList(existingVerifiedEmails2); - mockPrimaryEmail(newEmail); + mockPrimaryEmail(NEW_EMAIL); userEmailVerificationHandler.handleEvent(event2); Map userClaims2 = getUserClaimsFromEvent(event2); String updatedVerifiedEmails = userClaims2.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - Assert.assertTrue(StringUtils.contains(updatedVerifiedEmails, newEmail)); + Assert.assertTrue(StringUtils.contains(updatedVerifiedEmails, NEW_EMAIL)); } @Test(description = "Verification - Enabled, Multi attribute - Enabled, Remove email from email list") @@ -344,20 +344,20 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC4() t Expected: Removed email should be removed from the verified email list as well. */ Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, existingEmail1, null); + null, EXISTING_EMAIL_1, null); mockUtilMethods(true, true, false, false); - List existingEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + List existingEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1, EXISTING_EMAIL_2)); mockExistingEmailAddressesList(existingEmails); - List existingVerifiedEmails = new ArrayList<>(Arrays.asList(existingEmail1, existingEmail2)); + List existingVerifiedEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1, EXISTING_EMAIL_2)); mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); userEmailVerificationHandler.handleEvent(event); Map userClaims = getUserClaimsFromEvent(event); - Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), existingEmail1); - Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM), existingEmail1); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM), EXISTING_EMAIL_1); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM), EXISTING_EMAIL_1); } @Test @@ -365,11 +365,11 @@ public void testHandleEventThreadLocalValues() throws IdentityEventException, Us mockUtilMethods(true, false, false, false); - mockPendingVerificationEmail(existingEmail1); + mockPendingVerificationEmail(EXISTING_EMAIL_1); // Case 1: Thread local value = SKIP_ON_CONFIRM. Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockedUtils.when(Utils::getThreadLocalToSkipSendingEmailVerificationOnUpdate).thenReturn( IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_CONFIRM @@ -381,7 +381,7 @@ public void testHandleEventThreadLocalValues() throws IdentityEventException, Us // Case 2: Thread local value = SKIP_ON_CONFIRM. Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockedUtils.when(Utils::getThreadLocalToSkipSendingEmailVerificationOnUpdate).thenReturn( IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates.SKIP_ON_EMAIL_OTP_FLOW @@ -393,7 +393,7 @@ public void testHandleEventThreadLocalValues() throws IdentityEventException, Us // Case 2: Thread local value = random value. Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, - null, null, newEmail); + null, null, NEW_EMAIL); mockedUtils.when(Utils::getThreadLocalToSkipSendingEmailVerificationOnUpdate).thenReturn("test"); @@ -411,7 +411,7 @@ public void testHandleEventPostSetUserClaims() null, null, null); mockUtilMethods(true, true, false, true); - mockPendingVerificationEmail(existingEmail1); + mockPendingVerificationEmail(EXISTING_EMAIL_1); userEmailVerificationHandler.handleEvent(event); verify(identityEventService).handleEvent(any()); @@ -439,7 +439,7 @@ public void testHandleEventPreAddUserVerifyEmailClaim() throws IdentityEventExce Case 2: Provide the email address claim value. */ Event event2 = createEvent(IdentityEventConstants.Event.PRE_ADD_USER, IdentityRecoveryConstants.TRUE, - null, null, newEmail); + null, null, NEW_EMAIL); userEmailVerificationHandler.handleEvent(event2); mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), @@ -466,7 +466,7 @@ public void testHandleEventPreAddUserAskPasswordClaim() throws IdentityEventExce additionalClaims.put(IdentityRecoveryConstants.ASK_PASSWORD_CLAIM, Boolean.TRUE.toString()); Event event = createEvent(IdentityEventConstants.Event.PRE_ADD_USER, IdentityRecoveryConstants.FALSE, - null, null, newEmail, additionalProperties, additionalClaims); + null, null, NEW_EMAIL, additionalProperties, additionalClaims); userEmailVerificationHandler.handleEvent(event); @@ -619,8 +619,8 @@ private Event createEvent(String eventType, String verifyEmailClaim, String veri Map additionalClaims) { Map eventProperties = new HashMap<>(); - eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, username); - eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, tenantDomain); + eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, TEST_USERNAME); + eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, TEST_TENANT_DOMAIN); eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); if (additionalEventProperties != null) { From 5ce0934601d93d05aae979d3f188cbc85c484411 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 19 Sep 2024 00:15:11 +0530 Subject: [PATCH 33/54] Refactor Test Class with Annotations. --- .../MobileNumberVerificationHandlerTest.java | 1 - .../UserSelfRegistrationManagerTest.java | 104 +++++++++++------- 2 files changed, 64 insertions(+), 41 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index 3ae4195b5c..63d11a60f7 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -53,7 +53,6 @@ import java.util.ArrayList; import java.util.Arrays; -import java.util.Collections; import java.util.HashMap; import java.util.List; import java.util.Map; diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index 8875169672..fafe0f9b4d 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -25,12 +25,10 @@ import org.mockito.ArgumentMatchers; import org.mockito.Mock; import org.mockito.MockedStatic; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; -import org.testng.annotations.BeforeTest; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.consent.mgt.core.ConsentManager; @@ -39,6 +37,7 @@ import org.wso2.carbon.consent.mgt.core.model.AddReceiptResponse; import org.wso2.carbon.consent.mgt.core.model.ConsentManagerConfigurationHolder; import org.wso2.carbon.consent.mgt.core.model.ReceiptInput; +import org.wso2.carbon.context.PrivilegedCarbonContext; import org.wso2.carbon.identity.application.common.model.IdentityProvider; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerManager; @@ -80,6 +79,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; @@ -98,56 +98,86 @@ import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MULTIPLE_REGISTRATION_OPTIONS; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED_ERROR_VALIDATING_ATTRIBUTES; +/** + * Test class for UserSelfRegistrationManager class. + */ @WithCarbonHome public class UserSelfRegistrationManagerTest { - private UserSelfRegistrationManager userSelfRegistrationManager = UserSelfRegistrationManager.getInstance(); - private ReceiptInput resultReceipt; - private String TEST_TENANT_DOMAIN_NAME = "carbon.super"; - private String TEST_USERSTORE_DOMAIN = "PRIMARY"; + private UserSelfRegistrationManager userSelfRegistrationManager; + + @Mock + private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; + + @Mock + private UserRecoveryDataStore userRecoveryDataStore; + + @Mock + private IdentityEventService identityEventService; + + @Mock + private UserStoreManager userStoreManager; + + @Mock + private UserRealm userRealm; + + @Mock private IdentityProviderManager identityProviderManager; + + @Mock private AuthAttributeHandlerManager authAttributeHandlerManager; + + @Mock private IdentityGovernanceService identityGovernanceService; - private RealmService realmService; - private OTPGenerator otpGenerator; - private final String TEST_USER_NAME = "dummyUser"; - private final String TEST_CLAIM_URI = "ttp://wso2.org/claims/emailaddress"; - private final String TEST_CLAIM_VALUE = "dummyuser@wso2.com"; - private final String TEST_MOBILE_CLAIM_VALUE = "0775553443"; - private final UserRealm userRealm = Mockito.mock(UserRealm.class); - private final UserStoreManager userStoreManager = Mockito.mock(UserStoreManager.class); - private static final Log LOG = LogFactory.getLog(UserSelfRegistrationManagerTest.class); @Mock - UserRecoveryDataStore userRecoveryDataStore; + private ReceiptInput resultReceipt; + + @Mock + private RealmService realmService; @Mock - IdentityEventService identityEventService; + private OTPGenerator otpGenerator; + private MockedStatic mockedServiceDataHolder; private MockedStatic mockedIdentityUtil; private MockedStatic mockedJDBCRecoveryDataStore; private MockedStatic mockedIdentityProviderManager; private MockedStatic mockedIdentityTenantUtil; + private MockedStatic mockedPrivilegedCarbonContext; + + private final String TEST_TENANT_DOMAIN_NAME = "carbon.super"; + private final String TEST_USERSTORE_DOMAIN = "PRIMARY"; + private final String TEST_USER_NAME = "dummyUser"; + private final String TEST_CLAIM_URI = "ttp://wso2.org/claims/emailaddress"; + private final String TEST_CLAIM_VALUE = "dummyuser@wso2.com"; + private final String TEST_MOBILE_CLAIM_VALUE = "0775553443"; + + private static final Log LOG = LogFactory.getLog(UserSelfRegistrationManagerTest.class); @BeforeMethod public void setUp() { - mockedIdentityUtil = Mockito.mockStatic(IdentityUtil.class); - mockedJDBCRecoveryDataStore = Mockito.mockStatic(JDBCRecoveryDataStore.class); - mockedIdentityProviderManager = Mockito.mockStatic(IdentityProviderManager.class); - mockedIdentityTenantUtil = Mockito.mockStatic(IdentityTenantUtil.class); - identityProviderManager = Mockito.mock(IdentityProviderManager.class); - authAttributeHandlerManager = Mockito.mock(AuthAttributeHandlerManager.class); - identityGovernanceService = Mockito.mock(IdentityGovernanceService.class); - otpGenerator = Mockito.mock(OTPGenerator.class); - realmService = Mockito.mock(RealmService.class); + MockitoAnnotations.openMocks(this); + + userSelfRegistrationManager = UserSelfRegistrationManager.getInstance(); - IdentityRecoveryServiceDataHolder.getInstance().setIdentityEventService(identityEventService); - IdentityRecoveryServiceDataHolder.getInstance().setIdentityGovernanceService(identityGovernanceService); - IdentityRecoveryServiceDataHolder.getInstance().setOtpGenerator(otpGenerator); - IdentityRecoveryServiceDataHolder.getInstance().setAuthAttributeHandlerManager(authAttributeHandlerManager); - IdentityRecoveryServiceDataHolder.getInstance().setRealmService(realmService); + mockedServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + mockedIdentityUtil = mockStatic(IdentityUtil.class); + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedIdentityProviderManager = mockStatic(IdentityProviderManager.class); + mockedIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); + mockedPrivilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class); + mockedIdentityProviderManager.when(IdentityProviderManager::getInstance).thenReturn(identityProviderManager); + mockedServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) + .thenReturn(identityRecoveryServiceDataHolder); + + when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); + when(identityRecoveryServiceDataHolder.getIdentityGovernanceService()).thenReturn(identityGovernanceService); + when(identityRecoveryServiceDataHolder.getOtpGenerator()).thenReturn(otpGenerator); + when(identityRecoveryServiceDataHolder.getAuthAttributeHandlerManager()).thenReturn(authAttributeHandlerManager); + when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); } @AfterMethod @@ -157,13 +187,8 @@ public void tearDown() { mockedJDBCRecoveryDataStore.close(); mockedIdentityProviderManager.close(); mockedIdentityTenantUtil.close(); - } - - @BeforeTest - void setup() { - - MockitoAnnotations.openMocks(this); - this.resultReceipt = null; + mockedPrivilegedCarbonContext.close(); + mockedServiceDataHolder.close(); } String consentData = @@ -380,10 +405,9 @@ private void mockEmailTrigger() throws IdentityEventException { public void testAddConsent() throws Exception { IdentityProvider identityProvider = new IdentityProvider(); - mockedIdentityProviderManager.when(IdentityProviderManager::getInstance).thenReturn(identityProviderManager); when(identityProviderManager.getResidentIdP(ArgumentMatchers.anyString())).thenReturn(identityProvider); ConsentManager consentManager = new MyConsentManager(new ConsentManagerConfigurationHolder()); - IdentityRecoveryServiceDataHolder.getInstance().setConsentManager(consentManager); + when(identityRecoveryServiceDataHolder.getConsentManager()).thenReturn(consentManager); userSelfRegistrationManager.addUserConsent(consentData, "wso2.com"); Assert.assertEquals(IdentityRecoveryConstants.Consent.COLLECTION_METHOD_SELF_REGISTRATION, resultReceipt.getCollectionMethod()); From ef46d0a184fb14b1920c2d7395451e41a1ebb677 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Fri, 20 Sep 2024 00:19:59 +0530 Subject: [PATCH 34/54] Add unit tests - UserSelfRegistrationManager --- .../MobileNumberVerificationHandler.java | 26 +-- .../handler/UserEmailVerificationHandler.java | 29 ++- .../signup/UserSelfRegistrationManager.java | 70 ++++-- .../UserSelfRegistrationManagerTest.java | 215 +++++++++++++++++- 4 files changed, 285 insertions(+), 55 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 2c7618a954..cc0fc364c4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -388,6 +388,19 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use throw new IdentityEventException(error, e); } + if (supportMultipleMobileNumbers) { + if (!updatedVerifiedNumbersList.contains(existingMobileNumber)) { + updatedVerifiedNumbersList.add(existingMobileNumber); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); + } + if (!updatedAllNumbersList.contains(existingMobileNumber)) { + updatedAllNumbersList.add(existingMobileNumber); + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedAllNumbersList)); + } + } + if (StringUtils.equals(mobileNumber, existingMobileNumber)) { if (log.isDebugEnabled()) { log.debug(String.format("The mobile number to be updated: %s is same as the existing mobile " + @@ -398,19 +411,6 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); invalidatePendingMobileVerification(user, userStoreManager, claims); - - if (supportMultipleMobileNumbers) { - if (!updatedVerifiedNumbersList.contains(mobileNumber)) { - updatedVerifiedNumbersList.add(mobileNumber); - claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); - } - if (!updatedAllNumbersList.contains(mobileNumber)) { - updatedAllNumbersList.add(mobileNumber); - claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, updatedAllNumbersList)); - } - } return; } /* diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index adc72e4ae9..49cac51066 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -649,8 +649,20 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore return; } - String existingEmail; - existingEmail = getEmailClaimValue(user, userStoreManager); + String existingEmail = getEmailClaimValue(user, userStoreManager); + + if (StringUtils.isNotBlank(existingEmail) && supportMultipleEmails) { + if (!updatedVerifiedEmailAddresses.contains(existingEmail)) { + updatedVerifiedEmailAddresses.add(existingEmail); + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); + } + if (!updatedAllEmailAddresses.contains(existingEmail)) { + updatedAllEmailAddresses.add(existingEmail); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); + } + } if (emailAddress.equals(existingEmail)) { if (log.isDebugEnabled()) { @@ -661,19 +673,6 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString()); invalidatePendingEmailVerification(user, userStoreManager, claims); - - if (supportMultipleEmails) { - if (!updatedVerifiedEmailAddresses.contains(emailAddress)) { - updatedVerifiedEmailAddresses.add(emailAddress); - claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, - StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); - } - if (!updatedAllEmailAddresses.contains(emailAddress)) { - updatedAllEmailAddresses.add(emailAddress); - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, - StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); - } - } return; } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index d80ca37031..8f0756e5bd 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -34,6 +34,7 @@ import org.wso2.carbon.consent.mgt.core.model.ReceiptInput; import org.wso2.carbon.consent.mgt.core.model.ReceiptServiceInput; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.IdentityProvider; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerManager; @@ -760,6 +761,7 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi externallyVerifiedClaim, recoveryData.getRecoveryScenario().toString()); boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); if (RecoverySteps.VERIFY_EMAIL.equals(recoveryData.getRecoveryStep())) { String pendingEmailClaimValue = recoveryData.getRemainingSetIds(); @@ -768,20 +770,18 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM, StringUtils.EMPTY); if (supportMultipleEmailsAndMobileNumbers) { try { - List verifiedEmails = Utils.getMultiValuedClaim( - (org.wso2.carbon.user.core.UserStoreManager) userStoreManager, user, + List verifiedEmails = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); verifiedEmails.add(pendingEmailClaimValue); userClaims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join( - verifiedEmails, ",")); + verifiedEmails, multiAttributeSeparator)); - List allEmails = Utils.getMultiValuedClaim( - (org.wso2.carbon.user.core.UserStoreManager) userStoreManager, user, + List allEmails = Utils.getMultiValuedClaim(userStoreManager, user, IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); if (!allEmails.contains(pendingEmailClaimValue)) { allEmails.add(pendingEmailClaimValue); userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, StringUtils.join( - allEmails, ",")) ; + allEmails, multiAttributeSeparator)) ; } } catch (IdentityEventException e) { log.error("Error occurred while obtaining claim for the user : " + user.getUserName()); @@ -799,8 +799,35 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi if (RecoverySteps.VERIFY_MOBILE_NUMBER.equals(recoveryData.getRecoveryStep())) { String pendingMobileClaimValue = recoveryData.getRemainingSetIds(); if (StringUtils.isNotBlank(pendingMobileClaimValue)) { - userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); + if (supportMultipleEmailsAndMobileNumbers) { + try { + List existingVerifiedMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, + user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + if (!existingVerifiedMobileNumbersList.contains(pendingMobileClaimValue)) { + existingVerifiedMobileNumbersList.add(pendingMobileClaimValue); + userClaims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, existingVerifiedMobileNumbersList)); + } + + /* + VerifiedMobileNumbers is a subset of mobileNumbers. Hence, adding the verified number to + mobileNumbers claim as well. + */ + List allMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, + user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + if (!allMobileNumbersList.contains(pendingMobileClaimValue)) { + allMobileNumbersList.add(pendingMobileClaimValue); + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, allMobileNumbersList)); + } + } catch (IdentityEventException e) { + log.error("Error occurred while obtaining claim for the user : " + user.getUserName()); + throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + + "value for the user : " + user.getUserName(), e); + } + } userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileClaimValue); + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); // Todo passes when mobile number is properly set here. Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); @@ -961,37 +988,30 @@ public void confirmVerificationCodeMe(String code, Map propertie */ if (supportMultipleEmailsAndMobileNumbers) { try { - String existingVerifiedMobileNumbers = userStoreManager.getUserClaimValue(user.getUserName(), - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, null); - List existingVerifiedMobileNumbersList = new ArrayList<>(); - if (StringUtils.isNotBlank(existingVerifiedMobileNumbers)) { - existingVerifiedMobileNumbersList.addAll(Arrays.asList(existingVerifiedMobileNumbers.split( - ","))); - } + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); + List existingVerifiedMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, + user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); if (!existingVerifiedMobileNumbersList.contains(pendingMobileNumberClaimValue)) { existingVerifiedMobileNumbersList.add(pendingMobileNumberClaimValue); userClaims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, - String.join(",", existingVerifiedMobileNumbersList)); + String.join(multiAttributeSeparator, existingVerifiedMobileNumbersList)); } /* VerifiedMobileNumbers is a subset of mobileNumbers. Hence, adding the verified number to mobileNumbers claim as well. */ - String allMobileNumbers = userStoreManager.getUserClaimValue(user.getUserName(), - IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, null); - List allMobileNumbersList = new ArrayList<>(); - if (StringUtils.isNotBlank(allMobileNumbers)) { - allMobileNumbersList.addAll(Arrays.asList(allMobileNumbers.split(","))); - } + List allMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, + user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); if (!allMobileNumbersList.contains(pendingMobileNumberClaimValue)) { allMobileNumbersList.add(pendingMobileNumberClaimValue); userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, - String.join(",", allMobileNumbersList)); + String.join(multiAttributeSeparator, allMobileNumbersList)); } - } catch (UserStoreException e) {; - log.error("Error while retrieving mobile numbers claims for user : " + user.getUserName(), - e); + } catch (IdentityEventException e) { + log.error("Error occurred while obtaining claim for the user : " + user.getUserName()); + throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + + "value for the user : " + user.getUserName(), e); } } else { userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileNumberClaimValue); diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index fafe0f9b4d..a7cc1d39e4 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -22,7 +22,9 @@ import org.apache.commons.lang.StringUtils; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.mockito.ArgumentCaptor; import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; @@ -38,6 +40,7 @@ import org.wso2.carbon.consent.mgt.core.model.ConsentManagerConfigurationHolder; import org.wso2.carbon.consent.mgt.core.model.ReceiptInput; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.IdentityProvider; import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerManager; @@ -50,6 +53,7 @@ import org.wso2.carbon.identity.event.IdentityEventException; import org.wso2.carbon.identity.event.event.Event; import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.governance.IdentityGovernanceService; import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.governance.service.otp.OTPGenerator; @@ -64,13 +68,17 @@ import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; +import org.wso2.carbon.identity.recovery.util.Utils; import org.wso2.carbon.idp.mgt.IdentityProviderManager; import org.wso2.carbon.user.api.Claim; import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.api.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; import java.sql.Timestamp; +import java.util.HashMap; +import java.util.Map; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; @@ -78,10 +86,15 @@ import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertTrue; + import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SEND_OTP_IN_EMAIL; @@ -104,6 +117,7 @@ @WithCarbonHome public class UserSelfRegistrationManagerTest { + @InjectMocks private UserSelfRegistrationManager userSelfRegistrationManager; @Mock @@ -139,24 +153,31 @@ public class UserSelfRegistrationManagerTest { @Mock private OTPGenerator otpGenerator; + @Mock + private PrivilegedCarbonContext privilegedCarbonContext; + private MockedStatic mockedServiceDataHolder; private MockedStatic mockedIdentityUtil; private MockedStatic mockedJDBCRecoveryDataStore; private MockedStatic mockedIdentityProviderManager; private MockedStatic mockedIdentityTenantUtil; private MockedStatic mockedPrivilegedCarbonContext; + private MockedStatic mockedFrameworkUtils; private final String TEST_TENANT_DOMAIN_NAME = "carbon.super"; + private final int TEST_TENANT_ID = 12; private final String TEST_USERSTORE_DOMAIN = "PRIMARY"; private final String TEST_USER_NAME = "dummyUser"; private final String TEST_CLAIM_URI = "ttp://wso2.org/claims/emailaddress"; private final String TEST_CLAIM_VALUE = "dummyuser@wso2.com"; private final String TEST_MOBILE_CLAIM_VALUE = "0775553443"; + private final String TEST_PRIMARY_USER_STORE_DOMAIN = "PRIMARY"; + private final String TEST_RECOVERY_DATA_STORE_SECRET = "secret"; private static final Log LOG = LogFactory.getLog(UserSelfRegistrationManagerTest.class); @BeforeMethod - public void setUp() { + public void setUp() throws UserStoreException { MockitoAnnotations.openMocks(this); @@ -168,16 +189,28 @@ public void setUp() { mockedIdentityProviderManager = mockStatic(IdentityProviderManager.class); mockedIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); mockedPrivilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class); + mockedFrameworkUtils = mockStatic(FrameworkUtils.class); mockedIdentityProviderManager.when(IdentityProviderManager::getInstance).thenReturn(identityProviderManager); mockedServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) .thenReturn(identityRecoveryServiceDataHolder); + mockedPrivilegedCarbonContext.when(PrivilegedCarbonContext::getThreadLocalCarbonContext) + .thenReturn(privilegedCarbonContext); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(any())).thenReturn(TEST_TENANT_ID); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn(TEST_PRIMARY_USER_STORE_DOMAIN); + mockedIdentityUtil.when(() -> IdentityUtil.addDomainToName(eq(TEST_USER_NAME), anyString())) + .thenReturn(String.format("%s/%s", TEST_USER_NAME, TEST_USERSTORE_DOMAIN)); + mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); when(identityRecoveryServiceDataHolder.getIdentityGovernanceService()).thenReturn(identityGovernanceService); when(identityRecoveryServiceDataHolder.getOtpGenerator()).thenReturn(otpGenerator); when(identityRecoveryServiceDataHolder.getAuthAttributeHandlerManager()).thenReturn(authAttributeHandlerManager); when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); + + when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); + when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); } @AfterMethod @@ -189,6 +222,7 @@ public void tearDown() { mockedIdentityTenantUtil.close(); mockedPrivilegedCarbonContext.close(); mockedServiceDataHolder.close(); + mockedFrameworkUtils.close(); } String consentData = @@ -225,7 +259,7 @@ public void testResendConfirmationCode(String username, String userstore, String user.setUserStoreDomain(userstore); user.setTenantDomain(tenantDomain); - UserRecoveryData userRecoveryData = new UserRecoveryData(user, "1234-4567-890", RecoveryScenarios + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, RecoveryScenarios .SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); // Storing preferred notification channel in remaining set ids. userRecoveryData.setRemainingSetIds(preferredChannel); @@ -505,6 +539,183 @@ public void testAttributeVerificationFailures(String scenario, Property[] proper } } + @Test + public void testConfirmVerificationCodeMe() + throws IdentityRecoveryException, UserStoreException { + + // Case 1: Multiple email and mobile per user is enabled. + String code = "test-code"; + String verificationPendingMobileNumber = "0700000000"; + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); + + when(userRecoveryDataStore.load(eq(code))).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + mockMultiAttributeEnabled(true); + mockGetUserClaimValue(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, StringUtils.EMPTY); + mockGetUserClaimValue(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, StringUtils.EMPTY); + + userSelfRegistrationManager.confirmVerificationCodeMe(code, new HashMap<>()); + + ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); + + Map capturedClaims = claimsCaptor.getValue(); + String updatedVerifiedMobileNumbers = + capturedClaims.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + String updatedVerificationPendingMobile = + capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM); + + assertEquals(updatedVerificationPendingMobile, StringUtils.EMPTY); + assertTrue(StringUtils.contains(updatedVerifiedMobileNumbers, verificationPendingMobileNumber)); + + // Case 2: Multiple email and mobile per user is disabled. + mockMultiAttributeEnabled(false); + ArgumentCaptor> claimsCaptor2 = ArgumentCaptor.forClass(Map.class); + + userSelfRegistrationManager.confirmVerificationCodeMe(code, new HashMap<>()); + + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor2.capture(), isNull()); + Map capturedClaims2 = claimsCaptor2.getValue(); + String mobileNumberClaims = + capturedClaims2.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + String updatedVerificationPendingMobile2 = + capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM); + + assertEquals(updatedVerificationPendingMobile2, StringUtils.EMPTY); + assertEquals(mobileNumberClaims, verificationPendingMobileNumber); + } + + @Test + public void testGetConfirmedSelfRegisteredUserVerifyEmail() + throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { + + String code = "test-code"; + String verifiedChannelType = "EMAIL"; + String verifiedChannelClaim = "http://wso2.org/claims/emailaddress"; + String verificationPendingEmail = "pasindu@gmail.com"; + Map metaProperties = new HashMap<>(); + + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL); + // Setting verification pending email claim value. + userRecoveryData.setRemainingSetIds(verificationPendingEmail); + + when(userRecoveryDataStore.load(eq(code))).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(code), anyBoolean())).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + mockMultiAttributeEnabled(true); + mockGetUserClaimValue(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.EMPTY); + mockGetUserClaimValue(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, StringUtils.EMPTY); + + org.wso2.carbon.identity.application.common.model.Property property = + new org.wso2.carbon.identity.application.common.model.Property(); + org.wso2.carbon.identity.application.common.model.Property[] testProperties = + new org.wso2.carbon.identity.application.common.model.Property[]{property}; + + when(identityGovernanceService.getConfiguration(any(), anyString())).thenReturn(testProperties); + + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(code, verifiedChannelType, verifiedChannelClaim, + metaProperties); + + ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); + + Map capturedClaims = claimsCaptor.getValue(); + String updatedVerifiedEmailAddresses = + capturedClaims.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + String verificationPendingEmailAddress = + capturedClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM); + + assertTrue(StringUtils.contains(updatedVerifiedEmailAddresses, verificationPendingEmail)); + assertEquals(verificationPendingEmailAddress, StringUtils.EMPTY); + } + + @Test + public void testGetConfirmedSelfRegisteredUserVerifyMobile() + throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { + + String code = "test-code"; + String verifiedChannelType = "SMS"; + String verifiedChannelClaim = "http://wso2.org/claims/mobile"; + String verificationPendingMobileNumber = "077888888"; + Map metaProperties = new HashMap<>(); + + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + // Setting verification pending email claim value. + userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); + + when(userRecoveryDataStore.load(eq(code))).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(code), anyBoolean())).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + mockGetUserClaimValue(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, StringUtils.EMPTY); + mockGetUserClaimValue(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, StringUtils.EMPTY); + + org.wso2.carbon.identity.application.common.model.Property property = + new org.wso2.carbon.identity.application.common.model.Property(); + org.wso2.carbon.identity.application.common.model.Property[] testProperties = + new org.wso2.carbon.identity.application.common.model.Property[]{property}; + + when(identityGovernanceService.getConfiguration(any(), anyString())).thenReturn(testProperties); + + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(true); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER), + anyString())) + .thenReturn("true"); + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(code, verifiedChannelType, verifiedChannelClaim, + metaProperties); + } + + ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); + + Map capturedClaims = claimsCaptor.getValue(); + String updatedVerifiedMobileNumbers = + capturedClaims.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + String verificationPendingMobileNumberClaim = + capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM); + String updatedMobileNumberClaimValue = + capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + + assertTrue(StringUtils.contains(updatedVerifiedMobileNumbers, verificationPendingMobileNumber)); + assertEquals(verificationPendingMobileNumberClaim, StringUtils.EMPTY); + assertEquals(updatedMobileNumberClaimValue, verificationPendingMobileNumber); + } + + private User getUser() { + + User user = new User(); + user.setUserName(TEST_USER_NAME); + user.setUserStoreDomain(TEST_USERSTORE_DOMAIN); + user.setTenantDomain(TEST_TENANT_DOMAIN_NAME); + return user; + } + + private void mockMultiAttributeEnabled(Boolean isEnabled) { + + mockedIdentityUtil.when(() -> IdentityUtil.getProperty( + eq(IdentityRecoveryConstants.ConnectorConfig.SUPPORT_MULTI_EMAILS_AND_MOBILE_NUMBERS_PER_USER))) + .thenReturn(isEnabled.toString()); + } + + private void mockGetUserClaimValue(String claimUri, String claimValue) throws UserStoreException { + + when(userStoreManager.getUserClaimValue(any(), eq(claimUri), any())).thenReturn(claimValue); + } + /** * Sample consent manager class. */ From 64af0f5748bb9d95c7dc9240aadef9e0b0b81dd5 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Sun, 22 Sep 2024 01:57:26 +0530 Subject: [PATCH 35/54] Fix issues in multi email mobile scenarios. --- .../MobileNumberVerificationHandler.java | 58 +++++------- .../handler/UserEmailVerificationHandler.java | 94 +++++++------------ .../MobileNumberVerificationHandlerTest.java | 21 ++--- .../UserEmailVerificationHandlerTest.java | 12 +-- 4 files changed, 75 insertions(+), 110 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index cc0fc364c4..20c1847cdc 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -100,6 +100,13 @@ public void handleEvent(Event event) throws IdentityEventException { boolean enable = isMobileVerificationOnUpdateEnabled(user.getTenantDomain()); + if (!supportMultipleMobileNumbers) { + // Multiple mobile numbers per user support is disabled. + log.debug("Supporting multiple mobile numbers per user is disabled."); + claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + } + if (!enable) { // Mobile Number Verification feature is disabled. if (log.isDebugEnabled()) { @@ -139,24 +146,6 @@ public void handleEvent(Event event) throws IdentityEventException { claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); } return; - } else { - if (supportMultipleMobileNumbers) { - List verifiedMobileNumbers = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - if (claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM) && - !claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM).isEmpty() && - !verifiedMobileNumbers.contains(claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM))) { - throw new IdentityEventClientException(IdentityRecoveryConstants.ErrorMessages. - ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode(), - IdentityRecoveryConstants.ErrorMessages - .ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getMessage()); - } - } else { - // Multiple mobile numbers per user support is disabled. - log.debug("Supporting multiple mobile numbers per user is disabled."); - claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); - } } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { @@ -305,8 +294,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use invalidatePendingMobileVerification(user, userStoreManager, claims); return; } - - // TODO: Check why this was moved to the bottom before? This was not triggered due to that. + /* Within the SMS OTP flow, the mobile number is updated in the user profile after successfully verifying the OTP. Therefore, the mobile number is already verified & no need to verify it again. @@ -324,7 +312,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use boolean supportMultipleMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); - String mobileNumber = null; + String mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); List updatedVerifiedNumbersList = new ArrayList<>(); List updatedAllNumbersList; @@ -345,7 +333,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use /* Finds the verification pending mobile number and remove it from the verified numbers list in the payload. */ - if (CollectionUtils.isNotEmpty(updatedVerifiedNumbersList)) { + if (mobileNumber == null && CollectionUtils.isNotEmpty(updatedVerifiedNumbersList)) { mobileNumber = getVerificationPendingMobileNumber(exisitingVerifiedNumbersList, updatedVerifiedNumbersList); updatedVerifiedNumbersList.remove(mobileNumber); @@ -355,7 +343,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Finds the removed numbers from the existing mobile numbers list and remove them from the verified numbers list. As verified numbers list should not contain numbers that are not in the mobile numbers list. */ - if (CollectionUtils.isNotEmpty(updatedAllNumbersList)) { + if (updatedAllNumbersList != null) { updatedVerifiedNumbersList.removeIf(number -> !updatedAllNumbersList.contains(number)); } @@ -367,14 +355,6 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use updatedAllNumbersList = new ArrayList<>(); claims.remove(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - - mobileNumber = claims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); - } - - if (mobileNumber == null) { - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; } String existingMobileNumber; @@ -388,7 +368,7 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use throw new IdentityEventException(error, e); } - if (supportMultipleMobileNumbers) { + if (supportMultipleMobileNumbers && StringUtils.isNotBlank(existingMobileNumber)) { if (!updatedVerifiedNumbersList.contains(existingMobileNumber)) { updatedVerifiedNumbersList.add(existingMobileNumber); claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, @@ -401,6 +381,20 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } } + if (mobileNumber == null) { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } + + if (supportMultipleMobileNumbers && updatedVerifiedNumbersList.contains(mobileNumber)) { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( + IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString()); + invalidatePendingMobileVerification(user, userStoreManager, claims); + return; + } + if (StringUtils.equals(mobileNumber, existingMobileNumber)) { if (log.isDebugEnabled()) { log.debug(String.format("The mobile number to be updated: %s is same as the existing mobile " + diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 49cac51066..e7856caa92 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -114,6 +114,14 @@ public void handleEvent(Event event) throws IdentityEventException { enable = isEmailVerificationOnUpdateEnabled(user.getTenantDomain()); + if (!supportMultipleEmails) { + if (log.isDebugEnabled()) { + log.debug("Supporting multiple email addresses per user is disabled."); + } + claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); + claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); + } + if (!enable) { /* We need to empty 'EMAIL_ADDRESS_PENDING_VALUE_CLAIM' because having a value in that claim implies a verification is pending. But verification is not enabled anymore. */ @@ -148,33 +156,8 @@ public void handleEvent(Event event) throws IdentityEventException { String.join(FrameworkUtils.getMultiAttributeSeparator(), updatedAllEmailAddresses)); } } - } else { - // Supporting multiple email addresses per user is disabled. - if (log.isDebugEnabled()) { - log.debug("Supporting multiple email addresses per user is disabled."); - } - claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); } claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); - } else { - if (supportMultipleEmails) { - List verifiedEmails = Utils.getMultiValuedClaim(userStoreManager, user, - IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - if (claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM) && - !claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM).isEmpty() && - !verifiedEmails.contains(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM))) { - throw new IdentityEventClientException( - ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getCode(), - ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST.getMessage()); - } - } else { - // Multiple email addresses per user support is disabled. - if (log.isDebugEnabled()) { - log.debug("Supporting multiple mobile email addresses per user is disabled."); - } - claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); - } } } @@ -578,9 +561,9 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore boolean supportMultipleEmails = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); - String emailAddress = null; + String emailAddress = claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); List updatedVerifiedEmailAddresses = new ArrayList<>(); - List updatedAllEmailAddresses = new ArrayList<>(); + List updatedAllEmailAddresses; // Handle email addresses and verified email addresses claims. if (supportMultipleEmails) { @@ -596,38 +579,22 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : existingAllEmailAddresses; - // Find the verification pending email address and remove it from verified email addresses list in the payload. - if (updatedVerifiedEmailAddresses != null) { + // Find the verification pending email address and remove it from verified email addresses in the payload. + if (emailAddress == null && CollectionUtils.isNotEmpty(updatedVerifiedEmailAddresses)) { emailAddress = getVerificationPendingEmailAddress(existingVerifiedEmailAddresses, updatedVerifiedEmailAddresses); updatedVerifiedEmailAddresses.remove(emailAddress); } - if (existingVerifiedEmailAddresses.contains(emailAddress)) { - if (log.isDebugEnabled()) { - log.debug(String.format("The email address to be updated: %s is same as the existing email " + - "address for user: %s in domain %s. Hence an email verification will not be " + - "triggered.", emailAddress, user.getUserName(), user.getTenantDomain())); - } - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString()); - invalidatePendingEmailVerification(user, userStoreManager, claims); - return; - } - /* Find the removed numbers from the existing email addresses list and remove them from the verified email - addresses list, as verified email addresses list should not contain email addresses that are not in the email - addresses list. + addresses list, as verified email addresses list should not contain email addresses that are not in the + email addresses list. */ - if (!updatedAllEmailAddresses.isEmpty()) { - for (String existingEmailAddress : existingAllEmailAddresses) { - if (!updatedAllEmailAddresses.contains(existingEmailAddress) && - updatedVerifiedEmailAddresses != null) { - updatedVerifiedEmailAddresses.remove(existingEmailAddress); - } - } + if (updatedAllEmailAddresses != null) { + updatedVerifiedEmailAddresses.removeIf(number -> !updatedAllEmailAddresses.contains(number)); } + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, @@ -639,14 +606,7 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore */ claims.remove(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM); claims.remove(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); - - emailAddress = claims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); - } - - if (emailAddress == null) { - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; + updatedAllEmailAddresses = new ArrayList<>(); } String existingEmail = getEmailClaimValue(user, userStoreManager); @@ -664,6 +624,24 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } } + if (emailAddress == null) { + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } + + if (supportMultipleEmails && updatedVerifiedEmailAddresses.contains(emailAddress)) { + if (log.isDebugEnabled()) { + log.debug(String.format("The email address to be updated: %s is same as the existing email " + + "address for user: %s in domain %s. Hence an email verification will not be " + + "triggered.", emailAddress, user.getUserName(), user.getTenantDomain())); + } + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString()); + invalidatePendingEmailVerification(user, userStoreManager, claims); + return; + } + if (emailAddress.equals(existingEmail)) { if (log.isDebugEnabled()) { log.debug(String.format("The email address to be updated: %s is already verified and contains" + diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index 63d11a60f7..d38b31a9e7 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -300,21 +300,16 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabled() throws Ex public void testUpdatePrimaryMobileNotInVerifiedList() throws Exception { mockUtilMethods(true, true, false); - String newVerifiedMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; - String newMobileNumbers = EXISTING_NUMBER_1 + "," + NEW_MOBILE_NUMBER; Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, null, - newVerifiedMobileNumbers, newMobileNumbers, NEW_MOBILE_NUMBER); + null, null, NEW_MOBILE_NUMBER); mockExistingVerifiedNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); - mockExistingNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1, NEW_MOBILE_NUMBER))); + mockExistingNumbersList(new ArrayList<>(Arrays.asList(EXISTING_NUMBER_1))); + mockVerificationPendingMobileNumber(); - try { - mobileNumberVerificationHandler.handleEvent(event); - Assert.fail("Expected IdentityEventClientException was not thrown"); - } catch (IdentityEventClientException e) { - Assert.assertEquals(e.getErrorCode(), - IdentityRecoveryConstants.ErrorMessages - .ERROR_CODE_PRIMARY_MOBILE_NUMBER_SHOULD_BE_INCLUDED_IN_VERIFIED_MOBILES_LIST.getCode()); - } + mobileNumberVerificationHandler.handleEvent(event); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), + NEW_MOBILE_NUMBER); } @Test(description = "Verification enabled, Multi-attribute enabled, Update primary mobile in verified list") @@ -334,7 +329,7 @@ public void testUpdatePrimaryMobileInVerifiedList() throws Exception { mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( eq(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates - .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + .SKIP_ON_ALREADY_VERIFIED_MOBILE_NUMBERS.toString()))); } @Test(description = "Verification enabled, Multi-attribute enabled, Add new mobile to verified list") diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java index 68acf72e4e..b5481df576 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -245,7 +245,7 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC1() Try to change the primary email, new Email is not in the existing verified email address list. Expected: IdentityEventClientException should be thrown. */ - Event event1 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, null, null, NEW_EMAIL); mockUtilMethods(true, true, false, @@ -256,11 +256,9 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC1() List existingVerifiedEmails = new ArrayList<>(Arrays.asList(EXISTING_EMAIL_1)); mockExistingVerifiedEmailAddressesList(existingVerifiedEmails); - try { - userEmailVerificationHandler.handleEvent(event1); - } catch (Exception e) { - Assert.assertTrue(e instanceof IdentityEventClientException); - } + userEmailVerificationHandler.handleEvent(event); + Map userClaims = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), NEW_EMAIL); } @Test(description = "Verification - Enabled, Multi attribute - Enabled, Change primary email which is already " + @@ -285,7 +283,7 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC2() t userEmailVerificationHandler.handleEvent(event); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate( eq(IdentityRecoveryConstants.SkipEmailVerificationOnUpdateStates - .SKIP_ON_INAPPLICABLE_CLAIMS.toString()))); + .SKIP_ON_ALREADY_VERIFIED_EMAIL_ADDRESSES.toString()))); } @Test(description = "Verification - Enabled, Multi attribute - Enabled, Update verified list with new email") From 81568b9c17e108dc72d51d10c9dd6f787fa06cd6 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Sun, 22 Sep 2024 02:00:47 +0530 Subject: [PATCH 36/54] Add more unit tests. --- .../UserSelfRegistrationManagerTest.java | 272 ++++++++++++++++-- 1 file changed, 255 insertions(+), 17 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index a7cc1d39e4..40fe4077ea 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -48,6 +48,7 @@ import org.wso2.carbon.identity.auth.attribute.handler.exception.AuthAttributeHandlerException; import org.wso2.carbon.identity.auth.attribute.handler.model.ValidationResult; import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.consent.mgt.services.ConsentUtilityService; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventException; @@ -55,12 +56,17 @@ import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannelManager; import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.governance.service.otp.OTPGenerator; +import org.wso2.carbon.identity.input.validation.mgt.exceptions.InputValidationMgtException; +import org.wso2.carbon.identity.input.validation.mgt.model.ValidationConfiguration; +import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; import org.wso2.carbon.identity.recovery.RecoveryScenarios; import org.wso2.carbon.identity.recovery.RecoverySteps; +import org.wso2.carbon.identity.recovery.UserWorkflowManagementService; import org.wso2.carbon.identity.recovery.bean.NotificationResponseBean; import org.wso2.carbon.identity.recovery.exception.SelfRegistrationException; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; @@ -69,14 +75,20 @@ import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; +import org.wso2.carbon.idp.mgt.IdentityProviderManagementException; import org.wso2.carbon.idp.mgt.IdentityProviderManager; import org.wso2.carbon.user.api.Claim; -import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.core.config.RealmConfiguration; +import org.wso2.carbon.user.core.tenant.TenantManager; +import org.wso2.carbon.user.core.UserRealm; import org.wso2.carbon.user.api.UserStoreException; -import org.wso2.carbon.user.api.UserStoreManager; +import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.user.core.util.UserCoreUtil; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; import java.sql.Timestamp; +import java.util.Arrays; import java.util.HashMap; import java.util.Map; @@ -94,8 +106,10 @@ import static org.mockito.Mockito.verify; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertTrue; +import static org.testng.Assert.assertFalse; import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SEND_OTP_IN_EMAIL; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_USE_LOWERCASE_CHARACTERS_IN_OTP; @@ -132,12 +146,24 @@ public class UserSelfRegistrationManagerTest { @Mock private UserStoreManager userStoreManager; + @Mock + private ConsentManager consentManger; + + @Mock + private TenantManager tenantManager; + + @Mock + private ConsentUtilityService consentUtilityService; + @Mock private UserRealm userRealm; @Mock private IdentityProviderManager identityProviderManager; + @Mock + private IdentityProvider identityProvider; + @Mock private AuthAttributeHandlerManager authAttributeHandlerManager; @@ -156,6 +182,21 @@ public class UserSelfRegistrationManagerTest { @Mock private PrivilegedCarbonContext privilegedCarbonContext; + @Mock + private NotificationChannelManager notificationChannelManager; + + @Mock + private UserWorkflowManagementService userWorkflowManagementService; + + @Mock + private RealmConfiguration realmConfiguration; + + @Mock + private InputValidationManagementService inputValidationManagementService; + + @Mock + private ValidationConfiguration validationConfiguration; + private MockedStatic mockedServiceDataHolder; private MockedStatic mockedIdentityUtil; private MockedStatic mockedJDBCRecoveryDataStore; @@ -163,6 +204,8 @@ public class UserSelfRegistrationManagerTest { private MockedStatic mockedIdentityTenantUtil; private MockedStatic mockedPrivilegedCarbonContext; private MockedStatic mockedFrameworkUtils; + private MockedStatic mockedMultiTenantUtils; + private MockedStatic mockedUserCoreUtil; private final String TEST_TENANT_DOMAIN_NAME = "carbon.super"; private final int TEST_TENANT_ID = 12; @@ -173,11 +216,12 @@ public class UserSelfRegistrationManagerTest { private final String TEST_MOBILE_CLAIM_VALUE = "0775553443"; private final String TEST_PRIMARY_USER_STORE_DOMAIN = "PRIMARY"; private final String TEST_RECOVERY_DATA_STORE_SECRET = "secret"; + private final String TEST_CODE = "test-code"; private static final Log LOG = LogFactory.getLog(UserSelfRegistrationManagerTest.class); @BeforeMethod - public void setUp() throws UserStoreException { + public void setUp() throws UserStoreException, IdentityProviderManagementException, InputValidationMgtException { MockitoAnnotations.openMocks(this); @@ -190,6 +234,8 @@ public void setUp() throws UserStoreException { mockedIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); mockedPrivilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class); mockedFrameworkUtils = mockStatic(FrameworkUtils.class); + mockedMultiTenantUtils = mockStatic(MultitenantUtils.class); + mockedUserCoreUtil = mockStatic(UserCoreUtil.class); mockedIdentityProviderManager.when(IdentityProviderManager::getInstance).thenReturn(identityProviderManager); mockedServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) @@ -202,15 +248,25 @@ public void setUp() throws UserStoreException { mockedIdentityUtil.when(() -> IdentityUtil.addDomainToName(eq(TEST_USER_NAME), anyString())) .thenReturn(String.format("%s/%s", TEST_USER_NAME, TEST_USERSTORE_DOMAIN)); mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + when(identityProviderManager.getResidentIdP(anyString())).thenReturn(identityProvider); when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); when(identityRecoveryServiceDataHolder.getIdentityGovernanceService()).thenReturn(identityGovernanceService); when(identityRecoveryServiceDataHolder.getOtpGenerator()).thenReturn(otpGenerator); when(identityRecoveryServiceDataHolder.getAuthAttributeHandlerManager()).thenReturn(authAttributeHandlerManager); when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); + when(identityRecoveryServiceDataHolder.getConsentManager()).thenReturn(consentManger); + when(identityRecoveryServiceDataHolder.getConsentUtilityService()).thenReturn(consentUtilityService); + when(identityRecoveryServiceDataHolder.getInputValidationMgtService()) + .thenReturn(inputValidationManagementService); when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); + when(realmService.getTenantManager()).thenReturn(tenantManager); when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); + when(userStoreManager.getSecondaryUserStoreManager(anyString())).thenReturn(userStoreManager); + when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); + when(privilegedCarbonContext.getOSGiService(UserWorkflowManagementService.class, null)) + .thenReturn(userWorkflowManagementService); } @AfterMethod @@ -223,6 +279,8 @@ public void tearDown() { mockedPrivilegedCarbonContext.close(); mockedServiceDataHolder.close(); mockedFrameworkUtils.close(); + mockedMultiTenantUtils.close(); + mockedUserCoreUtil.close(); } String consentData = @@ -257,7 +315,6 @@ public void testResendConfirmationCode(String username, String userstore, String User user = new User(); user.setUserName(username); user.setUserStoreDomain(userstore); - user.setTenantDomain(tenantDomain); UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, RecoveryScenarios .SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); @@ -368,6 +425,11 @@ private void mockConfigurations(String enableSelfSignUp, String enableInternalNo selfRegistrationSMSCodeExpiryConfig.setName(SELF_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME); selfRegistrationSMSCodeExpiryConfig.setValue("1"); + org.wso2.carbon.identity.application.common.model.Property accountLockOnCreationConfig = + new org.wso2.carbon.identity.application.common.model.Property(); + accountLockOnCreationConfig.setName(ACCOUNT_LOCK_ON_CREATION); + accountLockOnCreationConfig.setValue("false"); + when(identityGovernanceService .getConfiguration(new String[]{ENABLE_SELF_SIGNUP}, TEST_TENANT_DOMAIN_NAME)) .thenReturn(new org.wso2.carbon.identity.application.common.model.Property[]{signupConfig}); @@ -404,6 +466,11 @@ private void mockConfigurations(String enableSelfSignUp, String enableInternalNo new String[]{SELF_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME}, TEST_TENANT_DOMAIN_NAME)) .thenReturn(new org.wso2.carbon.identity.application.common.model.Property[] {selfRegistrationSMSCodeExpiryConfig}); + when(identityGovernanceService + .getConfiguration( + new String[]{ACCOUNT_LOCK_ON_CREATION}, TEST_TENANT_DOMAIN_NAME)) + .thenReturn(new org.wso2.carbon.identity.application.common.model.Property[] + {accountLockOnCreationConfig}); when(otpGenerator.generateOTP(anyBoolean(), anyBoolean(), anyBoolean(), anyInt(), anyString())) .thenReturn("1234-4567-890"); mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn(TEST_USERSTORE_DOMAIN); @@ -544,14 +611,13 @@ public void testConfirmVerificationCodeMe() throws IdentityRecoveryException, UserStoreException { // Case 1: Multiple email and mobile per user is enabled. - String code = "test-code"; String verificationPendingMobileNumber = "0700000000"; User user = getUser(); UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); - when(userRecoveryDataStore.load(eq(code))).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); @@ -559,7 +625,7 @@ public void testConfirmVerificationCodeMe() mockGetUserClaimValue(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, StringUtils.EMPTY); mockGetUserClaimValue(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, StringUtils.EMPTY); - userSelfRegistrationManager.confirmVerificationCodeMe(code, new HashMap<>()); + userSelfRegistrationManager.confirmVerificationCodeMe(TEST_CODE, new HashMap<>()); ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); @@ -577,7 +643,7 @@ public void testConfirmVerificationCodeMe() mockMultiAttributeEnabled(false); ArgumentCaptor> claimsCaptor2 = ArgumentCaptor.forClass(Map.class); - userSelfRegistrationManager.confirmVerificationCodeMe(code, new HashMap<>()); + userSelfRegistrationManager.confirmVerificationCodeMe(TEST_CODE, new HashMap<>()); verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor2.capture(), isNull()); Map capturedClaims2 = claimsCaptor2.getValue(); @@ -594,7 +660,6 @@ public void testConfirmVerificationCodeMe() public void testGetConfirmedSelfRegisteredUserVerifyEmail() throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { - String code = "test-code"; String verifiedChannelType = "EMAIL"; String verifiedChannelClaim = "http://wso2.org/claims/emailaddress"; String verificationPendingEmail = "pasindu@gmail.com"; @@ -606,8 +671,8 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() // Setting verification pending email claim value. userRecoveryData.setRemainingSetIds(verificationPendingEmail); - when(userRecoveryDataStore.load(eq(code))).thenReturn(userRecoveryData); - when(userRecoveryDataStore.load(eq(code), anyBoolean())).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(TEST_CODE), anyBoolean())).thenReturn(userRecoveryData); when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); @@ -622,7 +687,7 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() when(identityGovernanceService.getConfiguration(any(), anyString())).thenReturn(testProperties); - userSelfRegistrationManager.getConfirmedSelfRegisteredUser(code, verifiedChannelType, verifiedChannelClaim, + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, verifiedChannelClaim, metaProperties); ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); @@ -642,7 +707,6 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() public void testGetConfirmedSelfRegisteredUserVerifyMobile() throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { - String code = "test-code"; String verifiedChannelType = "SMS"; String verifiedChannelClaim = "http://wso2.org/claims/mobile"; String verificationPendingMobileNumber = "077888888"; @@ -654,8 +718,8 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() // Setting verification pending email claim value. userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); - when(userRecoveryDataStore.load(eq(code))).thenReturn(userRecoveryData); - when(userRecoveryDataStore.load(eq(code), anyBoolean())).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(TEST_CODE), anyBoolean())).thenReturn(userRecoveryData); when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); @@ -675,8 +739,8 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER), anyString())) .thenReturn("true"); - userSelfRegistrationManager.getConfirmedSelfRegisteredUser(code, verifiedChannelType, verifiedChannelClaim, - metaProperties); + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, + verifiedChannelClaim, metaProperties); } ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); @@ -695,6 +759,180 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() assertEquals(updatedMobileNumberClaimValue, verificationPendingMobileNumber); } + @Test + public void testRegisterUser() throws Exception { + + User user = getUser(); + mockConfigurations("true", "true"); + when(userStoreManager.isExistingRole(eq(IdentityRecoveryConstants.SELF_SIGNUP_ROLE))).thenReturn(true); + when(privilegedCarbonContext.getOSGiService(any(), isNull())).thenReturn(notificationChannelManager); + when(notificationChannelManager.resolveCommunicationChannel(anyString(), anyString(), anyString(), any())) + .thenReturn("EMAIL"); + + Property property = new Property(IdentityRecoveryConstants.Consent.CONSENT, consentData); + NotificationResponseBean notificationResponseBean = + userSelfRegistrationManager.registerUser(user, "test-pwd", new Claim[0], + new Property[]{property}); + + User registeredUser = notificationResponseBean.getUser(); + assertEquals(user.getUserName(), registeredUser.getUserName()); + verify(userStoreManager).addUser(anyString(), anyString(), any(), any(), isNull()); + verify(consentManger).addConsent(any()); + verify(identityEventService, atLeastOnce()).handleEvent(any()); + } + + @Test + public void testIsUserConfirmed() throws IdentityRecoveryException { + + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(any())).thenReturn(userRecoveryData); + + // SELF_SIGN_UP scenario. + boolean isUserConfirmed = userSelfRegistrationManager.isUserConfirmed(user); + assertFalse(isUserConfirmed); + } + + @Test + public void testConfirmUserSelfRegistration() throws IdentityRecoveryException, UserStoreException { + + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); + when(userRecoveryDataStore.load(anyString())).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + userSelfRegistrationManager.confirmUserSelfRegistration(TEST_CODE); + + ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); + + Map capturedClaims = claimsCaptor.getValue(); + String updatedAccountLockedClaim = + capturedClaims.get(IdentityRecoveryConstants.ACCOUNT_LOCKED_CLAIM); + String updatedEmailVerifiedClaim = + capturedClaims.get(IdentityRecoveryConstants.EMAIL_VERIFIED_CLAIM); + assertEquals(updatedAccountLockedClaim, Boolean.FALSE.toString()); + assertEquals(updatedEmailVerifiedClaim, Boolean.TRUE.toString()); + } + + @Test + public void testIntrospectUserSelfRegistration() throws IdentityRecoveryException { + + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + String verifiedChannelType = "EMAIL"; + String verifiedChannelClaim = "http://wso2.org/claims/emailaddress"; + + UserRecoveryData resultUserRecoveryData = userSelfRegistrationManager.introspectUserSelfRegistration(TEST_CODE, + verifiedChannelType, verifiedChannelClaim, new HashMap<>()); + User resultUser = resultUserRecoveryData.getUser(); + assertEquals(resultUser.getUserName(), user.getUserName()); + + // Case 2: Provide invalid verifiedChannelType. + String verifiedChannelType2 = "TEST"; + String verifiedChannelClaim2 = "http://wso2.org/claims/emailaddress"; + + try { + userSelfRegistrationManager.introspectUserSelfRegistration(TEST_CODE, + verifiedChannelType2, verifiedChannelClaim2, new HashMap<>()); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + assertEquals(((IdentityRecoveryException) e).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNSUPPORTED_VERIFICATION_CHANNEL.getCode()); + } + } + + @Test + public void testIsValidTenantDomain() throws UserStoreException, IdentityRecoveryException { + + // Case 1: Valid tenant domain. + when(tenantManager.getTenantId(eq(TEST_TENANT_DOMAIN_NAME))).thenReturn(TEST_TENANT_ID); + + boolean isValid = userSelfRegistrationManager.isValidTenantDomain(TEST_TENANT_DOMAIN_NAME); + assertTrue(isValid); + + // Case 2: Invalid tenant domain + String invalidTenantDomain = "INVALID_TENANT"; + when(realmService.getTenantManager().getTenantId(eq(invalidTenantDomain))) + .thenThrow(new UserStoreException()); + try { + userSelfRegistrationManager.isValidTenantDomain("INVALID_TENANT"); + } catch (IdentityRecoveryException e) { + assertTrue(StringUtils.contains(e.getMessage(), invalidTenantDomain)); + } + } + + @Test + public void testIsValidUserStoreDomain() throws IdentityRecoveryException, UserStoreException { + + // Case 1: Valid user store domain. + when(tenantManager.getTenantId(eq(TEST_TENANT_DOMAIN_NAME))).thenReturn(TEST_TENANT_ID); + when(userStoreManager.getSecondaryUserStoreManager(eq(TEST_USERSTORE_DOMAIN))) + .thenReturn(userStoreManager); + + boolean isValid = userSelfRegistrationManager.isValidUserStoreDomain( + TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME); + assertTrue(isValid); + } + + @Test + public void testIsUsernameAlreadyTaken() throws UserStoreException, IdentityRecoveryException { + + mockedMultiTenantUtils.when(() -> + MultitenantUtils.getTenantDomain(anyString())).thenReturn(TEST_TENANT_DOMAIN_NAME); + mockedMultiTenantUtils.when(() -> MultitenantUtils.getTenantAwareUsername(anyString())) + .thenReturn(TEST_USER_NAME); + + when(userStoreManager.isExistingUser(anyString())).thenReturn(true); + + boolean isUsernameAlreadyTaken = userSelfRegistrationManager.isUsernameAlreadyTaken(TEST_USER_NAME); + assertTrue(isUsernameAlreadyTaken); + + // Case 2: Has pending workflow. + when(userStoreManager.isExistingUser(anyString())).thenReturn(false); + when(userWorkflowManagementService.isUserExists(anyString(), anyString())).thenReturn(true); + + boolean isUsernameAlreadyTaken2 = userSelfRegistrationManager.isUsernameAlreadyTaken(TEST_USER_NAME); + assertTrue(isUsernameAlreadyTaken2); + } + + @Test + public void testIsSelfRegistrationEnabled() throws Exception{ + + mockConfigurations("true", "true"); + boolean isSelfRegistrationEnabled = userSelfRegistrationManager + .isSelfRegistrationEnabled(TEST_TENANT_DOMAIN_NAME); + assertTrue(isSelfRegistrationEnabled); + } + + @Test + public void testIsMatchUserNameRegex() throws IdentityRecoveryException, InputValidationMgtException { + + mockedMultiTenantUtils.when(() -> MultitenantUtils + .getTenantAwareUsername(anyString())).thenReturn(TEST_USER_NAME); + mockedUserCoreUtil.when(() -> UserCoreUtil.removeDomainFromName(anyString())).thenReturn(TEST_USER_NAME); + mockedIdentityUtil.when(() -> IdentityUtil.extractDomainFromName(anyString())) + .thenReturn(TEST_USERSTORE_DOMAIN); + when(realmConfiguration.getTenantId()).thenReturn(TEST_TENANT_ID); + mockedIdentityUtil.when(() -> IdentityTenantUtil.getTenantDomain(TEST_TENANT_ID)) + .thenReturn(TEST_TENANT_DOMAIN_NAME); + + when(inputValidationManagementService.getValidators(anyString())).thenReturn(null); + when(inputValidationManagementService.getInputValidationConfiguration(anyString())) + .thenReturn(Arrays.asList(validationConfiguration)); + when(validationConfiguration.getField()).thenReturn("username"); + + boolean isMatchUsernameRegex = userSelfRegistrationManager + .isMatchUserNameRegex(TEST_TENANT_DOMAIN_NAME, TEST_USER_NAME); + assertTrue(isMatchUsernameRegex); + } + private User getUser() { User user = new User(); From f1a6daf408d662ef718a1d757932cc4431f7624e Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Sun, 22 Sep 2024 21:23:59 +0530 Subject: [PATCH 37/54] Add more unit tests for Utils. --- .../identity/recovery/util/UtilsTest.java | 615 +++++++++++++++++- 1 file changed, 609 insertions(+), 6 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java index b89018caec..543158f41a 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java @@ -31,24 +31,49 @@ import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.governance.IdentityGovernanceException; +import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.handler.event.account.lock.exception.AccountLockServiceException; +import org.wso2.carbon.identity.handler.event.account.lock.service.AccountLockService; import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; -import org.wso2.carbon.user.api.UserStoreException; +import org.wso2.carbon.user.api.Claim; +import org.wso2.carbon.user.core.UserStoreException; +import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.user.core.UserRealm; import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.identity.application.common.model.Property; +import org.wso2.carbon.utils.multitenancy.MultitenantUtils; +import java.io.UnsupportedEncodingException; +import java.net.MalformedURLException; +import java.net.URISyntaxException; +import java.security.NoSuchAlgorithmException; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; +import static junit.framework.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyMap; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.argThat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertFalse; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertNull; +import static org.testng.Assert.assertTrue; public class UtilsTest { @@ -60,12 +85,17 @@ public class UtilsTest { private RealmService realmService; @Mock private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; + @Mock + private IdentityGovernanceService identityGovernanceService; + @Mock + private AccountLockService accountLockService; private static MockedStatic mockedStaticIdentityTenantUtil; private static MockedStatic mockedStaticUserStoreManager; private static MockedStatic mockedIdentityRecoveryServiceDataHolder; private static MockedStatic mockedStaticIdentityUtil; private static MockedStatic mockedStaticFrameworkUtils; + private static MockedStatic mockedStaticMultiTenantUtils; private static final String TENANT_DOMAIN = "test.com"; private static final int TENANT_ID = 123; @@ -80,6 +110,7 @@ public static void beforeClass() { mockedIdentityRecoveryServiceDataHolder = Mockito.mockStatic(IdentityRecoveryServiceDataHolder.class); mockedStaticIdentityUtil = mockStatic(IdentityUtil.class); mockedStaticFrameworkUtils = mockStatic(FrameworkUtils.class); + mockedStaticMultiTenantUtils = mockStatic(MultitenantUtils.class); } @AfterClass @@ -90,23 +121,30 @@ public static void afterClass() { mockedIdentityRecoveryServiceDataHolder.close(); mockedStaticIdentityUtil.close(); mockedStaticFrameworkUtils.close(); + mockedStaticMultiTenantUtils.close(); } @BeforeMethod - public void setUp() throws UserStoreException { + public void setUp() throws org.wso2.carbon.user.api.UserStoreException { MockitoAnnotations.openMocks(this); mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) .thenReturn(identityRecoveryServiceDataHolder); + mockedStaticIdentityUtil.when(() -> IdentityTenantUtil.getTenantId(TENANT_DOMAIN)).thenReturn(TENANT_ID); + mockedStaticIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); + mockedStaticIdentityUtil.when(() -> IdentityUtil.addDomainToName(USER_NAME, USER_STORE_DOMAIN)) + .thenReturn(USER_STORE_DOMAIN + UserCoreConstants.DOMAIN_SEPARATOR + USER_NAME); + mockedStaticFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + mockedStaticMultiTenantUtils.when(() -> + MultitenantUtils.getTenantAwareUsername(USER_NAME)).thenReturn(USER_NAME); when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); + when(identityRecoveryServiceDataHolder.getIdentityGovernanceService()).thenReturn(identityGovernanceService); + when(identityRecoveryServiceDataHolder.getAccountLockService()).thenReturn(accountLockService); + when(realmService.getTenantUserRealm(TENANT_ID)).thenReturn(userRealm); when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); - - mockedStaticIdentityUtil.when(() -> IdentityTenantUtil.getTenantId(TENANT_DOMAIN)).thenReturn(TENANT_ID); - mockedStaticIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn("PRIMARY"); - mockedStaticFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); } @Test(expectedExceptions = IdentityRecoveryClientException.class) @@ -125,6 +163,72 @@ public void testCheckPasswordPatternViolationForInvalidDomain() throws Exception } } + @Test + public void testGetArbitraryProperties() { + + org.wso2.carbon.identity.recovery.model.Property property = + new org.wso2.carbon.identity.recovery.model.Property("key", "value"); + + org.wso2.carbon.identity.recovery.model.Property[] properties = + new org.wso2.carbon.identity.recovery.model.Property[]{property}; + Utils.setArbitraryProperties(properties); + + org.wso2.carbon.identity.recovery.model.Property[] result = Utils.getArbitraryProperties(); + + assertNotNull(result); + assertEquals(result.length, 1); + assertEquals(result[0].getKey(), "key"); + assertEquals(result[0].getValue(), "value"); + + Utils.clearArbitraryProperties(); + org.wso2.carbon.identity.recovery.model.Property[] result1 = Utils.getArbitraryProperties(); + assertNull(result1); + } + + @Test + public void testGetEmailVerifyTemporaryClaim() { + + Claim claim = new Claim(); + claim.setClaimUri(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); + Utils.setEmailVerifyTemporaryClaim(claim); + + Claim result = Utils.getEmailVerifyTemporaryClaim(); + assertNotNull(result); + assertEquals(result.getClaimUri(), IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); + + Utils.clearEmailVerifyTemporaryClaim(); + Claim result1 = Utils.getEmailVerifyTemporaryClaim(); + assertNull(result1); + } + + @Test + public void testThreadLocalToSkipSendingEmailVerificationOnUpdate() { + + String threadLocalValue = "test-value"; + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(threadLocalValue); + + String result = Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate(); + assertEquals(result, threadLocalValue); + + Utils.unsetThreadLocalToSkipSendingEmailVerificationOnUpdate(); + String result1 = Utils.getThreadLocalToSkipSendingEmailVerificationOnUpdate(); + assertNull(result1); + } + + @Test + public void ThreadLocalToSkipSendingSmsOtpVerificationOnUpdate() { + + String threadLocalValue = "test-value"; + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(threadLocalValue); + + String result = Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); + assertEquals(result, threadLocalValue); + + Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); + String result1 = Utils.getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); + assertNull(result1); + } + @Test public void testGetClaimFromUserStoreManager() throws Exception { @@ -138,6 +242,500 @@ public void testGetClaimFromUserStoreManager() throws Exception { assertEquals("testValue", result); } + @Test + public void testRemoveClaimFromUserStoreManager() throws Exception { + + User user = getUser(); + String[] claims = new String[]{"testClaim"}; + Utils.removeClaimFromUserStoreManager(user, claims); + String userStoreQualifiedUsername = USER_STORE_DOMAIN + UserCoreConstants.DOMAIN_SEPARATOR + USER_NAME; + + verify(userStoreManager).deleteUserClaimValues(eq(userStoreQualifiedUsername), eq(claims), anyString()); + } + + @Test + public void testHandleServerException() throws IdentityRecoveryServerException { + + Exception exception = + Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE, "data"); + assertEquals(exception.getClass(), IdentityRecoveryServerException.class); + assertEquals(((IdentityRecoveryServerException) exception).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode()); + + Exception exception1 = + Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE, + "data", new Exception("test")); + assertEquals(exception1.getClass(), IdentityRecoveryServerException.class); + assertEquals(((IdentityRecoveryServerException) exception1).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode()); + assertEquals(exception1.getCause().getMessage(), "test"); + + Exception exception2 = + Utils.handleServerException("code2", "message2%s", "data2"); + assertEquals(exception2.getClass(), IdentityRecoveryServerException.class); + assertEquals(((IdentityRecoveryServerException) exception2).getErrorCode(), "code2"); + assertEquals(exception2.getMessage(), String.format("message2%s", "data2")); + } + + @Test + public void testHandleClientException() throws IdentityRecoveryClientException { + + Exception exception1 = + Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE, "data1"); + assertEquals(exception1.getClass(), IdentityRecoveryClientException.class); + assertEquals(((IdentityRecoveryClientException) exception1).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode()); + assertEquals(exception1.getMessage(), + String.format(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getMessage(), "data1")); + + Exception exception2 = + Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE, + "data", new Exception("test")); + assertEquals(exception2.getClass(), IdentityRecoveryClientException.class); + assertEquals(((IdentityRecoveryClientException) exception2).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE.getCode()); + assertEquals(exception2.getCause().getMessage(), "test"); + + Exception exception3 = + Utils.handleClientException("code2", "message2%s", "data2"); + assertEquals(exception3.getClass(), IdentityRecoveryClientException.class); + assertEquals(((IdentityRecoveryClientException) exception3).getErrorCode(), "code2"); + assertEquals(exception3.getMessage(), String.format("message2%s", "data2")); + } + + @Test + public void testHandleFunctionalityLockMgtServerException() { + + IdentityRecoveryConstants.ErrorMessages error = IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_EXPIRED_CODE; + String userId = "testUser"; + String functionalityIdentifier = "PASSWORD_RECOVERY"; + boolean isDetailedErrorMessagesEnabled = true; + + try { + Utils.handleFunctionalityLockMgtServerException(error, userId, TENANT_ID, functionalityIdentifier, + isDetailedErrorMessagesEnabled); + } catch (IdentityRecoveryServerException e) { + String expectedErrorCode = IdentityRecoveryConstants.PASSWORD_RECOVERY_SCENARIO + "-" + error.getCode(); + assertEquals(e.getErrorCode(), expectedErrorCode); + String expectedErrorMessage = error.getMessage() + + String.format("functionality: %s \nuserId: %s \ntenantId: %d.", functionalityIdentifier, userId, + TENANT_ID); + assertEquals(e.getMessage(), expectedErrorMessage); + assertNull(e.getCause()); + } + + // Test with detailed error messages disabled. + isDetailedErrorMessagesEnabled = false; + try { + Utils.handleFunctionalityLockMgtServerException(error, userId, TENANT_ID, + functionalityIdentifier, isDetailedErrorMessagesEnabled); + } catch (IdentityRecoveryServerException e) { + // Verify that the error message doesn't contain the detailed information. + assertFalse(e.getMessage().contains("functionality:")); + assertFalse(e.getMessage().contains("userId:")); + assertFalse(e.getMessage().contains("tenantId:")); + } + } + + @Test + public void testDoHash() throws org.wso2.carbon.user.api.UserStoreException { + + String value = "testValue"; + String expectedHash = "expected_hash"; + + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(() -> Utils.hashCode(value)).thenReturn(expectedHash); + mockedUtils.when(() -> Utils.doHash(value)).thenCallRealMethod(); + + String result = Utils.doHash(value); + assertEquals(result, expectedHash); + } + + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(() -> Utils.hashCode(value)).thenThrow(new NoSuchAlgorithmException("Test exception")); + mockedUtils.when(() -> Utils.doHash(value)).thenCallRealMethod(); + + Utils.doHash(value); + } catch (Exception e) { + assertTrue(e instanceof org.wso2.carbon.user.api.UserStoreException); + } + } + + @Test + public void testSetClaimInUserStoreManager() throws org.wso2.carbon.user.api.UserStoreException { + + String claim = "testClaim"; + String value = "testValue"; + + Map existingValues = new HashMap<>(); + existingValues.put(claim, "oldValue"); + when(userStoreManager.getUserClaimValues(anyString(), any(String[].class), anyString())) + .thenReturn(existingValues); + + Utils.setClaimInUserStoreManager(getUser(), claim, value); + + verify(userStoreManager).setUserClaimValues(anyString(), + argThat(map -> map.containsKey(claim) && map.get(claim).equals(value)), anyString()); + } + + @Test + public void testGetClaimListOfUser() throws IdentityRecoveryClientException, IdentityRecoveryServerException, + UserStoreException { + + String[] claimsList = {"claim1", "claim2"}; + Map expectedClaims = new HashMap<>(); + expectedClaims.put("claim1", "value1"); + expectedClaims.put("claim2", "value2"); + + when(userStoreManager.getUserClaimValues(anyString(), eq(claimsList), anyString())) + .thenReturn(expectedClaims); + + Map result = Utils.getClaimListOfUser(getUser(), claimsList); + assertEquals(result, expectedClaims); + + // Case 2: Throw UserStoreException. + when(userStoreManager.getUserClaimValues(anyString(), eq(claimsList), anyString())) + .thenThrow(new UserStoreException()); + try { + Utils.getClaimListOfUser(getUser(), claimsList); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testSetClaimsListOfUser() throws Exception { + + User user = getUser(); + Map claims = new HashMap<>(); + claims.put("http://wso2.org/claims/givenname", "John"); + claims.put("http://wso2.org/claims/emailaddress", "john@example.com"); + + Utils.setClaimsListOfUser(user, claims); + + String userStoreDomainQualifiedUsername = getUserStoreQualifiedUsername(USER_NAME, USER_STORE_DOMAIN); + verify(userStoreManager).setUserClaimValues(eq(userStoreDomainQualifiedUsername), eq(claims), anyString()); + + // Case 2: Throw UserStoreException. + try { + doThrow(new UserStoreException("Test exception")) + .when(userStoreManager).setUserClaimValues(anyString(), anyMap(), anyString()); + Utils.setClaimsListOfUser(user, claims); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testGetRecoveryConfigs() throws Exception { + + String key = "recovery.key"; + String expectedValue = "test_value"; + + Property property = new Property(); + property.setName(key); + property.setValue(expectedValue); + Property[] properties = new Property[]{property}; + + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenReturn(properties); + + String result = Utils.getRecoveryConfigs(key, TENANT_DOMAIN); + assertEquals(result, expectedValue); + + // Case 2: Throw IdentityGovernanceException. + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenThrow(new IdentityGovernanceException("Test exception")); + try { + Utils.getRecoveryConfigs(key, TENANT_DOMAIN); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + + // Case 3: Return empty connectorConfigs. + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenReturn(new Property[0]); + try { + Utils.getRecoveryConfigs(key, TENANT_DOMAIN); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testGetSignUpConfigs_Success() throws Exception { + + String key = "recovery.key"; + String expectedValue = "test_value"; + Property property = new Property(); + property.setName(key); + property.setValue(expectedValue); + Property[] properties = new Property[]{property}; + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenReturn(properties); + + String result = Utils.getSignUpConfigs(key, TENANT_DOMAIN); + assertEquals(result, expectedValue); + + // Case 2: Throw IdentityGovernanceException. + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenThrow(new IdentityGovernanceException("Test exception")); + try { + Utils.getSignUpConfigs(key, TENANT_DOMAIN); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testGetConnectorConfig() throws Exception { + + String key = "recovery.key"; + String expectedValue = "test_value"; + Property property = new Property(); + property.setName(key); + property.setValue(expectedValue); + Property[] properties = new Property[]{property}; + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenReturn(properties); + + String result = Utils.getConnectorConfig(key, TENANT_DOMAIN); + assertEquals(result, expectedValue); + + // Case 2: Throw IdentityGovernanceException. + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenThrow(new IdentityGovernanceException("Test exception")); + try { + Utils.getSignUpConfigs(key, TENANT_DOMAIN); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testGetChallengeSetDirFromUri() { + + String uri1 = "http://wso2.org/claims/challengeQuestion1"; + String uri2 = "challengeQuestion1"; + String uri3 = null; + + assertEquals(Utils.getChallengeSetDirFromUri(uri1), "challengeQuestion1"); + assertEquals(Utils.getChallengeSetDirFromUri(uri2), "challengeQuestion1"); + assertNull(Utils.getChallengeSetDirFromUri(uri3)); + } + + @Test + public void testIsAccountLocked() throws Exception { + + User user = getUser(); + when(accountLockService.isAccountLocked(eq(USER_NAME), eq(TENANT_DOMAIN), eq(USER_STORE_DOMAIN))) + .thenReturn(true); + assertTrue(Utils.isAccountLocked(user)); + + // Case 2: Throws AccountLockServiceException. + when(accountLockService.isAccountLocked(anyString(), anyString(), anyString())) + .thenThrow(new AccountLockServiceException("Test exception")); + try { + Utils.isAccountLocked(user); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testIsAccountDisabled() throws Exception { + + User user = getUser(); + Map claimValues = new HashMap<>(); + claimValues.put(IdentityRecoveryConstants.ACCOUNT_DISABLED_CLAIM, Boolean.TRUE.toString()); + when(userStoreManager.getUserClaimValues(eq(getUserStoreQualifiedUsername(USER_NAME, USER_STORE_DOMAIN)), + eq(new String[]{IdentityRecoveryConstants.ACCOUNT_DISABLED_CLAIM}), anyString())) + .thenReturn(claimValues); + + assertTrue(Utils.isAccountDisabled(user)); + + // Case 2: Throws error while loading realm service. + when(realmService.getTenantUserRealm(anyInt())).thenThrow( + new UserStoreException("Test exception")); + + try { + Utils.isAccountDisabled(user); + fail("Expected IdentityRecoveryServerException was not thrown"); + } catch (IdentityRecoveryServerException e) { + assertEquals(e.getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_FAILED_TO_LOAD_REALM_SERVICE.getCode()); + } + + // Case 3: Throws error while loading user store manager. + doReturn(userRealm).when(realmService).getTenantUserRealm(anyInt()); + when(userRealm.getUserStoreManager()).thenThrow( + new UserStoreException("Test exception")); + + try { + Utils.isAccountDisabled(user); + fail("Expected IdentityRecoveryServerException was not thrown"); + } catch (IdentityRecoveryServerException e) { + assertEquals(e.getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_FAILED_TO_LOAD_USER_STORE_MANAGER.getCode()); + } + + // Case 4: Throws error while getting user claim values. + doReturn(userRealm).when(realmService).getTenantUserRealm(anyInt()); + doReturn(userStoreManager).when(userRealm).getUserStoreManager(); + when(userStoreManager.getUserClaimValues(eq(getUserStoreQualifiedUsername(USER_NAME, USER_STORE_DOMAIN)), + eq(new String[]{IdentityRecoveryConstants.ACCOUNT_DISABLED_CLAIM}), anyString())) + .thenThrow(new UserStoreException("Test exception")); + + try { + Utils.isAccountDisabled(user); + fail("Expected IdentityRecoveryServerException was not thrown"); + } catch (IdentityRecoveryServerException e) { + assertEquals(e.getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_FAILED_TO_LOAD_USER_CLAIMS.getCode()); + } + } + + @Test + public void testCreateUser() { + + User user = Utils.createUser(USER_NAME, TENANT_DOMAIN); + assertEquals(user.getUserName(), USER_NAME); + assertEquals(user.getTenantDomain(), TENANT_DOMAIN); + } + + @Test + public void testValidateCallbackURL() throws IdentityEventException, IdentityGovernanceException { + + String callbackURL = "https://example.com/callback"; + String tenantDomain = "example.com"; + String callbackRegexType = "RECOVERY_CALLBACK_REGEX"; + + String expectedValue = "https://.*\\.com/.*"; + Property property = new Property(); + property.setName(callbackRegexType); + property.setValue(expectedValue); + Property[] properties = new Property[]{property}; + when(identityGovernanceService.getConfiguration(new String[]{callbackRegexType,}, tenantDomain)) + .thenReturn(properties); + + boolean result = Utils.validateCallbackURL(callbackURL, tenantDomain, callbackRegexType); + assertTrue(result); + + result = Utils.validateCallbackURL("http://malicious.com", tenantDomain, callbackRegexType); + assertFalse(result); + } + + @Test + public void testGetCallbackURLFromRegistration() throws MalformedURLException, UnsupportedEncodingException { + + org.wso2.carbon.identity.recovery.model.Property[] properties = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property(IdentityRecoveryConstants.CALLBACK, + "https://example.com/callback?param=value"), + new org.wso2.carbon.identity.recovery.model.Property("other", "value") + }; + + String result = Utils.getCallbackURLFromRegistration(properties); + assertEquals(result, "https://example.com/callback"); + } + + @Test + public void testGetCallbackURL() throws UnsupportedEncodingException, URISyntaxException { + + org.wso2.carbon.identity.recovery.model.Property[] properties = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property(IdentityRecoveryConstants.CALLBACK, + "https://example.com/callback?param=value"), + new org.wso2.carbon.identity.recovery.model.Property("other", "value") + }; + + String result = Utils.getCallbackURL(properties); + assertEquals(result, "https://example.com/callback"); + } + + @Test + public void testIsAccessUrlAvailable() { + + org.wso2.carbon.identity.recovery.model.Property[] propertiesTrue = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property(IdentityRecoveryConstants.IS_ACCESS_URL_AVAILABLE, + "true") + }; + assertTrue(Utils.isAccessUrlAvailable(propertiesTrue)); + + // Case 2: When IS_ACCESS_URL_AVAILABLE property is not present. + org.wso2.carbon.identity.recovery.model.Property[] propertiesNoAccessUrl = new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property("someOtherKey", "someValue") + }; + assertFalse(Utils.isAccessUrlAvailable(propertiesNoAccessUrl)); + + // Case 3: Null properties. + assertFalse(Utils.isAccessUrlAvailable(null)); + } + + @Test + public void testIsLiteSignUp() { + + org.wso2.carbon.identity.recovery.model.Property[] propertiesTrue = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property(IdentityRecoveryConstants.IS_LITE_SIGN_UP, "true") + }; + assertTrue(Utils.isLiteSignUp(propertiesTrue)); + + // Case 2: Test when property is not present. + org.wso2.carbon.identity.recovery.model.Property[] propertiesNoLiteSignUp = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property("someOtherKey", "someValue") + }; + assertFalse(Utils.isLiteSignUp(propertiesNoLiteSignUp)); + + // Case 3: Test with null properties. + assertFalse(Utils.isLiteSignUp(null)); + } + + @Test + public void testIsUserPortalURL() { + + org.wso2.carbon.identity.recovery.model.Property[] propertiesTrue = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property(IdentityRecoveryConstants.IS_USER_PORTAL_URL, + "true") + }; + assertTrue(Utils.isUserPortalURL(propertiesTrue)); + + // Case 2: Test when property is not present. + org.wso2.carbon.identity.recovery.model.Property[] propertiesNoUserPortalURL = + new org.wso2.carbon.identity.recovery.model.Property[]{ + new org.wso2.carbon.identity.recovery.model.Property("someOtherKey", "someValue") + }; + assertFalse(Utils.isUserPortalURL(propertiesNoUserPortalURL)); + + // Case 3: Test with null properties. + assertFalse(Utils.isUserPortalURL(null)); + } + + @Test + public void testValidateEmailUsernameValidEmail() throws IdentityRecoveryClientException { + + mockedStaticIdentityUtil.when(IdentityUtil::isEmailUsernameEnabled).thenReturn(true); + User user = new User(); + user.setUserName("test@example.com"); + user.setTenantDomain("carbon.super"); + + Utils.validateEmailUsername(user); + + // Case 2: Invalid email username. + User user2 = new User(); + user2.setUserName("testuser"); + user2.setTenantDomain(TENANT_DOMAIN); + + try { + Utils.validateEmailUsername(user); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + } + @Test public void testGetMultiValuedClaim() throws IdentityEventException, org.wso2.carbon.user.core.UserStoreException { @@ -169,4 +767,9 @@ private static User getUser() { user.setUserStoreDomain(USER_STORE_DOMAIN); return user; } + + private static String getUserStoreQualifiedUsername(String username, String userStoreDomainName) { + + return userStoreDomainName + UserCoreConstants.DOMAIN_SEPARATOR + username; + } } From 5be64f31e2b3160f1739d28185553057b6aa7fcc Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Mon, 23 Sep 2024 21:31:47 +0530 Subject: [PATCH 38/54] Add more unit tests for Utils. --- .../identity/recovery/util/UtilsTest.java | 570 +++++++++++++++++- 1 file changed, 568 insertions(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java index 543158f41a..9ef21f1558 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java @@ -18,6 +18,7 @@ package org.wso2.carbon.identity.recovery.util; +import org.apache.commons.lang.StringUtils; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.Mockito; @@ -28,9 +29,15 @@ import org.testng.annotations.Test; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; import org.wso2.carbon.identity.application.common.model.User; +import org.wso2.carbon.identity.auth.attribute.handler.exception.AuthAttributeHandlerClientException; +import org.wso2.carbon.identity.auth.attribute.handler.exception.AuthAttributeHandlerException; +import org.wso2.carbon.identity.auth.attribute.handler.model.ValidationFailureReason; +import org.wso2.carbon.identity.auth.attribute.handler.model.ValidationResult; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.governance.IdentityGovernanceService; import org.wso2.carbon.identity.handler.event.account.lock.exception.AccountLockServiceException; @@ -38,21 +45,38 @@ import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; +import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; +import org.wso2.carbon.identity.recovery.exception.SelfRegistrationClientException; +import org.wso2.carbon.identity.recovery.exception.SelfRegistrationException; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.user.functionality.mgt.UserFunctionalityMgtConstants; import org.wso2.carbon.user.api.Claim; +import org.wso2.carbon.user.api.RealmConfiguration; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.user.core.UserRealm; import org.wso2.carbon.user.core.UserStoreManager; +import org.wso2.carbon.user.core.claim.ClaimManager; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; +import org.wso2.carbon.user.core.constants.UserCoreErrorConstants; import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.identity.application.common.model.Property; +import org.wso2.carbon.user.core.tenant.TenantManager; +import org.wso2.carbon.user.core.util.UserCoreUtil; import org.wso2.carbon.utils.multitenancy.MultitenantUtils; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; +import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URISyntaxException; import java.security.NoSuchAlgorithmException; +import java.sql.Timestamp; +import java.util.ArrayList; import java.util.Arrays; +import java.util.Calendar; +import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -66,6 +90,7 @@ import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -74,6 +99,7 @@ import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertNull; import static org.testng.Assert.assertTrue; +import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; public class UtilsTest { @@ -84,11 +110,21 @@ public class UtilsTest { @Mock private RealmService realmService; @Mock + private RealmConfiguration realmConfiguration; + @Mock private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; @Mock private IdentityGovernanceService identityGovernanceService; @Mock private AccountLockService accountLockService; + @Mock + private TenantManager tenantManager; + @Mock + private ClaimManager claimManager; + @Mock + private AbstractUserStoreManager abstractUserStoreManager; + @Mock + private IdentityEventService identityEventService; private static MockedStatic mockedStaticIdentityTenantUtil; private static MockedStatic mockedStaticUserStoreManager; @@ -96,6 +132,7 @@ public class UtilsTest { private static MockedStatic mockedStaticIdentityUtil; private static MockedStatic mockedStaticFrameworkUtils; private static MockedStatic mockedStaticMultiTenantUtils; + private static MockedStatic mockedStaticUserCoreUtil; private static final String TENANT_DOMAIN = "test.com"; private static final int TENANT_ID = 123; @@ -111,6 +148,7 @@ public static void beforeClass() { mockedStaticIdentityUtil = mockStatic(IdentityUtil.class); mockedStaticFrameworkUtils = mockStatic(FrameworkUtils.class); mockedStaticMultiTenantUtils = mockStatic(MultitenantUtils.class); + mockedStaticUserCoreUtil = mockStatic(UserCoreUtil.class); } @AfterClass @@ -122,6 +160,7 @@ public static void afterClass() { mockedStaticIdentityUtil.close(); mockedStaticFrameworkUtils.close(); mockedStaticMultiTenantUtils.close(); + mockedStaticUserCoreUtil.close(); } @BeforeMethod @@ -142,9 +181,15 @@ public void setUp() throws org.wso2.carbon.user.api.UserStoreException { when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); when(identityRecoveryServiceDataHolder.getIdentityGovernanceService()).thenReturn(identityGovernanceService); when(identityRecoveryServiceDataHolder.getAccountLockService()).thenReturn(accountLockService); + when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); when(realmService.getTenantUserRealm(TENANT_ID)).thenReturn(userRealm); + when(realmService.getBootstrapRealm()).thenReturn(userRealm); + when(realmService.getTenantManager()).thenReturn(tenantManager); when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); + when(userRealm.getClaimManager()).thenReturn(claimManager); + when(userStoreManager.getRealmConfiguration()).thenReturn(realmConfiguration); + when(tenantManager.getTenantId(eq(TENANT_DOMAIN))).thenReturn(TENANT_ID); } @Test(expectedExceptions = IdentityRecoveryClientException.class) @@ -431,7 +476,6 @@ public void testGetRecoveryConfigs() throws Exception { String key = "recovery.key"; String expectedValue = "test_value"; - Property property = new Property(); property.setName(key); property.setValue(expectedValue); @@ -664,7 +708,8 @@ public void testIsAccessUrlAvailable() { assertTrue(Utils.isAccessUrlAvailable(propertiesTrue)); // Case 2: When IS_ACCESS_URL_AVAILABLE property is not present. - org.wso2.carbon.identity.recovery.model.Property[] propertiesNoAccessUrl = new org.wso2.carbon.identity.recovery.model.Property[]{ + org.wso2.carbon.identity.recovery.model.Property[] propertiesNoAccessUrl = + new org.wso2.carbon.identity.recovery.model.Property[]{ new org.wso2.carbon.identity.recovery.model.Property("someOtherKey", "someValue") }; assertFalse(Utils.isAccessUrlAvailable(propertiesNoAccessUrl)); @@ -714,6 +759,122 @@ public void testIsUserPortalURL() { assertFalse(Utils.isUserPortalURL(null)); } + @Test + public void testCheckPasswordPatternViolation() throws Exception { + + String PROPERTY_PASSWORD_ERROR_MSG = "PasswordJavaRegExViolationErrorMsg"; + String errorCode = UserCoreErrorConstants.ErrorMessages + .ERROR_CODE_ERROR_DURING_PRE_UPDATE_CREDENTIAL_BY_ADMIN.getCode(); + String errorMessage = "TEST_CUSTOM_PASSWORD_ERROR_MSG"; + User user = getUser(); + user.setUserStoreDomain(UserCoreConstants.PRIMARY_DEFAULT_DOMAIN_NAME); + + when(realmConfiguration.getUserStoreProperty(PROPERTY_PASSWORD_ERROR_MSG)).thenReturn(errorMessage); + UserStoreException exceptionWithViolation = + new UserStoreException(String.format("%s: %s", errorCode, errorMessage)); + + try { + Utils.checkPasswordPatternViolation(exceptionWithViolation, user); + fail("Expected IdentityRecoveryClientException was not thrown"); + } catch (IdentityRecoveryClientException e) { + assertEquals(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_POLICY_VIOLATION.getCode(), + e.getErrorCode()); + } + + // Case 2 - Password error message property is not set. + when(realmConfiguration.getUserStoreProperty(PROPERTY_PASSWORD_ERROR_MSG)).thenReturn(""); + UserStoreException exceptionWithInvalidPasswordCode = new UserStoreException( + UserCoreErrorConstants.ErrorMessages.ERROR_CODE_INVALID_PASSWORD.getCode()); + when(realmConfiguration.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_JAVA_REG_EX)) + .thenReturn("^[a-zA-Z0-9]{5,10}$"); + try { + Utils.checkPasswordPatternViolation(exceptionWithInvalidPasswordCode, user); + fail("Expected IdentityRecoveryClientException was not thrown"); + } catch (IdentityRecoveryClientException e) { + assertEquals(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_POLICY_VIOLATION.getCode(), e.getErrorCode()); + assertTrue(e.getMessage().contains("^[a-zA-Z0-9]{5,10}$")); + } + } + + @Test + public void testIsAccountStateClaimExisting() throws Exception { + + Claim mockClaim = mock(Claim.class); + when(claimManager.getClaim(IdentityRecoveryConstants.ACCOUNT_STATE_CLAIM_URI)).thenReturn(mockClaim); + boolean result = Utils.isAccountStateClaimExisting(TENANT_DOMAIN); + assertTrue(result); + + // Case 2: Throws UserStoreException. + when(claimManager.getClaim(IdentityRecoveryConstants.ACCOUNT_STATE_CLAIM_URI)) + .thenThrow(new UserStoreException("Test exception")); + try { + Utils.isAccountStateClaimExisting(TENANT_DOMAIN); + } catch (Exception e) { + assertTrue(e instanceof IdentityEventException); + } + } + + @Test + public void testPrependOperationScenarioToErrorCode() { + + String scenario = "USR"; + String errorCode = "20045"; + + // Test with valid scenario and error code. + String result = Utils.prependOperationScenarioToErrorCode(errorCode, scenario); + assertEquals(result, "USR-20045"); + + // Test with empty error code. + result = Utils.prependOperationScenarioToErrorCode("", scenario); + assertEquals(result, ""); + + // Test with scenario already in error code. + result = Utils.prependOperationScenarioToErrorCode("USR-20045", scenario); + assertEquals(result, "USR-20045"); + } + + @Test + public void testIsNotificationsInternallyManaged() throws Exception { + + Property[] properties = new Property[1]; + properties[0] = new Property(); + properties[0].setName(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE); + properties[0].setValue(Boolean.TRUE.toString()); + when(identityGovernanceService.getConfiguration(any(String[].class), eq(TENANT_DOMAIN))) + .thenReturn(properties); + + // Case 1: Empty properties. + assertTrue(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, new HashMap<>())); + + // Case 2: Properties with MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY set to true. + Map props = new HashMap<>(); + props.put(IdentityRecoveryConstants.MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY, "true"); + assertTrue(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, props)); + + // Case 4: Properties without MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY. + props.remove(IdentityRecoveryConstants.MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY); + assertTrue(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, props)); + + // Case 5: Invalid boolean value. + props.put(IdentityRecoveryConstants.MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY, "invalid"); + assertFalse(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, props)); + } + + @Test + public void testResolveEventName() { + + // Case 1: SMS channel. + String smsChannel = NotificationChannels.SMS_CHANNEL.getChannelType(); + String expectedSmsEventName = IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_PREFIX + smsChannel + + IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_SUFFIX + + IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_SUFFIX_LOCAL; + assertEquals(Utils.resolveEventName(smsChannel), expectedSmsEventName); + + // Case 2: Other channels. + String otherChannel = "EMAIL"; + assertEquals(Utils.resolveEventName(otherChannel), IdentityEventConstants.Event.TRIGGER_NOTIFICATION); + } + @Test public void testValidateEmailUsernameValidEmail() throws IdentityRecoveryClientException { @@ -736,6 +897,395 @@ public void testValidateEmailUsernameValidEmail() throws IdentityRecoveryClientE } } + @Test + public void testBuildUser() { + + mockedStaticUserCoreUtil.when(() -> UserCoreUtil.removeDomainFromName(USER_NAME)).thenReturn(USER_NAME); + User user = Utils.buildUser(USER_NAME, TENANT_DOMAIN); + assertEquals(user.getUserName(), USER_NAME); + assertEquals(user.getTenantDomain(), TENANT_DOMAIN); + } + + @Test + public void testIsPerUserFunctionalityLockingEnabled() { + + mockedStaticIdentityUtil.when(() -> IdentityUtil.getProperty( + UserFunctionalityMgtConstants.ENABLE_PER_USER_FUNCTIONALITY_LOCKING)).thenReturn("true"); + assertTrue(Utils.isPerUserFunctionalityLockingEnabled()); + } + + @Test + public void testIsDetailedErrorResponseEnabled() { + + mockedStaticIdentityUtil.when(() -> IdentityUtil.getProperty( + IdentityRecoveryConstants.ENABLE_DETAILED_ERROR_RESPONSE)).thenReturn("true"); + assertTrue(Utils.isDetailedErrorResponseEnabled()); + } + + @Test + public void testGetUserId() throws Exception { + + String expectedUserId = "12345-67890-abcde-fghij"; + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getUserIDFromUserName(USER_NAME)).thenReturn(expectedUserId); + + String userId = Utils.getUserId(USER_NAME, TENANT_ID); + assertEquals(userId, expectedUserId); + + // Case 2: RealmService is null. + when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(null); + try { + Utils.getUserId(USER_NAME, TENANT_ID); + fail("Expected IdentityRecoveryServerException was not thrown"); + } catch (IdentityRecoveryServerException e) { + assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_FAILED_TO_LOAD_REALM_SERVICE.getCode()); + } + + // Reset RealmService mock. + when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); + + // Case 4: UserStoreException when getting UserStoreManager. + when(realmService.getTenantUserRealm(TENANT_ID)).thenThrow(new UserStoreException("Test exception")); + try { + Utils.getUserId(USER_NAME, TENANT_ID); + fail("Expected IdentityRecoveryServerException was not thrown"); + } catch (IdentityRecoveryServerException e) { + assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages + .ERROR_CODE_FAILED_TO_LOAD_REALM_SERVICE.getCode()); + } + + // Reset UserStoreManager mock. + doReturn(userRealm).when(realmService).getTenantUserRealm(TENANT_ID); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + + // Case 5: UserStoreException when getting user ID + when(abstractUserStoreManager.getUserIDFromUserName(USER_NAME)).thenThrow( + new UserStoreException("Test exception")); + try { + Utils.getUserId(USER_NAME, TENANT_ID); + fail("Expected IdentityRecoveryServerException was not thrown"); + } catch (IdentityRecoveryServerException e) { + assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_FAILED_TO_UPDATE_USER_CLAIMS.getCode()); + } + } + + @Test + public void testIsSkipRecoveryWithChallengeQuestionsForInsufficientAnswersEnabled() { + + mockedStaticIdentityUtil.when(() -> IdentityUtil.getProperty( + IdentityRecoveryConstants.RECOVERY_QUESTION_PASSWORD_SKIP_ON_INSUFFICIENT_ANSWERS)) + .thenReturn("true"); + assertTrue(Utils.isSkipRecoveryWithChallengeQuestionsForInsufficientAnswersEnabled()); + } + + @Test + public void testIsUseVerifyClaimEnabled() { + + mockedStaticIdentityUtil.when(() ->IdentityUtil.getProperty + (IdentityRecoveryConstants.ConnectorConfig.USE_VERIFY_CLAIM_ON_UPDATE)).thenReturn("true"); + assertTrue(Utils.isUseVerifyClaimEnabled()); + } + + @Test + public void testPublishRecoveryEvent() throws IdentityEventException { + + Map map = new HashMap<>(); + String eventName = "testEvent"; + String confirmationCode = "123456"; + + Utils.publishRecoveryEvent(map, eventName, confirmationCode); + verify(identityEventService).handleEvent(any()); + } + + @Test + public void testGetAccountState() throws UserStoreException { + + String expectedAccountState = "testValue"; + User user = getUser(); + + // Case 1: Existing user. + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.isExistingUser(user.getUserName())).thenReturn(true); + + Map claimMap = new HashMap<>(); + claimMap.put(IdentityRecoveryConstants.ACCOUNT_STATE_CLAIM_URI, expectedAccountState); + when(abstractUserStoreManager.getUserClaimValues(user.getUserName(), + new String[]{IdentityRecoveryConstants.ACCOUNT_STATE_CLAIM_URI}, "default")) + .thenReturn(claimMap); + String accountState = Utils.getAccountState(user); + assertEquals(accountState, expectedAccountState); + + // Case 2: User doesn't exist in primary user store, secondary user store null. + when(abstractUserStoreManager.isExistingUser(user.getUserName())).thenReturn(false); + when(abstractUserStoreManager.getSecondaryUserStoreManager()).thenReturn(null); + + accountState = Utils.getAccountState(user); + assertEquals(accountState, StringUtils.EMPTY); + } + + @Test + public void testGetAccountStateForUserNameWithoutUserDomain() throws UserStoreException { + + User user = getUser(); + String expectedAccountState = "testValue"; + String userStoreDomainQualifiedUsername = USER_STORE_DOMAIN + UserCoreConstants.DOMAIN_SEPARATOR + USER_NAME; + mockedStaticUserCoreUtil.when(() -> UserCoreUtil.addDomainToName(USER_NAME, USER_STORE_DOMAIN)) + .thenReturn(userStoreDomainQualifiedUsername); + + // Case 1: Existing user. + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.isExistingUser(user.getUserName())).thenReturn(true); + + Map claimMap = new HashMap<>(); + claimMap.put(IdentityRecoveryConstants.ACCOUNT_STATE_CLAIM_URI, expectedAccountState); + when(abstractUserStoreManager.getUserClaimValues(userStoreDomainQualifiedUsername, + new String[]{IdentityRecoveryConstants.ACCOUNT_STATE_CLAIM_URI}, "default")) + .thenReturn(claimMap); + + String accountState = Utils.getAccountStateForUserNameWithoutUserDomain(user); + assertEquals(accountState, expectedAccountState); + } + + @Test + public void testGenerateRandomPassword() { + + int passwordLength = 10; + char[] result = Utils.generateRandomPassword(passwordLength); + assertEquals(result.length, passwordLength); + } + + @Test + public void testReIssueExistingConfirmationCodeNotificationBasedPasswordRecovery() { + + int recoveryConfirmationTolerancePeriod = 10; + int recoveryCodeExpiryTime = 20; + + UserRecoveryData recoveryData = new UserRecoveryData(getUser(), "12345", + RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD); + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, -5); + Date creationTime = calendar.getTime(); + Timestamp timestamp = new Timestamp(creationTime.getTime()); + + recoveryData.setTimeCreated(timestamp); + + // Case 1: With RECOVERY_CONFIRMATION_CODE_DEFAULT_TOLERANCE, ConfirmationTolerancePeriod = 10. + boolean result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + + // Case 2: With RECOVERY_CODE_DEFAULT_EXPIRY_TIME, ConfirmationTolerancePeriod = 10. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.RECOVERY_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(recoveryConfirmationTolerancePeriod)); + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + + // Case 3: RecoveryCodeExpiryTime = 20, ConfirmationTolerancePeriod = 10. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CODE_EXPIRY_TIME, + String.valueOf(recoveryCodeExpiryTime)); + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertTrue(result); + + // Case 4: Invalid value for RECOVERY_CODE_EXPIRY_TIME. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CODE_EXPIRY_TIME, + "invalid"); + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + + // Case 5: Invalid value for RECOVERY_CONFIRMATION_CODE_TOLERANCE_PERIOD. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.RECOVERY_CONFIRMATION_CODE_TOLERANCE_PERIOD, "invalid"); + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + } + + @Test + public void testReIssueExistingConfirmationCodeSelfSignupEmail() throws IdentityGovernanceException { + + int selfRegistrationCodeTolerance = 10; + int selfRegistrationCodeExpiryTime = 20; + + UserRecoveryData recoveryData = new UserRecoveryData(getUser(), "12345", + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.UPDATE_PASSWORD); + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, -5); + Date creationTime = calendar.getTime(); + Timestamp timestamp = new Timestamp(creationTime.getTime()); + + recoveryData.setTimeCreated(timestamp); + + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.SELF_SIGN_UP_EMAIL_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(selfRegistrationCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, String.valueOf(selfRegistrationCodeExpiryTime)); + + boolean result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertTrue(result); + + // Case 2: Invalid value for SELF_SIGN_UP_EMAIL_CONFIRMATION_CODE_TOLERANCE_PERIOD. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.SELF_SIGN_UP_EMAIL_CONFIRMATION_CODE_TOLERANCE_PERIOD, + "invalid"); + + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + + // Case 3: Invalid value for SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.SELF_SIGN_UP_EMAIL_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(selfRegistrationCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, "invalid"); + + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + + // Case 4: SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME less than tolerance period. + selfRegistrationCodeTolerance = 10; + selfRegistrationCodeExpiryTime = 5; + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.SELF_SIGN_UP_EMAIL_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(selfRegistrationCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, String.valueOf(selfRegistrationCodeExpiryTime)); + + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + } + + @Test + public void testReIssueExistingConfirmationCodeSelfSignupSMS() throws IdentityGovernanceException { + + int selfRegistrationCodeTolerance = 10; + int selfRegistrationCodeExpiryTime = 20; + + UserRecoveryData recoveryData = new UserRecoveryData(getUser(), "12345", + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.UPDATE_PASSWORD); + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, -5); + Date creationTime = calendar.getTime(); + Timestamp timestamp = new Timestamp(creationTime.getTime()); + + recoveryData.setTimeCreated(timestamp); + + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.SELF_SIGN_UP_SMS_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(selfRegistrationCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME, + String.valueOf(selfRegistrationCodeExpiryTime)); + + boolean result = Utils.reIssueExistingConfirmationCode(recoveryData, "SMS"); + assertTrue(result); + + // Other notification channel. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.SELF_SIGN_UP_EMAIL_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(5)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, String.valueOf(selfRegistrationCodeExpiryTime)); + + result = Utils.reIssueExistingConfirmationCode(recoveryData, "OTHER"); + assertFalse(result); + } + + @Test + public void testReIssueExistingConfirmationCodeAskPassword() throws IdentityGovernanceException { + + int askPasswordCodeTolerance = 10; + int askPasswordCodeExpiryTime = 20; + + UserRecoveryData recoveryData = new UserRecoveryData(getUser(), "12345", + RecoveryScenarios.ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD); + + Calendar calendar = Calendar.getInstance(); + calendar.add(Calendar.MINUTE, -5); + Date creationTime = calendar.getTime(); + Timestamp timestamp = new Timestamp(creationTime.getTime()); + + recoveryData.setTimeCreated(timestamp); + + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.ASK_PASSWORD_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(askPasswordCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .ASK_PASSWORD_EXPIRY_TIME, + String.valueOf(askPasswordCodeExpiryTime)); + + boolean result = Utils.reIssueExistingConfirmationCode(recoveryData, "SMS"); + assertTrue(result); + + // Case 3: Invalid value for SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME. + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.ASK_PASSWORD_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(askPasswordCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .ASK_PASSWORD_EXPIRY_TIME, "invalid"); + + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + + // Case 4: Code expiry time is less than tolerance period. + askPasswordCodeExpiryTime = 5; + mockIdentityUtilsGetProperty(IdentityRecoveryConstants.ASK_PASSWORD_CONFIRMATION_CODE_TOLERANCE_PERIOD, + String.valueOf(askPasswordCodeTolerance)); + mockGetRecoveryConfig(IdentityRecoveryConstants.ConnectorConfig + .ASK_PASSWORD_EXPIRY_TIME, String.valueOf(askPasswordCodeExpiryTime)); + + result = Utils.reIssueExistingConfirmationCode(recoveryData, "EMAIL"); + assertFalse(result); + } + + @Test + public void testHandleAttributeValidationFailureWithValidationResult() { + + // Case 1: Validation result is null. + ValidationResult validationResult = null; + try { + Utils.handleAttributeValidationFailure(validationResult); + } catch (Exception e) { + assertTrue(e instanceof SelfRegistrationClientException); + assertEquals(((SelfRegistrationClientException) e).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_UNEXPECTED_ERROR_VALIDATING_ATTRIBUTES + .getCode()); + } + + // Case 2: Validation result is not null. + ValidationFailureReason validationFailureReason = new ValidationFailureReason(); + validationFailureReason.setErrorCode("test-code"); + validationFailureReason.setReason("test-reason"); + validationFailureReason.setAuthAttribute("test-auth-attribute"); + + validationResult = new ValidationResult(); + validationResult.setValidationFailureReasons(new ArrayList<>(Arrays.asList(validationFailureReason))); + + try { + Utils.handleAttributeValidationFailure(validationResult); + } catch (Exception e) { + assertTrue(e instanceof SelfRegistrationClientException); + assertEquals(((SelfRegistrationClientException) e).getErrorCode(), + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_USER_ATTRIBUTES_FOR_REGISTRATION + .getCode()); + } + } + + @Test + public void testHandleAttributeValidationFailure() { + + AuthAttributeHandlerException exception = + new AuthAttributeHandlerClientException(ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND.getCode(), + ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND.getMessage()); + try { + Utils.handleAttributeValidationFailure(exception); + } catch (Exception e) { + assertTrue(e instanceof SelfRegistrationClientException); + } + + // Case 2: AuthAttributeHandlerException exception. + AuthAttributeHandlerException exception1 = + new AuthAttributeHandlerException("test-code", "test-message"); + try { + Utils.handleAttributeValidationFailure(exception1); + } catch (Exception e) { + assertTrue(e instanceof SelfRegistrationException); + } + } + @Test public void testGetMultiValuedClaim() throws IdentityEventException, org.wso2.carbon.user.core.UserStoreException { @@ -768,6 +1318,22 @@ private static User getUser() { return user; } + private static void mockIdentityUtilsGetProperty(String key, String value) { + + mockedStaticIdentityUtil.when(() -> IdentityUtil.getProperty(key)).thenReturn(value); + } + + private void mockGetRecoveryConfig(String key, String value) throws IdentityGovernanceException { + + Property property = new Property(); + property.setName(key); + property.setValue(value); + Property[] properties = new Property[]{property}; + + when(identityGovernanceService.getConfiguration(eq(new String[]{key}), eq(TENANT_DOMAIN))) + .thenReturn(properties); + } + private static String getUserStoreQualifiedUsername(String username, String userStoreDomainName) { return userStoreDomainName + UserCoreConstants.DOMAIN_SEPARATOR + username; From f3a49a1cffcabe0bb678748b18ae4f82f9d1cebb Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Tue, 24 Sep 2024 16:39:35 +0530 Subject: [PATCH 39/54] Add more unit tests for UserSelfRegistrationManager --- .../UserSelfRegistrationManagerTest.java | 508 +++++++++++++++++- 1 file changed, 495 insertions(+), 13 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index 40fe4077ea..7b176cb7ca 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -33,6 +33,7 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.wso2.carbon.CarbonConstants; import org.wso2.carbon.consent.mgt.core.ConsentManager; import org.wso2.carbon.consent.mgt.core.ConsentManagerImpl; import org.wso2.carbon.consent.mgt.core.exception.ConsentManagementException; @@ -60,10 +61,15 @@ import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.governance.service.otp.OTPGenerator; import org.wso2.carbon.identity.input.validation.mgt.exceptions.InputValidationMgtException; +import org.wso2.carbon.identity.input.validation.mgt.model.RulesConfiguration; import org.wso2.carbon.identity.input.validation.mgt.model.ValidationConfiguration; +import org.wso2.carbon.identity.input.validation.mgt.model.Validator; import org.wso2.carbon.identity.input.validation.mgt.services.InputValidationManagementService; +import org.wso2.carbon.identity.input.validation.mgt.utils.Constants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; import org.wso2.carbon.identity.recovery.RecoveryScenarios; import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.UserWorkflowManagementService; @@ -78,6 +84,8 @@ import org.wso2.carbon.idp.mgt.IdentityProviderManagementException; import org.wso2.carbon.idp.mgt.IdentityProviderManager; import org.wso2.carbon.user.api.Claim; +import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import org.wso2.carbon.user.core.config.RealmConfiguration; import org.wso2.carbon.user.core.tenant.TenantManager; import org.wso2.carbon.user.core.UserRealm; @@ -90,6 +98,7 @@ import java.sql.Timestamp; import java.util.Arrays; import java.util.HashMap; +import java.util.HashSet; import java.util.Map; import static org.mockito.ArgumentMatchers.any; @@ -101,16 +110,21 @@ import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertFalse; +import static org.testng.Assert.fail; import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP; +import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_SEND_OTP_IN_EMAIL; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_USE_LOWERCASE_CHARACTERS_IN_OTP; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.SELF_REGISTRATION_USE_NUMBERS_IN_OTP; @@ -197,6 +211,9 @@ public class UserSelfRegistrationManagerTest { @Mock private ValidationConfiguration validationConfiguration; + @Mock + private AbstractUserStoreManager abstractUserStoreManager; + private MockedStatic mockedServiceDataHolder; private MockedStatic mockedIdentityUtil; private MockedStatic mockedJDBCRecoveryDataStore; @@ -514,9 +531,9 @@ public void testAddConsent() throws Exception { resultReceipt.getCollectionMethod()); Assert.assertEquals("someJurisdiction", resultReceipt.getJurisdiction()); Assert.assertEquals("en", resultReceipt.getLanguage()); - Assert.assertNotNull(resultReceipt.getServices()); + assertNotNull(resultReceipt.getServices()); Assert.assertEquals(1, resultReceipt.getServices().size()); - Assert.assertNotNull(resultReceipt.getServices().get(0).getPurposes()); + assertNotNull(resultReceipt.getServices().get(0).getPurposes()); Assert.assertEquals(1, resultReceipt.getServices().get(0).getPurposes().size()); Assert.assertEquals(new Integer(3), resultReceipt.getServices().get(0).getPurposes().get(0).getPurposeId()); Assert.assertEquals(IdentityRecoveryConstants.Consent.EXPLICIT_CONSENT_TYPE, @@ -544,8 +561,8 @@ public void testAttributeVerification() throws Exception { Property property = new Property("registrationOption", "MagicLinkAuthAttributeHandler"); - Boolean response = userSelfRegistrationManager.verifyUserAttributes(user, "password", new Claim[]{claim}, - new Property[]{property}); + Boolean response = userSelfRegistrationManager.verifyUserAttributes(user, "password", + new Claim[]{claim}, new Property[]{property}); Assert.assertTrue(response); } @@ -660,7 +677,7 @@ public void testConfirmVerificationCodeMe() public void testGetConfirmedSelfRegisteredUserVerifyEmail() throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { - String verifiedChannelType = "EMAIL"; + String verifiedChannelType = NotificationChannels.EMAIL_CHANNEL.getChannelType(); String verifiedChannelClaim = "http://wso2.org/claims/emailaddress"; String verificationPendingEmail = "pasindu@gmail.com"; Map metaProperties = new HashMap<>(); @@ -701,13 +718,24 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() assertTrue(StringUtils.contains(updatedVerifiedEmailAddresses, verificationPendingEmail)); assertEquals(verificationPendingEmailAddress, StringUtils.EMPTY); + + // Case 2 : Throws user store exception while getting user store manager. + when(userRealm.getUserStoreManager()).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, + verifiedChannelClaim, metaProperties); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } } + @Test public void testGetConfirmedSelfRegisteredUserVerifyMobile() throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { - String verifiedChannelType = "SMS"; + String verifiedChannelType = NotificationChannels.SMS_CHANNEL.getChannelType(); String verifiedChannelClaim = "http://wso2.org/claims/mobile"; String verificationPendingMobileNumber = "077888888"; Map metaProperties = new HashMap<>(); @@ -739,6 +767,9 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER), anyString())) .thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_NOTIFY_ACCOUNT_CONFIRMATION), + anyString())).thenReturn("true"); userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, verifiedChannelClaim, metaProperties); } @@ -757,6 +788,87 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() assertTrue(StringUtils.contains(updatedVerifiedMobileNumbers, verificationPendingMobileNumber)); assertEquals(verificationPendingMobileNumberClaim, StringUtils.EMPTY); assertEquals(updatedMobileNumberClaimValue, verificationPendingMobileNumber); + + // Case 3: External Verified Channel type. + verifiedChannelType = NotificationChannels.EXTERNAL_CHANNEL.getChannelType(); + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(true); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER), + anyString())) + .thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_NOTIFY_ACCOUNT_CONFIRMATION), + anyString())).thenReturn("true"); + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, + verifiedChannelClaim, metaProperties); + } + + ArgumentCaptor> claimsCaptor1 = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor1.capture(), isNull()); + + Map capturedClaims1 = claimsCaptor1.getValue(); + String emailVerifiedClaim = + capturedClaims1.get(IdentityRecoveryConstants.EMAIL_VERIFIED_CLAIM); + assertEquals(emailVerifiedClaim, Boolean.TRUE.toString()); + + // Case 4: With wrong verified channel claim value. + verifiedChannelType = NotificationChannels.SMS_CHANNEL.getChannelType(); + verifiedChannelClaim = "http://wso2.org/claims/invalid"; + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(true); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER), + anyString())) + .thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_NOTIFY_ACCOUNT_CONFIRMATION), + anyString())).thenReturn("true"); + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, + verifiedChannelClaim, metaProperties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + } + } + + @Test + public void testGetConfirmedSelfRegisteredUserConfirmSignUp() + throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { + + String verifiedChannelType = NotificationChannels.EMAIL_CHANNEL.getChannelType(); + String verifiedChannelClaim = "http://wso2.org/claims/emailaddress"; + String verificationPendingEmail = "pasindu@gmail.com"; + Map metaProperties = new HashMap<>(); + + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); + // Setting verification pending email claim value. + userRecoveryData.setRemainingSetIds(verificationPendingEmail); + + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); + when(userRecoveryDataStore.load(eq(TEST_CODE), anyBoolean())).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + mockMultiAttributeEnabled(false); + + org.wso2.carbon.identity.application.common.model.Property property = + new org.wso2.carbon.identity.application.common.model.Property(); + org.wso2.carbon.identity.application.common.model.Property[] testProperties = + new org.wso2.carbon.identity.application.common.model.Property[]{property}; + + when(identityGovernanceService.getConfiguration(any(), anyString())).thenReturn(testProperties); + + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, verifiedChannelClaim, + metaProperties); + + ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); + Map capturedClaims = claimsCaptor.getValue(); + String updatedVerifiedMobileNumbers = + capturedClaims.get(IdentityRecoveryConstants.ACCOUNT_LOCKED_CLAIM); + assertEquals(updatedVerifiedMobileNumbers, Boolean.FALSE.toString()); } @Test @@ -767,7 +879,7 @@ public void testRegisterUser() throws Exception { when(userStoreManager.isExistingRole(eq(IdentityRecoveryConstants.SELF_SIGNUP_ROLE))).thenReturn(true); when(privilegedCarbonContext.getOSGiService(any(), isNull())).thenReturn(notificationChannelManager); when(notificationChannelManager.resolveCommunicationChannel(anyString(), anyString(), anyString(), any())) - .thenReturn("EMAIL"); + .thenReturn(NotificationChannels.EMAIL_CHANNEL.getChannelType()); Property property = new Property(IdentityRecoveryConstants.Consent.CONSENT, consentData); NotificationResponseBean notificationResponseBean = @@ -782,9 +894,85 @@ public void testRegisterUser() throws Exception { } @Test - public void testIsUserConfirmed() throws IdentityRecoveryException { + public void testRegisterUserNotificationExternallyAccountLockedOnCreation() throws Exception { + User user = new User(); + user.setUserName(TEST_USER_NAME); + Property property = new Property(IdentityRecoveryConstants.Consent.CONSENT, consentData); + + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(ACCOUNT_LOCK_ON_CREATION), anyString())) + .thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(NOTIFICATION_INTERNALLY_MANAGE), anyString())) + .thenReturn("false"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(ENABLE_SELF_SIGNUP), anyString())) + .thenReturn("true"); + mockedUtils.when(Utils::getNotificationChannelManager).thenReturn(notificationChannelManager); + + when(notificationChannelManager.resolveCommunicationChannel(anyString(), anyString(), anyString(), any())) + .thenReturn(NotificationChannels.EMAIL_CHANNEL.getChannelType()); + + NotificationResponseBean notificationResponseBean = + userSelfRegistrationManager.registerUser(user, "test-pwd", new Claim[0], + new Property[]{property}); + + assertEquals(IdentityRecoveryConstants.SuccessEvents + .SUCCESS_STATUS_CODE_SUCCESSFUL_USER_CREATION_EXTERNAL_VERIFICATION.getCode(), + notificationResponseBean.getCode()); + verify(userStoreManager).addUser(anyString(), anyString(), any(), any(), isNull()); + verify(consentManger).addConsent(any()); + verify(identityEventService, atLeastOnce()).handleEvent(any()); + } + } + + @Test + public void testRegisterUserVerifiedPreferredChannel() throws Exception { + + String emailVerifiedClaimURI = "http://wso2.org/claims/identity/emailVerified"; User user = getUser(); + Property property = new Property(IdentityRecoveryConstants.Consent.CONSENT, consentData); + mockedIdentityUtil.when(() -> IdentityUtil.getProperty( + eq(IdentityRecoveryConstants.ConnectorConfig + .ENABLE_ACCOUNT_LOCK_FOR_VERIFIED_PREFERRED_CHANNEL))) + .thenReturn("false"); + when(consentUtilityService.filterPIIsFromReceipt(any(), any())).thenReturn( + new HashSet<>(Arrays.asList(emailVerifiedClaimURI))); + + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(ACCOUNT_LOCK_ON_CREATION), anyString())) + .thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(NOTIFICATION_INTERNALLY_MANAGE), anyString())) + .thenReturn("false"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(ENABLE_SELF_SIGNUP), anyString())) + .thenReturn("true"); + mockedUtils.when(Utils::getNotificationChannelManager).thenReturn(notificationChannelManager); + + when(notificationChannelManager.resolveCommunicationChannel(anyString(), anyString(), anyString(), any())) + .thenReturn(NotificationChannels.EMAIL_CHANNEL.getChannelType()); + + Claim claim = new Claim(); + claim.setClaimUri(emailVerifiedClaimURI); + claim.setValue("true"); + Claim[] claims = new Claim[]{claim}; + + NotificationResponseBean notificationResponseBean = + userSelfRegistrationManager.registerUser(user, "test-pwd", claims, + new Property[]{property}); + + assertEquals(IdentityRecoveryConstants.SuccessEvents + .SUCCESS_STATUS_CODE_SUCCESSFUL_USER_CREATION_WITH_VERIFIED_CHANNEL.getCode(), + notificationResponseBean.getCode()); + verify(userStoreManager).addUser(anyString(), anyString(), any(), any(), isNull()); + verify(consentManger).addConsent(any()); + verify(identityEventService, atLeastOnce()).handleEvent(any()); + } + } + + @Test + public void testIsUserConfirmed() throws IdentityRecoveryException { + + User user = new User(); + user.setUserName(TEST_USER_NAME); UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(any())).thenReturn(userRecoveryData); @@ -815,6 +1003,30 @@ public void testConfirmUserSelfRegistration() throws IdentityRecoveryException, capturedClaims.get(IdentityRecoveryConstants.EMAIL_VERIFIED_CLAIM); assertEquals(updatedAccountLockedClaim, Boolean.FALSE.toString()); assertEquals(updatedEmailVerifiedClaim, Boolean.TRUE.toString()); + + // Case 2: Invalid Tenant. + User user1 = getUser(); + user1.setTenantDomain("invalid"); + userRecoveryData = new UserRecoveryData(user1, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP); + when(userRecoveryDataStore.load(anyString())).thenReturn(userRecoveryData); + try { + userSelfRegistrationManager.confirmUserSelfRegistration(TEST_CODE); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + // Case 3: Invalid recovery step. + userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.UPDATE_PASSWORD); + when(userRecoveryDataStore.load(anyString())).thenReturn(userRecoveryData); + try { + userSelfRegistrationManager.confirmUserSelfRegistration(TEST_CODE); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } } @Test @@ -826,7 +1038,7 @@ public void testIntrospectUserSelfRegistration() throws IdentityRecoveryExceptio when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); - String verifiedChannelType = "EMAIL"; + String verifiedChannelType = NotificationChannels.EMAIL_CHANNEL.getChannelType(); String verifiedChannelClaim = "http://wso2.org/claims/emailaddress"; UserRecoveryData resultUserRecoveryData = userSelfRegistrationManager.introspectUserSelfRegistration(TEST_CODE, @@ -879,6 +1091,14 @@ public void testIsValidUserStoreDomain() throws IdentityRecoveryException, UserS boolean isValid = userSelfRegistrationManager.isValidUserStoreDomain( TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME); assertTrue(isValid); + + // Case 2: Throws UserStoreException. + when(userRealm.getUserStoreManager()).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + userSelfRegistrationManager.isValidUserStoreDomain(TEST_USERSTORE_DOMAIN, TEST_TENANT_DOMAIN_NAME); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + } } @Test @@ -900,6 +1120,14 @@ public void testIsUsernameAlreadyTaken() throws UserStoreException, IdentityReco boolean isUsernameAlreadyTaken2 = userSelfRegistrationManager.isUsernameAlreadyTaken(TEST_USER_NAME); assertTrue(isUsernameAlreadyTaken2); + + // Case 3: Throw UserStoreException. + when(userRealm.getUserStoreManager()).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + userSelfRegistrationManager.isUsernameAlreadyTaken(TEST_USER_NAME); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + } } @Test @@ -909,10 +1137,21 @@ public void testIsSelfRegistrationEnabled() throws Exception{ boolean isSelfRegistrationEnabled = userSelfRegistrationManager .isSelfRegistrationEnabled(TEST_TENANT_DOMAIN_NAME); assertTrue(isSelfRegistrationEnabled); + + // Case 2: Throw Governance Exception. + when(identityGovernanceService.getConfiguration(any(), anyString())) + .thenThrow(new IdentityGovernanceException("test error")); + try { + userSelfRegistrationManager.isSelfRegistrationEnabled(TEST_TENANT_DOMAIN_NAME); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + } } @Test - public void testIsMatchUserNameRegex() throws IdentityRecoveryException, InputValidationMgtException { + public void testIsMatchUserNameRegex() + throws IdentityRecoveryException, InputValidationMgtException, + UserStoreException { mockedMultiTenantUtils.when(() -> MultitenantUtils .getTenantAwareUsername(anyString())).thenReturn(TEST_USER_NAME); @@ -923,14 +1162,257 @@ public void testIsMatchUserNameRegex() throws IdentityRecoveryException, InputVa mockedIdentityUtil.when(() -> IdentityTenantUtil.getTenantDomain(TEST_TENANT_ID)) .thenReturn(TEST_TENANT_DOMAIN_NAME); - when(inputValidationManagementService.getValidators(anyString())).thenReturn(null); when(inputValidationManagementService.getInputValidationConfiguration(anyString())) .thenReturn(Arrays.asList(validationConfiguration)); when(validationConfiguration.getField()).thenReturn("username"); + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(Constants.INPUT_VALIDATION_USERNAME_ENABLED_CONFIG)) + .thenReturn("false"); + when(realmConfiguration.getUserStoreProperty(UserCoreConstants.RealmConfig.PROPERTY_USER_NAME_JAVA_REG_EX)) + .thenReturn("^[\\S]{5,30}$"); + + boolean isMatchUsernameRegex = + userSelfRegistrationManager.isMatchUserNameRegex(TEST_TENANT_DOMAIN_NAME, TEST_USER_NAME); + assertTrue(isMatchUsernameRegex); + + /* + Case 2: INPUT_VALIDATION_USERNAME_ENABLED_CONFIG enabled. + */ + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(Constants.INPUT_VALIDATION_USERNAME_ENABLED_CONFIG)) + .thenReturn("true"); + + Map validators = new HashMap<>(); + Validator validator = mock(Validator.class); + validators.put("MockValidator", validator); + when(inputValidationManagementService.getValidators(anyString())).thenReturn(validators); + + RulesConfiguration rulesConfiguration = mock(RulesConfiguration.class); + when(rulesConfiguration.getValidatorName()).thenReturn("MockValidator"); + when(rulesConfiguration.getProperties()).thenReturn(new HashMap<>()); + when(validationConfiguration.getRegEx()).thenReturn(Arrays.asList(rulesConfiguration)); - boolean isMatchUsernameRegex = userSelfRegistrationManager - .isMatchUserNameRegex(TEST_TENANT_DOMAIN_NAME, TEST_USER_NAME); + isMatchUsernameRegex = + userSelfRegistrationManager.isMatchUserNameRegex(TEST_TENANT_DOMAIN_NAME, TEST_USER_NAME); assertTrue(isMatchUsernameRegex); + + /* + Case 3: Throw UserStoreException while getting user store manager. + */ + when(userRealm.getUserStoreManager()).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + userSelfRegistrationManager.isMatchUserNameRegex(TEST_TENANT_DOMAIN_NAME, TEST_USER_NAME); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + } + + /* + Case 4: Throw Error while getting tenant id. + */ + when(tenantManager.getTenantId(anyString())).thenThrow(new UserStoreException()); + try { + userSelfRegistrationManager.isMatchUserNameRegex(TEST_TENANT_DOMAIN_NAME, TEST_USER_NAME); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryException); + } + } + + @Test + public void testPreValidatePasswordWithConfirmationKey() throws Exception { + + String confirmationKey = "testConfirmationKey"; + String password = "testPassword"; + User user = getUser(); + + UserRecoveryData recoveryData = new UserRecoveryData(user, confirmationKey, + RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD); + when(userRecoveryDataStore.load(confirmationKey)).thenReturn(recoveryData); + + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getSecondaryUserStoreManager(TEST_USERSTORE_DOMAIN)).thenReturn(userStoreManager); + + userSelfRegistrationManager.preValidatePasswordWithConfirmationKey(confirmationKey, password); + verify(identityEventService).handleEvent(any(Event.class)); + + // Case 2: Throws UserStoreException. + when(userRealm.getUserStoreManager()).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + userSelfRegistrationManager.preValidatePasswordWithConfirmationKey(confirmationKey, password); + } catch(Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } + + @Test + public void testPreValidatePasswordWithUsername() throws Exception { + + String password = "testPassword"; + String username = TEST_USERSTORE_DOMAIN + CarbonConstants.DOMAIN_SEPARATOR + TEST_USER_NAME; + mockedUserCoreUtil.when(() -> UserCoreUtil.removeDomainFromName(username)).thenReturn(TEST_USER_NAME); + + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(abstractUserStoreManager.getSecondaryUserStoreManager(TEST_USERSTORE_DOMAIN)).thenReturn(userStoreManager); + + userSelfRegistrationManager.preValidatePasswordWithUsername(username, password); + verify(identityEventService).handleEvent(any(Event.class)); + } + + @Test + public void testRegisterLiteUser() throws Exception { + + String emailVerifiedClaimURI = "http://wso2.org/claims/identity/emailVerified"; + User user = new User(); + user.setUserName(TEST_USER_NAME); + + Claim claim1 = new Claim(); + claim1.setClaimUri("http://wso2.org/claims/emailaddress"); + claim1.setValue("test@example.com"); + + Claim[] claims = new Claim[]{claim1}; + + mockedIdentityUtil.when(() -> IdentityUtil.getProperty( + eq(IdentityRecoveryConstants.ConnectorConfig + .ENABLE_ACCOUNT_LOCK_FOR_VERIFIED_PREFERRED_CHANNEL))) + .thenReturn("false"); + when(consentUtilityService.filterPIIsFromReceipt(any(), any())).thenReturn( + new HashSet<>(Arrays.asList(emailVerifiedClaimURI))); + + Property[] properties = new Property[]{ + new Property(IdentityRecoveryConstants.Consent.CONSENT, consentData) + }; + when(notificationChannelManager.resolveCommunicationChannel(anyString(), anyString(), anyString(), anyMap())) + .thenReturn(NotificationChannels.EMAIL_CHANNEL.getChannelType()); + + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(Utils::getNotificationChannelManager).thenReturn(notificationChannelManager); + mockedUtils.when(() -> Utils.getCallbackURLFromRegistration(any())).thenReturn("https://test.com"); + mockedUtils.when(() -> Utils.validateCallbackURL(anyString(), anyString(), anyString())) + .thenReturn(true); + mockedUtils.when(() -> Utils.generateRandomPassword(anyInt())).thenReturn("test-pwd-123".toCharArray()); + mockedUtils.when(() -> Utils.handleClientException(any(), anyString())) + .thenThrow(new IdentityRecoveryClientException("error-code", "message")); + mockedUtils.when(() -> Utils.handleClientException(any(IdentityRecoveryConstants.ErrorMessages.class), + anyString(), any())) + .thenReturn(new IdentityRecoveryClientException("error-code", "message")); + mockedUtils.when(() -> Utils.handleServerException(any(), anyString())) + .thenThrow(new IdentityRecoveryServerException("error-code", "message")); + mockedUtils.when(() -> Utils.handleServerException(any(IdentityRecoveryConstants.ErrorMessages.class), + anyString(), any())) + .thenReturn(new IdentityRecoveryServerException("error-code", "message")); + + mockedUtils.when(() -> Utils.getSignUpConfigs( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_LITE_SIGN_UP), + anyString())).thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq( + IdentityRecoveryConstants.ConnectorConfig.LITE_ACCOUNT_LOCK_ON_CREATION), + anyString())).thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs( + eq(IdentityRecoveryConstants.ConnectorConfig.LITE_SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE), + anyString())).thenReturn("true"); + + /* + Case 1: Notification internally managed. Account Lock on creation enabled. + */ + NotificationResponseBean response = userSelfRegistrationManager.registerLiteUser(user, claims, properties); + + verify(userStoreManager).addUser(anyString(), any(), any(), anyMap(), isNull()); + assertNotNull(response); + assertEquals(IdentityRecoveryConstants.SuccessEvents + .SUCCESS_STATUS_CODE_SUCCESSFUL_USER_CREATION_INTERNAL_VERIFICATION.getCode(), + response.getCode()); + assertEquals(NotificationChannels.EMAIL_CHANNEL.getChannelType(), response.getNotificationChannel()); + + /* + Case 2: Lite sign-up disabled. + Expected: Client error should be thrown. + */ + mockedUtils.when(() -> Utils.getSignUpConfigs( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_LITE_SIGN_UP), + anyString())).thenReturn("false"); + + try { + userSelfRegistrationManager.registerLiteUser(user, claims, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + /* + Case 3: Preferred channel is verified. + */ + mockedUtils.when(() -> Utils.getSignUpConfigs( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_LITE_SIGN_UP), + anyString())).thenReturn("true"); + + Claim claim2 = new Claim(); + claim2.setClaimUri(emailVerifiedClaimURI); + claim2.setValue("true"); + claims = new Claim[]{claim1, claim2}; + + response = userSelfRegistrationManager.registerLiteUser(user, claims, properties); + + assertNotNull(response); + assertEquals(IdentityRecoveryConstants.SuccessEvents + .SUCCESS_STATUS_CODE_SUCCESSFUL_USER_CREATION_WITH_VERIFIED_CHANNEL.getCode(), + response.getCode()); + + /* + Case 4: Notification externally managed. Account Lock on creation enabled. + */ + mockedUtils.when(() -> Utils.getSignUpConfigs(eq( + IdentityRecoveryConstants.ConnectorConfig.LITE_ACCOUNT_LOCK_ON_CREATION), + anyString())).thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs( + eq(IdentityRecoveryConstants.ConnectorConfig.LITE_SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE), + anyString())).thenReturn("false"); + claims = new Claim[]{claim1}; + + response = userSelfRegistrationManager.registerLiteUser(user, claims, properties); + assertNotNull(response); + assertEquals(IdentityRecoveryConstants.SuccessEvents + .SUCCESS_STATUS_CODE_SUCCESSFUL_USER_CREATION_EXTERNAL_VERIFICATION.getCode(), + response.getCode()); + + /* + Case 5: Account Lock on creation is disabled. + */ + mockedUtils.when(() -> Utils.getSignUpConfigs(eq( + IdentityRecoveryConstants.ConnectorConfig.LITE_ACCOUNT_LOCK_ON_CREATION), + anyString())).thenReturn("false"); + mockedUtils.when(() -> Utils.getSignUpConfigs( + eq(IdentityRecoveryConstants.ConnectorConfig.LITE_SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE), + anyString())).thenReturn("true"); + claims = new Claim[]{claim1}; + + response = userSelfRegistrationManager.registerLiteUser(user, claims, properties); + assertNotNull(response); + assertEquals(IdentityRecoveryConstants.SuccessEvents + .SUCCESS_STATUS_CODE_SUCCESSFUL_USER_CREATION_UNLOCKED_WITH_NO_VERIFICATION.getCode(), + response.getCode()); + + /* + Case 6: Invalid callback URL. + */ + mockedUtils.when(() -> Utils.validateCallbackURL(anyString(), anyString(), anyString())) + .thenReturn(false); + try { + userSelfRegistrationManager.registerLiteUser(user, claims, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + + mockedUtils.when(() -> Utils.validateCallbackURL(anyString(), anyString(), anyString())) + .thenReturn(true); + /* + Case 7: Throw UserStoreException while adding user. + */ + doThrow(new org.wso2.carbon.user.core.UserStoreException("test-msg")) + .when(userStoreManager).addUser(anyString(), any(), any(), anyMap(), isNull()); + try { + userSelfRegistrationManager.registerLiteUser(user, claims, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + } } private User getUser() { From 35ad822720d4d1bab07da40f10b8b3b4f3db8b74 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 26 Sep 2024 10:53:14 +0530 Subject: [PATCH 40/54] Add more unit tests for UserSelfRegistrationManager, Utils --- .../UserSelfRegistrationManagerTest.java | 73 ++++++++++++++++++- .../identity/recovery/util/UtilsTest.java | 9 +++ 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index 7b176cb7ca..8fdf1efca2 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -673,6 +673,28 @@ public void testConfirmVerificationCodeMe() assertEquals(mobileNumberClaims, verificationPendingMobileNumber); } + @Test(expectedExceptions = IdentityRecoveryServerException.class) + public void testConfirmVerificationCodeMeUserStoreException() + throws IdentityRecoveryException, UserStoreException { + + // Case 3: Throws user store exception while getting user claim values. + String verificationPendingMobileNumber = "0700000000"; + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); + + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + mockMultiAttributeEnabled(true); + when(userStoreManager.getUserClaimValue(any(), eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM), + any())).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + + userSelfRegistrationManager.confirmVerificationCodeMe(TEST_CODE, new HashMap<>()); + } + @Test public void testGetConfirmedSelfRegisteredUserVerifyEmail() throws IdentityRecoveryException, UserStoreException, IdentityGovernanceException { @@ -719,7 +741,21 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() assertTrue(StringUtils.contains(updatedVerifiedEmailAddresses, verificationPendingEmail)); assertEquals(verificationPendingEmailAddress, StringUtils.EMPTY); - // Case 2 : Throws user store exception while getting user store manager. + // Case 2: Multiple email and mobile per user is disabled. + mockMultiAttributeEnabled(false); + + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, verifiedChannelClaim, + metaProperties); + + ArgumentCaptor> claimsCaptor2 = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager, atLeastOnce()).setUserClaimValues(anyString(), claimsCaptor2.capture(), isNull()); + + Map capturedClaims2 = claimsCaptor2.getValue(); + String emailAddressClaim = + capturedClaims2.get(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM); + assertEquals(emailAddressClaim, verificationPendingEmail); + + // Case 3 : Throws user store exception while getting user store manager. when(userRealm.getUserStoreManager()).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); try { userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, @@ -728,6 +764,17 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() } catch (Exception e) { assertTrue(e instanceof IdentityRecoveryServerException); } + + // Case 4 : Throws user store exception while getting user claim values. + when(userStoreManager.getUserClaimValue(any(), eq(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM), + any())).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, + verifiedChannelClaim, metaProperties); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } } @@ -789,7 +836,7 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() assertEquals(verificationPendingMobileNumberClaim, StringUtils.EMPTY); assertEquals(updatedMobileNumberClaimValue, verificationPendingMobileNumber); - // Case 3: External Verified Channel type. + // Case 2: External Verified Channel type. verifiedChannelType = NotificationChannels.EXTERNAL_CHANNEL.getChannelType(); try (MockedStatic mockedUtils = mockStatic(Utils.class)) { mockedUtils.when(Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(true); @@ -812,6 +859,28 @@ public void testGetConfirmedSelfRegisteredUserVerifyMobile() capturedClaims1.get(IdentityRecoveryConstants.EMAIL_VERIFIED_CLAIM); assertEquals(emailVerifiedClaim, Boolean.TRUE.toString()); + // Case 3: Throws user store exception while getting user claim values. + try (MockedStatic mockedUtils = mockStatic(Utils.class)) { + mockedUtils.when(Utils::isMultiEmailsAndMobileNumbersPerUserEnabled).thenReturn(true); + mockedUtils.when(() -> Utils.getConnectorConfig( + eq(IdentityRecoveryConstants.ConnectorConfig.ENABLE_MOBILE_VERIFICATION_BY_PRIVILEGED_USER), + anyString())) + .thenReturn("true"); + mockedUtils.when(() -> Utils.getSignUpConfigs(eq(IdentityRecoveryConstants.ConnectorConfig + .SELF_REGISTRATION_NOTIFY_ACCOUNT_CONFIRMATION), + anyString())).thenReturn("true"); + + when(userStoreManager.getUserClaimValue(anyString(), eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM), isNull())) + .thenThrow(new org.wso2.carbon.user.core.UserStoreException("test exception")); + mockedUtils.when(() -> Utils.getMultiValuedClaim(eq(userStoreManager), eq(user), + eq(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM))).thenCallRealMethod(); + + userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, + verifiedChannelClaim, metaProperties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + // Case 4: With wrong verified channel claim value. verifiedChannelType = NotificationChannels.SMS_CHANNEL.getChannelType(); verifiedChannelClaim = "http://wso2.org/claims/invalid"; diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java index 9ef21f1558..c675bf11b5 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java @@ -1297,6 +1297,15 @@ public void testGetMultiValuedClaim() throws IdentityEventException, org.wso2.ca List result = Utils.getMultiValuedClaim(userStoreManager, user, "testClaim"); assertEquals(expectedClaimList, result); + + // Case 2: Throw user store exception when retrieving user claim value. + when(userStoreManager.getUserClaimValue(any(), anyString(), any())) + .thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + Utils.getMultiValuedClaim(userStoreManager, user, "testClaim"); + } catch (Exception e) { + assertTrue(e instanceof IdentityEventException); + } } @Test From 09c229d3b761ae97831472b1de0ce7d1307534ef Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 26 Sep 2024 14:33:24 +0530 Subject: [PATCH 41/54] Add more unit tests for UserEmailVerificationHandler --- .../UserEmailVerificationHandlerTest.java | 140 +++++++++++++++++- 1 file changed, 139 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java index b5481df576..e3eb95a923 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -28,15 +28,20 @@ import org.testng.annotations.BeforeMethod; import org.testng.annotations.Test; import org.wso2.carbon.identity.application.authentication.framework.util.FrameworkUtils; +import org.wso2.carbon.identity.core.bean.context.MessageContext; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.event.IdentityEventClientException; import org.wso2.carbon.identity.event.IdentityEventException; import org.wso2.carbon.identity.event.event.Event; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; +import org.wso2.carbon.identity.recovery.RecoveryScenarios; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; @@ -44,6 +49,7 @@ import org.wso2.carbon.user.core.UserCoreConstants; import org.wso2.carbon.user.core.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; +import org. wso2.carbon. identity. application. common. model.User; import org.wso2.carbon.user.core.config.RealmConfiguration; import java.util.Arrays; @@ -56,6 +62,10 @@ import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.atLeastOnce; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -80,6 +90,9 @@ public class UserEmailVerificationHandlerTest { @Mock private IdentityRecoveryServiceDataHolder serviceDataHolder; + @Mock + private MessageContext messageContext; + private MockedStatic mockedJDBCRecoveryDataStore; private MockedStatic mockedUtils; private MockedStatic mockedIdentityRecoveryServiceDataHolder; @@ -157,10 +170,20 @@ public void testHandleEventPreSetUserClaimsVerificationDisabledMultiDisabled() Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), StringUtils.EMPTY); + // Case 2: Throw error when triggering event. + doThrow(new IdentityEventException("error")).when(identityEventService).handleEvent(any()); + try { + userEmailVerificationHandler.handleEvent(event); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventException); + } + + // Reset. + doNothing().when(identityEventService).handleEvent(any()); + // Case 2: Throw UserStoreException when getting the primary email. Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, null, null, NEW_EMAIL); - mockUtilMethods(false, false, false, true); mockPendingVerificationEmail(EXISTING_EMAIL_2); @@ -234,6 +257,43 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiDisabled() userEmailVerificationHandler.handleEvent(event2); Map userClaimsC2 = getUserClaimsFromEvent(event2); Assert.assertFalse(userClaimsC2.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM)); + + // Case 3: Try to change the primary email value with existing primary email value. + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, NEW_EMAIL); + mockPendingVerificationEmail(EXISTING_EMAIL_1); + mockPrimaryEmail(NEW_EMAIL); + userEmailVerificationHandler.handleEvent(event3); + Map userClaimsC3 = getUserClaimsFromEvent(event3); + Assert.assertEquals(userClaimsC3.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); + } + + @Test(description = "Verification - Enabled, Multi attribute - Disabled, User verify - Enabled") + public void testHandleEventPreSetUserClaimsVerificationEnabledMultiDisabledUserVerifyEnabled() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + Event event = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.TRUE, + null, null, NEW_EMAIL); + mockUtilMethods(true, false, true, + false); + mockPendingVerificationEmail(EXISTING_EMAIL_1); + + userEmailVerificationHandler.handleEvent(event); + Map userClaimsC1 = getUserClaimsFromEvent(event); + Assert.assertEquals(userClaimsC1.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), NEW_EMAIL); + + // Case 2: verifyEmail claim is false. + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, NEW_EMAIL); + mockUtilMethods(true, false, true, + false); + mockPendingVerificationEmail(EXISTING_EMAIL_1); + + userEmailVerificationHandler.handleEvent(event2); + Map userClaimsC2 = getUserClaimsFromEvent(event2); + Assert.assertEquals(userClaimsC2.get(IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM), + StringUtils.EMPTY); } @Test(description = "Verification - Enabled, Multi attribute - Enabled, Change primary email which is not in the " + @@ -332,6 +392,26 @@ public void testHandleEventPreSetUserClaimsVerificationEnabledMultiEnabledC3() Map userClaims2 = getUserClaimsFromEvent(event2); String updatedVerifiedEmails = userClaims2.get(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM); Assert.assertTrue(StringUtils.contains(updatedVerifiedEmails, NEW_EMAIL)); + + /* + Case 3: Add multiple new emails to verified emails list. + Expected: Error should be thrown. + */ + String newVerifiedEmails3 = String.format("%s,%s", EXISTING_EMAIL_1, NEW_EMAIL); + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + newVerifiedEmails3, null, null); + + mockUtilMethods(true, true, false, + false); + mockExistingEmailAddressesList(new ArrayList<>()); + mockExistingVerifiedEmailAddressesList(new ArrayList<>()); + + try { + userEmailVerificationHandler.handleEvent(event3); + } catch(IdentityEventClientException e) { + Assert.assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages. + ERROR_CODE_VERIFY_MULTIPLE_EMAILS.getCode()); + } } @Test(description = "Verification - Enabled, Multi attribute - Enabled, Remove email from email list") @@ -413,6 +493,20 @@ public void testHandleEventPostSetUserClaims() userEmailVerificationHandler.handleEvent(event); verify(identityEventService).handleEvent(any()); + + // Case 2: Error is thrown when retrieving verification pending email. + Event event1 = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, null); + mockUtilMethods(true, true, false, + true); + when(userStoreManager.getUserClaimValues(TEST_USERNAME, new String[]{ + IdentityRecoveryConstants.EMAIL_ADDRESS_PENDING_VALUE_CLAIM}, null)) + .thenThrow(new UserStoreException()); + try { + userEmailVerificationHandler.handleEvent(event1); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventException); + } } @Test @@ -524,6 +618,33 @@ public void testHandleEventPostAddUserAskPasswordClaimNotificationInternallyMana mockedUtils.verify(() -> Utils.publishRecoveryEvent(any(), eq(IdentityEventConstants.Event.POST_ADD_USER_WITH_ASK_PASSWORD), any())); + + // Case 2: Utils.generateSecretKey() throws an exception. + mockedUtils.when(() -> Utils.generateSecretKey( + NotificationChannels.EMAIL_CHANNEL.getChannelType(), RecoveryScenarios.ASK_PASSWORD.name(), + TEST_TENANT_DOMAIN, "EmailVerification")) + .thenThrow(new IdentityRecoveryServerException("test_error")); + try { + userEmailVerificationHandler.handleEvent(event); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventException); + } + + // Reset. + mockedUtils.when(() -> Utils.generateSecretKey( + NotificationChannels.EMAIL_CHANNEL.getChannelType(), RecoveryScenarios.ASK_PASSWORD.name(), + TEST_TENANT_DOMAIN, "EmailVerification")) + .thenReturn("test_key"); + + // Case 3: Claims are null. + Map eventProperties = new HashMap<>(); + eventProperties.put(IdentityEventConstants.EventProperty.USER_NAME, TEST_USERNAME); + eventProperties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, TEST_TENANT_DOMAIN); + eventProperties.put(IdentityEventConstants.EventProperty.USER_STORE_MANAGER, userStoreManager); + + Event event3 = new Event(IdentityEventConstants.Event.POST_ADD_USER, eventProperties); + userEmailVerificationHandler.handleEvent(event3); + verify(userRecoveryDataStore, atLeastOnce()).store(any()); } @Test @@ -552,6 +673,23 @@ public void testHandleEventPostAddUserAskPasswordClaimNotificationExternallyMana any())); } + @Test + public void testGetPriority() { + + Assert.assertEquals(userEmailVerificationHandler.getPriority(messageContext), 65); + } + + @Test + public void testGetRecoveryData() throws IdentityEventException, IdentityRecoveryException { + + User user = new User(); + UserRecoveryData userRecoveryData = mock(UserRecoveryData.class); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user)).thenReturn(userRecoveryData); + + UserRecoveryData response = userEmailVerificationHandler.getRecoveryData(user); + Assert.assertEquals(response, userRecoveryData); + } + private void mockExistingEmailAddressesList(List existingEmails) { mockedUtils.when(() -> Utils.getMultiValuedClaim(any(), any(), From 10bf7cc4aacc3bf9871777273a7ce677bf206f0f Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 26 Sep 2024 15:00:49 +0530 Subject: [PATCH 42/54] Add more unit tests for MobileNumberVerificationHandler --- .../MobileNumberVerificationHandlerTest.java | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index d38b31a9e7..b6d054ecdc 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -62,6 +62,7 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; @@ -293,7 +294,30 @@ public void testHandleEventVerificationEnabledMultiAttributeDisabled() throws Ex mobileNumberVerificationHandler.handleEvent(event3); mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates - .SKIP_ON_INAPPLICABLE_CLAIMS.toString())); + .SKIP_ON_INAPPLICABLE_CLAIMS.toString()), atLeastOnce()); + + /* + Case 4: Mobile number claim is null. + */ + Event event4 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, null); + + mobileNumberVerificationHandler.handleEvent(event4); + mockedUtils.verify(() -> Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( + IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates + .SKIP_ON_INAPPLICABLE_CLAIMS.toString()), atLeastOnce()); + + /* + Case 5: Throw error when retrieving existing mobile number. + */ + when(userStoreManager.getUserClaimValue(anyString(), + eq(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM), isNull())) + .thenThrow(new org.wso2.carbon.user.core.UserStoreException()); + try { + mobileNumberVerificationHandler.handleEvent(event); + } catch (Exception e) { + Assert.assertTrue(e instanceof IdentityEventException); + } } @Test(description = "Verification enabled, Multi-attribute enabled, Update primary mobile not in verified list") From 0eb3a2d2a30e03340123a8510a410baa91388a86 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Fri, 27 Sep 2024 23:13:42 +0530 Subject: [PATCH 43/54] Refactor code --- .../handler/MobileNumberVerificationHandler.java | 12 ++++++------ .../handler/UserEmailVerificationHandler.java | 13 ++++++------- 2 files changed, 12 insertions(+), 13 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 20c1847cdc..73e8840021 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -357,6 +357,12 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use claims.remove(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); } + if (StringUtils.isBlank(mobileNumber)) { + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } + String existingMobileNumber; String username = user.getUserName(); try { @@ -381,12 +387,6 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use } } - if (mobileNumber == null) { - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; - } - if (supportMultipleMobileNumbers && updatedVerifiedNumbersList.contains(mobileNumber)) { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index e7856caa92..b896011006 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -609,8 +609,13 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore updatedAllEmailAddresses = new ArrayList<>(); } - String existingEmail = getEmailClaimValue(user, userStoreManager); + if (StringUtils.isBlank(emailAddress)) { + Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants + .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); + return; + } + String existingEmail = getEmailClaimValue(user, userStoreManager); if (StringUtils.isNotBlank(existingEmail) && supportMultipleEmails) { if (!updatedVerifiedEmailAddresses.contains(existingEmail)) { updatedVerifiedEmailAddresses.add(existingEmail); @@ -624,12 +629,6 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } } - if (emailAddress == null) { - Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants - .SkipEmailVerificationOnUpdateStates.SKIP_ON_INAPPLICABLE_CLAIMS.toString()); - return; - } - if (supportMultipleEmails && updatedVerifiedEmailAddresses.contains(emailAddress)) { if (log.isDebugEnabled()) { log.debug(String.format("The email address to be updated: %s is same as the existing email " + From 0ade20ad87a15ecd6f6e7b76bf35c7380df0ece2 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Mon, 30 Sep 2024 10:18:12 +0530 Subject: [PATCH 44/54] Introduce ON_VERIFIED_LIST_UPDATE recovery scenarios to handle verified list update scenes explicitly. --- .../user/endpoint/impl/MeApiServiceImpl.java | 19 +++++++- .../impl/ResendCodeApiServiceImpl.java | 16 +++++++ .../identity/recovery/RecoveryScenarios.java | 5 +- .../ResendConfirmationManager.java | 46 +++++++++++-------- .../MobileNumberVerificationHandler.java | 24 ++++++++-- .../handler/UserEmailVerificationHandler.java | 22 ++++++--- .../signup/UserSelfRegistrationManager.java | 15 ++++-- .../recovery/store/JDBCRecoveryDataStore.java | 6 ++- .../carbon/identity/recovery/util/Utils.java | 45 +++++++++++++++++- .../MobileNumberVerificationHandlerTest.java | 8 ++++ 10 files changed, 168 insertions(+), 38 deletions(-) diff --git a/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImpl.java b/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImpl.java index cfc126baf2..6516a669eb 100644 --- a/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImpl.java +++ b/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImpl.java @@ -280,7 +280,9 @@ private String getRecoveryScenarioFromProperties(List propertyDTOS) RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK.toString().equals(recoveryScenario) || RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_OTP.toString().equals(recoveryScenario) || RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) || - RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) { + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario) || + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) || + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario)) { return recoveryScenario; } @@ -293,7 +295,8 @@ private NotificationResponseBean doResendConfirmationCode(String recoveryScenari UserRecoveryData userRecoveryData = null; // Currently this me/resend-code API supports resend code during mobile verification scenario only. - if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) { + if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) || + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario)) { userRecoveryData = Utils.getUserRecoveryData(resendCodeRequestDTO, recoveryScenario); } if (userRecoveryData == null) { @@ -312,6 +315,18 @@ private NotificationResponseBean doResendConfirmationCode(String recoveryScenari resendCodeRequestDTO); } + if (RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario) && + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(userRecoveryData.getRecoveryScenario()) && + RecoverySteps.VERIFY_MOBILE_NUMBER.equals(userRecoveryData.getRecoveryStep())) { + + notificationResponseBean = setNotificationResponseBean(resendConfirmationManager, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, + resendCodeRequestDTO); + } + return notificationResponseBean; } diff --git a/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImpl.java b/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImpl.java index f4c1efb666..f74188b799 100644 --- a/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImpl.java +++ b/components/org.wso2.carbon.identity.api.user.governance/src/main/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImpl.java @@ -187,6 +187,14 @@ private NotificationResponseBean doResendConfirmationCode(String recoveryScenari notificationResponseBean = setNotificationResponseBean(resendConfirmationManager, RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString(), RecoverySteps.VERIFY_EMAIL.toString(), IdentityRecoveryConstants.NOTIFICATION_TYPE_RESEND_VERIFY_EMAIL_ON_UPDATE, resendCodeRequestDTO); + } else if (RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario) && + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(userRecoveryData.getRecoveryScenario()) && + RecoverySteps.VERIFY_EMAIL.equals(userRecoveryData.getRecoveryStep())) { + notificationResponseBean = setNotificationResponseBean(resendConfirmationManager, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_RESEND_VERIFY_EMAIL_ON_UPDATE, resendCodeRequestDTO); } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) && RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.equals(userRecoveryData.getRecoveryScenario()) && RecoverySteps.VERIFY_MOBILE_NUMBER.equals(userRecoveryData.getRecoveryStep())) { @@ -194,6 +202,14 @@ private NotificationResponseBean doResendConfirmationCode(String recoveryScenari RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, resendCodeRequestDTO); + } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario) && + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(userRecoveryData.getRecoveryScenario()) && + RecoverySteps.VERIFY_MOBILE_NUMBER.equals(userRecoveryData.getRecoveryStep())) { + notificationResponseBean = setNotificationResponseBean(resendConfirmationManager, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, resendCodeRequestDTO); } return notificationResponseBean; diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/RecoveryScenarios.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/RecoveryScenarios.java index dc07ac6471..3c0a765917 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/RecoveryScenarios.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/RecoveryScenarios.java @@ -31,7 +31,9 @@ public enum RecoveryScenarios { ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, EMAIL_VERIFICATION_ON_UPDATE, + EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, MOBILE_VERIFICATION_ON_UPDATE, + MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, LITE_SIGN_UP, TENANT_ADMIN_ASK_PASSWORD, PASSWORD_EXPIRY; @@ -49,7 +51,8 @@ public static RecoveryScenarios getRecoveryScenario(String scenarioName) throws NOTIFICATION_BASED_PW_RECOVERY, QUESTION_BASED_PWD_RECOVERY, USERNAME_RECOVERY, SELF_SIGN_UP, ASK_PASSWORD, ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, LITE_SIGN_UP, TENANT_ADMIN_ASK_PASSWORD, EMAIL_VERIFICATION_ON_UPDATE, MOBILE_VERIFICATION_ON_UPDATE, - PASSWORD_EXPIRY + PASSWORD_EXPIRY, EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE }; if (StringUtils.isNotEmpty(scenarioName)) { for (RecoveryScenarios scenario : scenarios) { diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java index 71e52de240..fbd799c538 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java @@ -267,20 +267,11 @@ private void triggerNotification(User user, String notificationChannel, String t notificationChannel = NotificationChannels.EMAIL_CHANNEL.getChannelType(); } properties.put(IdentityEventConstants.EventProperty.NOTIFICATION_CHANNEL, notificationChannel); - if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { - String sendTo = Utils.getUserClaim(user, IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); - if (StringUtils.isEmpty(sendTo)) { - throw Utils.handleClientException( - IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MOBILE_NOT_FOUND, user.getUserName()); - } - properties.put(IdentityRecoveryConstants.SEND_TO, sendTo); - } if (StringUtils.isNotBlank(code)) { if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel)) { properties.put(IdentityRecoveryConstants.OTP_TOKEN_STRING, code); - } else { - properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); } + properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); } if (metaProperties != null) { for (Property metaProperty : metaProperties) { @@ -289,6 +280,15 @@ private void triggerNotification(User user, String notificationChannel, String t } } } + if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannel) && + properties.get(IdentityRecoveryConstants.SEND_TO) == null) { + String sendTo = Utils.getUserClaim(user, IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); + if (StringUtils.isEmpty(sendTo)) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_MOBILE_NOT_FOUND, user.getUserName()); + } + properties.put(IdentityRecoveryConstants.SEND_TO, sendTo); + } if (properties.containsKey(IdentityRecoveryConstants.RESEND_EMAIL_TEMPLATE_NAME)) { properties.put(IdentityRecoveryConstants.TEMPLATE_TYPE, properties.get(IdentityRecoveryConstants.RESEND_EMAIL_TEMPLATE_NAME)); @@ -391,7 +391,8 @@ private void addRecoveryDataObject(String secretKey, String recoveryFlowId, Stri RecoveryScenarios recoveryScenario, RecoverySteps recoveryStep, User user) throws IdentityRecoveryServerException { - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, recoveryFlowId, secretKey, recoveryScenario, recoveryStep); + UserRecoveryData recoveryDataDO = new UserRecoveryData(user, recoveryFlowId, secretKey, recoveryScenario, + recoveryStep); // Store available channels in remaining setIDs. recoveryDataDO.setRemainingSetIds(recoveryData); @@ -474,6 +475,12 @@ private NotificationResponseBean resendAccountRecoveryNotification(User user, St resolveUserAttributes(user); boolean notificationInternallyManage = isNotificationInternallyManage(user, recoveryScenario); + boolean mobileVerificationOnUpdateScenario = RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString() + .equals(recoveryScenario) + || RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario); + boolean emailVerificationOnUpdateScenario = RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString() + .equals(recoveryScenario) + || RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario); NotificationResponseBean notificationResponseBean = new NotificationResponseBean(user); UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); @@ -499,10 +506,10 @@ private NotificationResponseBean resendAccountRecoveryNotification(User user, St preferredChannel = NotificationChannels.EXTERNAL_CHANNEL.getChannelType(); } } - if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) { + if (emailVerificationOnUpdateScenario) { preferredChannel = NotificationChannels.EMAIL_CHANNEL.getChannelType(); } - if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) { + if (mobileVerificationOnUpdateScenario) { preferredChannel = NotificationChannels.SMS_CHANNEL.getChannelType(); } @@ -524,13 +531,12 @@ private NotificationResponseBean resendAccountRecoveryNotification(User user, St notificationResponseBean.setNotificationChannel(preferredChannel); } - if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) && - RecoverySteps.VERIFY_EMAIL.toString().equals(recoveryStep)) { + if (emailVerificationOnUpdateScenario && RecoverySteps.VERIFY_EMAIL.toString().equals(recoveryStep)) { String verificationPendingEmailClaimValue = userRecoveryData.getRemainingSetIds(); properties = new Property[]{new Property(IdentityRecoveryConstants.SEND_TO, verificationPendingEmailClaimValue)}; recoveryDataDO.setRemainingSetIds(verificationPendingEmailClaimValue); - } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) && + } else if (mobileVerificationOnUpdateScenario && RecoverySteps.VERIFY_MOBILE_NUMBER.toString().equals(recoveryStep)) { String verificationPendingMobileNumber = userRecoveryData.getRemainingSetIds(); properties = new Property[]{new Property(IdentityRecoveryConstants.SEND_TO, @@ -653,10 +659,12 @@ private boolean isNotificationInternallyManage(User user, String recoveryScenari return Boolean.parseBoolean(Utils.getSignUpConfigs (IdentityRecoveryConstants.ConnectorConfig.LITE_SIGN_UP_NOTIFICATION_INTERNALLY_MANAGE, user.getTenantDomain())); - } else if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) { + } else if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) || + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario)) { // We manage the notifications internally for EMAIL_VERIFICATION_ON_UPDATE. return true; - } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario)) { + } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString().equals(recoveryScenario) + || RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString().equals(recoveryScenario)) { // We manage the notifications internally for MOBILE_VERIFICATION_ON_UPDATE. return true; } else { @@ -732,7 +740,9 @@ private String getSecretKey(String preferredChannel, String recoveryScenario, St return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain, "Recovery.Notification.Password"); case EMAIL_VERIFICATION_ON_UPDATE: + case EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE: case MOBILE_VERIFICATION_ON_UPDATE: + case MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE: return Utils.generateSecretKey(preferredChannel, recoveryScenario, tenantDomain, "UserClaimUpdate"); case ASK_PASSWORD: diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 73e8840021..0a65a0d4c2 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -149,6 +149,9 @@ public void handleEvent(Event event) throws IdentityEventException { } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { + if (supportMultipleMobileNumbers && !claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)) { + Utils.setThreadLocalIsOnlyVerifiedMobileNumbersUpdated(true); + } preSetUserClaimOnMobileNumberUpdate(claims, userStoreManager, user); claims.remove(IdentityRecoveryConstants.VERIFY_MOBILE_CLAIM); } @@ -188,13 +191,25 @@ private void initNotificationForMobileNumberVerificationOnUpdate(User user, Stri UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); try { - userRecoveryDataStore.invalidate(user, RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, - RecoverySteps.VERIFY_MOBILE_NUMBER); String secretKey = Utils.generateSecretKey(NotificationChannels.SMS_CHANNEL.getChannelType(), String.valueOf(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), user.getTenantDomain(), "UserClaimUpdate"); - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, - RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + + UserRecoveryData recoveryDataDO; + if (Utils.getThreadLocalIsOnlyVerifiedMobileNumbersUpdated()) { + userRecoveryDataStore.invalidate(user, RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER); + recoveryDataDO = new UserRecoveryData(user, secretKey, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER); + } else { + userRecoveryDataStore.invalidate(user, RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER); + recoveryDataDO = new UserRecoveryData(user, secretKey, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER); + } + /* Mobile number is persisted in remaining set ids to maintain context information about the mobile number associated with the verification code generated. */ recoveryDataDO.setRemainingSetIds(verificationPendingMobileNumber); @@ -498,6 +513,7 @@ private void postSetUserClaimOnMobileNumberUpdate(User user, UserStoreManager us } } finally { Utils.unsetThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(); + Utils.unsetThreadLocalIsOnlyVerifiedMobileNumbersUpdated(); } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index b896011006..581e5f280a 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -63,8 +63,6 @@ import java.util.UUID; import java.util.stream.Collectors; -import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages - .ERROR_CODE_PRIMARY_EMAIL_SHOULD_BE_INCLUDED_IN_VERIFIED_EMAILS_LIST; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ErrorMessages .ERROR_CODE_VERIFICATION_EMAIL_NOT_FOUND; @@ -288,6 +286,9 @@ public void handleEvent(Event event) throws IdentityEventException { } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { + if (supportMultipleEmails && !claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM)) { + Utils.setThreadLocalIsOnlyVerifiedEmailAddressesUpdated(true); + } preSetUserClaimsOnEmailUpdate(claims, userStoreManager, user); claims.remove(IdentityRecoveryConstants.VERIFY_EMAIL_CLIAM); } @@ -389,10 +390,18 @@ private void initNotificationForEmailVerificationOnUpdate(User user, String secr UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); try { - userRecoveryDataStore.invalidate(user, RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, - RecoverySteps.VERIFY_EMAIL); - UserRecoveryData recoveryDataDO = new UserRecoveryData(user, secretKey, - RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL); + UserRecoveryData recoveryDataDO; + if (Utils.getThreadLocalIsOnlyVerifiedEmailAddressesUpdated()) { + userRecoveryDataStore.invalidate(user, RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_EMAIL); + recoveryDataDO = new UserRecoveryData(user, secretKey, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL); + } else { + userRecoveryDataStore.invalidate(user, RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, + RecoverySteps.VERIFY_EMAIL); + recoveryDataDO = new UserRecoveryData(user, secretKey, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL); + } /* Email address persisted in remaining set ids to maintain context information about the email address associated with the verification code generated. */ recoveryDataDO.setRemainingSetIds(verificationPendingEmailAddress); @@ -736,6 +745,7 @@ private void postSetUserClaimsOnEmailUpdate(User user, UserStoreManager userStor } } finally { Utils.unsetThreadLocalToSkipSendingEmailVerificationOnUpdate(); + Utils.unsetThreadLocalIsOnlyVerifiedEmailAddressesUpdated(); } } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 8f0756e5bd..9573989360 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -788,7 +788,9 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + "value for the user : " + user.getUserName(), e); } - } else { + } + if (!RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(recoveryData.getRecoveryScenario())) { userClaims.put(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM, pendingEmailClaimValue); } // Todo passes when email address is properly set here. @@ -826,7 +828,10 @@ private UserRecoveryData validateSelfRegistrationCode(String code, String verifi "value for the user : " + user.getUserName(), e); } } - userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileClaimValue); + if (!RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(recoveryData.getRecoveryScenario())) { + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileClaimValue); + } userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); // Todo passes when mobile number is properly set here. Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants @@ -1013,10 +1018,12 @@ public void confirmVerificationCodeMe(String code, Map propertie throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + "value for the user : " + user.getUserName(), e); } - } else { + } + if (!RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(recoveryData.getRecoveryScenario())) { userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileNumberClaimValue); - userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); } + userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java index 568ee5b299..82070936d2 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStore.java @@ -983,7 +983,8 @@ private boolean isCodeExpired(String tenantDomain, Enum recoveryScenario, Enum r notificationExpiryTimeInMinutes = Integer.parseInt( Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME, tenantDomain)); } - } else if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.equals(recoveryScenario)) { + } else if (RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.equals(recoveryScenario) || + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.equals(recoveryScenario)) { notificationExpiryTimeInMinutes = Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants .ConnectorConfig.EMAIL_VERIFICATION_ON_UPDATE_EXPIRY_TIME, tenantDomain)); } else if (RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD.equals(recoveryScenario)) { @@ -1024,7 +1025,8 @@ private boolean isCodeExpired(String tenantDomain, Enum recoveryScenario, Enum r IdentityRecoveryConstants.ConnectorConfig.LITE_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, tenantDomain)); } - } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.equals(recoveryScenario)) { + } else if (RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.equals(recoveryScenario) || + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.equals(recoveryScenario)) { notificationExpiryTimeInMinutes = Integer.parseInt(Utils.getRecoveryConfigs(IdentityRecoveryConstants .ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, tenantDomain)); } else if (RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK.equals(recoveryScenario) || diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java index 6d97548a42..07cd7ae89c 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/util/Utils.java @@ -87,7 +87,6 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; -import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.UUID; @@ -131,6 +130,18 @@ public class Utils { */ private static ThreadLocal skipSendingSmsOtpVerificationOnUpdate = new ThreadLocal<>(); + /** + * This thread local variable is used to stop verification pending mobile number from being set as the primary + * mobile number when only updating the verifiedMobileNumbers claim. + */ + private static ThreadLocal isOnlyVerifiedMobileNumbersUpdated = new ThreadLocal<>(); + + /** + * This thread local variable is used to stop verification pending email address from being set as the primary + * email address when only updating the verifiedEmailAddress claim. + */ + private static ThreadLocal isOnlyVerifiedEmailAddressesUpdated = new ThreadLocal<>(); + //Error messages that are caused by password pattern violations private static final String[] pwdPatternViolations = new String[]{UserCoreErrorConstants.ErrorMessages .ERROR_CODE_ERROR_DURING_PRE_UPDATE_CREDENTIAL_BY_ADMIN.getCode(), UserCoreErrorConstants.ErrorMessages @@ -255,6 +266,38 @@ public static void setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(String skipSendingSmsOtpVerificationOnUpdate.set(value); } + public static void unsetThreadLocalIsOnlyVerifiedMobileNumbersUpdated() { + + isOnlyVerifiedMobileNumbersUpdated.remove(); + } + + public static void setThreadLocalIsOnlyVerifiedMobileNumbersUpdated(Boolean value) { + + isOnlyVerifiedMobileNumbersUpdated.set(value); + } + + public static boolean getThreadLocalIsOnlyVerifiedMobileNumbersUpdated() { + + Boolean value = isOnlyVerifiedMobileNumbersUpdated.get(); + return value != null && value; + } + + public static void unsetThreadLocalIsOnlyVerifiedEmailAddressesUpdated() { + + isOnlyVerifiedEmailAddressesUpdated.remove(); + } + + public static void setThreadLocalIsOnlyVerifiedEmailAddressesUpdated(Boolean value) { + + isOnlyVerifiedEmailAddressesUpdated.set(value); + } + + public static boolean getThreadLocalIsOnlyVerifiedEmailAddressesUpdated() { + + Boolean value = isOnlyVerifiedEmailAddressesUpdated.get(); + return value != null && value; + } + public static String getClaimFromUserStoreManager(User user, String claim) throws UserStoreException { diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index b6d054ecdc..4052efe6fd 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -414,6 +414,12 @@ public void testHandleEventPostSet() throws IdentityEventException, IdentityReco null, null, null); mockUtilMethods(true, true, false); + MockedStatic mockedStaticRecoveryScenarios = mockStatic(RecoveryScenarios.class); + mockedStaticRecoveryScenarios.when(() -> + RecoveryScenarios.getRecoveryScenario( + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString())) + .thenReturn(RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE); + /* Case 1: skipSendingSmsOtpVerificationOnUpdate set to skip. Expected: Invalidation should not be triggered. @@ -443,6 +449,8 @@ public void testHandleEventPostSet() throws IdentityEventException, IdentityReco mobileNumberVerificationHandler.handleEvent(event); } catch (Exception e) { Assert.assertTrue(e instanceof IdentityEventException); + } finally { + mockedStaticRecoveryScenarios.close(); } } From 88e1bd4b85e6f21714dc34d61cecc0bac187b2d6 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Mon, 30 Sep 2024 20:59:34 +0530 Subject: [PATCH 45/54] Update unit tests to cover new recovery scenarios. --- .../MobileNumberVerificationHandlerTest.java | 52 ++++++++++++++++++- .../UserEmailVerificationHandlerTest.java | 40 ++++++++++++++ 2 files changed, 90 insertions(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index 4052efe6fd..61139d9673 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -19,6 +19,7 @@ package org.wso2.carbon.identity.recovery.handler; import org.apache.commons.lang.StringUtils; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -38,6 +39,7 @@ import org.wso2.carbon.identity.recovery.RecoveryScenarios; import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; import org.wso2.carbon.identity.recovery.util.Utils; @@ -65,6 +67,7 @@ import static org.mockito.Mockito.atLeastOnce; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -150,6 +153,10 @@ public void tearDown() { mockedUtils.close(); mockedIdentityRecoveryServiceDataHolder.close(); mockedFrameworkUtils.close(); + Utils.unsetThreadLocalIsOnlyVerifiedEmailAddressesUpdated(); + Utils.unsetThreadLocalIsOnlyVerifiedMobileNumbersUpdated(); + Utils.unsetThreadLocalToSkipSendingEmailVerificationOnUpdate(); + Utils.unsetThreadLocalIsOnlyVerifiedMobileNumbersUpdated(); } @Test @@ -427,9 +434,9 @@ public void testHandleEventPostSet() throws IdentityEventException, IdentityReco mockedUtils.when(Utils::getThreadLocalToSkipSendingSmsOtpVerificationOnUpdate) .thenReturn(IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates .SKIP_ON_EXISTING_MOBILE_NUM.toString()); + mobileNumberVerificationHandler.handleEvent(event); - verify(userRecoveryDataStore, never()).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), - eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + verify(identityEventService, never()).handleEvent(any()); /* Case 2: skipSendingSmsOtpVerificationOnUpdate set to null. @@ -454,6 +461,47 @@ public void testHandleEventPostSet() throws IdentityEventException, IdentityReco } } + @Test + public void testHandleEventPostSetRecoveryScenarios() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + Event event = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, null, + null, null, null); + mockUtilMethods(true, true, false); + + MockedStatic mockedStaticRecoveryScenarios = mockStatic(RecoveryScenarios.class); + mockedStaticRecoveryScenarios.when(() -> + RecoveryScenarios.getRecoveryScenario( + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString())) + .thenReturn(RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE); + mockVerificationPendingMobileNumber(); + + // Case 1: Update the primary mobile number. + mockedUtils.when(Utils::getThreadLocalIsOnlyVerifiedMobileNumbersUpdated).thenReturn(false); + mobileNumberVerificationHandler.handleEvent(event); + + ArgumentCaptor recoveryDataCaptor = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor.capture()); + UserRecoveryData capturedRecoveryData = recoveryDataCaptor.getValue(); + Assert.assertEquals(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, + capturedRecoveryData.getRecoveryScenario()); + Assert.assertEquals(RecoverySteps.VERIFY_MOBILE_NUMBER, capturedRecoveryData.getRecoveryStep()); + + reset(userRecoveryDataStore); + // Case 2: Update the verified list. + Event event2 = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, null, + null, null, null); + mockedUtils.when(Utils::getThreadLocalIsOnlyVerifiedMobileNumbersUpdated).thenReturn(true); + mobileNumberVerificationHandler.handleEvent(event2); + + ArgumentCaptor recoveryDataCaptor2 = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor2.capture()); + UserRecoveryData capturedRecoveryData2 = recoveryDataCaptor2.getValue(); + Assert.assertEquals(RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + capturedRecoveryData2.getRecoveryScenario()); + Assert.assertEquals(RecoverySteps.VERIFY_MOBILE_NUMBER, capturedRecoveryData2.getRecoveryStep()); + } + private void mockExistingPrimaryMobileNumber(String mobileNumber) throws UserStoreException { when(userStoreManager.getUserClaimValue(anyString(), diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java index e3eb95a923..aaf196c914 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandlerTest.java @@ -19,6 +19,7 @@ package org.wso2.carbon.identity.recovery.handler; import org.apache.commons.lang.StringUtils; +import org.mockito.ArgumentCaptor; import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -40,6 +41,7 @@ import org.wso2.carbon.identity.recovery.IdentityRecoveryException; import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; @@ -67,6 +69,7 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -509,6 +512,43 @@ public void testHandleEventPostSetUserClaims() } } + @Test + public void testHandleEventPostSetUserClaimsRecoveryScenarios() + throws IdentityEventException, IdentityRecoveryException, UserStoreException { + + Event event = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, null); + mockUtilMethods(true, true, false, + true); + mockPendingVerificationEmail(EXISTING_EMAIL_1); + + // Case 1: Change primary email address. + mockedUtils.when(Utils::getThreadLocalIsOnlyVerifiedEmailAddressesUpdated).thenReturn(false); + userEmailVerificationHandler.handleEvent(event); + + ArgumentCaptor recoveryDataCaptor = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor.capture()); + UserRecoveryData capturedRecoveryData = recoveryDataCaptor.getValue(); + Assert.assertEquals(RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, + capturedRecoveryData.getRecoveryScenario()); + Assert.assertEquals(RecoverySteps.VERIFY_EMAIL, capturedRecoveryData.getRecoveryStep()); + + reset(userRecoveryDataStore); + + // Case 2: Change verified list. + Event event2 = createEvent(IdentityEventConstants.Event.POST_SET_USER_CLAIMS, IdentityRecoveryConstants.FALSE, + null, null, null); + mockedUtils.when(Utils::getThreadLocalIsOnlyVerifiedEmailAddressesUpdated).thenReturn(true); + userEmailVerificationHandler.handleEvent(event2); + + ArgumentCaptor recoveryDataCaptor2 = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor2.capture()); + UserRecoveryData capturedRecoveryData2 = recoveryDataCaptor2.getValue(); + Assert.assertEquals(RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + capturedRecoveryData2.getRecoveryScenario()); + Assert.assertEquals(RecoverySteps.VERIFY_EMAIL, capturedRecoveryData2.getRecoveryStep()); + } + @Test public void testHandleEventPreAddUserVerifyEmailClaim() throws IdentityEventException { From 5b45640848acea48b7a603305474cd71703ed0a2 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Mon, 30 Sep 2024 21:00:01 +0530 Subject: [PATCH 46/54] Add unit tests for ResendConfirmationManager. --- .../ResendConfirmationManagerTest.java | 512 ++++++++++++++++++ .../src/test/resources/testng.xml | 7 +- 2 files changed, 514 insertions(+), 5 deletions(-) create mode 100644 components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java new file mode 100644 index 0000000000..ca80e11282 --- /dev/null +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java @@ -0,0 +1,512 @@ +/* + * Copyright (c) 2018, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * + * WSO2 Inc. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.recovery.confirmation; + +import org.mockito.ArgumentCaptor; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.Test; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.common.model.User; +import org.wso2.carbon.identity.common.testng.WithCarbonHome; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; +import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; +import org.wso2.carbon.identity.recovery.bean.NotificationResponseBean; +import org.wso2.carbon.identity.recovery.dto.ResendConfirmationDTO; +import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.internal.service.impl.UserAccountRecoveryManager; +import org.wso2.carbon.identity.recovery.model.Property; +import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.model.UserRecoveryFlowData; +import org.wso2.carbon.identity.recovery.store.JDBCRecoveryDataStore; +import org.wso2.carbon.identity.recovery.store.UserRecoveryDataStore; +import org.wso2.carbon.identity.recovery.util.Utils; +import org.wso2.carbon.user.core.UserCoreConstants; +import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.identity.event.event.Event; + +import java.util.Map; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; + +@WithCarbonHome +public class ResendConfirmationManagerTest { + + @InjectMocks + private ResendConfirmationManager resendConfirmationManager; + + @Mock + private UserRecoveryDataStore userRecoveryDataStore; + + @Mock + private IdentityEventService identityEventService; + + @Mock + private IdentityGovernanceService identityGovernanceService; + + @Mock + private RealmService realmService; + + @Mock + private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; + + @Mock + private PrivilegedCarbonContext threadLocalCarbonContext; + + @Mock + private UserAccountRecoveryManager userAccountRecoveryManager; + + private MockedStatic mockedServiceDataHolder; + private MockedStatic mockedIdentityUtil; + private MockedStatic mockedIdentityTenantUtil; + private MockedStatic mockedUtils; + private MockedStatic mockedPrivilegedCarbonContext; + private MockedStatic mockedJDBCRecoveryDataStore; + private MockedStatic mockedUserAccountRecoveryManager; + + private static final String TEST_USERNAME = "test-user"; + private static final String TEST_TENANT_DOMAIN = "test.com"; + private static final String TEST_USER_STORE_DOMAIN = "TESTING"; + + @BeforeMethod + public void setUp() throws Exception { + + MockitoAnnotations.openMocks(this); + resendConfirmationManager = ResendConfirmationManager.getInstance(); + + mockedServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + mockedIdentityUtil = mockStatic(IdentityUtil.class); + mockedIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); + mockedUtils = mockStatic(Utils.class); + mockedPrivilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class); + mockedJDBCRecoveryDataStore = mockStatic(JDBCRecoveryDataStore.class); + mockedUserAccountRecoveryManager = mockStatic(UserAccountRecoveryManager.class); + + when(IdentityRecoveryServiceDataHolder.getInstance()).thenReturn(identityRecoveryServiceDataHolder); + mockedPrivilegedCarbonContext.when(PrivilegedCarbonContext::getThreadLocalCarbonContext) + .thenReturn(threadLocalCarbonContext); + mockedIdentityUtil.when(IdentityUtil::getPrimaryDomainName).thenReturn( + UserCoreConstants.PRIMARY_DEFAULT_DOMAIN_NAME); + mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(userRecoveryDataStore); + mockedUserAccountRecoveryManager.when(UserAccountRecoveryManager::getInstance) + .thenReturn(userAccountRecoveryManager); + + when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); + when(identityRecoveryServiceDataHolder.getIdentityGovernanceService()).thenReturn(identityGovernanceService); + when(identityRecoveryServiceDataHolder.getRealmService()).thenReturn(realmService); + + when(threadLocalCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN); + } + + @AfterMethod + public void tearDown() { + + mockedServiceDataHolder.close(); + mockedIdentityUtil.close(); + mockedIdentityTenantUtil.close(); + mockedUtils.close(); + mockedPrivilegedCarbonContext.close(); + mockedJDBCRecoveryDataStore.close(); + mockedUserAccountRecoveryManager.close(); + } + + @Test + public void testResendConfirmationCodeMobileVerificationOnUpdate() throws Exception { + + String verificationPendingMobile = "0777897621"; + String oldCode = "dummy-code"; + String newCode = "new-code"; + User user = getUser(); + Property[] properties = new Property[]{new Property("testKey", "testValue")}; + + UserRecoveryData userRecoveryData = new UserRecoveryData(user, oldCode, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + userRecoveryData.setRemainingSetIds(verificationPendingMobile); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE)).thenReturn(userRecoveryData); + + mockedUtils.when(() -> Utils.reIssueExistingConfirmationCode(userRecoveryData, + NotificationChannels.SMS_CHANNEL.getChannelType())).thenReturn(false); + mockedUtils.when(() -> Utils.generateSecretKey(anyString(), anyString(), anyString(), anyString())) + .thenReturn(newCode); + + NotificationResponseBean responseBean = resendConfirmationManager.resendConfirmationCode( + user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + assertNotNull(responseBean); + + ArgumentCaptor recoveryDataCaptor = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor.capture()); + UserRecoveryData capturedRecoveryData = recoveryDataCaptor.getValue(); + Assert.assertEquals(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, + capturedRecoveryData.getRecoveryScenario()); + Assert.assertEquals(verificationPendingMobile, + capturedRecoveryData.getRemainingSetIds()); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + verify(identityEventService).handleEvent(eventCaptor.capture()); + Event capturedEvent = eventCaptor.getValue(); + Map eventProperties = capturedEvent.getEventProperties(); + Assert.assertEquals(verificationPendingMobile, eventProperties.get(IdentityRecoveryConstants.SEND_TO)); + Assert.assertEquals(newCode, eventProperties.get(IdentityRecoveryConstants.CONFIRMATION_CODE)); + + // Reset data. + reset(userRecoveryDataStore); + reset(identityEventService); + + // Case 2: MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE recovery scenario. + UserRecoveryData userRecoveryData2 = new UserRecoveryData(user, oldCode, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + userRecoveryData2.setRemainingSetIds(verificationPendingMobile); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE)).thenReturn(userRecoveryData2); + + NotificationResponseBean responseBean2 = resendConfirmationManager.resendConfirmationCode( + user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + assertNotNull(responseBean2); + + ArgumentCaptor recoveryDataCaptor2 = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor2.capture()); + UserRecoveryData capturedRecoveryData2 = recoveryDataCaptor2.getValue(); + Assert.assertEquals(RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + capturedRecoveryData2.getRecoveryScenario()); + Assert.assertEquals(verificationPendingMobile, + capturedRecoveryData2.getRemainingSetIds()); + } + + @Test + public void testResendConfirmationCodeEmailVerificationOnUpdate() throws Exception { + + String verificationPendingEmail = "testuser@gmail.com"; + String oldCode = "dummy-code"; + String newCode = "new-code"; + User user = getUser(); + Property[] properties = new Property[]{new Property("testKey", "testValue")}; + + UserRecoveryData userRecoveryData = new UserRecoveryData(user, oldCode, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL); + userRecoveryData.setRemainingSetIds(verificationPendingEmail); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE)).thenReturn(userRecoveryData); + + mockedUtils.when(() -> Utils.reIssueExistingConfirmationCode(userRecoveryData, + NotificationChannels.EMAIL_CHANNEL.getChannelType())).thenReturn(false); + mockedUtils.when(() -> Utils.generateSecretKey(anyString(), anyString(), anyString(), anyString())) + .thenReturn(newCode); + mockUtilsErrors(); + + NotificationResponseBean responseBean = resendConfirmationManager.resendConfirmationCode( + user, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_EMAIL_ON_UPDATE, properties); + + assertNotNull(responseBean); + + ArgumentCaptor recoveryDataCaptor = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor.capture()); + UserRecoveryData capturedRecoveryData = recoveryDataCaptor.getValue(); + Assert.assertEquals(RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, + capturedRecoveryData.getRecoveryScenario()); + Assert.assertEquals(verificationPendingEmail, + capturedRecoveryData.getRemainingSetIds()); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + verify(identityEventService).handleEvent(eventCaptor.capture()); + Event capturedEvent = eventCaptor.getValue(); + Map eventProperties = capturedEvent.getEventProperties(); + Assert.assertEquals(verificationPendingEmail, eventProperties.get(IdentityRecoveryConstants.SEND_TO)); + Assert.assertEquals(newCode, eventProperties.get(IdentityRecoveryConstants.CONFIRMATION_CODE)); + + // Reset. + reset(userRecoveryDataStore); + reset(identityEventService); + + // Case 2: EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE recovery scenario. + UserRecoveryData userRecoveryData2 = new UserRecoveryData(user, oldCode, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL); + userRecoveryData2.setRemainingSetIds(verificationPendingEmail); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE)).thenReturn(userRecoveryData2); + + NotificationResponseBean responseBean2 = resendConfirmationManager.resendConfirmationCode( + user, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_EMAIL_ON_UPDATE, properties); + assertNotNull(responseBean2); + + ArgumentCaptor recoveryDataCaptor2 = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor2.capture()); + UserRecoveryData capturedRecoveryData2 = recoveryDataCaptor2.getValue(); + Assert.assertEquals(RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + capturedRecoveryData2.getRecoveryScenario()); + Assert.assertEquals(verificationPendingEmail, + capturedRecoveryData2.getRemainingSetIds()); + } + + @Test + public void testResendConfirmationCodeErrorScenarios() throws Exception { + + String verificationPendingMobile = "0777897621"; + String oldCode = "dummy-code"; + String newCode = "new-code"; + User user = getUser(); + Property[] properties = new Property[]{new Property("testKey", "testValue")}; + + UserRecoveryData userRecoveryData = new UserRecoveryData(user, oldCode, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + userRecoveryData.setRemainingSetIds(verificationPendingMobile); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE)).thenReturn(userRecoveryData); + + mockedUtils.when(() -> Utils.reIssueExistingConfirmationCode(userRecoveryData, + NotificationChannels.SMS_CHANNEL.getChannelType())).thenReturn(false); + mockedUtils.when(() -> Utils.generateSecretKey(anyString(), anyString(), anyString(), anyString())) + .thenReturn(newCode); + mockUtilsErrors(); + + // Case 1: Null user. + try { + resendConfirmationManager.resendConfirmationCode(null, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + // Case 2: Empty Recovery scenario. + try { + resendConfirmationManager.resendConfirmationCode(user, + "", + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + // Case 3: Empty Recovery step. + try { + resendConfirmationManager.resendConfirmationCode(user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + "", + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + // Case 4: Empty Notification type. + try { + resendConfirmationManager.resendConfirmationCode(user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + "", properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + } + + @Test + public void testResendConfirmationCodeWithCode() throws IdentityRecoveryException, IdentityEventException { + + String verificationPendingEmail = "testuser@gmail.com"; + String oldCode = "dummy-code"; + String newCode = "new-code"; + User user = getUser(); + Property[] properties = new Property[]{new Property("testKey", "testValue")}; + + UserRecoveryData userRecoveryData = new UserRecoveryData(user, oldCode, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL); + userRecoveryData.setRemainingSetIds(verificationPendingEmail); + when(userRecoveryDataStore.loadWithoutCodeExpiryValidation(user, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE)).thenReturn(userRecoveryData); + + mockedUtils.when(() -> Utils.reIssueExistingConfirmationCode(userRecoveryData, + NotificationChannels.EMAIL_CHANNEL.getChannelType())).thenReturn(false); + mockedUtils.when(() -> Utils.generateSecretKey(anyString(), anyString(), anyString(), anyString())) + .thenReturn(newCode); + mockUtilsErrors(); + + NotificationResponseBean responseBean = resendConfirmationManager.resendConfirmationCode( + user, + oldCode, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_EMAIL_ON_UPDATE, properties); + + assertNotNull(responseBean); + + ArgumentCaptor recoveryDataCaptor = ArgumentCaptor.forClass(UserRecoveryData.class); + verify(userRecoveryDataStore).store(recoveryDataCaptor.capture()); + UserRecoveryData capturedRecoveryData = recoveryDataCaptor.getValue(); + Assert.assertEquals(RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, + capturedRecoveryData.getRecoveryScenario()); + Assert.assertEquals(verificationPendingEmail, + capturedRecoveryData.getRemainingSetIds()); + + ArgumentCaptor eventCaptor = ArgumentCaptor.forClass(Event.class); + verify(identityEventService).handleEvent(eventCaptor.capture()); + Event capturedEvent = eventCaptor.getValue(); + Map eventProperties = capturedEvent.getEventProperties(); + Assert.assertEquals(verificationPendingEmail, eventProperties.get(IdentityRecoveryConstants.SEND_TO)); + Assert.assertEquals(newCode, eventProperties.get(IdentityRecoveryConstants.CONFIRMATION_CODE)); + + // Case 2: Code not given. + try { + resendConfirmationManager.resendConfirmationCode( + user, + null, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + } + + @Test + public void testResendConfirmation() throws IdentityRecoveryException { + + String resendCode = "dummy-code"; + String recoveryFlowId = "dummy-flow-id"; + int resendCount = 2; + User user = getUser(); + Property[] properties = new Property[]{new Property("testKey", "testValue")}; + + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + RECOVERY_NOTIFICATION_PASSWORD_MAX_RESEND_ATTEMPTS, TEST_TENANT_DOMAIN)).thenReturn("5"); + mockUtilsErrors(); + + UserRecoveryData userRecoveryData = new UserRecoveryData(user, resendCode, + RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.VERIFY_EMAIL); + userRecoveryData.setRemainingSetIds(NotificationChannels.EMAIL_CHANNEL.getChannelType()); + userRecoveryData.setRecoveryFlowId(recoveryFlowId); + + when(userAccountRecoveryManager.getUserRecoveryData(anyString(), any())).thenReturn(userRecoveryData); + + UserRecoveryFlowData userRecoveryFlowData = mock(UserRecoveryFlowData.class); + when(userAccountRecoveryManager.loadUserRecoveryFlowData(userRecoveryData)) + .thenReturn(userRecoveryFlowData); + when(userRecoveryFlowData.getResendCount()).thenReturn(resendCount); + + ResendConfirmationDTO resendConfirmationDTO = resendConfirmationManager.resendConfirmation( + TEST_TENANT_DOMAIN, resendCode, RecoveryScenarios.SELF_SIGN_UP.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_EMAIL_CONFIRM, + properties); + assertEquals(resendConfirmationDTO.getSuccessCode(), + IdentityRecoveryConstants.SuccessEvents.SUCCESS_STATUS_CODE_RESEND_CONFIRMATION_CODE.getCode()); + verify(userAccountRecoveryManager).updateRecoveryDataResendCount(recoveryFlowId, resendCount + 1); + + // Case 2: Resend count exceeds the maximum limit. + when(userRecoveryFlowData.getResendCount()).thenReturn(5); + try { + resendConfirmationManager.resendConfirmation( + TEST_TENANT_DOMAIN, resendCode, RecoveryScenarios.SELF_SIGN_UP.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_EMAIL_CONFIRM, + properties); + verify(userAccountRecoveryManager).invalidateRecoveryData(recoveryFlowId); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + // Case 2: When recovery scenarios doesn't match. + when(userRecoveryFlowData.getResendCount()).thenReturn(1); + try { + resendConfirmationManager.resendConfirmation( + TEST_TENANT_DOMAIN, resendCode, RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_EMAIL_CONFIRM, + properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + // Case 3: When tenant domain doesn't match. + try { + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants.ConnectorConfig. + RECOVERY_NOTIFICATION_PASSWORD_MAX_RESEND_ATTEMPTS, "OTHER_DOMAIN")) + .thenReturn("5"); + resendConfirmationManager.resendConfirmation( + "OTHER_DOMAIN", resendCode, RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), + RecoverySteps.VERIFY_EMAIL.toString(), + IdentityRecoveryConstants.NOTIFICATION_TYPE_EMAIL_CONFIRM, + properties); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + + } + + private static User getUser() { + + User user = new User(); + user.setUserName(TEST_USERNAME); + user.setTenantDomain(TEST_TENANT_DOMAIN); + user.setUserStoreDomain(TEST_USER_STORE_DOMAIN); + return user; + } + + private void mockUtilsErrors() { + + mockedUtils.when(() -> Utils.handleClientException(any(IdentityRecoveryConstants.ErrorMessages.class), + any())).thenReturn(new IdentityRecoveryClientException("test-code", "test-dec")); + + mockedUtils.when(() -> Utils.handleClientException(any(IdentityRecoveryConstants.ErrorMessages.class), + isNull())).thenReturn(new IdentityRecoveryClientException("test-code", "test-dec")); + + mockedUtils.when(() -> Utils.handleClientException(anyString(), anyString(), any())) + .thenReturn(new IdentityRecoveryClientException("test-code", "test-dec")); + + mockedUtils.when(() -> Utils.handleClientException(any(IdentityRecoveryConstants.ErrorMessages.class), + anyString())).thenReturn(new IdentityRecoveryClientException("test-code", "test-dec")); + + } +} diff --git a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml index d8278d5b0d..1abefbf250 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml @@ -28,12 +28,9 @@ - - - - - + + From 0188c6d4845751ba21168c3160b610bc5482f80e Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Tue, 1 Oct 2024 11:45:15 +0530 Subject: [PATCH 47/54] Add unit tests for JDBCRecoveryDataStore. --- .../signup/UserSelfRegistrationManager.java | 78 +++-- .../ResendConfirmationManagerTest.java | 5 + .../UserSelfRegistrationManagerTest.java | 73 ++++- .../store/JDBCRecoveryDataStoreTest.java | 285 ++++++++++++++++++ .../identity/recovery/util/UtilsTest.java | 38 +++ 5 files changed, 427 insertions(+), 52 deletions(-) create mode 100644 components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java index 9573989360..c0a11d56ea 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManager.java @@ -984,50 +984,48 @@ public void confirmVerificationCodeMe(String code, Map propertie boolean supportMultipleEmailsAndMobileNumbers = Utils.isMultiEmailsAndMobileNumbersPerUserEnabled(); - if (RecoverySteps.VERIFY_MOBILE_NUMBER.equals(recoveryData.getRecoveryStep())) { - String pendingMobileNumberClaimValue = recoveryData.getRemainingSetIds(); - if (StringUtils.isNotBlank(pendingMobileNumberClaimValue)) { - /* - Verifying whether user is trying to add a mobile number to http://wso2.org/claims/verifedMobileNumbers - claim. - */ - if (supportMultipleEmailsAndMobileNumbers) { - try { - String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); - List existingVerifiedMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, - user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); - if (!existingVerifiedMobileNumbersList.contains(pendingMobileNumberClaimValue)) { - existingVerifiedMobileNumbersList.add(pendingMobileNumberClaimValue); - userClaims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, existingVerifiedMobileNumbersList)); - } + String pendingMobileNumberClaimValue = recoveryData.getRemainingSetIds(); + if (StringUtils.isNotBlank(pendingMobileNumberClaimValue)) { + /* + Verifying whether user is trying to add a mobile number to http://wso2.org/claims/verifedMobileNumbers + claim. + */ + if (supportMultipleEmailsAndMobileNumbers) { + try { + String multiAttributeSeparator = FrameworkUtils.getMultiAttributeSeparator(); + List existingVerifiedMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, + user, IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); + if (!existingVerifiedMobileNumbersList.contains(pendingMobileNumberClaimValue)) { + existingVerifiedMobileNumbersList.add(pendingMobileNumberClaimValue); + userClaims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, existingVerifiedMobileNumbersList)); + } - /* - VerifiedMobileNumbers is a subset of mobileNumbers. Hence, adding the verified number to - mobileNumbers claim as well. - */ - List allMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, - user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); - if (!allMobileNumbersList.contains(pendingMobileNumberClaimValue)) { - allMobileNumbersList.add(pendingMobileNumberClaimValue); - userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, allMobileNumbersList)); - } - } catch (IdentityEventException e) { - log.error("Error occurred while obtaining claim for the user : " + user.getUserName()); - throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + - "value for the user : " + user.getUserName(), e); + /* + VerifiedMobileNumbers is a subset of mobileNumbers. Hence, adding the verified number to + mobileNumbers claim as well. + */ + List allMobileNumbersList = Utils.getMultiValuedClaim(userStoreManager, + user, IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM); + if (!allMobileNumbersList.contains(pendingMobileNumberClaimValue)) { + allMobileNumbersList.add(pendingMobileNumberClaimValue); + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, allMobileNumbersList)); } + } catch (IdentityEventException e) { + log.error("Error occurred while obtaining claim for the user : " + user.getUserName()); + throw new IdentityRecoveryServerException("Error occurred while obtaining existing claim " + + "value for the user : " + user.getUserName(), e); } - if (!RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE - .equals(recoveryData.getRecoveryScenario())) { - userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileNumberClaimValue); - } - userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); - userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); - Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants - .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); } + if (!RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE + .equals(recoveryData.getRecoveryScenario())) { + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, pendingMobileNumberClaimValue); + } + userClaims.put(NotificationChannels.SMS_CHANNEL.getVerifiedClaimUrl(), Boolean.TRUE.toString()); + userClaims.put(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM, StringUtils.EMPTY); + Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants + .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_CONFIRM.toString()); } // Update the user claims. updateUserClaims(userStoreManager, user, userClaims); diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java index ca80e11282..193a6ff5e6 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManagerTest.java @@ -68,6 +68,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; @WithCarbonHome public class ResendConfirmationManagerTest { @@ -317,6 +318,7 @@ public void testResendConfirmationCodeErrorScenarios() throws Exception { RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + fail(); } catch (Exception e) { assertTrue(e instanceof IdentityRecoveryClientException); } @@ -327,6 +329,7 @@ public void testResendConfirmationCodeErrorScenarios() throws Exception { "", RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + fail(); } catch (Exception e) { assertTrue(e instanceof IdentityRecoveryClientException); } @@ -337,6 +340,7 @@ public void testResendConfirmationCodeErrorScenarios() throws Exception { RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), "", IdentityRecoveryConstants.NOTIFICATION_TYPE_VERIFY_MOBILE_ON_UPDATE, properties); + fail(); } catch (Exception e) { assertTrue(e instanceof IdentityRecoveryClientException); } @@ -347,6 +351,7 @@ public void testResendConfirmationCodeErrorScenarios() throws Exception { RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.toString(), RecoverySteps.VERIFY_MOBILE_NUMBER.toString(), "", properties); + fail(); } catch (Exception e) { assertTrue(e instanceof IdentityRecoveryClientException); } diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java index 8fdf1efca2..e8d2ae950a 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/signup/UserSelfRegistrationManagerTest.java @@ -113,14 +113,15 @@ import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNotNull; import static org.testng.Assert.assertTrue; import static org.testng.Assert.assertFalse; - import static org.testng.Assert.fail; + import static org.wso2.carbon.identity.auth.attribute.handler.AuthAttributeHandlerConstants.ErrorMessages.ERROR_CODE_AUTH_ATTRIBUTE_HANDLER_NOT_FOUND; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ACCOUNT_LOCK_ON_CREATION; import static org.wso2.carbon.identity.recovery.IdentityRecoveryConstants.ConnectorConfig.ENABLE_SELF_SIGNUP; @@ -652,8 +653,10 @@ public void testConfirmVerificationCodeMe() capturedClaims.get(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM); String updatedVerificationPendingMobile = capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM); + String updatedPrimaryMobile = capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM); assertEquals(updatedVerificationPendingMobile, StringUtils.EMPTY); + assertEquals(updatedPrimaryMobile, verificationPendingMobileNumber); assertTrue(StringUtils.contains(updatedVerifiedMobileNumbers, verificationPendingMobileNumber)); // Case 2: Multiple email and mobile per user is disabled. @@ -671,6 +674,63 @@ public void testConfirmVerificationCodeMe() assertEquals(updatedVerificationPendingMobile2, StringUtils.EMPTY); assertEquals(mobileNumberClaims, verificationPendingMobileNumber); + + // Case 3: Wrong recovery step. + UserRecoveryData userRecoveryData3 = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VALIDATE_ALL_CHALLENGE_QUESTION); + userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData3); + try { + userSelfRegistrationManager.confirmVerificationCodeMe(TEST_CODE, new HashMap<>()); + fail(); + } catch (IdentityRecoveryException e) { + assertEquals(e.getErrorCode(), IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CODE.getCode()); + } + } + + @Test + public void testConfirmVerificationCodeMeVerificationOnVerifiedListUpdate() + throws IdentityRecoveryException, UserStoreException { + + // Case 1: Recovery Scenario - MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE. + String verificationPendingMobileNumber = "0700000000"; + User user = getUser(); + UserRecoveryData userRecoveryData = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + userRecoveryData.setRemainingSetIds(verificationPendingMobileNumber); + + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData); + when(privilegedCarbonContext.getUsername()).thenReturn(TEST_USER_NAME); + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TEST_TENANT_DOMAIN_NAME); + + mockMultiAttributeEnabled(true); + mockGetUserClaimValue(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM, verificationPendingMobileNumber); + mockGetUserClaimValue(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, verificationPendingMobileNumber); + + userSelfRegistrationManager.confirmVerificationCodeMe(TEST_CODE, new HashMap<>()); + + ArgumentCaptor> claimsCaptor = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager).setUserClaimValues(anyString(), claimsCaptor.capture(), isNull()); + Map capturedClaims = claimsCaptor.getValue(); + String updatedVerificationPendingMobile = + capturedClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM); + + assertFalse(capturedClaims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)); + assertEquals(updatedVerificationPendingMobile, StringUtils.EMPTY); + + reset(userStoreManager); + + // Case 2: When pending mobile number claim value is null. + UserRecoveryData userRecoveryData2 = new UserRecoveryData(user, TEST_RECOVERY_DATA_STORE_SECRET, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER); + when(userRecoveryDataStore.load(eq(TEST_CODE))).thenReturn(userRecoveryData2); + + userSelfRegistrationManager.confirmVerificationCodeMe(TEST_CODE, new HashMap<>()); + + ArgumentCaptor> claimsCaptor2 = ArgumentCaptor.forClass(Map.class); + verify(userStoreManager).setUserClaimValues(anyString(), claimsCaptor2.capture(), isNull()); + assertFalse(claimsCaptor2.getValue().containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)); + assertFalse(claimsCaptor2.getValue().containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)); } @Test(expectedExceptions = IdentityRecoveryServerException.class) @@ -764,17 +824,6 @@ public void testGetConfirmedSelfRegisteredUserVerifyEmail() } catch (Exception e) { assertTrue(e instanceof IdentityRecoveryServerException); } - - // Case 4 : Throws user store exception while getting user claim values. - when(userStoreManager.getUserClaimValue(any(), eq(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM), - any())).thenThrow(new org.wso2.carbon.user.core.UserStoreException()); - try { - userSelfRegistrationManager.getConfirmedSelfRegisteredUser(TEST_CODE, verifiedChannelType, - verifiedChannelClaim, metaProperties); - fail(); - } catch (Exception e) { - assertTrue(e instanceof IdentityRecoveryServerException); - } } diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java new file mode 100644 index 0000000000..bb460d3818 --- /dev/null +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java @@ -0,0 +1,285 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (http://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 + * in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package org.wso2.carbon.identity.recovery.store; + +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.Assert; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.common.model.User; +import org.wso2.carbon.identity.base.IdentityException; +import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; +import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; +import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; +import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; +import org.wso2.carbon.identity.recovery.model.UserRecoveryData; +import org.wso2.carbon.identity.recovery.util.Utils; + +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.ResultSet; +import java.sql.SQLException; +import java.sql.Timestamp; +import java.util.Calendar; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.assertTrue; +import static org.testng.Assert.fail; + +public class JDBCRecoveryDataStoreTest { + + private UserRecoveryDataStore userRecoveryDataStore; + + @Mock + private Connection mockConnection; + + @Mock + private PreparedStatement mockPreparedStatement; + + @Mock + private ResultSet mockResultSet; + + @Mock + private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; + + @Mock + private IdentityEventService identityEventService; + + private MockedStatic mockedIdentityDatabaseUtil; + private MockedStatic mockedIdentityTenantUtils; + private MockedStatic mockedIdentityUtil; + private MockedStatic mockedUtils; + private MockedStatic mockedIdentityRecoveryServiceDataHolder; + + private static final int TEST_TENANT_ID = 12; + private static final String TEST_TENANT_DOMAIN = "test.com"; + private static final String TEST_USER_NAME = "testUser"; + private static final String TEST_USER_STORE_DOMAIN = "testUserStore"; + private static final String TEST_SECRET_CODE = "test-sec"; + + @BeforeMethod + public void setUp() throws Exception { + + MockitoAnnotations.openMocks(this); + userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + + mockedIdentityDatabaseUtil = mockStatic(IdentityDatabaseUtil.class); + mockedIdentityTenantUtils = mockStatic(IdentityTenantUtil.class); + mockedIdentityUtil = mockStatic(IdentityUtil.class); + mockedUtils = mockStatic(Utils.class); + mockedIdentityRecoveryServiceDataHolder = mockStatic(IdentityRecoveryServiceDataHolder.class); + + mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) + .thenReturn(identityRecoveryServiceDataHolder); + mockedIdentityDatabaseUtil.when(() -> IdentityDatabaseUtil.getDBConnection(anyBoolean())) + .thenReturn(mockConnection); + mockedIdentityTenantUtils.when(() -> IdentityTenantUtil.getTenantId(TEST_TENANT_DOMAIN)) + .thenReturn(TEST_TENANT_ID); + mockedIdentityUtil.when(() -> IdentityUtil.isUserStoreCaseSensitive(anyString(), anyInt())) + .thenReturn(true); + + when(identityRecoveryServiceDataHolder.getIdentityEventService()).thenReturn(identityEventService); + when(mockConnection.prepareStatement(anyString())).thenReturn(mockPreparedStatement); + } + + @AfterMethod + public void tearDown() { + + mockedIdentityDatabaseUtil.close(); + mockedIdentityTenantUtils.close(); + mockedIdentityUtil.close(); + mockedUtils.close(); + mockedIdentityRecoveryServiceDataHolder.close(); + + reset(mockConnection, mockPreparedStatement, mockResultSet); + } + + @Test + public void testStore() throws Exception { + + UserRecoveryData recoveryData = createSampleUserRecoveryData(); + + userRecoveryDataStore.store(recoveryData); + + verify(mockPreparedStatement).setString(1, recoveryData.getUser().getUserName()); + verify(mockPreparedStatement).setString(2, + recoveryData.getUser().getUserStoreDomain().toUpperCase()); + verify(mockPreparedStatement).setInt(3, TEST_TENANT_ID); + verify(mockPreparedStatement).setString(4, TEST_SECRET_CODE); + verify(mockPreparedStatement).setString(5, String.valueOf(recoveryData.getRecoveryScenario())); + verify(mockPreparedStatement).setString(6, String.valueOf(recoveryData.getRecoveryStep())); + verify(mockPreparedStatement).execute(); + mockedIdentityDatabaseUtil.verify(() -> IdentityDatabaseUtil.commitTransaction(mockConnection)); + mockedIdentityDatabaseUtil.verify(() -> IdentityDatabaseUtil.closeConnection(mockConnection)); + + // Case 2: SQL Exception. + mockUtilsErrors(); + doThrow(new SQLException()).when(mockPreparedStatement).execute(); + try { + userRecoveryDataStore.store(recoveryData); + fail("Expected IdentityRecoveryException was not thrown."); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryServerException); + } + mockedIdentityDatabaseUtil.verify(() -> IdentityDatabaseUtil.closeConnection(mockConnection), + times(2)); + } + + @DataProvider(name = "loadData") + private Object[][] loadData() { + + return new Object[][] { + { RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER }, + { RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER }, + { RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL }, + { RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL } + }; + } + + @Test(dataProvider = "loadData") + public void testLoadSuccessful(RecoveryScenarios recoveryScenario, RecoverySteps recoveryStep) throws Exception { + + User user = createSampleUser(); + String remainingSetId = "0777898721"; + + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + when(mockResultSet.next()).thenReturn(true); + when(mockResultSet.getString("REMAINING_SETS")).thenReturn(remainingSetId); + when(mockResultSet.getTimestamp(eq("TIME_CREATED"), any(Calendar.class))) + .thenReturn(new Timestamp(System.currentTimeMillis() - 60000)); + + mockExpiryTimes(); + UserRecoveryData result = userRecoveryDataStore.load(user, recoveryScenario, recoveryStep, TEST_SECRET_CODE); + + assertNotNull(result); + assertEquals(result.getUser().getUserName(), user.getUserName()); + assertEquals(result.getSecret(), TEST_SECRET_CODE); + assertEquals(result.getRecoveryScenario(), recoveryScenario); + assertEquals(result.getRecoveryStep(), recoveryStep); + assertEquals(result.getRemainingSetIds(), remainingSetId); + + verify(identityEventService, times(2)).handleEvent(any()); + mockedIdentityDatabaseUtil.verify(() -> IdentityDatabaseUtil + .closeAllConnections(mockConnection, mockResultSet, mockPreparedStatement)); + } + + @Test() + public void testLoadExpiredCode() throws Exception { + + User user = createSampleUser(); + String remainingSetId = "0777898721"; + + when(mockPreparedStatement.executeQuery()).thenReturn(mockResultSet); + when(mockResultSet.next()).thenReturn(true); + when(mockResultSet.getString("REMAINING_SETS")).thenReturn(remainingSetId); + when(mockResultSet.getTimestamp(eq("TIME_CREATED"), any(Calendar.class))) + .thenReturn(new Timestamp(System.currentTimeMillis() - 600000)); + + mockExpiryTimes(); + mockUtilsErrors(); + try { + userRecoveryDataStore.load(user, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + TEST_SECRET_CODE); + fail(); + } catch (Exception e) { + assertTrue(e instanceof IdentityRecoveryClientException); + } + } + + private void mockExpiryTimes() { + + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.EMAIL_VERIFICATION_ON_UPDATE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + } + + private void mockUtilsErrors() { + + mockedUtils.when(() -> Utils.handleServerException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_ISSUE_IN_LOADING_RECOVERY_CONFIGS, null)) + .thenReturn(IdentityException.error(IdentityRecoveryServerException.class, + "err-code", + "")); + + mockedUtils.when(() -> Utils.handleServerException( + any(IdentityRecoveryConstants.ErrorMessages.class), + isNull(), + any())) + .thenReturn(IdentityException.error(IdentityRecoveryServerException.class, + "err-code", + new Throwable())); + + mockedUtils.when(() -> Utils.handleClientException(any(IdentityRecoveryConstants.ErrorMessages.class), + anyString())).thenReturn(IdentityException.error(IdentityRecoveryClientException.class, + "err-code", + "")); + } + + private UserRecoveryData createSampleUserRecoveryData() { + + User user = createSampleUser(); + return new UserRecoveryData(user, TEST_SECRET_CODE, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER); + } + + private User createSampleUser() { + + User user = new User(); + user.setUserName(TEST_USER_NAME); + user.setTenantDomain(TEST_TENANT_DOMAIN); + user.setUserStoreDomain(TEST_USER_STORE_DOMAIN); + return user; + } +} diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java index c675bf11b5..d5a6c4f244 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/util/UtilsTest.java @@ -274,6 +274,44 @@ public void ThreadLocalToSkipSendingSmsOtpVerificationOnUpdate() { assertNull(result1); } + @Test + public void testIsOnlyVerifiedMobileNumbersUpdatedThreadLocal() { + + // Initially should be false. + assertFalse(Utils.getThreadLocalIsOnlyVerifiedMobileNumbersUpdated()); + + // Set to true. + Utils.setThreadLocalIsOnlyVerifiedMobileNumbersUpdated(true); + assertTrue(Utils.getThreadLocalIsOnlyVerifiedMobileNumbersUpdated()); + + // Set to false. + Utils.setThreadLocalIsOnlyVerifiedMobileNumbersUpdated(false); + assertFalse(Utils.getThreadLocalIsOnlyVerifiedMobileNumbersUpdated()); + + // Unset. + Utils.unsetThreadLocalIsOnlyVerifiedMobileNumbersUpdated(); + assertFalse(Utils.getThreadLocalIsOnlyVerifiedMobileNumbersUpdated()); + } + + @Test + public void testIsOnlyVerifiedEmailAddressesUpdatedThreadLocal() { + + // Initially should be false. + assertFalse(Utils.getThreadLocalIsOnlyVerifiedEmailAddressesUpdated()); + + // Set to true. + Utils.setThreadLocalIsOnlyVerifiedEmailAddressesUpdated(true); + assertTrue(Utils.getThreadLocalIsOnlyVerifiedEmailAddressesUpdated()); + + // Set to false. + Utils.setThreadLocalIsOnlyVerifiedEmailAddressesUpdated(false); + assertFalse(Utils.getThreadLocalIsOnlyVerifiedEmailAddressesUpdated()); + + // Unset. + Utils.unsetThreadLocalIsOnlyVerifiedEmailAddressesUpdated(); + assertFalse(Utils.getThreadLocalIsOnlyVerifiedEmailAddressesUpdated()); + } + @Test public void testGetClaimFromUserStoreManager() throws Exception { From 89ed1b1bc8f80bce9d5aedcae58d806d81c1581c Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Tue, 1 Oct 2024 13:02:40 +0530 Subject: [PATCH 48/54] Add more unit tests. --- .../endpoint/impl/MeApiServiceImplTest.java | 62 +++++++++++++-- .../impl/ResendCodeApiServiceImplTest.java | 79 ++++++++++++++++++- .../store/JDBCRecoveryDataStoreTest.java | 12 +-- .../src/test/resources/testng.xml | 1 + 4 files changed, 134 insertions(+), 20 deletions(-) diff --git a/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImplTest.java b/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImplTest.java index 6643281440..f040bde5ee 100644 --- a/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImplTest.java +++ b/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/MeApiServiceImplTest.java @@ -24,6 +24,7 @@ import org.mockito.MockitoAnnotations; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.base.CarbonBaseConstants; import org.wso2.carbon.context.PrivilegedCarbonContext; @@ -165,23 +166,68 @@ public void testMeResendCodePost() throws IdentityRecoveryException { Mockito.when(userRecoveryData.getRecoveryStep()).thenReturn( RecoverySteps.getRecoveryStep("VERIFY_MOBILE_NUMBER")); - assertEquals(meApiService.meResendCodePost(meResendCodeRequestDTO()).getStatus(), 201); + assertEquals(meApiService.meResendCodePost( + meResendCodeRequestDTO(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.name())).getStatus(), + 201); + assertEquals(meApiService.meResendCodePost( meResendCodeRequestDTOWithInvalidScenarioProperty()).getStatus(), 400); mockedUtils.when(() -> Utils.getUserRecoveryData(any(ResendCodeRequestDTO.class), anyString())) .thenReturn(null); - assertEquals(meApiService.meResendCodePost(meResendCodeRequestDTO()).getStatus(), 400); + assertEquals(meApiService.meResendCodePost( + meResendCodeRequestDTO(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.name())).getStatus(), + 400); Mockito.when(userRecoveryData.getRecoveryScenario()).thenReturn(RecoveryScenarios. getRecoveryScenario("ASK_PASSWORD")); - assertEquals(meApiService.meResendCodePost(meResendCodeRequestDTO()).getStatus(), 400); + assertEquals(meApiService.meResendCodePost( + meResendCodeRequestDTO(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE.name())).getStatus(), + 400); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } + + @DataProvider(name = "recoveryScenarioProvider") + public Object[][] recoveryScenarioProvider() { + return new Object[][] { + {RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, 400}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL, 400}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, 201}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER, 201}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, 201} + }; + } + + @Test(dataProvider = "recoveryScenarioProvider") + public void testMeResendCodePostRecoveryScenarios(RecoveryScenarios recoveryScenario, RecoverySteps recoveryStep, + int expectedStatusCode) throws IdentityRecoveryException { + + try { + String carbonHome = Paths.get(System.getProperty("user.dir"), "src", "test", "resources").toString(); + System.setProperty(CarbonBaseConstants.CARBON_HOME, carbonHome); + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setUsername(USERNAME); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantId(-1234); + Mockito.when(resendConfirmationManager.resendConfirmationCode(isNull(), anyString(), anyString(), + anyString(), isNull())).thenReturn(notificationResponseBean); + mockedUtils.when(() -> Utils.getUserRecoveryData(any(ResendCodeRequestDTO.class), anyString())) + .thenReturn(userRecoveryData); + mockedUtils.when(Utils::getResendConfirmationManager).thenReturn(resendConfirmationManager); + Mockito.when(userRecoveryData.getRecoveryScenario()).thenReturn(recoveryScenario); + Mockito.when(userRecoveryData.getRecoveryStep()).thenReturn(recoveryStep); + + assertEquals(meApiService.meResendCodePost( + meResendCodeRequestDTO(recoveryScenario.name())).getStatus(), + expectedStatusCode); } finally { PrivilegedCarbonContext.endTenantFlow(); } } - private SelfRegistrationUserDTO buildSelfRegistartion() { + private SelfRegistrationUserDTO buildSelfRegistration() { SelfRegistrationUserDTO selfRegistrationUserDTO = new SelfRegistrationUserDTO(); selfRegistrationUserDTO.setUsername("TestUser"); @@ -212,11 +258,11 @@ private SelfUserRegistrationRequestDTO selfUserRegistrationRequestDTO() { SelfUserRegistrationRequestDTO selfUserRegistrationRequestDTO = new SelfUserRegistrationRequestDTO(); List listClaimDTO = new ArrayList<>(); listClaimDTO.add(buildClaimDTO()); - buildSelfRegistartion().setClaims(listClaimDTO); + buildSelfRegistration().setClaims(listClaimDTO); List listPropertyDTOs = new ArrayList<>(); listPropertyDTOs.add(buildSelfUserRegistrationRequestDTO()); selfUserRegistrationRequestDTO.setProperties(listPropertyDTOs); - selfUserRegistrationRequestDTO.setUser(buildSelfRegistartion()); + selfUserRegistrationRequestDTO.setUser(buildSelfRegistration()); return selfUserRegistrationRequestDTO; } @@ -227,11 +273,11 @@ private MeCodeValidationRequestDTO createMeCodeValidationRequestDTO() { return codeValidationRequestDTO; } - private MeResendCodeRequestDTO meResendCodeRequestDTO() { + private MeResendCodeRequestDTO meResendCodeRequestDTO(String recoveryScenario) { MeResendCodeRequestDTO meResendCodeRequestDTO = new MeResendCodeRequestDTO(); List listProperty = new ArrayList<>(); - listProperty.add(buildPropertyDTO("RecoveryScenario", "MOBILE_VERIFICATION_ON_UPDATE")); + listProperty.add(buildPropertyDTO("RecoveryScenario", recoveryScenario)); meResendCodeRequestDTO.setProperties(listProperty); return meResendCodeRequestDTO; } diff --git a/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java b/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java index 65349ba385..9a55976bc0 100644 --- a/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java +++ b/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java @@ -23,13 +23,18 @@ import org.mockito.MockitoAnnotations; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; import org.wso2.carbon.identity.application.authentication.framework.internal.FrameworkServiceDataHolder; +import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.multi.attribute.login.mgt.MultiAttributeLoginService; import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; +import org.wso2.carbon.identity.recovery.RecoveryScenarios; +import org.wso2.carbon.identity.recovery.RecoverySteps; import org.wso2.carbon.identity.recovery.bean.NotificationResponseBean; +import org.wso2.carbon.identity.recovery.confirmation.ResendConfirmationManager; import org.wso2.carbon.identity.recovery.model.UserRecoveryData; import org.wso2.carbon.identity.recovery.signup.UserSelfRegistrationManager; import org.wso2.carbon.identity.user.endpoint.dto.PropertyDTO; @@ -39,8 +44,13 @@ import java.util.ArrayList; import java.util.List; +import javax.ws.rs.core.Response; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.when; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; /** * This class contains unit tests for ResendCodeApiServiceImpl.java @@ -62,9 +72,16 @@ public class ResendCodeApiServiceImplTest { @Mock private MultiAttributeLoginService mockedMultiAttributeLoginService; + @Mock + private ResendConfirmationManager resendConfirmationManager; + @InjectMocks private ResendCodeApiServiceImpl resendCodeApiService; + private final String TEST_USERNAME = "testUser"; + private final String TEST_TENANT_DOMAIN = "testTenantDomain"; + private static final String RECOVERY_SCENARIO_KEY = "RecoveryScenario"; + @BeforeMethod public void setUp() { @@ -86,9 +103,10 @@ public void tearDown() { @Test public void testResendCodePost() throws IdentityRecoveryException { - Mockito.when(userSelfRegistrationManager.resendConfirmationCode( + when(userSelfRegistrationManager.resendConfirmationCode( Utils.getUser(resendCodeRequestDTO().getUser()), Utils.getProperties(resendCodeRequestDTO().getProperties()))).thenReturn(notificationResponseBean); + assertEquals(resendCodeApiService.resendCodePost(resendCodeRequestDTO()).getStatus(), 201); assertEquals(resendCodeApiService.resendCodePost(emptyResendCodeRequestDTO()).getStatus(), 201); assertEquals(resendCodeApiService.resendCodePost(emptyPropertyResendCodeRequestDTO()).getStatus(), 201); @@ -106,7 +124,7 @@ public void testResendCodePost() throws IdentityRecoveryException { @Test public void testIdentityRecoveryExceptioninResendCodePost() throws IdentityRecoveryException { - Mockito.when(userSelfRegistrationManager.resendConfirmationCode( + when(userSelfRegistrationManager.resendConfirmationCode( Utils.getUser(resendCodeRequestDTO().getUser()), Utils.getProperties(resendCodeRequestDTO().getProperties()))).thenThrow(new IdentityRecoveryException("Recovery Exception")); assertEquals(resendCodeApiService.resendCodePost(resendCodeRequestDTO()).getStatus(), 400); @@ -115,7 +133,7 @@ public void testIdentityRecoveryExceptioninResendCodePost() throws IdentityRecov @Test public void testIdentityRecoveryClientExceptioninResendCodePost() throws IdentityRecoveryException { - Mockito.when(userSelfRegistrationManager.resendConfirmationCode( + when(userSelfRegistrationManager.resendConfirmationCode( Utils.getUser(resendCodeRequestDTO().getUser()), Utils.getProperties(resendCodeRequestDTO().getProperties()))).thenThrow(new IdentityRecoveryClientException("Recovery Exception")); assertEquals(resendCodeApiService.resendCodePost(resendCodeRequestDTO()).getStatus(), 400); @@ -206,4 +224,59 @@ private PropertyDTO recoveryScenarioPropertyDTO() { propertyDTO.setValue("ASK_PASSWORD"); return propertyDTO; } + + @DataProvider(name = "recoveryScenarioProvider") + public Object[][] recoveryScenarioProvider() { + return new Object[][] { + {RecoveryScenarios.ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD}, + {RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD}, + {RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP}, + {RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, RecoverySteps.UPDATE_PASSWORD}, + {RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, RecoverySteps.UPDATE_PASSWORD}, + {RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD}, + {RecoveryScenarios.LITE_SIGN_UP, RecoverySteps.CONFIRM_LITE_SIGN_UP}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER}, + }; + } + + @Test(dataProvider = "recoveryScenarioProvider") + public void testRecoveryScenarios(RecoveryScenarios scenario, RecoverySteps step) throws Exception { + + ResendCodeRequestDTO requestDTO = createResendCodeRequestDTO(scenario.name()); + User user = new User(); + UserRecoveryData recoveryData = new UserRecoveryData(user, "test-secret", scenario, step); + when(Utils.getUserRecoveryData(any(), anyString())).thenReturn(recoveryData); + when(Utils.getResendConfirmationManager()).thenReturn(resendConfirmationManager); + + NotificationResponseBean expectedResponse = new NotificationResponseBean(user); + when(resendConfirmationManager.resendConfirmationCode(any(), anyString(), anyString(), anyString(), any())) + .thenReturn(expectedResponse); + + Response result = resendCodeApiService.resendCodePost(requestDTO); + + assertNotNull(result); + assertEquals(result.getStatus(), Response.Status.CREATED.getStatusCode()); + } + + private ResendCodeRequestDTO createResendCodeRequestDTO(String recoveryScenario) { + + ResendCodeRequestDTO requestDTO = new ResendCodeRequestDTO(); + UserDTO userDTO = new UserDTO(); + userDTO.setTenantDomain(TEST_TENANT_DOMAIN); + userDTO.setUsername(TEST_USERNAME); + requestDTO.setUser(userDTO); + + List properties = new ArrayList<>(); + + PropertyDTO propertyDTO = new PropertyDTO(); + propertyDTO.setKey(RECOVERY_SCENARIO_KEY); + propertyDTO.setValue(recoveryScenario); + + properties.add(propertyDTO); + requestDTO.setProperties(properties); + return requestDTO; + } } diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java index bb460d3818..6692ea1c9b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java @@ -18,16 +18,9 @@ package org.wso2.carbon.identity.recovery.store; -import org.apache.commons.lang.StringUtils; -import org.apache.commons.logging.Log; -import org.apache.commons.logging.LogFactory; -import org.mockito.ArgumentCaptor; -import org.mockito.ArgumentMatchers; -import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; import org.mockito.MockitoAnnotations; -import org.testng.Assert; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.DataProvider; @@ -38,10 +31,8 @@ import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.services.IdentityEventService; -import org.wso2.carbon.identity.governance.IdentityGovernanceService; import org.wso2.carbon.identity.recovery.IdentityRecoveryClientException; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; -import org.wso2.carbon.identity.recovery.IdentityRecoveryException; import org.wso2.carbon.identity.recovery.IdentityRecoveryServerException; import org.wso2.carbon.identity.recovery.RecoveryScenarios; import org.wso2.carbon.identity.recovery.RecoverySteps; @@ -73,6 +64,9 @@ import static org.testng.Assert.assertTrue; import static org.testng.Assert.fail; +/** + * Unit tests for JDBCRecoveryDataStore. + */ public class JDBCRecoveryDataStoreTest { private UserRecoveryDataStore userRecoveryDataStore; diff --git a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml index 1abefbf250..29d14933fd 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.recovery/src/test/resources/testng.xml @@ -31,6 +31,7 @@ + From 97d5614d417203904b394ab3f1b56629134424f3 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Tue, 1 Oct 2024 20:38:33 +0530 Subject: [PATCH 49/54] Refine unit tests. --- .../impl/ResendCodeApiServiceImplTest.java | 84 ++++++++++++++----- .../store/JDBCRecoveryDataStoreTest.java | 37 +++++++- 2 files changed, 101 insertions(+), 20 deletions(-) diff --git a/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java b/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java index 9a55976bc0..8b949b7624 100644 --- a/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java +++ b/components/org.wso2.carbon.identity.api.user.governance/src/test/java/org/wso2/carbon/identity/user/endpoint/impl/ResendCodeApiServiceImplTest.java @@ -109,16 +109,21 @@ public void testResendCodePost() throws IdentityRecoveryException { assertEquals(resendCodeApiService.resendCodePost(resendCodeRequestDTO()).getStatus(), 201); assertEquals(resendCodeApiService.resendCodePost(emptyResendCodeRequestDTO()).getStatus(), 201); - assertEquals(resendCodeApiService.resendCodePost(emptyPropertyResendCodeRequestDTO()).getStatus(), 201); + assertEquals(resendCodeApiService.resendCodePost(emptyPropertyResendCodeRequestDTO()).getStatus(), + 201); assertEquals(resendCodeApiService.resendCodePost(multipleResendCodeRequestDTO()).getStatus(), 201); - mockedUtils.when(() -> Utils.getUserRecoveryData(recoveryScenarioResendCodeRequestDTO())).thenReturn(null); - assertEquals(resendCodeApiService.resendCodePost(recoveryScenarioResendCodeRequestDTO()).getStatus(), 400); + mockedUtils.when(() -> Utils.getUserRecoveryData(recoveryScenarioResendCodeRequestDTO())) + .thenReturn(null); + assertEquals(resendCodeApiService.resendCodePost(recoveryScenarioResendCodeRequestDTO()).getStatus(), + 400); mockedUtils.when(() -> Utils.getUserRecoveryData(recoveryScenarioResendCodeRequestDTO())).thenReturn( userRecoveryData); - assertEquals(resendCodeApiService.resendCodePost(recoveryScenarioResendCodeRequestDTO()).getStatus(), 400); - assertEquals(resendCodeApiService.resendCodePost(duplicateScenarioResendCodeRequestDTO()).getStatus(), 201); + assertEquals(resendCodeApiService.resendCodePost(recoveryScenarioResendCodeRequestDTO()).getStatus(), + 400); + assertEquals(resendCodeApiService.resendCodePost(duplicateScenarioResendCodeRequestDTO()).getStatus(), + 201); } @Test @@ -227,38 +232,79 @@ private PropertyDTO recoveryScenarioPropertyDTO() { @DataProvider(name = "recoveryScenarioProvider") public Object[][] recoveryScenarioProvider() { + return new Object[][] { - {RecoveryScenarios.ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD}, - {RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD}, - {RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP}, - {RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, RecoverySteps.UPDATE_PASSWORD}, - {RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, RecoverySteps.UPDATE_PASSWORD}, - {RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD}, - {RecoveryScenarios.LITE_SIGN_UP, RecoverySteps.CONFIRM_LITE_SIGN_UP}, - {RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL}, - {RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL}, - {RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER}, - {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER}, + {RecoveryScenarios.ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD, RecoveryScenarios.ASK_PASSWORD, + RecoverySteps.UPDATE_PASSWORD, 201}, + {RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD, + RecoveryScenarios.NOTIFICATION_BASED_PW_RECOVERY, RecoverySteps.UPDATE_PASSWORD, 201}, + {RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP, RecoveryScenarios.SELF_SIGN_UP, + RecoverySteps.CONFIRM_SIGN_UP, 201}, + {RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, RecoverySteps.UPDATE_PASSWORD, + RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, RecoverySteps.UPDATE_PASSWORD, 201}, + {RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, RecoverySteps.UPDATE_PASSWORD, + RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, RecoverySteps.UPDATE_PASSWORD, 201}, + {RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD, + RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD, 201}, + {RecoveryScenarios.LITE_SIGN_UP, RecoverySteps.CONFIRM_LITE_SIGN_UP, RecoveryScenarios.LITE_SIGN_UP, + RecoverySteps.CONFIRM_LITE_SIGN_UP, 201}, + + {RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, 201}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL, 400}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, 400}, + + {RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL, 201}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL, + RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, 400}, + {RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL, + RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER, 400}, + + {RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, 201}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER, 400}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL, 400}, + + {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER, 201}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, + RecoverySteps.VERIFY_MOBILE_NUMBER, 400}, + {RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER, + RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, + RecoverySteps.VERIFY_EMAIL, 400}, }; } @Test(dataProvider = "recoveryScenarioProvider") - public void testRecoveryScenarios(RecoveryScenarios scenario, RecoverySteps step) throws Exception { + public void testRecoveryScenarios(RecoveryScenarios scenario, RecoverySteps step, + RecoveryScenarios userRecoveryDataScenario, RecoverySteps userRecoveryDataStep, + int expectedStatusCode) throws Exception { ResendCodeRequestDTO requestDTO = createResendCodeRequestDTO(scenario.name()); User user = new User(); - UserRecoveryData recoveryData = new UserRecoveryData(user, "test-secret", scenario, step); + UserRecoveryData recoveryData = new UserRecoveryData(user, "test-secret", + userRecoveryDataScenario, userRecoveryDataStep); when(Utils.getUserRecoveryData(any(), anyString())).thenReturn(recoveryData); when(Utils.getResendConfirmationManager()).thenReturn(resendConfirmationManager); NotificationResponseBean expectedResponse = new NotificationResponseBean(user); + expectedResponse.setKey("test-key"); when(resendConfirmationManager.resendConfirmationCode(any(), anyString(), anyString(), anyString(), any())) .thenReturn(expectedResponse); Response result = resendCodeApiService.resendCodePost(requestDTO); assertNotNull(result); - assertEquals(result.getStatus(), Response.Status.CREATED.getStatusCode()); + assertEquals(result.getStatus(), expectedStatusCode); } private ResendCodeRequestDTO createResendCodeRequestDTO(String recoveryScenario) { diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java index 6692ea1c9b..623d5ca0e5 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/store/JDBCRecoveryDataStoreTest.java @@ -173,7 +173,15 @@ private Object[][] loadData() { { RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER }, { RecoveryScenarios.MOBILE_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_MOBILE_NUMBER }, { RecoveryScenarios.EMAIL_VERIFICATION_ON_UPDATE, RecoverySteps.VERIFY_EMAIL }, - { RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL } + { RecoveryScenarios.EMAIL_VERIFICATION_ON_VERIFIED_LIST_UPDATE, RecoverySteps.VERIFY_EMAIL }, + { RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD, RecoverySteps.VERIFY_EMAIL }, + { RecoveryScenarios.SELF_SIGN_UP, RecoverySteps.CONFIRM_SIGN_UP }, + { RecoveryScenarios.ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD }, + { RecoveryScenarios.TENANT_ADMIN_ASK_PASSWORD, RecoverySteps.UPDATE_PASSWORD }, + { RecoveryScenarios.LITE_SIGN_UP, RecoverySteps.CONFIRM_LITE_SIGN_UP }, + { RecoveryScenarios.LITE_SIGN_UP, RecoverySteps.VALIDATE_CHALLENGE_QUESTION }, + { RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_OTP, RecoverySteps.UPDATE_PASSWORD }, + { RecoveryScenarios.ADMIN_FORCED_PASSWORD_RESET_VIA_EMAIL_LINK, RecoverySteps.UPDATE_PASSWORD }, }; } @@ -236,6 +244,33 @@ private void mockExpiryTimes() { mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants .ConnectorConfig.MOBILE_NUM_VERIFICATION_ON_UPDATE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(IdentityRecoveryConstants + .ConnectorConfig.TENANT_ADMIN_ASK_PASSWORD_EXPIRY_TIME)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.ASK_PASSWORD_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.ADMIN_PASSWORD_RESET_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.SELF_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.SELF_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.RESEND_CODE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.LITE_REGISTRATION_VERIFICATION_CODE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); + mockedUtils.when(() -> Utils.getRecoveryConfigs(IdentityRecoveryConstants + .ConnectorConfig.LITE_REGISTRATION_SMSOTP_VERIFICATION_CODE_EXPIRY_TIME, TEST_TENANT_DOMAIN)) + .thenReturn("10"); } private void mockUtilsErrors() { From 744c4e8458d2435edfd0247f74bcccf019e31a4e Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Wed, 2 Oct 2024 12:22:44 +0530 Subject: [PATCH 50/54] Refine unit tests. --- .../MobileNumberVerificationHandlerTest.java | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index 61139d9673..a60c1dbf27 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -68,6 +68,7 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -184,6 +185,14 @@ public void testHandleEventVerificationDisabledMultiAttributeDisabled() Map userClaims = getUserClaimsFromEvent(event); Assert.assertEquals(userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBER_PENDING_VALUE_CLAIM), StringUtils.EMPTY); + + reset(userRecoveryDataStore); + // Case 2: User claims null. + Event event2 = + createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, + null, null, null); + mobileNumberVerificationHandler.handleEvent(event2); + verify(userRecoveryDataStore, never()).invalidate(any(), eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), any()); } @Test(description = "Verification disabled, Multi-attribute enabled, Change primary mobile") @@ -204,6 +213,36 @@ public void testHandleEventVerificationDisabledMultiAttributeEnabled() Map userClaims = getUserClaimsFromEvent(event); Assert.assertTrue(StringUtils.contains( userClaims.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), NEW_MOBILE_NUMBER)); + + // Case 2: Send mobile numbers claim with mobile number claim. + Event event2 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, + null, EXISTING_NUMBER_1, NEW_MOBILE_NUMBER); + mockVerificationPendingMobileNumber(); + mockUtilMethods(false, true, false); + + // New primary mobile number is not included in existing all mobile numbers list. + mockExistingNumbersList(null); + + // Expectation: New mobile number should be added to the mobile numbers claim. + mobileNumberVerificationHandler.handleEvent(event2); + Map userClaims2 = getUserClaimsFromEvent(event2); + Assert.assertTrue(StringUtils.contains( + userClaims2.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), NEW_MOBILE_NUMBER)); + Assert.assertTrue(StringUtils.contains( + userClaims2.get(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM), EXISTING_NUMBER_1)); + + // Case 3: Updated verified mobile numbers list. + Event event3 = createEvent(IdentityEventConstants.Event.PRE_SET_USER_CLAIM, null, + NEW_MOBILE_NUMBER, null, null); + mockVerificationPendingMobileNumber(); + + // Expectation: Verified mobile number claim should be removed from user claims. + mobileNumberVerificationHandler.handleEvent(event3); + verify(userRecoveryDataStore, times(3)).invalidate(any(), + eq(RecoveryScenarios.MOBILE_VERIFICATION_ON_UPDATE), + eq(RecoverySteps.VERIFY_MOBILE_NUMBER)); + Map userClaims3 = getUserClaimsFromEvent(event3); + Assert.assertFalse(userClaims3.containsKey(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM)); } @Test(description = "PRE_SET_USER_CLAIMS: Verification enabled, Multi-attribute disabled, Claims null") From c0c234325a64fa29b295b589672b886232f9ef91 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Wed, 2 Oct 2024 15:14:19 +0530 Subject: [PATCH 51/54] Add missing theadlocal logic. --- .../recovery/handler/MobileNumberVerificationHandler.java | 1 + .../identity/recovery/handler/UserEmailVerificationHandler.java | 1 + 2 files changed, 2 insertions(+) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 0a65a0d4c2..151588cfab 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -149,6 +149,7 @@ public void handleEvent(Event event) throws IdentityEventException { } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { + Utils.unsetThreadLocalIsOnlyVerifiedMobileNumbersUpdated(); if (supportMultipleMobileNumbers && !claims.containsKey(IdentityRecoveryConstants.MOBILE_NUMBER_CLAIM)) { Utils.setThreadLocalIsOnlyVerifiedMobileNumbersUpdated(true); } diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 581e5f280a..6274f08bd8 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -286,6 +286,7 @@ public void handleEvent(Event event) throws IdentityEventException { } if (IdentityEventConstants.Event.PRE_SET_USER_CLAIMS.equals(eventName)) { + Utils.unsetThreadLocalIsOnlyVerifiedEmailAddressesUpdated(); if (supportMultipleEmails && !claims.containsKey(IdentityRecoveryConstants.EMAIL_ADDRESS_CLAIM)) { Utils.setThreadLocalIsOnlyVerifiedEmailAddressesUpdated(true); } From 6943e3c3e770d553b2f5f2c7e136a839f00cd2e5 Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Wed, 2 Oct 2024 16:40:10 +0530 Subject: [PATCH 52/54] Refactor code. --- .../MobileNumberVerificationHandler.java | 26 ++++++------- .../handler/UserEmailVerificationHandler.java | 37 +++++++++++-------- 2 files changed, 35 insertions(+), 28 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index 151588cfab..e3abd6e477 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -390,19 +390,6 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use throw new IdentityEventException(error, e); } - if (supportMultipleMobileNumbers && StringUtils.isNotBlank(existingMobileNumber)) { - if (!updatedVerifiedNumbersList.contains(existingMobileNumber)) { - updatedVerifiedNumbersList.add(existingMobileNumber); - claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); - } - if (!updatedAllNumbersList.contains(existingMobileNumber)) { - updatedAllNumbersList.add(existingMobileNumber); - claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, - String.join(multiAttributeSeparator, updatedAllNumbersList)); - } - } - if (supportMultipleMobileNumbers && updatedVerifiedNumbersList.contains(mobileNumber)) { Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate( IdentityRecoveryConstants.SkipMobileNumberVerificationOnUpdateStates @@ -421,6 +408,19 @@ private void preSetUserClaimOnMobileNumberUpdate(Map claims, Use Utils.setThreadLocalToSkipSendingSmsOtpVerificationOnUpdate(IdentityRecoveryConstants .SkipMobileNumberVerificationOnUpdateStates.SKIP_ON_EXISTING_MOBILE_NUM.toString()); invalidatePendingMobileVerification(user, userStoreManager, claims); + + if (supportMultipleMobileNumbers) { + if (!updatedVerifiedNumbersList.contains(existingMobileNumber)) { + updatedVerifiedNumbersList.add(existingMobileNumber); + claims.put(IdentityRecoveryConstants.VERIFIED_MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedVerifiedNumbersList)); + } + if (!updatedAllNumbersList.contains(existingMobileNumber)) { + updatedAllNumbersList.add(existingMobileNumber); + claims.put(IdentityRecoveryConstants.MOBILE_NUMBERS_CLAIM, + String.join(multiAttributeSeparator, updatedAllNumbersList)); + } + } return; } /* diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java index 6274f08bd8..8555927cfa 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/UserEmailVerificationHandler.java @@ -589,6 +589,13 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore getListOfEmailAddressesFromString(claims.get(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM)) : existingAllEmailAddresses; + if (updatedAllEmailAddresses == null) { + updatedAllEmailAddresses = new ArrayList<>(); + } + if (updatedVerifiedEmailAddresses == null) { + updatedVerifiedEmailAddresses = new ArrayList<>(); + } + // Find the verification pending email address and remove it from verified email addresses in the payload. if (emailAddress == null && CollectionUtils.isNotEmpty(updatedVerifiedEmailAddresses)) { emailAddress = getVerificationPendingEmailAddress(existingVerifiedEmailAddresses, @@ -601,9 +608,8 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore addresses list, as verified email addresses list should not contain email addresses that are not in the email addresses list. */ - if (updatedAllEmailAddresses != null) { - updatedVerifiedEmailAddresses.removeIf(number -> !updatedAllEmailAddresses.contains(number)); - } + final List tempUpdatedAllEmailAddresses = new ArrayList<>(updatedAllEmailAddresses); + updatedVerifiedEmailAddresses.removeIf(number -> !tempUpdatedAllEmailAddresses.contains(number)); claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); @@ -626,18 +632,6 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore } String existingEmail = getEmailClaimValue(user, userStoreManager); - if (StringUtils.isNotBlank(existingEmail) && supportMultipleEmails) { - if (!updatedVerifiedEmailAddresses.contains(existingEmail)) { - updatedVerifiedEmailAddresses.add(existingEmail); - claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, - StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); - } - if (!updatedAllEmailAddresses.contains(existingEmail)) { - updatedAllEmailAddresses.add(existingEmail); - claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, - StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); - } - } if (supportMultipleEmails && updatedVerifiedEmailAddresses.contains(emailAddress)) { if (log.isDebugEnabled()) { @@ -660,6 +654,19 @@ private void preSetUserClaimsOnEmailUpdate(Map claims, UserStore Utils.setThreadLocalToSkipSendingEmailVerificationOnUpdate(IdentityRecoveryConstants .SkipEmailVerificationOnUpdateStates.SKIP_ON_EXISTING_EMAIL.toString()); invalidatePendingEmailVerification(user, userStoreManager, claims); + + if (supportMultipleEmails) { + if (!updatedVerifiedEmailAddresses.contains(existingEmail)) { + updatedVerifiedEmailAddresses.add(existingEmail); + claims.put(IdentityRecoveryConstants.VERIFIED_EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedVerifiedEmailAddresses, multiAttributeSeparator)); + } + if (!updatedAllEmailAddresses.contains(existingEmail)) { + updatedAllEmailAddresses.add(existingEmail); + claims.put(IdentityRecoveryConstants.EMAIL_ADDRESSES_CLAIM, + StringUtils.join(updatedAllEmailAddresses, multiAttributeSeparator)); + } + } return; } From 4a4d01028836612bf7c85a1b1c5ff14c11c96f7a Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Thu, 3 Oct 2024 12:22:14 +0530 Subject: [PATCH 53/54] Change SMS notification handler. --- .../recovery/handler/MobileNumberVerificationHandler.java | 3 ++- .../recovery/handler/MobileNumberVerificationHandlerTest.java | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java index e3abd6e477..fc33313e8b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandler.java @@ -240,7 +240,7 @@ private void triggerNotification(User user, String code, Property[] props, Strin log.debug("Sending: " + notificationType + " notification to user: " + user.toFullQualifiedUsername()); } - String eventName = IdentityEventConstants.Event.TRIGGER_SMS_NOTIFICATION; + String eventName = Utils.resolveEventName(NotificationChannels.SMS_CHANNEL.getChannelType()); HashMap properties = new HashMap<>(); properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUserName()); properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, user.getTenantDomain()); @@ -258,6 +258,7 @@ private void triggerNotification(User user, String code, Property[] props, Strin } if (StringUtils.isNotBlank(code)) { properties.put(IdentityRecoveryConstants.CONFIRMATION_CODE, code); + properties.put(IdentityRecoveryConstants.OTP_TOKEN_STRING, code); } Event identityMgtEvent = new Event(eventName, properties); diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java index a60c1dbf27..3d407f715b 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/handler/MobileNumberVerificationHandlerTest.java @@ -34,6 +34,7 @@ import org.testng.Assert; import org.wso2.carbon.identity.event.IdentityEventConstants; import org.wso2.carbon.identity.event.services.IdentityEventService; +import org.wso2.carbon.identity.governance.service.notification.NotificationChannels; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryException; import org.wso2.carbon.identity.recovery.RecoveryScenarios; @@ -134,6 +135,8 @@ public void setUpMethod() throws UserStoreException { mockedIdentityRecoveryServiceDataHolder.when(IdentityRecoveryServiceDataHolder::getInstance) .thenReturn(serviceDataHolder); mockedFrameworkUtils.when(FrameworkUtils::getMultiAttributeSeparator).thenReturn(","); + mockedUtils.when(() -> Utils.resolveEventName(NotificationChannels.SMS_CHANNEL.getChannelType())).thenReturn( + "TRIGGER_SMS_NOTIFICATION_LOCAL"); when(serviceDataHolder.getRealmService()).thenReturn(realmService); when(serviceDataHolder.getIdentityEventService()).thenReturn(identityEventService); From 1929c98274745be73ac47dc01505852e17437f7a Mon Sep 17 00:00:00 2001 From: Pasindu Yeshan Date: Tue, 8 Oct 2024 11:02:21 +0530 Subject: [PATCH 54/54] Change SMS notification handler to local. --- .../recovery/confirmation/ResendConfirmationManager.java | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java index fbd799c538..c9477e6ce7 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/confirmation/ResendConfirmationManager.java @@ -570,8 +570,7 @@ private String resolveEventName(String preferredChannel, String userName, String String eventName; if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(preferredChannel)) { - eventName = IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_PREFIX + preferredChannel - + IdentityRecoveryConstants.NOTIFICATION_EVENTNAME_SUFFIX; + eventName = Utils.resolveEventName(preferredChannel); } else { eventName = IdentityEventConstants.Event.TRIGGER_NOTIFICATION; }