diff --git a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImpl.java b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImpl.java index 1c320016e..63b51ed0e 100644 --- a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImpl.java +++ b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImpl.java @@ -56,6 +56,9 @@ public class IdentityGovernanceServiceImpl implements IdentityGovernanceService private static final String EMAIL_LINK_PASSWORD_RECOVERY_PROPERTY = "Recovery.Notification.Password.emailLink.Enable"; private static final String SMS_OTP_PASSWORD_RECOVERY_PROPERTY = "Recovery.Notification.Password.smsOtp.Enable"; + private static final String USERNAME_RECOVERY_ENABLE = "Recovery.Notification.Username.Enable"; + private static final String USERNAME_RECOVERY_EMAIL_ENABLE = "Recovery.Notification.Username.Email.Enable"; + private static final String USERNAME_RECOVERY_SMS_ENABLE = "Recovery.Notification.Username.SMS.Enable"; private static final String FALSE_STRING = "false"; public void updateConfiguration(String tenantDomain, Map configurationDetails) @@ -70,6 +73,7 @@ public void updateConfiguration(String tenantDomain, Map configu updateEmailOTPNumericPropertyValue(configurationDetails); IdPManagementUtil.validatePasswordRecoveryPropertyValues(configurationDetails); updatePasswordRecoveryPropertyValues(configurationDetails, identityMgtProperties); + updateUsernameRecoveryPropertyValues(configurationDetails, identityMgtProperties); for (IdentityProviderProperty identityMgtProperty : identityMgtProperties) { IdentityProviderProperty prop = new IdentityProviderProperty(); String key = identityMgtProperty.getName(); @@ -407,4 +411,63 @@ private void updatePasswordRecoveryPropertyValues(Map configurat } } } + + /** + * This method updates the username recovery property values based on the new configurations. + * + * @param configurationDetails Updating configuration details of the resident identity provider. + * @param identityMgtProperties Identity management properties of the resident identity provider. + */ + private void updateUsernameRecoveryPropertyValues(Map configurationDetails, + IdentityProviderProperty[] identityMgtProperties) { + + if (configurationDetails.containsKey(USERNAME_RECOVERY_ENABLE) || + configurationDetails.containsKey(USERNAME_RECOVERY_EMAIL_ENABLE) || + configurationDetails.containsKey(USERNAME_RECOVERY_SMS_ENABLE)) { + + String usernameRecoveryProp = configurationDetails.get(USERNAME_RECOVERY_ENABLE); + String usernameRecoveryEmailProp = configurationDetails.get(USERNAME_RECOVERY_EMAIL_ENABLE); + String usernameRecoverySmsProp = configurationDetails.get(USERNAME_RECOVERY_SMS_ENABLE); + + boolean usernameRecoveryProperty = Boolean.parseBoolean(usernameRecoveryProp); + boolean usernameRecoveryEmailProperty = Boolean.parseBoolean(usernameRecoveryEmailProp); + boolean usernameRecoverySmsProperty = Boolean.parseBoolean(usernameRecoverySmsProp); + + if(usernameRecoveryProperty) { + configurationDetails.put(USERNAME_RECOVERY_EMAIL_ENABLE, + String.valueOf(usernameRecoveryEmailProperty || + StringUtils.isBlank(usernameRecoveryEmailProp))); + configurationDetails.put(USERNAME_RECOVERY_SMS_ENABLE, + String.valueOf(usernameRecoverySmsProperty || + StringUtils.isBlank(usernameRecoverySmsProp))); + } else if (StringUtils.isBlank(usernameRecoveryProp)) { + // Connector is not explicitly enabled or disabled. The connector state is derived from new and existing + // configurations. + boolean isUsernameEmailRecoveryCurrentlyEnabled = false; + boolean isUsernameSmsRecoveryCurrentlyEnabled = false; + for (IdentityProviderProperty identityMgtProperty : identityMgtProperties) { + if (USERNAME_RECOVERY_EMAIL_ENABLE.equals(identityMgtProperty.getName())) { + isUsernameEmailRecoveryCurrentlyEnabled = Boolean.parseBoolean(identityMgtProperty.getValue()); + } else if (USERNAME_RECOVERY_SMS_ENABLE.equals(identityMgtProperty.getName())) { + isUsernameSmsRecoveryCurrentlyEnabled = Boolean.parseBoolean(identityMgtProperty.getValue()); + } + } + boolean enableUsernameEmailRecovery = usernameRecoveryEmailProperty || + ( StringUtils.isBlank(usernameRecoveryEmailProp) && + isUsernameEmailRecoveryCurrentlyEnabled ); + boolean enableUsernameSmsRecovery = usernameRecoverySmsProperty || + ( StringUtils.isBlank(usernameRecoverySmsProp) && + isUsernameSmsRecoveryCurrentlyEnabled ); + configurationDetails.put(USERNAME_RECOVERY_EMAIL_ENABLE, + String.valueOf(enableUsernameEmailRecovery)); + configurationDetails.put(USERNAME_RECOVERY_SMS_ENABLE, + String.valueOf(enableUsernameSmsRecovery)); + configurationDetails.put(USERNAME_RECOVERY_ENABLE, + String.valueOf(enableUsernameEmailRecovery || enableUsernameSmsRecovery)); + } else { + configurationDetails.put(USERNAME_RECOVERY_EMAIL_ENABLE, FALSE_STRING); + configurationDetails.put(USERNAME_RECOVERY_SMS_ENABLE, FALSE_STRING); + } + } + } } diff --git a/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImplTest.java b/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImplTest.java new file mode 100644 index 000000000..d080d8a5e --- /dev/null +++ b/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/IdentityGovernanceServiceImplTest.java @@ -0,0 +1,255 @@ +/* + * Copyright (c) 2024, WSO2 LLC. (https://www.wso2.org) + * + * 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.governance; + +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockedStatic; +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.common.model.FederatedAuthenticatorConfig; +import org.wso2.carbon.identity.application.common.model.IdentityProvider; +import org.wso2.carbon.identity.application.common.model.IdentityProviderProperty; +import org.wso2.carbon.identity.governance.internal.IdentityMgtServiceDataHolder; +import org.wso2.carbon.idp.mgt.IdentityProviderManagementException; +import org.wso2.carbon.idp.mgt.IdpManager; + +import java.util.HashMap; +import java.util.Map; + +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; +import static org.testng.AssertJUnit.assertEquals; + +public class IdentityGovernanceServiceImplTest { + + // Constants. + private static final String TENANT_DOMAIN = "carbon.super"; + private static final String TRUE_STRING = "true"; + private static final String FALSE_STRING = "false"; + private static final String USERNAME_RECOVERY_ENABLE = "Recovery.Notification.Username.Enable"; + private static final String USERNAME_RECOVERY_EMAIL_ENABLE = "Recovery.Notification.Username.Email.Enable"; + private static final String USERNAME_RECOVERY_SMS_ENABLE = "Recovery.Notification.Username.SMS.Enable"; + + @Mock + IdentityMgtServiceDataHolder identityMgtServiceDataHolder; + + @Mock + IdpManager idpManager; + + @Mock + IdentityProvider identityProvider; + + MockedStatic identityMgtServiceDataHolderMockedStatic; + + private IdentityGovernanceServiceImpl identityGovernanceService; + + @BeforeMethod + public void setup() throws IdentityProviderManagementException { + + MockitoAnnotations.openMocks(this); + identityMgtServiceDataHolderMockedStatic = mockStatic(IdentityMgtServiceDataHolder.class); + identityMgtServiceDataHolderMockedStatic.when(IdentityMgtServiceDataHolder:: + getInstance).thenReturn(identityMgtServiceDataHolder); + when(identityMgtServiceDataHolder.getIdpManager()).thenReturn(idpManager); + when(idpManager.getResidentIdP(TENANT_DOMAIN)).thenReturn(identityProvider); + + FederatedAuthenticatorConfig[] authenticatorConfigs = new FederatedAuthenticatorConfig[0]; + when(identityProvider.getFederatedAuthenticatorConfigs()).thenReturn(authenticatorConfigs); + + identityGovernanceService = new IdentityGovernanceServiceImpl(); + } + + @AfterMethod + public void tearDown() { + + identityMgtServiceDataHolderMockedStatic.close(); + } + + @Test(dataProvider = "updateConfigurations") + public void testUpdateConfiguration(Map configurationDetails, + IdentityProviderProperty[] identityProviderProperties, + Map expected) throws IdentityGovernanceException { + + when(identityProvider.getIdpProperties()).thenReturn(identityProviderProperties); + + identityGovernanceService.updateConfiguration(TENANT_DOMAIN, configurationDetails); + + // Capture the arguments passed to setIdpProperties + ArgumentCaptor + argumentCaptor = ArgumentCaptor.forClass(IdentityProviderProperty[].class); + verify(identityProvider).setIdpProperties(argumentCaptor.capture()); + + // Assert + IdentityProviderProperty[] capturedProperties = argumentCaptor.getValue(); + for (IdentityProviderProperty capturedProperty : capturedProperties) { + assertEquals(expected.get(capturedProperty.getName()), capturedProperty.getValue()); + } + + } + + @DataProvider(name = "updateConfigurations") + public Object[][] buildConfigurations() { + + // Only email config is true. Preconditions: all the configs false. + Map usernameConfig1 = new HashMap<>(); + usernameConfig1.put(USERNAME_RECOVERY_EMAIL_ENABLE, TRUE_STRING); + + IdentityProviderProperty[] identityProviderProperties1 = getIdentityProviderProperties( + false, false, false); + Map expected1 = getExpectedPropertyValues(true, true, false); + + // Only sms config is true. Preconditions: all the configs false. + Map usernameConfig2 = new HashMap<>(); + usernameConfig2.put(USERNAME_RECOVERY_SMS_ENABLE, TRUE_STRING); + + IdentityProviderProperty[] identityProviderProperties2 = getIdentityProviderProperties( + false, false, false); + Map expected2 = getExpectedPropertyValues(true, false, true); + + // Only sms is false. Preconditions: sms and username is true. + Map usernameConfig3 = new HashMap<>(); + usernameConfig3.put(USERNAME_RECOVERY_SMS_ENABLE, FALSE_STRING); + + IdentityProviderProperty[] identityProviderProperties3 = getIdentityProviderProperties( + true, false, true); + Map expected3 = getExpectedPropertyValues(false, false, false); + + // Only email is false. Preconditions: email and username is true. + Map usernameConfig4 = new HashMap<>(); + usernameConfig4.put(USERNAME_RECOVERY_EMAIL_ENABLE, FALSE_STRING); + + IdentityProviderProperty[] identityProviderProperties4 = getIdentityProviderProperties( + true, true, false); + Map expected4 = getExpectedPropertyValues(false, false, false); + + // Only email is true. Preconditions: sms and username is true. + Map usernameConfig5 = new HashMap<>(); + usernameConfig5.put(USERNAME_RECOVERY_EMAIL_ENABLE, TRUE_STRING); + + IdentityProviderProperty[] identityProviderProperties5 = getIdentityProviderProperties( + true, false, true); + Map expected5 = getExpectedPropertyValues(true, true, true); + + // Only sms is true. Preconditions: email and username is true. + Map usernameConfig6 = new HashMap<>(); + usernameConfig6.put(USERNAME_RECOVERY_SMS_ENABLE, TRUE_STRING); + + IdentityProviderProperty[] identityProviderProperties6 = getIdentityProviderProperties( + true, true, false); + Map expected6 = getExpectedPropertyValues(true, true, true); + + // Sms config true and email config false. Preconditions: all the configs false. + Map usernameConfig7 = new HashMap<>(); + usernameConfig7.put(USERNAME_RECOVERY_SMS_ENABLE, TRUE_STRING); + usernameConfig7.put(USERNAME_RECOVERY_EMAIL_ENABLE, FALSE_STRING); + + IdentityProviderProperty[] identityProviderProperties7 = getIdentityProviderProperties( + false, false, false); + Map expected7 = getExpectedPropertyValues(true, false, true); + + // Email config true and sms config false. Preconditions: all the configs false. + Map usernameConfig8 = new HashMap<>(); + usernameConfig8.put(USERNAME_RECOVERY_EMAIL_ENABLE, TRUE_STRING); + usernameConfig8.put(USERNAME_RECOVERY_SMS_ENABLE, FALSE_STRING); + + IdentityProviderProperty[] identityProviderProperties8 = getIdentityProviderProperties( + false, false, false); + Map expected8 = getExpectedPropertyValues(true, true, false); + + // Sms config true and email config true. Preconditions: all the configs false. + Map usernameConfig9 = new HashMap<>(); + usernameConfig9.put(USERNAME_RECOVERY_SMS_ENABLE, TRUE_STRING); + usernameConfig9.put(USERNAME_RECOVERY_EMAIL_ENABLE, TRUE_STRING); + + IdentityProviderProperty[] identityProviderProperties9 = getIdentityProviderProperties( + false, false, false); + Map expected9 = getExpectedPropertyValues(true, true, true); + + // Only username config true. Preconditions: all the configs false. + Map usernameConfig10 = new HashMap<>(); + usernameConfig10.put(USERNAME_RECOVERY_ENABLE, TRUE_STRING); + + IdentityProviderProperty[] identityProviderProperties10 = getIdentityProviderProperties( + false, false, false); + Map expected10 = getExpectedPropertyValues(true, true, true); + + // Only username config false. Preconditions: all the configs true. + Map usernameConfig11 = new HashMap<>(); + usernameConfig11.put(USERNAME_RECOVERY_ENABLE, FALSE_STRING); + + IdentityProviderProperty[] identityProviderProperties11 = getIdentityProviderProperties( + true, true, true); + Map expected11 = getExpectedPropertyValues(false, false, false); + + return new Object[][]{ + {usernameConfig1, identityProviderProperties1, expected1}, + {usernameConfig2, identityProviderProperties2, expected2}, + {usernameConfig3, identityProviderProperties3, expected3}, + {usernameConfig4, identityProviderProperties4, expected4}, + {usernameConfig5, identityProviderProperties5, expected5}, + {usernameConfig6, identityProviderProperties6, expected6}, + {usernameConfig7, identityProviderProperties7, expected7}, + {usernameConfig8, identityProviderProperties8, expected8}, + {usernameConfig9, identityProviderProperties9, expected9}, + {usernameConfig10, identityProviderProperties10, expected10}, + {usernameConfig11, identityProviderProperties11, expected11} + }; + + } + + private IdentityProviderProperty[] getIdentityProviderProperties(boolean usernameEnable, + boolean usernameEmailEnable, + boolean usernameSmsEnable) { + + IdentityProviderProperty identityProviderProperty1 = new IdentityProviderProperty(); + identityProviderProperty1.setName(USERNAME_RECOVERY_ENABLE); + identityProviderProperty1.setValue(usernameEnable ? TRUE_STRING : FALSE_STRING); + + IdentityProviderProperty identityProviderProperty2 = new IdentityProviderProperty(); + identityProviderProperty2.setName(USERNAME_RECOVERY_SMS_ENABLE); + identityProviderProperty2.setValue(usernameSmsEnable ? TRUE_STRING : FALSE_STRING); + + IdentityProviderProperty identityProviderProperty3 = new IdentityProviderProperty(); + identityProviderProperty3.setName(USERNAME_RECOVERY_EMAIL_ENABLE); + identityProviderProperty3.setValue(usernameEmailEnable ? TRUE_STRING : FALSE_STRING); + + return new IdentityProviderProperty[]{ + identityProviderProperty1, + identityProviderProperty2, + identityProviderProperty3 + }; + } + + private HashMap getExpectedPropertyValues(boolean usernameEnable, + boolean usernameEmailEnable, + boolean usernameSmsEnable) { + + HashMap expected = new HashMap<>(); + expected.put(USERNAME_RECOVERY_ENABLE, usernameEnable ? TRUE_STRING : FALSE_STRING); + expected.put(USERNAME_RECOVERY_EMAIL_ENABLE, usernameEmailEnable ? TRUE_STRING : FALSE_STRING); + expected.put(USERNAME_RECOVERY_SMS_ENABLE, usernameSmsEnable ? TRUE_STRING : FALSE_STRING); + + return expected; + } +} diff --git a/components/org.wso2.carbon.identity.governance/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.governance/src/test/resources/testng.xml index 6a4dc0744..0c10670da 100644 --- a/components/org.wso2.carbon.identity.governance/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.governance/src/test/resources/testng.xml @@ -23,8 +23,9 @@ - - + + + 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 89a02917e..53b3fa0ed 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 @@ -615,6 +615,8 @@ public static class ConnectorConfig { public static final String FORCE_ADD_PW_RECOVERY_QUESTION = "Recovery.Question.Password.Forced.Enable"; public static final String FORCE_MIN_NO_QUESTION_ANSWERED = "Recovery.Question.MinQuestionsToAnswer"; public static final String USERNAME_RECOVERY_ENABLE = "Recovery.Notification.Username.Enable"; + public static final String USERNAME_RECOVERY_EMAIL_ENABLE = "Recovery.Notification.Username.Email.Enable"; + public static final String USERNAME_RECOVERY_SMS_ENABLE = "Recovery.Notification.Username.SMS.Enable"; public static final String USERNAME_RECOVERY_NON_UNIQUE_USERNAME = "Recovery.Notification.Username.NonUniqueUsername"; public static final String QUESTION_CHALLENGE_SEPARATOR = "Recovery.Question.Password.Separator"; public static final String QUESTION_MIN_NO_ANSWER = "Recovery.Question.Password.MinAnswers"; diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java index 74ef59ffb..218e43aa0 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImpl.java @@ -99,6 +99,10 @@ public Map getPropertyNameMapping() { nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.QUESTION_MIN_NO_ANSWER, "Number of questions " + "required for password recovery"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE, "Username recovery"); + nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + "Notification based username recovery via EMAIL"); + nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + "Notification based username recovery via SMS"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE, "Enable reCaptcha for username recovery"); nameMapping.put(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME, "Recovery link expiry time in minutes"); @@ -193,6 +197,8 @@ public String[] getPropertyNames() { properties.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_ENABLE); properties.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_MAX_FAILED_ATTEMPTS); properties.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE); + properties.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE); + properties.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE); properties.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE); properties.add(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE); properties.add(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_NOTIFICATION_SUCCESS); @@ -227,6 +233,8 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG String enableRecoveryQuestionPasswordReCaptcha = "true"; String recoveryQuestionPasswordReCaptchaMaxFailedAttempts = "2"; String enableUsernameRecovery = "false"; + String enableUsernameRecoveryEmail = "false"; + String enableUsernameRecoverySMS = "false"; String enableNotificationInternallyManage = "true"; String expiryTime = "1440"; String expiryTimeSMSOTP = "1"; @@ -272,6 +280,10 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_MAX_FAILED_ATTEMPTS); String usernameRecovery = IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE); + String usernameRecoveryEmail = IdentityUtil.getProperty( + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE); + String usernameRecoverySMS = IdentityUtil.getProperty( + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE); String notificationInternallyManged = IdentityUtil.getProperty( IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE); String expiryTimeProperty = IdentityUtil.getProperty(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME); @@ -351,6 +363,14 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG } if (StringUtils.isNotEmpty(usernameRecovery)) { enableUsernameRecovery = usernameRecovery; + // Setting the username recovery value to keep backward compatibility. + enableUsernameRecoveryEmail = usernameRecovery; + } + if (StringUtils.isNotEmpty(usernameRecoveryEmail)){ + enableUsernameRecoveryEmail = usernameRecoveryEmail; + } + if (StringUtils.isNotEmpty(usernameRecoverySMS)) { + enableUsernameRecoverySMS = usernameRecoverySMS; } if (StringUtils.isNotEmpty(expiryTimeProperty)) { expiryTime = expiryTimeProperty; @@ -433,6 +453,10 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG recoveryQuestionPasswordReCaptchaMaxFailedAttempts); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE, enableUsernameRecovery); + defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + enableUsernameRecoveryEmail); + defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + enableUsernameRecoverySMS); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE, enableUsernameRecoveryReCaptcha); defaultProperties.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE, @@ -518,6 +542,12 @@ public Map getMetaData() { meta.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE, getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); + meta.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); + + meta.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); + meta.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE, getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue())); diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java index 79f04f274..e9c5e32d8 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManager.java @@ -23,6 +23,7 @@ import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.application.common.model.User; +import org.wso2.carbon.identity.base.IdentityException; import org.wso2.carbon.identity.claim.metadata.mgt.exception.ClaimMetadataException; import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.core.util.IdentityUtil; @@ -108,6 +109,107 @@ public static UserAccountRecoveryManager getInstance() { return instance; } + /** + * Initiate the username recovery flow for the user with matching claims when non-unique user config enabled. + * + * @param claims User claims + * @param tenantDomain Tenant domain + * @param properties Meta properties + * @return RecoveryChannelInfoDTO object. + */ + public RecoveryChannelInfoDTO retrieveUsersRecoveryInformationForUsername(Map claims, + String tenantDomain, + Map properties) + throws IdentityRecoveryException { + + RecoveryScenarios recoveryScenario = RecoveryScenarios.USERNAME_RECOVERY; + // Retrieve the user who matches the given set of claims. + ArrayList resultedUserList = getUserListByClaims(claims, tenantDomain); + + if (!resultedUserList.isEmpty()) { + StringBuilder usernameCombined = new StringBuilder(); + // Get the notification management mechanism. + List notificationChannels; + boolean isNotificationsInternallyManaged = Utils.isNotificationsInternallyManaged(tenantDomain, properties); + String recoveryFlowId = null; + String recoveryCode = null; + String notificationChannelList = null; + String username = null; + NotificationChannelDTO[] notificationChannelDTOS = null; + + for (org.wso2.carbon.user.core.common.User resultedUser : resultedUserList) { + username = resultedUser.getUsername(); + User user = Utils.buildUser(username, tenantDomain); + + try { + // If the account is locked or disabled, do not let the user, recover the account. + checkAccountLockedStatus(user); + + } catch (IdentityException e) { + if (log.isDebugEnabled()) { + log.debug(username + " is locked."); + } + continue; + } + + /* If the notification is internally managed, then notification channels available for the user needs to + be retrieved. If external notifications are enabled, external channel list should be returned.*/ + if (isNotificationsInternallyManaged) { + notificationChannels = getInternalNotificationChannelList(username, tenantDomain, + recoveryScenario); + } else { + notificationChannels = getExternalNotificationChannelList(); + } + + // Validate whether the user account is eligible for account recovery. + checkUserValidityForAccountRecovery(user, recoveryScenario, notificationChannels, properties); + // This flow will be initiated only if the user has any verified channels. + notificationChannelDTOS = getNotificationChannelsResponseDTOList( + tenantDomain, notificationChannels); + UserRecoveryDataStore userRecoveryDataStore = JDBCRecoveryDataStore.getInstance(); + // Get the existing RESEND_CONFIRMATION_CODE details if there is any. + UserRecoveryData recoveryDataDO = userRecoveryDataStore.loadWithoutCodeExpiryValidation( + user, recoveryScenario, RecoverySteps.RESEND_CONFIRMATION_CODE); + + notificationChannelList = getNotificationChannelListForRecovery(notificationChannels); + recoveryCode = UUID.randomUUID().toString(); + recoveryFlowId = UUID.randomUUID().toString(); + + if (Utils.reIssueExistingConfirmationCode(recoveryDataDO, + NotificationChannels.EMAIL_CHANNEL.getChannelType())) { + /* Update the existing RESEND_CONFIRMATION_CODE details with new code details without changing the + time created of the RESEND_CONFIRMATION_CODE. */ + userRecoveryDataStore.invalidateWithoutChangeTimeCreated(recoveryDataDO.getSecret(), recoveryCode, + RecoverySteps.SEND_RECOVERY_INFORMATION, notificationChannelList); + } else { + if (usernameCombined.length() > 0) { + usernameCombined.append(","); + } + usernameCombined.append(username); + } + } + if (StringUtils.isBlank(usernameCombined.toString())) { + if (log.isDebugEnabled()) { + log.debug("No valid user found for the given claims"); + } + throw Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, + null); + } + addRecoveryDataObject(usernameCombined.toString(), tenantDomain, recoveryFlowId, recoveryCode, + recoveryScenario, + notificationChannelList); + + return buildUserRecoveryInformationResponseDTO(username, recoveryFlowId, recoveryCode, + notificationChannelDTOS); + + } else { + if (log.isDebugEnabled()) { + log.debug("No valid user found for the given claims"); + } + throw Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null); + } + } + /** * Initiate the recovery flow for the user with matching claims. * diff --git a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java index f661daec6..742155fe9 100644 --- a/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java +++ b/components/org.wso2.carbon.identity.recovery/src/main/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImpl.java @@ -37,6 +37,8 @@ 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.dto.NotificationChannelDTO; +import org.wso2.carbon.identity.recovery.dto.RecoveryChannelInfoDTO; import org.wso2.carbon.identity.recovery.dto.RecoveryInformationDTO; import org.wso2.carbon.identity.recovery.dto.UsernameRecoverDTO; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; @@ -55,6 +57,7 @@ import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import static org.wso2.carbon.identity.application.authentication.framework.util.FrameworkConstants.AUDIT_FAILED; @@ -134,9 +137,35 @@ public RecoveryInformationDTO initiate(Map claims, String tenant Map metaProperties = new HashMap<>(); metaProperties.put(IdentityRecoveryConstants.MANAGE_NOTIFICATIONS_INTERNALLY_PROPERTY_KEY, Boolean.toString(manageNotificationsInternally)); - recoveryInformationDTO.setRecoveryChannelInfoDTO(userAccountRecoveryManager - .retrieveUserRecoveryInformation(claims, tenantDomain, RecoveryScenarios.USERNAME_RECOVERY, - metaProperties)); + + boolean nonUniqueUsernameEnabled = Boolean.parseBoolean(IdentityUtil.getProperty( + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME)); + + RecoveryChannelInfoDTO recoveryChannelInfoDTO; + if (nonUniqueUsernameEnabled) { + recoveryChannelInfoDTO = userAccountRecoveryManager + .retrieveUsersRecoveryInformationForUsername(claims, tenantDomain, metaProperties); + } else { + recoveryChannelInfoDTO = userAccountRecoveryManager + .retrieveUserRecoveryInformation(claims, tenantDomain, RecoveryScenarios.USERNAME_RECOVERY, + metaProperties); + } + + // Filtering the notification channel list. + List enabledNotificationChannelDTOs = new ArrayList<>(); + for (NotificationChannelDTO notificationChannelDTO : recoveryChannelInfoDTO.getNotificationChannelDTOs()) { + if (isRecoveryChannelEnabled(notificationChannelDTO.getType(), tenantDomain)) { + enabledNotificationChannelDTOs.add(notificationChannelDTO); + } + } + recoveryChannelInfoDTO.setNotificationChannelDTOs( + enabledNotificationChannelDTOs.toArray(new NotificationChannelDTO[0])); + String username = recoveryChannelInfoDTO.getUsername(); + String recoveryFlowId = recoveryChannelInfoDTO.getRecoveryFlowId(); + recoveryInformationDTO.setUsername(username); + recoveryInformationDTO.setRecoveryFlowId(recoveryFlowId); + // Do not add recovery channel information if Notification based recovery is not enabled. + recoveryInformationDTO.setRecoveryChannelInfoDTO(recoveryChannelInfoDTO); return recoveryInformationDTO; } @@ -263,9 +292,15 @@ private UsernameRecoverDTO buildUserNameRecoveryResponseDTO(User user, String no // If notifications are externally managed, username needs to be sent with the request. // Build username for external notification. - String username = - String.format(qualifiedUsernameRegexPattern, user.getUserName(), user.getTenantDomain()); - usernameRecoverDTO.setUsername(username); + StringBuilder usernameCombined = new StringBuilder(); + String[] usernames = user.getUserName().split(","); + for (String username : usernames) { + if(usernameCombined.length() > 0) { + usernameCombined.append(","); + } + usernameCombined.append(String.format(qualifiedUsernameRegexPattern, username, user.getTenantDomain())); + } + usernameRecoverDTO.setUsername(usernameCombined.toString()); } else { usernameRecoverDTO.setCode( IdentityRecoveryConstants.SuccessEvents.SUCCESS_STATUS_CODE_USERNAME_INTERNALLY_NOTIFIED.getCode()); @@ -313,27 +348,36 @@ private void triggerNotification(User user, String notificationChannel, String e Map metaProperties) throws IdentityRecoveryException { - HashMap properties = new HashMap<>(); - properties.put(IdentityEventConstants.EventProperty.USER_NAME, user.getUserName()); - properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, user.getTenantDomain()); - properties.put(IdentityEventConstants.EventProperty.USER_STORE_DOMAIN, user.getUserStoreDomain()); - properties.put(IdentityEventConstants.EventProperty.NOTIFICATION_CHANNEL, notificationChannel); - if (metaProperties != null) { - for (String key : metaProperties.keySet()) { - String value = metaProperties.get(key); - if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) { - properties.put(key, value); + if (!isRecoveryChannelEnabled(notificationChannel, user.getTenantDomain())) { + throw Utils.handleClientException( + IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CHANNEL_ID, null); + } + + String combinedUsernames = user.getUserName(); + String[] usernames = combinedUsernames.split(","); + for (String username : usernames) { + HashMap properties = new HashMap<>(); + properties.put(IdentityEventConstants.EventProperty.USER_NAME, username); + properties.put(IdentityEventConstants.EventProperty.TENANT_DOMAIN, user.getTenantDomain()); + properties.put(IdentityEventConstants.EventProperty.USER_STORE_DOMAIN, user.getUserStoreDomain()); + properties.put(IdentityEventConstants.EventProperty.NOTIFICATION_CHANNEL, notificationChannel); + if (metaProperties != null) { + for (String key : metaProperties.keySet()) { + String value = metaProperties.get(key); + if (StringUtils.isNotBlank(key) && StringUtils.isNotBlank(value)) { + properties.put(key, value); + } } } - } - properties.put(IdentityRecoveryConstants.TEMPLATE_TYPE, - IdentityRecoveryConstants.NOTIFICATION_ACCOUNT_ID_RECOVERY); - Event identityMgtEvent = new Event(eventName, properties); - try { - IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService().handleEvent(identityMgtEvent); - } catch (IdentityEventException e) { - throw Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_TRIGGER_NOTIFICATION, - user.getUserName(), e); + properties.put(IdentityRecoveryConstants.TEMPLATE_TYPE, + IdentityRecoveryConstants.NOTIFICATION_ACCOUNT_ID_RECOVERY); + Event identityMgtEvent = new Event(eventName, properties); + try { + IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService().handleEvent(identityMgtEvent); + } catch (IdentityEventException e) { + throw Utils.handleServerException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_TRIGGER_NOTIFICATION, + user.getUserName(), e); + } } } @@ -467,4 +511,47 @@ private void auditUserNameRecovery(String action, Map claims, St } Utils.createAuditMessage(action, target, dataObject, result); } + + private boolean isRecoveryChannelEnabled(String notificationChannelType, String tenantDomain) + throws IdentityRecoveryServerException { + + if (NotificationChannels.EMAIL_CHANNEL.getChannelType().equals(notificationChannelType)) { + return isEmailBasedRecoveryEnabled(tenantDomain); + } else if (NotificationChannels.SMS_CHANNEL.getChannelType().equals(notificationChannelType)) { + return isSMSBasedRecoveryEnabled(tenantDomain); + } + return false; + } + + private boolean isEmailBasedRecoveryEnabled(String tenantDomain) throws IdentityRecoveryServerException { + + try { + return Boolean.parseBoolean( + Utils.getRecoveryConfigs( + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + tenantDomain)); + } catch (IdentityRecoveryServerException e) { + // Prepend scenario to the thrown exception. + String errorCode = Utils + .prependOperationScenarioToErrorCode(IdentityRecoveryConstants.USER_NAME_RECOVERY, + e.getErrorCode()); + throw Utils.handleServerException(errorCode, e.getMessage(), null); + } + } + + private boolean isSMSBasedRecoveryEnabled(String tenantDomain) throws IdentityRecoveryServerException { + + try { + return Boolean.parseBoolean( + Utils.getRecoveryConfigs( + IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + tenantDomain)); + } catch (IdentityRecoveryServerException e) { + // Prepend scenario to the thrown exception. + String errorCode = Utils + .prependOperationScenarioToErrorCode(IdentityRecoveryConstants.USER_NAME_RECOVERY, + e.getErrorCode()); + throw Utils.handleServerException(errorCode, e.getMessage(), null); + } + } } diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java index beaf4d247..8975a13b8 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/connector/RecoveryConfigImplTest.java @@ -18,8 +18,13 @@ package org.wso2.carbon.identity.recovery.connector; import org.apache.commons.lang.StringUtils; +import org.mockito.MockedStatic; +import org.mockito.Mockito; import org.testng.annotations.BeforeTest; +import org.testng.annotations.DataProvider; import org.testng.annotations.Test; +import org.wso2.carbon.identity.application.common.model.Property; +import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.governance.IdentityMgtConstants; import org.wso2.carbon.identity.recovery.IdentityRecoveryConstants; @@ -32,6 +37,7 @@ import static org.testng.Assert.assertEquals; import static org.testng.Assert.assertNull; +import static org.wso2.carbon.identity.governance.IdentityGovernanceUtil.getPropertyObject; /** * This class does unit test coverage for RecoveryConfigImpl class. @@ -105,6 +111,10 @@ public void testGetPropertyNameMapping() { nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.QUESTION_MIN_NO_ANSWER, "Number of " + "questions required for password recovery"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE, "Username recovery"); + nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + "Notification based username recovery via EMAIL"); + nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + "Notification based username recovery via SMS"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE, "Enable reCaptcha for username recovery"); nameMappingExpected.put(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME, @@ -206,6 +216,8 @@ public void testGetPropertyNames() { propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_ENABLE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_MAX_FAILED_ATTEMPTS); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE); + propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE); + propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE); propertiesExpected.add(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_NOTIFICATION_SUCCESS); @@ -241,6 +253,8 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { String testEnableRecoveryQuestionPasswordReCaptcha = "true"; String testRecoveryQuestionPasswordReCaptchaMaxFailedAttempts = "2"; String testEnableUsernameRecovery = "false"; + String testEnableUsernameRecoveryEmail = "false"; + String testEnableUsernameRecoverySMS = "false"; String testEnableNotificationInternallyManage = "true"; String testExpiryTime = "1440"; String testExpiryTimeSMSOTP = "1"; @@ -286,6 +300,10 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { testRecoveryQuestionPasswordReCaptchaMaxFailedAttempts); defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE, testEnableUsernameRecovery); + defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + testEnableUsernameRecoveryEmail); + defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + testEnableUsernameRecoverySMS); defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE, enableUsernameRecoveryReCaptcha); defaultPropertiesExpected.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE, @@ -327,6 +345,199 @@ public void testGetDefaultPropertyValues() throws IdentityGovernanceException { assertEquals(defaultProperties, defaultPropertiesExpected, "Maps are not equal"); } + @Test(dataProvider = "defaultPropertyNames") + public void testGetDefaultPropertyValuesConditional(String property) throws IdentityGovernanceException{ + + String tenantDomain = "admin"; + String testPropertyValue = "testValue"; + try(MockedStatic identityUtilMockedStatic = Mockito.mockStatic(IdentityUtil.class)) { + identityUtilMockedStatic.when(() -> IdentityUtil.getProperty(property)).thenReturn(testPropertyValue); + + Properties properties = recoveryConfigImpl.getDefaultPropertyValues(tenantDomain); + assertEquals(testPropertyValue, properties.getProperty(property)); + } + } + + @DataProvider(name="defaultPropertyNames") + public Object[][] buildPropertyNameList(){ + + return new Object[][] { + {IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_BASED_PW_RECOVERY}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SEND_OTP_IN_EMAIL}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_USE_UPPERCASE_CHARACTERS_IN_OTP}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_USE_LOWERCASE_CHARACTERS_IN_OTP}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_USE_NUMBERS_IN_OTP}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_OTP_LENGTH}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_RECAPTCHA_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.QUESTION_BASED_PW_RECOVERY}, + {IdentityRecoveryConstants.ConnectorConfig.QUESTION_MIN_NO_ANSWER}, + {IdentityRecoveryConstants.ConnectorConfig.CHALLENGE_QUESTION_ANSWER_REGEX}, + {IdentityRecoveryConstants.ConnectorConfig.ENFORCE_CHALLENGE_QUESTION_ANSWER_UNIQUENESS}, + {IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_MAX_FAILED_ATTEMPTS}, + {IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE}, + {IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_EXPIRY_TIME}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_REGEX}, + {IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_NOTIFICATION_SUCCESS}, + {IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_SECURITY_START}, + {IdentityRecoveryConstants.ConnectorConfig.FORCE_ADD_PW_RECOVERY_QUESTION}, + {IdentityRecoveryConstants.ConnectorConfig.FORCE_MIN_NO_QUESTION_ANSWERED}, + {IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET}, + {IdentityRecoveryConstants.ConnectorConfig.RECOVERY_NOTIFICATION_PASSWORD_MAX_FAILED_ATTEMPTS}, + {IdentityRecoveryConstants.ConnectorConfig.RECOVERY_NOTIFICATION_PASSWORD_MAX_RESEND_ATTEMPTS}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_EMAIL_LINK_ENABLE}, + {IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_ENABLE} + }; + } + + @Test + public void testGetMetaData() { + + Map metaDataExpected = new HashMap<>(); + + Property testNotificationBasedPasswordRecovery = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoverySendOtpInEmail = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoveryUseUppercaseCharactersInOtp = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoveryUseLowercaseCharactersInOtp = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoveryUseNumbersInOtp = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoveryOtpLength = + getPropertyObject(IdentityMgtConstants.DataTypes.STRING.getValue()); + Property testPasswordRecoveryRecaptchaEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testQuestionBasedPwRecovery = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testQuestionMinNoAnswer = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testEnforceChallengeQuestionAnswerUniqueness = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testRecoveryQuestionPasswordRecaptchaEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testRecoveryQuestionPasswordRecaptchaMaxFailedAttempts = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testUsernameRecoveryEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testUsernameRecoveryEmailEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testUsernameRecoverySmsEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testUsernameRecoveryRecaptchaEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testNotificationInternallyManage = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testExpiryTime = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testPasswordRecoverySmsOtpExpiryTime = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testNotificationSendRecoveryNotificationSuccess = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testNotificationSendRecoverySecurityStart = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testForceAddPwRecoveryQuestion = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testForceMinNoQuestionAnswered = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testEnableAutoLoginAfterPasswordReset = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testChallengeQuestionAnswerRegex = + getPropertyObject(IdentityMgtConstants.DataTypes.STRING.getValue()); + Property testRecoveryCallbackRegex = + getPropertyObject(IdentityMgtConstants.DataTypes.STRING.getValue()); + Property testRecoveryNotificationPasswordMaxFailedAttempts = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testRecoveryNotificationPasswordMaxResendAttempts = + getPropertyObject(IdentityMgtConstants.DataTypes.INTEGER.getValue()); + Property testPasswordRecoveryEmailLinkEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoverySmsOtpEnable = + getPropertyObject(IdentityMgtConstants.DataTypes.BOOLEAN.getValue()); + Property testPasswordRecoverySmsOtpRegex = + getPropertyObject(IdentityMgtConstants.DataTypes.STRING.getValue()); + + // Adding properties to the expected metadata map. + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_BASED_PW_RECOVERY, + testNotificationBasedPasswordRecovery); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SEND_OTP_IN_EMAIL, + testPasswordRecoverySendOtpInEmail); + metaDataExpected.put( + IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_USE_UPPERCASE_CHARACTERS_IN_OTP, + testPasswordRecoveryUseUppercaseCharactersInOtp); + metaDataExpected.put( + IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_USE_LOWERCASE_CHARACTERS_IN_OTP, + testPasswordRecoveryUseLowercaseCharactersInOtp); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_USE_NUMBERS_IN_OTP, + testPasswordRecoveryUseNumbersInOtp); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_OTP_LENGTH, + testPasswordRecoveryOtpLength); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_RECAPTCHA_ENABLE, + testPasswordRecoveryRecaptchaEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.QUESTION_BASED_PW_RECOVERY, + testQuestionBasedPwRecovery); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.QUESTION_MIN_NO_ANSWER, testQuestionMinNoAnswer); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENFORCE_CHALLENGE_QUESTION_ANSWER_UNIQUENESS, + testEnforceChallengeQuestionAnswerUniqueness); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_ENABLE, + testRecoveryQuestionPasswordRecaptchaEnable); + metaDataExpected.put( + IdentityRecoveryConstants.ConnectorConfig.RECOVERY_QUESTION_PASSWORD_RECAPTCHA_MAX_FAILED_ATTEMPTS, + testRecoveryQuestionPasswordRecaptchaMaxFailedAttempts); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_ENABLE, + testUsernameRecoveryEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_EMAIL_ENABLE, + testUsernameRecoveryEmailEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE, + testUsernameRecoverySmsEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_RECAPTCHA_ENABLE, + testUsernameRecoveryRecaptchaEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_INTERNALLY_MANAGE, + testNotificationInternallyManage); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.EXPIRY_TIME, testExpiryTime); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_EXPIRY_TIME, + testPasswordRecoverySmsOtpExpiryTime); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_NOTIFICATION_SUCCESS, + testNotificationSendRecoveryNotificationSuccess); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.NOTIFICATION_SEND_RECOVERY_SECURITY_START, + testNotificationSendRecoverySecurityStart); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.FORCE_ADD_PW_RECOVERY_QUESTION, + testForceAddPwRecoveryQuestion); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.FORCE_MIN_NO_QUESTION_ANSWERED, + testForceMinNoQuestionAnswered); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.ENABLE_AUTO_LGOIN_AFTER_PASSWORD_RESET, + testEnableAutoLoginAfterPasswordReset); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.CHALLENGE_QUESTION_ANSWER_REGEX, + testChallengeQuestionAnswerRegex); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.RECOVERY_CALLBACK_REGEX, + testRecoveryCallbackRegex); + metaDataExpected.put( + IdentityRecoveryConstants.ConnectorConfig.RECOVERY_NOTIFICATION_PASSWORD_MAX_FAILED_ATTEMPTS, + testRecoveryNotificationPasswordMaxFailedAttempts); + metaDataExpected.put( + IdentityRecoveryConstants.ConnectorConfig.RECOVERY_NOTIFICATION_PASSWORD_MAX_RESEND_ATTEMPTS, + testRecoveryNotificationPasswordMaxResendAttempts); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_EMAIL_LINK_ENABLE, + testPasswordRecoveryEmailLinkEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_ENABLE, + testPasswordRecoverySmsOtpEnable); + metaDataExpected.put(IdentityRecoveryConstants.ConnectorConfig.PASSWORD_RECOVERY_SMS_OTP_REGEX, + testPasswordRecoverySmsOtpRegex); + + // Fetching actual metadata from the method. + Map metaData = recoveryConfigImpl.getMetaData(); + + // Asserting that the expected and actual maps are equal. + assertEquals(metaData, metaDataExpected); + } + + @Test public void testGetDefaultProperties() throws IdentityGovernanceException { diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java index 46daf5e74..87e96a880 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/UserAccountRecoveryManagerTest.java @@ -19,6 +19,7 @@ import org.mockito.InjectMocks; import org.mockito.Mock; import org.mockito.MockedStatic; +import org.mockito.Spy; import org.testng.annotations.AfterMethod; import org.testng.annotations.BeforeMethod; import org.testng.annotations.BeforeTest; @@ -65,11 +66,14 @@ import java.util.UUID; import java.util.concurrent.atomic.AtomicInteger; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.nullable; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; @@ -88,9 +92,14 @@ */ public class UserAccountRecoveryManagerTest { + private static final String TENANT_DOMAIN = "carbon.super"; + @InjectMocks private UserAccountRecoveryManager userAccountRecoveryManager; + @Spy + private UserAccountRecoveryManager userAccountRecoveryManagerSpy; + @Mock private IdentityRecoveryServiceDataHolder identityRecoveryServiceDataHolder; @@ -175,6 +184,80 @@ public void testRetrieveUserRecoveryInformation() throws Exception { testGetUserWithNotificationsInternallyManaged(); } + /** + * Test retrieve user recovery information for username recovery. + * + * @throws Exception Error while getting user recovery information + */ + @Test + public void testRetrieveUsersRecoveryInformationForUsername() throws Exception { + + ArrayList userList = new ArrayList<>(); + userList.add(new org.wso2.carbon.user.core.common.User(UUID.randomUUID().toString(), "user1", "user1")); + userList.add(new org.wso2.carbon.user.core.common.User(UUID.randomUUID().toString(), "user2", "user2")); + + doReturn(userList).when(userAccountRecoveryManagerSpy).getUserListByClaims(any(), any()); + + mockBuildUser(); + mockRecoveryConfigs(true); + mockUserstoreManager(); + mockJDBCRecoveryDataStore(); + mockClaimMetadataManagementService(); + + when(abstractUserStoreManager + .getUserClaimValues(anyString(), any(String[].class), eq(null))).thenReturn(userClaims); + when(IdentityRecoveryServiceDataHolder.getInstance().getIdentityEventService()) + .thenReturn(identityEventService); + doNothing().when(identityEventService).handleEvent(any()); + + RecoveryChannelInfoDTO result = userAccountRecoveryManagerSpy.retrieveUsersRecoveryInformationForUsername(null, + TENANT_DOMAIN, null); + + assertEquals(result.getNotificationChannelDTOs().length, 2); + assertEquals(result.getUsername(), "user2"); + } + + /** + * Test throw no user found error in retrieve user recovery information for username recovery. + * + * @throws IdentityRecoveryException Exception Error while getting user recovery information + */ + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testThrowNoUserFoundRetrieveUserRecoveryInformationForUsername() throws IdentityRecoveryException { + + ArrayList resultedUserList = new ArrayList<>(); + doReturn(resultedUserList).when(userAccountRecoveryManagerSpy).getUserListByClaims(any(), any()); + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null)). + thenReturn(new IdentityRecoveryClientException( + null, null, null)); + userAccountRecoveryManagerSpy.retrieveUsersRecoveryInformationForUsername(null, TENANT_DOMAIN, null); + } + + /** + * Test throw no user found error when combined usernames become empty in retrieving user recovery information for + * username recovery. + * + * @throws IdentityRecoveryException Exception Error while getting user recovery information + */ + @Test(expectedExceptions = IdentityRecoveryClientException.class) + public void testThrowNoUserFoundEmptyUsernamesRetrieveUserRecoveryInformationForUsername() + throws IdentityRecoveryException { + + mockBuildUser(); + ArrayList resultedUserList = new ArrayList<>(); + resultedUserList.add(new org.wso2.carbon.user.core.common.User(UUID.randomUUID().toString(), "user1", "user1")); + doReturn(resultedUserList).when(userAccountRecoveryManagerSpy).getUserListByClaims(any(), any()); + + when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null)). + thenReturn(new IdentityRecoveryClientException( + null, null, null)); + when(Utils.handleClientException(nullable(String.class), anyString(), anyString())). + thenReturn(new IdentityRecoveryClientException( + null, null, null)); + mockedUtils.when(() -> Utils.isAccountDisabled(any(User.class))).thenReturn(true); + userAccountRecoveryManagerSpy.retrieveUsersRecoveryInformationForUsername(null, TENANT_DOMAIN, null); + } + /** * Tests that a NullPointerException is thrown during user recovery when a UserStoreException * occurs. diff --git a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java index d12714ab7..e96fb1225 100644 --- a/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java +++ b/components/org.wso2.carbon.identity.recovery/src/test/java/org/wso2/carbon/identity/recovery/internal/service/impl/username/UsernameRecoveryManagerImplTest.java @@ -25,10 +25,14 @@ import org.wso2.carbon.identity.application.common.model.User; import org.wso2.carbon.identity.core.util.IdentityUtil; import org.wso2.carbon.identity.event.services.IdentityEventService; +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.dto.NotificationChannelDTO; +import org.wso2.carbon.identity.recovery.dto.RecoveryChannelInfoDTO; import org.wso2.carbon.identity.recovery.dto.RecoveryInformationDTO; import org.wso2.carbon.identity.recovery.dto.UsernameRecoverDTO; import org.wso2.carbon.identity.recovery.internal.IdentityRecoveryServiceDataHolder; @@ -42,15 +46,19 @@ import java.net.URLDecoder; import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.UUID; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.mockito.MockitoAnnotations.openMocks; import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNull; /** * Test class for UsernameRecoveryManagerImpl. @@ -152,9 +160,10 @@ public void testConfigValidation() throws IdentityRecoveryException { */ @DataProvider public Object[][] channelIDProvider() { - return new Object[][] { - { null }, - { "0" } + + return new Object[][]{ + {null}, + {"0"} }; } @@ -188,7 +197,8 @@ public void testInvalidateRecoveryCode() throws IdentityRecoveryException { Map properties = new HashMap<>(); properties.put("useLegacyAPI", FALSE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); @@ -207,7 +217,8 @@ public void testInvalidateRecoveryCodeWithException() throws IdentityRecoveryExc Map properties = new HashMap<>(); properties.put("useLegacyAPI", FALSE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(mockUserRecoveryData.getRecoveryFlowId()).thenReturn("FlowID"); @@ -227,7 +238,8 @@ public void testExtractChannelDetails() throws IdentityRecoveryException { Map properties = new HashMap<>(); properties.put("useLegacyAPI", FALSE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("123"); @@ -249,19 +261,21 @@ public void testNotifyUser() throws IdentityRecoveryException { Map properties = new HashMap<>(); properties.put("useLegacyAPI", FALSE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("EXTERNAL,EXTERNAL"); when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); User mockUser = new User(); - mockUser.setUserName("KD123"); + mockUser.setUserName("user1,user2"); mockUser.setTenantDomain(TENANT_DOMAIN); when(mockUserRecoveryData.getUser()).thenReturn(mockUser); when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_INVALID_CHANNEL_ID, null)) .thenReturn(new IdentityRecoveryClientException(null)); UsernameRecoverDTO code = usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); assertEquals(code.getCode(), "UNR-02002"); + assertEquals(code.getUsername(), String.format("user1@%s,user2@%s", TENANT_DOMAIN, TENANT_DOMAIN)); } /** @@ -277,7 +291,8 @@ public void testNotifyUserException() throws IdentityRecoveryException { Map properties = new HashMap<>(); properties.put("useLegacyAPI", FALSE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("SMS,SMS"); @@ -289,7 +304,8 @@ public void testNotifyUserException() throws IdentityRecoveryException { when(mockUserRecoveryData.getUser()).thenReturn(mockUser); UsernameRecoverDTO result = usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); assertEquals(result.getCode(), "UNR-02001"); - assertEquals(result.getMessage(), "Username recovery information sent via user preferred notification channel."); + assertEquals(result.getMessage(), + "Username recovery information sent via user preferred notification channel."); } /** @@ -307,7 +323,8 @@ public void testCallbackURLValidation() throws IdentityRecoveryException { properties.put("useLegacyAPI", TRUE); properties.put(IdentityRecoveryConstants.CALLBACK, callbackURL); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("SMS,SMS"); @@ -338,13 +355,15 @@ public void testCallbackURLDecoding() throws IdentityRecoveryException { properties.put("useLegacyAPI", TRUE); properties.put(IdentityRecoveryConstants.CALLBACK, callbackURL); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserRecoveryData(recoveryCode, RecoverySteps.SEND_RECOVERY_INFORMATION)) .thenReturn(mockUserRecoveryData); when(mockUserRecoveryData.getRemainingSetIds()).thenReturn("SMS,SMS"); when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); when(Utils.resolveEventName(anyString())).thenReturn("TRIGGER_SMS_NOTIFICATION_LOCAL"); - mockURLDecoder.when(() -> URLDecoder.decode(anyString(), anyString())).thenThrow(new UnsupportedEncodingException()); + mockURLDecoder.when(() -> URLDecoder.decode(anyString(), anyString())) + .thenThrow(new UnsupportedEncodingException()); usernameRecoveryManager.notify(recoveryCode, "2", TENANT_DOMAIN, properties); } @@ -366,7 +385,8 @@ public void testInitiateRecoveryWithNullUsername() throws IdentityRecoveryExcept properties.put("useLegacyAPI", TRUE); when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserListByClaims(null, TENANT_DOMAIN)).thenReturn(userList); when(IdentityUtil.getProperty(anyString())).thenReturn(TRUE); when(Utils.handleClientException(IdentityRecoveryConstants.ErrorMessages.ERROR_CODE_NO_USER_FOUND, null)) @@ -391,7 +411,8 @@ public void testInitiateRecoveryValidUsername() throws IdentityRecoveryException properties.put("useLegacyAPI", TRUE); when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); mockedJDBCRecoveryDataStore.when(JDBCRecoveryDataStore::getInstance).thenReturn(mockUserRecoveryDataStore); - mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance).thenReturn(mockUserAccountRecoveryManager); + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); when(mockUserAccountRecoveryManager.getUserListByClaims(null, TENANT_DOMAIN)).thenReturn(userList); when(IdentityUtil.getProperty(anyString())).thenReturn(TRUE); when(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, properties)).thenReturn(true); @@ -401,6 +422,71 @@ public void testInitiateRecoveryValidUsername() throws IdentityRecoveryException assertEquals(result.getUsername(), "testUser"); } + /** + * Test to initiate recovery with useLegacyAPI false. + * + * @throws IdentityRecoveryException if an error occurs during initiation. + */ + @Test + public void testInitiateRecoveryWithUseLegacyAPIFalse() throws IdentityRecoveryException { + + String TEST_USER = "testUser"; + mockIdentityEventService(); + RecoveryChannelInfoDTO recoveryChannelInfoDTO = new RecoveryChannelInfoDTO(); + recoveryChannelInfoDTO.setUsername("testUser"); + List notificationChannelDTOList = new ArrayList<>(); + + NotificationChannelDTO notificationChannelDTO1 = new NotificationChannelDTO(); + notificationChannelDTO1.setId(1); + notificationChannelDTO1.setType(NotificationChannels.EMAIL_CHANNEL.getChannelType()); + + NotificationChannelDTO notificationChannelDTO2 = new NotificationChannelDTO(); + notificationChannelDTO2.setId(2); + notificationChannelDTO2.setType(NotificationChannels.SMS_CHANNEL.getChannelType()); + + notificationChannelDTOList.add(notificationChannelDTO1); + notificationChannelDTOList.add(notificationChannelDTO2); + recoveryChannelInfoDTO.setNotificationChannelDTOs( + notificationChannelDTOList.toArray(new NotificationChannelDTO[0])); + + Map properties = new HashMap<>(); + when(Utils.getRecoveryConfigs(anyString(), anyString())).thenReturn(TRUE); + + // Case 1: When Recovery.Notification.Username.NonUniqueUsername is enabled. + when(IdentityUtil.getProperty( + eq(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME))).thenReturn(TRUE); + + mockedRecoveryManagerStatic.when(UserAccountRecoveryManager::getInstance) + .thenReturn(mockUserAccountRecoveryManager); + when(mockUserAccountRecoveryManager.retrieveUsersRecoveryInformationForUsername(eq(null), eq(TENANT_DOMAIN), + any())).thenReturn(recoveryChannelInfoDTO); + when(Utils.isNotificationsInternallyManaged(TENANT_DOMAIN, properties)).thenReturn(true); + RecoveryInformationDTO result = usernameRecoveryManager.initiate(null, TENANT_DOMAIN, properties); + assertEquals(result.getRecoveryChannelInfoDTO().getUsername(), TEST_USER); + assertEquals(result.getRecoveryChannelInfoDTO().getNotificationChannelDTOs().length, 2); + + // Case 2: When Recovery.Notification.Username.NonUniqueUsername is disabled. + when(IdentityUtil.getProperty( + eq(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_NON_UNIQUE_USERNAME))).thenReturn(FALSE); + when(mockUserAccountRecoveryManager.retrieveUserRecoveryInformation(eq(null), eq(TENANT_DOMAIN), + eq(RecoveryScenarios.USERNAME_RECOVERY), + any())).thenReturn(recoveryChannelInfoDTO); + result = usernameRecoveryManager.initiate(null, TENANT_DOMAIN, properties); + assertEquals(result.getRecoveryChannelInfoDTO().getUsername(), TEST_USER); + assertEquals(result.getRecoveryChannelInfoDTO().getNotificationChannelDTOs().length, 2); + + // Case 3: Disable the sms channel. + when(Utils.getRecoveryConfigs(eq(IdentityRecoveryConstants.ConnectorConfig.USERNAME_RECOVERY_SMS_ENABLE), + eq(TENANT_DOMAIN))) + .thenReturn(FALSE); + result = usernameRecoveryManager.initiate(null, TENANT_DOMAIN, properties); + assertEquals(result.getRecoveryChannelInfoDTO().getUsername(), TEST_USER); + assertEquals(result.getRecoveryChannelInfoDTO().getNotificationChannelDTOs().length, 1); + assertEquals(result.getRecoveryChannelInfoDTO().getNotificationChannelDTOs()[0].getType(), + NotificationChannels.EMAIL_CHANNEL.getChannelType()); + + } + /** * Mock the IdentityEventService. */