diff --git a/components/org.wso2.carbon.identity.password.expiry/pom.xml b/components/org.wso2.carbon.identity.password.expiry/pom.xml index 9d89cfc225..5b09f408fe 100644 --- a/components/org.wso2.carbon.identity.password.expiry/pom.xml +++ b/components/org.wso2.carbon.identity.password.expiry/pom.xml @@ -89,6 +89,11 @@ mockito-inline test + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.testutil + test + org.wso2.carbon.identity.organization.management.core org.wso2.carbon.identity.organization.management.service @@ -149,6 +154,9 @@ org.wso2.carbon.user.core; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.user.core.util; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.user.core.common; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.user.core.listener; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.user.core.model; version="${carbon.kernel.package.import.version.range}", + org.wso2.carbon.context; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.user.api.*; version="${carbon.user.api.imp.pkg.version.range}", org.wso2.carbon.identity.application.common.model.*; version="${carbon.identity.framework.imp.pkg.version.range}", diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java index f48eb32e1d..14985c0eaa 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/constants/PasswordPolicyConstants.java @@ -28,6 +28,7 @@ public class PasswordPolicyConstants { "http://wso2.org/claims/identity/lastPasswordUpdateTime"; public static final String LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY = "http://wso2.org/claims/lastPasswordChangedTimestamp"; + public static final String PASSWORD_EXPIRY_TIME_CLAIM = "http://wso2.org/claims/identity/passwordExpiryTime"; public static final String PASSWORD_RESET_PAGE = "/accountrecoveryendpoint/password-recovery-confirm.jsp"; public static final String PASSWORD_CHANGE_EVENT_HANDLER_NAME = "enforcePasswordResetEventHandler"; public static final String ENFORCE_PASSWORD_RESET_HANDLER = "EnforcePasswordResetHandler"; @@ -57,6 +58,7 @@ public class PasswordPolicyConstants { public static final String AUTHENTICATION_STATUS = "authenticationStatus"; public static final String BASIC_AUTHENTICATOR = "BasicAuthenticator"; public static final String FALSE = "false"; + public static final String TRUE = "true"; public static final String CONFIRMATION_QUERY_PARAM = "&confirmation="; public static final String PASSWORD_EXPIRED_QUERY_PARAMS = "&passwordExpired=true"; public static final String PASSWORD_EXPIRED_MSG_QUERY_PARAM = "&passwordExpiredMsg="; diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java index 536128988b..a3bdce3194 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponent.java @@ -35,8 +35,10 @@ import org.wso2.carbon.identity.event.handler.AbstractEventHandler; import org.wso2.carbon.identity.governance.IdentityGovernanceService; import org.wso2.carbon.identity.governance.common.IdentityConnectorConfig; +import org.wso2.carbon.identity.password.expiry.listener.PasswordExpiryEventListener; import org.wso2.carbon.identity.password.expiry.services.ExpiredPasswordIdentificationService; import org.wso2.carbon.identity.password.expiry.services.impl.ExpiredPasswordIdentificationServiceImpl; +import org.wso2.carbon.user.core.listener.UserOperationEventListener; import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; @@ -56,6 +58,10 @@ public class EnforcePasswordResetComponent { protected void activate(ComponentContext context) { try { + // Register the listener to capture user operations. + PasswordExpiryEventListener listener = new PasswordExpiryEventListener(); + context.getBundleContext().registerService(UserOperationEventListener.class, listener, null); + EnforcePasswordResetAuthenticationHandler enforcePasswordResetAuthenticationHandler = new EnforcePasswordResetAuthenticationHandler(); BundleContext bundleContext = context.getBundleContext(); diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListener.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListener.java new file mode 100644 index 0000000000..6f1babafdd --- /dev/null +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListener.java @@ -0,0 +1,115 @@ +/* + * 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.password.expiry.listener; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.authentication.framework.exception.PostAuthenticationFailedException; +import org.wso2.carbon.identity.core.AbstractIdentityUserOperationEventListener; +import org.wso2.carbon.identity.core.util.IdentityCoreConstants; +import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants; +import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException; +import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRule; +import org.wso2.carbon.identity.password.expiry.util.PasswordPolicyUtils; +import org.wso2.carbon.user.core.UserStoreException; +import org.wso2.carbon.user.core.UserStoreManager; +import org.wso2.carbon.user.core.model.UserClaimSearchEntry; + +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; + +/** + * This is an implementation of UserOperationEventListener. This defines additional operations for some of + * the core user management operations. + */ +public class PasswordExpiryEventListener extends AbstractIdentityUserOperationEventListener { + + private static final Log log = LogFactory.getLog(PasswordExpiryEventListener.class); + + public int getExecutionOrderId() { + + int orderId = getOrderId(); + if (orderId != IdentityCoreConstants.EVENT_LISTENER_ORDER_ID) { + return orderId; + } + return 102; + } + + @Override + public boolean doPostGetUserClaimValues(String username, String[] claims, String profileName, + Map claimMap, UserStoreManager userStoreManager) + throws UserStoreException { + + if (!isEnable() || !Arrays.asList(claims).contains(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)) { + return true; + } + log.debug("post get user claim values with id is called in PasswordExpiryEventListener"); + + try { + String userTenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + Optional passwordExpiryTime = + PasswordPolicyUtils.getUserPasswordExpiryTime(userTenantDomain, username); + passwordExpiryTime.ifPresent(expiryTime -> claimMap.put(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM, + String.valueOf(expiryTime))); + } catch (ExpiredPasswordIdentificationException e) { + throw new UserStoreException("Error while retrieving password expiry time.", e); + } + return true; + } + + @Override + public boolean doPostGetUsersClaimValues(String[] userNames, String[] claims, String profileName, + UserClaimSearchEntry[] userClaimSearchEntries) throws UserStoreException { + + if (!isEnable() || !Arrays.asList(claims).contains(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)) { + return true; + } + log.debug("Method doPostGetUsersClaimValues getting executed in the PasswordExpiryEventListener."); + + try { + String tenantDomain = PrivilegedCarbonContext.getThreadLocalCarbonContext().getTenantDomain(); + if (!PasswordPolicyUtils.isPasswordExpiryEnabled(tenantDomain)) return true; + + boolean isSkipIfNoApplicableRulesEnabled = + PasswordPolicyUtils.isSkipIfNoApplicableRulesEnabled(tenantDomain); + int defaultPasswordExpiryInDays = PasswordPolicyUtils.getPasswordExpiryInDays(tenantDomain); + List passwordExpiryRules = PasswordPolicyUtils.getPasswordExpiryRules(tenantDomain); + + for (UserClaimSearchEntry userClaimSearchEntry : userClaimSearchEntries) { + String username = userClaimSearchEntry.getUserName(); + + if (userClaimSearchEntry.getClaims() == null) { + userClaimSearchEntry.setClaims(new HashMap()); + } + Optional passwordExpiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime( + tenantDomain, username, true, isSkipIfNoApplicableRulesEnabled, + passwordExpiryRules, defaultPasswordExpiryInDays); + passwordExpiryTime.ifPresent(expiryTime -> userClaimSearchEntry.getClaims() + .put(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM, String.valueOf(expiryTime))); + } + } catch (PostAuthenticationFailedException | ExpiredPasswordIdentificationException e) { + throw new UserStoreException("Error while retrieving password expiry time.", e); + } + return true; + } +} diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java index 8469fde2ec..6d1a998667 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/util/PasswordPolicyUtils.java @@ -48,11 +48,13 @@ import org.wso2.carbon.user.core.util.UserCoreUtil; import org.wso2.carbon.utils.multitenancy.MultitenantConstants; import org.wso2.carbon.user.core.common.Group; +import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException; import java.util.ArrayList; import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.Set; import java.util.stream.Collectors; @@ -159,6 +161,8 @@ public static boolean isPasswordExpired(String tenantDomain, String tenantAwareU throws PostAuthenticationFailedException { try { + if (!isPasswordExpiryEnabled(tenantDomain)) return false; + UserRealm userRealm = getUserRealm(tenantDomain); UserStoreManager userStoreManager = getUserStoreManager(userRealm); String userId = ((AbstractUserStoreManager) userStoreManager).getUserIDFromUserName(tenantAwareUsername); @@ -176,11 +180,8 @@ public static boolean isPasswordExpired(String tenantDomain, String tenantAwareU skipIfNoApplicableRules); } - // If the default behavior is to skip the password expiry, rules with skip logic are not necessary. - List filteredRules = passwordExpiryRules.stream() - .filter(rule -> !skipIfNoApplicableRules || - !PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator())) - .collect(Collectors.toList()); + List filteredRules = + filterApplicableExpiryRules(passwordExpiryRules, skipIfNoApplicableRules); Map> fetchedUserAttributes = new EnumMap<>(PasswordExpiryRuleAttributeEnum.class); @@ -193,7 +194,7 @@ public static boolean isPasswordExpired(String tenantDomain, String tenantAwareU } int expiryDays = rule.getExpiryDays() > 0 ? rule.getExpiryDays() : getPasswordExpiryInDays(tenantDomain); - return daysDifference >= expiryDays || lastPasswordUpdatedTime == null; + return daysDifference >= expiryDays || StringUtils.isBlank(lastPasswordUpdatedTime); } } // Apply default password expiry policy if no specific rule applies. @@ -292,7 +293,137 @@ private static boolean isPasswordExpiredUnderDefaultPolicy(String tenantDomain, throws PostAuthenticationFailedException { if (skipIfNoApplicableRules) return false; - return lastPasswordUpdatedTime == null || daysDifference >= getPasswordExpiryInDays(tenantDomain); + return StringUtils.isBlank(lastPasswordUpdatedTime) || daysDifference >= getPasswordExpiryInDays(tenantDomain); + } + + /** + * This method returns password expiry time for the given user. + * + * @param tenantDomain The tenant domain. + * @param tenantAwareUsername The tenant aware username. + * @return Optional containing the password expiry time in milliseconds, or empty if not applicable. + * @throws ExpiredPasswordIdentificationException If an error occurred while getting the password expiry time. + */ + public static Optional getUserPasswordExpiryTime(String tenantDomain, String tenantAwareUsername) + throws ExpiredPasswordIdentificationException { + + return getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername, null, + null, null, null); + } + + /** + * This method returns password expiry time for the given user. + * + * @param tenantDomain The tenant domain. + * @param tenantAwareUsername The tenant aware username. + * @param isPasswordExpiryEnabled Whether password expiry is enabled. + * @param isSkipIfNoApplicableRulesEnabled Whether skip if no applicable rules config is enabled. + * @param passwordExpiryRules Password expiry rules. + * @param defaultPasswordExpiryInDays Default password expiry in days. + * @return Optional containing the password expiry time in milliseconds, or empty if not applicable. + * @throws ExpiredPasswordIdentificationException If an error occurred while getting the password expiry time. + */ + public static Optional getUserPasswordExpiryTime(String tenantDomain, + String tenantAwareUsername, + Boolean isPasswordExpiryEnabled, + Boolean isSkipIfNoApplicableRulesEnabled, + List passwordExpiryRules, + Integer defaultPasswordExpiryInDays) + throws ExpiredPasswordIdentificationException { + + try { + if (isPasswordExpiryEnabled == null) { + isPasswordExpiryEnabled = isPasswordExpiryEnabled(tenantDomain); + } + // If the password expiry is not enabled, password expiry time is not applicable. + if (!isPasswordExpiryEnabled) return Optional.empty(); + + if (isSkipIfNoApplicableRulesEnabled == null) { + isSkipIfNoApplicableRulesEnabled = isSkipIfNoApplicableRulesEnabled(tenantDomain); + } + if (defaultPasswordExpiryInDays == null) { + defaultPasswordExpiryInDays = getPasswordExpiryInDays(tenantDomain); + } + if (passwordExpiryRules == null) { + passwordExpiryRules = getPasswordExpiryRules(tenantDomain); + } + + UserRealm userRealm = getUserRealm(tenantDomain); + UserStoreManager userStoreManager = getUserStoreManager(userRealm); + String userId = ((AbstractUserStoreManager) userStoreManager).getUserIDFromUserName(tenantAwareUsername); + String lastPasswordUpdatedTime = + getLastPasswordUpdatedTime(tenantAwareUsername, userStoreManager, userRealm); + + long lastPasswordUpdatedTimeInMillis = 0L; + boolean isLastPasswordUpdatedTimeBlank = StringUtils.isBlank(lastPasswordUpdatedTime); + if (!isLastPasswordUpdatedTimeBlank) { + lastPasswordUpdatedTimeInMillis = getLastPasswordUpdatedTimeInMillis(lastPasswordUpdatedTime); + } + + // If no rules are defined, use the default expiry time if "skipIfNoApplicableRules" is disabled. + if (CollectionUtils.isEmpty(passwordExpiryRules)) { + if (isSkipIfNoApplicableRulesEnabled) return Optional.empty(); + // If lastPasswordUpdatedTime is blank, set expiry time to now. + if (isLastPasswordUpdatedTimeBlank) { + return Optional.of(System.currentTimeMillis()); + } + return Optional.of( + lastPasswordUpdatedTimeInMillis + getDaysTimeInMillis(defaultPasswordExpiryInDays)); + } + + Map> userAttributes = + new EnumMap<>(PasswordExpiryRuleAttributeEnum.class); + + List filteredRules = + filterApplicableExpiryRules(passwordExpiryRules, isSkipIfNoApplicableRulesEnabled); + for (PasswordExpiryRule rule : filteredRules) { + if (isRuleApplicable(rule, userAttributes, tenantDomain, userId, userStoreManager)) { + // Skip the rule if the operator is not equals. + if (PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator())) { + return Optional.empty(); + } + if (isLastPasswordUpdatedTimeBlank) { + return Optional.of(System.currentTimeMillis()); + } + int expiryDays = + rule.getExpiryDays() > 0 ? rule.getExpiryDays() : getPasswordExpiryInDays(tenantDomain); + return Optional.of(lastPasswordUpdatedTimeInMillis + getDaysTimeInMillis(expiryDays)); + } + } + + if (isSkipIfNoApplicableRulesEnabled) return Optional.empty(); + if (isLastPasswordUpdatedTimeBlank) { + return Optional.of(System.currentTimeMillis()); + } + return Optional.of( + lastPasswordUpdatedTimeInMillis + getDaysTimeInMillis(defaultPasswordExpiryInDays)); + } catch (UserStoreException | PostAuthenticationFailedException e) { + throw new ExpiredPasswordIdentificationException(PasswordPolicyConstants.ErrorMessages. + ERROR_WHILE_GETTING_USER_STORE_DOMAIN.getCode(), + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_GETTING_USER_STORE_DOMAIN.getMessage()); + } + } + + private static List filterApplicableExpiryRules(List passwordExpiryRules, + boolean skipIfNoApplicableRules) { + + if (!skipIfNoApplicableRules) { + return passwordExpiryRules; + } + // If the default behavior is to skip the password expiry, rules with skip logic are not required. + return passwordExpiryRules.stream().filter( + rule -> !PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator())).collect(Collectors.toList()); + } + + /** + * This method returns the time in milliseconds for the given number of days. + * + * @param days The number of days. + * @return The time in milliseconds. + */ + private static long getDaysTimeInMillis(int days) { + + return (long) days * 24 * 60 * 60 * 1000; } /** diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java index 09f15c6226..1600a8024d 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordPolicyUtilsTest.java @@ -19,7 +19,12 @@ package org.wso2.carbon.identity.password.expiry; import org.testng.annotations.DataProvider; +import org.wso2.carbon.base.MultitenantConstants; +import org.wso2.carbon.identity.core.ServiceURL; +import org.wso2.carbon.identity.core.ServiceURLBuilder; +import org.wso2.carbon.identity.core.URLBuilderException; import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants; +import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException; import org.wso2.carbon.identity.password.expiry.internal.EnforcePasswordResetComponentDataHolder; import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRuleAttributeEnum; import org.wso2.carbon.identity.governance.bean.ConnectorConfig; @@ -56,6 +61,8 @@ import java.util.List; import java.util.Map; import java.util.HashMap; +import java.util.Optional; +import java.util.stream.Collectors; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -65,6 +72,7 @@ import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; import static org.mockito.Mockito.verify; +import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.PASSWORD_RESET_PAGE; /** * Tests for password change utils. @@ -93,11 +101,20 @@ public class PasswordPolicyUtilsTest { @Mock private RoleManagementService roleManagementService; + @Mock + private ServiceURLBuilder serviceURLBuilder; + + @Mock + private ServiceURL serviceURL; + private MockedStatic mockedStaticUserCoreUtil; + private MockedStatic mockedStaticServiceURLBuilder; private final String tenantDomain = "test.com"; private final String tenantAwareUsername = "tom@gmail.com"; private final String userId = "testUserId"; + private static final long TIME_TOLERANCE_MS = 2000; + private static final int DEFAULT_EXPIRY_DAYS = 30; private static final Map ROLE_MAP = new HashMap<>(); static { @@ -116,6 +133,7 @@ public void beforeTest() { mockedStaticIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); mockedStaticUserCoreUtil = mockStatic(UserCoreUtil.class); + mockedStaticServiceURLBuilder = mockStatic(ServiceURLBuilder.class); } @AfterClass @@ -144,13 +162,7 @@ public void testGetPasswordExpiryPropertyNames() { @Test public void testPasswordExpiryEnabled() throws PostAuthenticationFailedException, IdentityGovernanceException { - Property property = new Property(); - property.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY); - property.setValue(PasswordPolicyConstants.FALSE); - Property[] properties = new Property[1]; - properties[0] = property; - when(identityGovernanceService.getConfiguration(new String[]{ - PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY}, tenantDomain)).thenReturn(properties); + mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.FALSE); Assert.assertFalse(PasswordPolicyUtils.isPasswordExpiryEnabled(tenantDomain)); } @@ -189,6 +201,40 @@ public void testGetPasswordExpiryRules() throws PostAuthenticationFailedExceptio Assert.assertEquals(Arrays.asList(ROLE_MAP.get("employee"), ROLE_MAP.get("manager")), rule3.getValues()); } + @Test + public void testGetPasswordExpiryRulesWithInvalidRules() throws PostAuthenticationFailedException, IdentityGovernanceException { + + Property expiryRule1 = new Property(); + Property expiryRule2 = new Property(); + Property expiryRule3 = new Property(); + Property expiryRule4 = new Property(); + expiryRule1.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"1"); + expiryRule1.setValue(String.format("1,0,groups,ne,%s", GROUP_MAP.get("admin"))); + expiryRule2.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"2"); + expiryRule2.setValue( + String.format("2,40,invalid_rule,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor"))); + expiryRule3.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"3"); + expiryRule3.setValue( + String.format("bbb,40,groups,ne,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor"))); + expiryRule4.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"4"); + expiryRule4.setValue( + String.format("-1,40,groups,ne,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor"))); + + Property[] properties = new Property[4]; + properties[0] = expiryRule1; + properties[1] = expiryRule2; + properties[2] = expiryRule3; + properties[3] = expiryRule4; + ConnectorConfig connectorConfig = new ConnectorConfig(); + connectorConfig.setProperties(properties); + + when(identityGovernanceService.getConnectorWithConfigs(tenantDomain, + PasswordPolicyConstants.CONNECTOR_CONFIG_NAME)).thenReturn(connectorConfig); + + List rules = PasswordPolicyUtils.getPasswordExpiryRules(tenantDomain); + Assert.assertEquals(rules.size(), 1); + } + @Test public void testGetUserRoles() throws PostAuthenticationFailedException, IdentityRoleManagementException { @@ -216,9 +262,10 @@ public void testIsPasswordExpiredWithoutRules(Integer daysAgo, boolean expectedE when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); when(userRealm.getClaimManager()).thenReturn(claimManager); when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); - when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); + mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE); + // Mock last password updated time. Long updateTime = getUpdateTime(daysAgo); mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager); @@ -242,17 +289,17 @@ public void testIsPasswordExpiredWithoutRules(Integer daysAgo, boolean expectedE public Object[][] passwordExpiryTestCases() { return new Object[][] { // {daysAgo, roles, groups, skipIfNoApplicableRules, expectedExpired, description}. - {55, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("manager")}, new String[]{}, false, false, + {55, new String[]{"employee", "manager"}, new String[]{}, false, false, "Not expired: 3rd rule (60) applies"}, - {55, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("manager"), ROLE_MAP.get("contractor")}, + {55, new String[]{"employee", "manager", "contractor"}, new String[]{}, false, true, "Expired: 2nd rule (40) applies"}, - {35, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")}, new String[]{}, false, false, + {35, new String[]{"employee", "contractor"}, new String[]{}, false, false, "Not expired: 2nd rule (40) applies"}, - {35, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")}, new String[]{"admin"}, false, + {35, new String[]{"employee", "contractor"}, new String[]{"admin"}, false, false, "Not expired: 1st rule (skip) applies."}, - {35, new String[]{ROLE_MAP.get("employee")}, new String[]{}, false, true, + {35, new String[]{"employee"}, new String[]{}, false, true, "Expired: Default expiry policy applies."}, - {35, new String[]{ROLE_MAP.get("employee")}, new String[]{}, true, false, + {35, new String[]{"employee"}, new String[]{}, true, false, "Not expired: Default expiry policy applies - skip if no rules applicable."}, }; } @@ -271,13 +318,9 @@ public void testIsPasswordExpiredWithRules(int daysAgo, String[] roles, String[] when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); when(roleManagementService.getRoleListOfUser(userId, tenantDomain)).thenReturn(getRoles(roles)); - List userGroups = new ArrayList<>(); - Arrays.stream(groups).forEach(groupName -> { - Group groupObj = new Group(); - groupObj.setGroupID(GROUP_MAP.get(groupName)); - userGroups.add(groupObj); - }); - when(abstractUserStoreManager.getGroupListOfUser(userId, null, null)).thenReturn(userGroups); + mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE); + + when(abstractUserStoreManager.getGroupListOfUser(userId, null, null)).thenReturn(getGroups(groups)); // Mock last password update time. Long updateTime = getUpdateTime(daysAgo); @@ -300,22 +343,216 @@ public void testIsPasswordExpiredWithRules(int daysAgo, String[] roles, String[] Assert.assertEquals(isExpired, expectedExpired, description); } + @DataProvider(name = "passwordExpiryTimeTestCases") + public Object[][] passwordExpiryTimeTestCases() { + return new Object[][] { + // {daysAgo, roles, groups, expiryDays, description} + {null, new String[]{"employee", "manager"}, new String[]{}, 0, "Expiry time: Now"}, + {30, new String[]{"employee", "manager"}, new String[]{}, 60, "60 days expiry: 3rd rule applies"}, + {100, new String[]{"employee"}, new String[]{"admin"}, null, "1st rule (skip) applies."}, + {10, new String[]{"employee"}, new String[]{}, 30, "30 days expiry: Default expiry policy applies"}, + {50, new String[]{"employee", "contractor"}, new String[]{}, 40, "40 days expiry: 2nd rule applies"} + }; + } + + @Test(dataProvider = "passwordExpiryTimeTestCases") + public void testGetUserPasswordExpiryTime(Integer daysAgo, String[] roles, String[] groups, Integer expiryDays, + String description) + throws IdentityGovernanceException, UserStoreException, ExpiredPasswordIdentificationException, + IdentityRoleManagementException { + + when(IdentityTenantUtil.getTenantId(anyString())).thenReturn(3); + when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(userRealm.getClaimManager()).thenReturn(claimManager); + when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); + when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); + + // Mock last password update time. + Long updateTime = daysAgo != null ? System.currentTimeMillis() - getDaysTimeInMillis(daysAgo) : null; + mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager); + + mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE); + + // Mock password expiry rules. + ConnectorConfig connectorConfig = new ConnectorConfig(); + connectorConfig.setProperties(getPasswordExpiryRulesProperties()); + when(identityGovernanceService.getConnectorWithConfigs(tenantDomain, + PasswordPolicyConstants.CONNECTOR_CONFIG_NAME)).thenReturn(connectorConfig); + + when(identityGovernanceService.getConfiguration( + new String[]{PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS}, + tenantDomain)).thenReturn(getPasswordExpiryInDaysProperty()); + when(identityGovernanceService.getConfiguration( + new String[]{PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES}, + tenantDomain)).thenReturn(getSkipIfNoRulesApplicableProperty(PasswordPolicyConstants.FALSE)); + + // Mock user roles. + when(roleManagementService.getRoleListOfUser(userId, tenantDomain)).thenReturn(getRoles(roles)); + + // Mock user groups. + when(abstractUserStoreManager.getGroupListOfUser(userId, null, null)) + .thenReturn(getGroups(groups)); + + long testStartTime = System.currentTimeMillis(); + Optional expiryTime = + PasswordPolicyUtils.getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername); + long testEndTime = System.currentTimeMillis(); + + if (expiryDays == null) { + Assert.assertFalse(expiryTime.isPresent(), description); + } else if (expiryDays == 0) { + Assert.assertTrue(expiryTime.isPresent()); + Assert.assertTrue(expiryTime.get() >= testStartTime && expiryTime.get() <= testEndTime); + } else { + Assert.assertTrue(expiryTime.isPresent()); + Assert.assertNotNull(updateTime); + long expectedExpiryTime = updateTime + getDaysTimeInMillis(expiryDays); + Assert.assertTrue(Math.abs(expiryTime.get() - expectedExpiryTime) <= TIME_TOLERANCE_MS); + } + } + + @Test + public void testGetUserPasswordExpiryTime() + throws IdentityGovernanceException, UserStoreException, ExpiredPasswordIdentificationException { + + // Case 1: Password expiry disabled. + Optional expiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime( + tenantDomain, tenantAwareUsername, false, null, + null, null); + Assert.assertFalse(expiryTime.isPresent()); + + // Case 2: Password expiry enabled, but no rules. + mockPasswordExpiryEnabled(identityGovernanceService, PasswordPolicyConstants.TRUE); + when(IdentityTenantUtil.getTenantId(anyString())).thenReturn(3); + when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(userRealm.getClaimManager()).thenReturn(claimManager); + when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); + when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); + + // Mock last password update time to 20 days. + Long updateTime = System.currentTimeMillis() - getDaysTimeInMillis(20); + mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager); + + expiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime( + tenantDomain, tenantAwareUsername, true, false, + Collections.emptyList(), DEFAULT_EXPIRY_DAYS); + + long expectedExpiryTime = updateTime + getDaysTimeInMillis(DEFAULT_EXPIRY_DAYS); + Assert.assertTrue(Math.abs(expiryTime.get() - expectedExpiryTime) <= TIME_TOLERANCE_MS); + + // Case 3: Password expiry enabled, no applicable rules, skipIfNoApplicableRules enabled. + when(identityGovernanceService.getConfiguration( + new String[]{PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES}, + tenantDomain)).thenReturn(getSkipIfNoRulesApplicableProperty(PasswordPolicyConstants.TRUE)); + + expiryTime = PasswordPolicyUtils.getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername, + true, true, Collections.emptyList(), + DEFAULT_EXPIRY_DAYS); + Assert.assertFalse(expiryTime.isPresent()); + + // Case 4: UserStoreException. + when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenThrow( + new org.wso2.carbon.user.core.UserStoreException()); + try { + PasswordPolicyUtils.getUserPasswordExpiryTime(tenantDomain, tenantAwareUsername, + true, true, Collections.emptyList(), + DEFAULT_EXPIRY_DAYS); + Assert.fail("Expected PostAuthenticationFailedException was not thrown"); + } catch (Exception e) { + Assert.assertTrue(e instanceof ExpiredPasswordIdentificationException); + } + } + + @Test + public void testGetPasswordResetPageUrl() throws Exception { + + // Mocking ServiceURLBuilder + mockedStaticServiceURLBuilder.when( + (MockedStatic.Verification) ServiceURLBuilder.create()).thenReturn(serviceURLBuilder); + when(serviceURLBuilder.addPath(PASSWORD_RESET_PAGE)).thenReturn(serviceURLBuilder); + when(serviceURLBuilder.setTenant(anyString())).thenReturn(serviceURLBuilder); + when(serviceURLBuilder.build()).thenReturn(serviceURL); + + // Case 1: Tenant qualified URLs enabled. + mockedStaticIdentityTenantUtil.when(IdentityTenantUtil::isTenantQualifiedUrlsEnabled).thenReturn(true); + String tenantQualifiedURL = + String.format("https://example.com/t/%s/accountrecoveryendpoint/password-reset", tenantDomain); + when(serviceURL.getAbsolutePublicURL()).thenReturn(tenantQualifiedURL); + + String result = PasswordPolicyUtils.getPasswordResetPageUrl(tenantDomain); + Assert.assertEquals(tenantQualifiedURL, result); + + // Case 2: Tenant qualified URLs disabled, non-super tenant. + mockedStaticIdentityTenantUtil.when(IdentityTenantUtil::isTenantQualifiedUrlsEnabled).thenReturn(false); + String serverURL = "https://example.com"; + when(serviceURL.getAbsolutePublicURL()).thenReturn(serverURL); + + result = PasswordPolicyUtils.getPasswordResetPageUrl(tenantDomain); + Assert.assertEquals( + String.format("%s/t/%s%s?tenantDomain=%s", serverURL, tenantDomain, PASSWORD_RESET_PAGE, tenantDomain), + result); + + // Case 3: Tenant qualified URLs disabled, super tenant. + result = PasswordPolicyUtils.getPasswordResetPageUrl(MultitenantConstants.SUPER_TENANT_DOMAIN_NAME); + Assert.assertEquals(String.format("%s%s", serverURL, PASSWORD_RESET_PAGE), result); + + // Case 4: URLBuilderException. + when(serviceURLBuilder.build()).thenThrow(new URLBuilderException("Test exception")); + try { + PasswordPolicyUtils.getPasswordResetPageUrl(tenantDomain); + Assert.fail("Expected PostAuthenticationFailedException was not thrown"); + } catch (PostAuthenticationFailedException e) { + Assert.assertEquals( + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_BUILDING_PASSWORD_RESET_PAGE_URL.getCode(), + e.getErrorCode()); + } + } + + private void mockPasswordExpiryEnabled(IdentityGovernanceService identityGovernanceService, String enabled) throws IdentityGovernanceException { + + Property property = new Property(); + property.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY); + property.setValue(enabled); + Property[] properties = new Property[1]; + properties[0] = property; + when(identityGovernanceService.getConfiguration(new String[]{ + PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY}, tenantDomain)).thenReturn(properties); + } + + private static Long getDaysTimeInMillis(Integer days) { + + return days != null ? (long) days * 24 * 60 * 60 * 1000 : null; + } + private static Long getUpdateTime(Integer daysAgo) { - return daysAgo != null ? System.currentTimeMillis() - daysAgo * 24 * 60 * 60 * 1000L : null; + return daysAgo != null ? System.currentTimeMillis() - getDaysTimeInMillis(daysAgo) : null; } - private List getRoles(String[] roleIds) { + private List getRoles(String[] roleNames) { List userRoles = new ArrayList<>(); - for (String roleId : roleIds) { + for (String roleId : roleNames) { RoleBasicInfo roleInfo = new RoleBasicInfo(); - roleInfo.setId(roleId); + roleInfo.setId(ROLE_MAP.get(roleId)); userRoles.add(roleInfo); } return userRoles; } + private static List getGroups(String[] groupNames) { + + List userGroups = new ArrayList<>(); + Arrays.stream(groupNames).forEach(groupName -> { + Group groupObj = new Group(); + groupObj.setGroupID(GROUP_MAP.get(groupName)); + userGroups.add(groupObj); + }); + return userGroups; + } + private Property[] getPasswordExpiryRulesProperties() { Property expiryRule1 = new Property(); @@ -341,7 +578,7 @@ private Property[] getPasswordExpiryInDaysProperty() { Property property1 = new Property(); property1.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS); - property1.setValue(String.valueOf(30)); + property1.setValue(String.valueOf(DEFAULT_EXPIRY_DAYS)); Property[] properties = new Property[1]; properties[0] = property1; return properties; @@ -359,10 +596,18 @@ private Property[] getSkipIfNoRulesApplicableProperty(String value) { private void mockLastPasswordUpdateTime(Long updateTime, UserStoreManager userStoreManager) throws UserStoreException { - Map claims = new HashMap<>(); - claims.put(PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM, - updateTime != null ? String.valueOf(updateTime) : null); - String[] claimURIs = new String[]{PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM}; - when(userStoreManager.getUserClaimValues(anyString(), eq(claimURIs), isNull())).thenReturn(claims); + String updateTimeString = updateTime != null ? String.valueOf(updateTime) : null; + + // Mock for LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM. + Map claims1 = new HashMap<>(); + claims1.put(PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM, updateTimeString); + String[] claimURIs1 = new String[]{PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM}; + when(userStoreManager.getUserClaimValues(anyString(), eq(claimURIs1), isNull())).thenReturn(claims1); + + // Mock for LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY. + Map claims2 = new HashMap<>(); + claims2.put(PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY, updateTimeString); + String[] claimURIs2 = new String[]{PasswordPolicyConstants.LAST_CREDENTIAL_UPDATE_TIMESTAMP_CLAIM_NON_IDENTITY}; + when(userStoreManager.getUserClaimValues(anyString(), eq(claimURIs2), isNull())).thenReturn(claims2); } } diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java index 1e15901c78..2ee2a2e4d4 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordResetEnforcerHandlerTest.java @@ -155,6 +155,13 @@ public void testHandle() throws Exception { when(authenticationContext.getCurrentAuthenticatedIdPs()).thenReturn(idPs); idPs.put(AUTHENTICATOR_TYPE, authenticatedIdPData); + // Case 1 : Password expiry is not enabled. + when(PasswordPolicyUtils.isPasswordExpiryEnabled(anyString())).thenReturn(false); + PostAuthnHandlerFlowStatus flowStatus1 = enforcePasswordResetAuthenticationHandler.handle(httpServletRequest, + httpServletResponse, authenticationContext); + Assert.assertEquals(flowStatus1, PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED); + + // Case 2 : Password expiry is enabled. List authenticators = getAuthenticatorConfigs(); when(PasswordPolicyUtils.isPasswordExpiryEnabled(anyString())).thenReturn(true); when(PasswordPolicyUtils.isPasswordExpired(anyString(), anyString())).thenReturn(true); @@ -179,6 +186,12 @@ public void testHandle() throws Exception { httpServletResponse, authenticationContext); Assert.assertEquals(flowStatus, PostAuthnHandlerFlowStatus.INCOMPLETE); verify(httpServletResponse).sendRedirect(captor.capture()); + + // Case 3 : Password expiry is enabled and password is not expired. + when(PasswordPolicyUtils.isPasswordExpired(anyString(), anyString())).thenReturn(false); + PostAuthnHandlerFlowStatus flowStatus2 = enforcePasswordResetAuthenticationHandler.handle(httpServletRequest, + httpServletResponse, authenticationContext); + Assert.assertEquals(flowStatus2, PostAuthnHandlerFlowStatus.SUCCESS_COMPLETED); } private static List getAuthenticatorConfigs() { diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListenerTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListenerTest.java new file mode 100644 index 0000000000..93941822b0 --- /dev/null +++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/listener/PasswordExpiryEventListenerTest.java @@ -0,0 +1,199 @@ +/* + * 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.password.expiry.listener; + +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants; +import org.wso2.carbon.identity.password.expiry.exceptions.ExpiredPasswordIdentificationException; +import org.wso2.carbon.identity.password.expiry.util.PasswordPolicyUtils; +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.common.testng.WithCarbonHome; +import org.wso2.carbon.user.core.UserStoreException; +import org.wso2.carbon.user.core.UserStoreManager; +import org.wso2.carbon.user.core.model.UserClaimSearchEntry; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +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.when; + +/** + * Unit test cases for PasswordExpiryEventListener. + */ +@WithCarbonHome +public class PasswordExpiryEventListenerTest { + + private static final String TENANT_DOMAIN = "test.com"; + private PasswordExpiryEventListener passwordExpiryEventListener; + + @Mock + PrivilegedCarbonContext privilegedCarbonContext; + @Mock + UserStoreManager userStoreManager; + + private MockedStatic mockedPrivilegedCarbonContext; + private MockedStatic mockedPasswordPolicyUtils; + + @BeforeMethod + public void setUp() { + + MockitoAnnotations.openMocks(this); + passwordExpiryEventListener = new PasswordExpiryEventListener(); + + mockedPrivilegedCarbonContext.when(PrivilegedCarbonContext::getThreadLocalCarbonContext) + .thenReturn(privilegedCarbonContext); + + when(privilegedCarbonContext.getTenantDomain()).thenReturn(TENANT_DOMAIN); + } + + @BeforeClass + public void init() { + + mockedPrivilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class); + mockedPasswordPolicyUtils = mockStatic(PasswordPolicyUtils.class); + } + + @AfterClass + public void close() { + + mockedPrivilegedCarbonContext.close(); + mockedPasswordPolicyUtils.close(); + } + + @Test + public void testGetExecutionOrderId() { + + Assert.assertEquals(passwordExpiryEventListener.getExecutionOrderId(), 102); + } + + @Test + public void testDoPostGetUserClaimValuesWithPasswordExpiryClaim() throws UserStoreException { + + String username = "testUser"; + String[] claims; + Map claimMap = new HashMap<>(); + String profileName = "default"; + + // Case 1: When claims contains PASSWORD_EXPIRY_TIME_CLAIM. + claims = new String[]{PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM}; + + mockedPasswordPolicyUtils.when(() -> PasswordPolicyUtils.getUserPasswordExpiryTime( + eq(TENANT_DOMAIN), eq(username))).thenReturn(Optional.of(1000L)); + + passwordExpiryEventListener.doPostGetUserClaimValues(username, claims, profileName, claimMap, userStoreManager); + Assert.assertNotNull(claimMap.get(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)); + + // Case 2: PostAuthenticationFailedException is thrown. + mockedPasswordPolicyUtils.when(() -> + PasswordPolicyUtils.getUserPasswordExpiryTime(eq(TENANT_DOMAIN), eq(username))) + .thenThrow(new ExpiredPasswordIdentificationException("test-error", "test-error")); + try { + passwordExpiryEventListener.doPostGetUserClaimValues(username, claims, profileName, claimMap, userStoreManager); + } catch (Exception e) { + Assert.assertTrue(e instanceof UserStoreException); + } + } + + @Test + public void testDoPostGetUserClaimValuesWithoutPasswordExpiryClaim() throws UserStoreException { + + String username = "testUser"; + String[] claims; + Map claimMap = new HashMap<>(); + String profileName = "default"; + claims = new String[]{"claim1", "claim2"}; + + passwordExpiryEventListener.doPostGetUserClaimValues(username, claims, profileName, claimMap, userStoreManager); + Assert.assertFalse(claimMap.containsKey(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)); + } + + @Test + public void testDoPostGetUsersClaimValuesWithPasswordExpiryClaim() throws UserStoreException { + + String[] userNames = new String[]{"testUser1", "testUser2"}; + String[] claims = new String[]{PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM}; + String profileName = "default"; + + UserClaimSearchEntry[] userClaimSearchEntries = new UserClaimSearchEntry[2]; + userClaimSearchEntries[0] = new UserClaimSearchEntry(); + userClaimSearchEntries[0].setUserName("testUser1"); + userClaimSearchEntries[1] = new UserClaimSearchEntry(); + userClaimSearchEntries[1].setUserName("testUser1"); + + mockedPasswordPolicyUtils.when(() -> + PasswordPolicyUtils.isPasswordExpiryEnabled(TENANT_DOMAIN)).thenReturn(true); + mockedPasswordPolicyUtils.when(() -> + PasswordPolicyUtils.isSkipIfNoApplicableRulesEnabled(TENANT_DOMAIN)).thenReturn(false); + mockedPasswordPolicyUtils.when(() -> + PasswordPolicyUtils.getPasswordExpiryInDays(TENANT_DOMAIN)).thenReturn(30); + mockedPasswordPolicyUtils.when(() -> + PasswordPolicyUtils.getPasswordExpiryRules(TENANT_DOMAIN)).thenReturn(Collections.emptyList()); + mockedPasswordPolicyUtils.when(() -> PasswordPolicyUtils.getUserPasswordExpiryTime( + eq(TENANT_DOMAIN), anyString(), eq(true), eq(false), any(), eq(30))) + .thenReturn(Optional.of(1000L)); + + passwordExpiryEventListener.doPostGetUsersClaimValues(userNames, claims, profileName, userClaimSearchEntries); + Assert.assertNotNull( + userClaimSearchEntries[0].getClaims().get(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)); + Assert.assertNotNull( + userClaimSearchEntries[1].getClaims().get(PasswordPolicyConstants.PASSWORD_EXPIRY_TIME_CLAIM)); + + // Case 2: PostAuthenticationFailedException is thrown. + mockedPasswordPolicyUtils.when(() -> PasswordPolicyUtils.getUserPasswordExpiryTime( + eq(TENANT_DOMAIN), anyString(), eq(true), eq(false), any(), eq(30))) + .thenThrow(new ExpiredPasswordIdentificationException("test-error", "test-error")); + try { + passwordExpiryEventListener.doPostGetUsersClaimValues(userNames, claims, + profileName, userClaimSearchEntries); + } catch (Exception e) { + Assert.assertTrue(e instanceof UserStoreException); + } + } + + @Test + public void testDoPostGetUsersClaimValuesWithoutPasswordExpiryClaims() throws UserStoreException { + + String[] userNames = new String[]{"testUser1", "testUser2"}; + String[] claims = new String[]{"claim1", "claim2"}; + String profileName = "default"; + + UserClaimSearchEntry[] userClaimSearchEntries = new UserClaimSearchEntry[2]; + userClaimSearchEntries[0] = new UserClaimSearchEntry(); + userClaimSearchEntries[0].setUserName("testUser1"); + userClaimSearchEntries[1] = new UserClaimSearchEntry(); + userClaimSearchEntries[1].setUserName("testUser1"); + + passwordExpiryEventListener.doPostGetUsersClaimValues(userNames, claims, profileName, userClaimSearchEntries); + Assert.assertNull(userClaimSearchEntries[0].getClaims()); + Assert.assertNull(userClaimSearchEntries[1].getClaims()); + } +} diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml b/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml index 2aac16e379..ed19db6cef 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml +++ b/components/org.wso2.carbon.identity.password.expiry/src/test/resources/testng.xml @@ -14,6 +14,7 @@ +