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 58f2128bc7..1c320016e2 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 @@ -38,8 +38,10 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.Set; /** * Class which contains exposed identity governance services. @@ -168,6 +170,7 @@ public List getConnectorListWithConfigs(String tenantDomain) th Property[] properties = this.getConfiguration(tenantDomain); List configs = new ArrayList<>(list.size()); String[] connectorProperties; + String connectorName; for (int i = 0; i < list.size(); i++) { ConnectorConfig config = new ConnectorConfig(); Map propertyFriendlyNames = list.get(i).getPropertyNameMapping(); @@ -180,28 +183,38 @@ public List getConnectorListWithConfigs(String tenantDomain) th config.setSubCategory(list.get(i).getSubCategory()); config.setOrder(list.get(i).getOrder()); connectorProperties = list.get(i).getPropertyNames(); - Property[] configProperties = new Property[connectorProperties.length]; - for (int j = 0; j < connectorProperties.length; j++) { + connectorName = list.get(i).getName(); + List configProperties = new ArrayList<>(); + Set addedProperties = new HashSet<>(); + + for (String connectorProperty : connectorProperties) { for (Property property : properties) { - if (connectorProperties[j].equals(property.getName())) { - configProperties[j] = property; - String resourceName = configProperties[j].getName(); - configProperties[j].setDisplayName(propertyFriendlyNames.get(resourceName)); - configProperties[j].setDescription(propertyDescriptions.get(resourceName)); - if (metaData != null && metaData.containsKey(resourceName)) { - configProperties[j].setType(metaData.get(resourceName).getType()); - configProperties[j].setRegex(metaData.get(resourceName).getRegex()); - configProperties[j].setGroupId(metaData.get(resourceName).getGroupId()); + if (StringUtils.isBlank(property.getName()) || addedProperties.contains(property.getName())) { + continue; + } + if (connectorProperty.equals(property.getName()) || + (StringUtils.isNotBlank(connectorName) && property.getName().startsWith(connectorName))) { + Property configProperty = new Property(); + configProperty.setName(property.getName()); + configProperty.setValue(property.getValue()); + configProperty.setDisplayName(propertyFriendlyNames.get(property.getName())); + configProperty.setDescription(propertyDescriptions.get(property.getName())); + + if (metaData != null && metaData.containsKey(property.getName())) { + configProperty.setType(metaData.get(property.getName()).getType()); + configProperty.setRegex(metaData.get(property.getName()).getRegex()); + configProperty.setGroupId(metaData.get(property.getName()).getGroupId()); } if (confidentialProperties != null && - confidentialProperties.contains(configProperties[j].getName())) { - configProperties[j].setConfidential(true); + confidentialProperties.contains(configProperty.getName())) { + configProperty.setConfidential(true); } - break; + configProperties.add(configProperty); + addedProperties.add(property.getName()); } } } - config.setProperties(configProperties); + config.setProperties(configProperties.toArray(new Property[0])); configs.add(i, config); } return configs; diff --git a/components/org.wso2.carbon.identity.password.expiry/pom.xml b/components/org.wso2.carbon.identity.password.expiry/pom.xml index 57e87a8df8..cbe340532b 100644 --- a/components/org.wso2.carbon.identity.password.expiry/pom.xml +++ b/components/org.wso2.carbon.identity.password.expiry/pom.xml @@ -172,6 +172,7 @@ org.apache.commons.logging; version="${commons-logging.osgi.version.range}", + org.apache.commons.collections; version="${commons-collections.wso2.osgi.version.range}", org.osgi.framework; version="${osgi.framework.imp.pkg.version.range}", org.osgi.service.component; version="${osgi.service.component.imp.pkg.version.range}", org.wso2.carbon.identity.event.handler; @@ -197,6 +198,8 @@ 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}", + org.wso2.carbon.identity.role.v2.mgt.core.*; + version="${carbon.identity.framework.imp.pkg.version.range}", org.wso2.carbon.utils.multitenancy; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.user.core.service; version="${carbon.kernel.package.import.version.range}", org.wso2.carbon.identity.event.bean; diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImpl.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImpl.java index 48f111797d..aebdc0a6fc 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImpl.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImpl.java @@ -78,6 +78,8 @@ public Map getPropertyNameMapping() { PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY_DISPLAYED_NAME); nameMapping.put(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS, PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS_DISPLAYED_NAME); + nameMapping.put(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES, + PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES_DISPLAYED_NAME); return nameMapping; } @@ -90,6 +92,8 @@ public Map getPropertyDescriptionMapping() { PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY_DESCRIPTION); nameMapping.put(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS, PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS_DESCRIPTION); + nameMapping.put(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES, + PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES_DESCRIPTION); return nameMapping; } @@ -107,11 +111,14 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG String enablePasswordExpiry = PasswordPolicyConstants.FALSE; String passwordExpiryInDays = String.valueOf(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS_DEFAULT_VALUE); + String skipIfNoApplicableRules = PasswordPolicyConstants.FALSE; String enablePasswordExpiryProperty = IdentityUtil.getProperty( PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY); String passwordExpiryInDaysProperty = IdentityUtil.getProperty( PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS); + String skipIfNoApplicableRulesProperty = IdentityUtil.getProperty( + PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES); if (StringUtils.isNotBlank(enablePasswordExpiryProperty)) { enablePasswordExpiry = enablePasswordExpiryProperty; @@ -119,9 +126,15 @@ public Properties getDefaultPropertyValues(String tenantDomain) throws IdentityG if (StringUtils.isNotBlank(passwordExpiryInDaysProperty)) { passwordExpiryInDays = passwordExpiryInDaysProperty; } + if (StringUtils.isNotBlank(skipIfNoApplicableRulesProperty)) { + skipIfNoApplicableRules = skipIfNoApplicableRulesProperty; + } + Map defaultProperties = new HashMap<>(); defaultProperties.put(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY, enablePasswordExpiry); defaultProperties.put(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS, passwordExpiryInDays); + defaultProperties.put(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES, + skipIfNoApplicableRules); Properties properties = new Properties(); properties.putAll(defaultProperties); 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 92b16d0ef4..e7641dcab6 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-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 @@ -42,6 +42,12 @@ public class PasswordPolicyConstants { "Enable Password Expiry"; public static final String CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY_DESCRIPTION = "Allow users to reset the password after configured number of days"; + public static final String CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES = + "passwordExpiry.skipIfNoApplicableRules"; + public static final String CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES_DISPLAYED_NAME = + "Skip password expiry if no applicable rules"; + public static final String CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES_DESCRIPTION = + "Skip password expiry if no applicable rules are found for the user"; public static final String CONNECTOR_CONFIG_SUB_CATEGORY = "DEFAULT"; public static final String PASSWORD_EXPIRED_ERROR_MESSAGE = "Password has expired"; public static final String CONNECTOR_CONFIG_NAME = "passwordExpiry"; @@ -53,6 +59,7 @@ public class PasswordPolicyConstants { public static final String FALSE = "false"; public static final String CONFIRMATION_QUERY_PARAM = "&confirmation="; public static final String PASSWORD_EXPIRED_QUERY_PARAMS = "&passwordExpired=true"; + public static final String PASSWORD_EXPIRY_RULES_PREFIX = "passwordExpiry.rule"; public enum ErrorMessages { ERROR_WHILE_GETTING_USER_STORE_DOMAIN("80001", @@ -70,7 +77,9 @@ public enum ErrorMessages { ERROR_WHILE_UPDATING_PASSWORD("80011", "Error while updating the password"), ERROR_RETRIEVE_PASSWORD_EXPIRED_USERS_FROM_DB("80012", "" + "Error while retrieving password expired users from database."), - ERROR_RETRIEVE_USER_STORE_MANAGER("80013", "Error while retrieving user store manager."); + ERROR_RETRIEVE_USER_STORE_MANAGER("80013", "Error while retrieving user store manager."), + ERROR_WHILE_RETRIEVING_USER_ROLES("80014", "Error while retrieving user roles."), + ERROR_WHILE_RETRIEVING_USER_GROUPS("80015", "Error while retrieving user groups."); private final String code; private final String message; 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 48de86c2cc..536128988b 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-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 @@ -38,6 +38,7 @@ 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.service.RealmService; +import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; /** * OSGi declarative services component which handles registration and un-registration of password enforce reset handler @@ -124,4 +125,21 @@ protected void unsetIdentityDataStoreService(IdentityDataStoreService identityDa EnforcePasswordResetComponentDataHolder.getInstance().setIdentityDataStoreService(null); } + + @Reference( + name = "role.management.service", + service = RoleManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetRoleManagementService" + ) + protected void setRoleManagementService(RoleManagementService roleManagementService) { + + EnforcePasswordResetComponentDataHolder.getInstance().setRoleManagementService(roleManagementService); + } + + protected void unsetRoleManagementService(RoleManagementService roleManagementService) { + + EnforcePasswordResetComponentDataHolder.getInstance().setRoleManagementService(null); + } } diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponentDataHolder.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponentDataHolder.java index 73301b78fa..5837c802e2 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponentDataHolder.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/internal/EnforcePasswordResetComponentDataHolder.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-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 @@ -21,6 +21,7 @@ import org.wso2.carbon.identity.governance.IdentityGovernanceService; import org.wso2.carbon.identity.governance.service.IdentityDataStoreService; import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; /** * A class to keep the data of the enforce password reset handler component. @@ -32,6 +33,7 @@ public class EnforcePasswordResetComponentDataHolder { private RealmService realmService = null; private IdentityGovernanceService identityGovernanceService; private IdentityDataStoreService identityDataStoreService; + private RoleManagementService roleManagementService; private EnforcePasswordResetComponentDataHolder() { @@ -70,4 +72,14 @@ public void setIdentityDataStoreService(IdentityDataStoreService identityDataSto this.identityDataStoreService = identityDataStoreService; } + + public RoleManagementService getRoleManagementService() { + + return roleManagementService; + } + + public void setRoleManagementService(RoleManagementService roleManagementService) { + + this.roleManagementService = roleManagementService; + } } diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRule.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRule.java new file mode 100644 index 0000000000..aa548f83c3 --- /dev/null +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRule.java @@ -0,0 +1,123 @@ +/* + * 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.models; + +import org.apache.commons.lang.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +/** + * Class to represent a password expiry rule. + */ +public class PasswordExpiryRule { + + private int priority; + private int expiryDays; + private PasswordExpiryRuleAttributeEnum attribute; + private PasswordExpiryRuleOperatorEnum operator; + private List values = new ArrayList<>(); + private static final String RULE_SPLIT_REGEX = ",(?=(?:[^']*'[^']*')*[^']*$)"; + + public PasswordExpiryRule(String rule) throws IllegalArgumentException{ + + try { + // Rule format: "priority,expiryDays,attribute,operator,value1,value2, ...". + // At least 5 parts are required in the rule definition. + int ruleSectionLength = 4; + + String[] ruleSections = rule.split(RULE_SPLIT_REGEX); + if (ruleSections.length < ruleSectionLength) { + throw new IllegalArgumentException("Invalid rule format: not enough parts in the rule definition."); + } + + this.priority = Integer.parseInt(ruleSections[0].trim()); + this.expiryDays = Integer.parseInt(ruleSections[1].trim()); + this.attribute = PasswordExpiryRuleAttributeEnum.fromString(ruleSections[2].trim()); + this.operator = PasswordExpiryRuleOperatorEnum.fromString(ruleSections[3].trim()); + + // Extract values from the rule removing quotes if present. + // Eg: "1,40,roles,eq,'12ec01e1-aa45,8a485d10c8fa',cc40ad49-8435-75fa1b627332". + for (int i = 4; i < ruleSections.length; i++) { + String value = ruleSections[i].trim(); + if ((StringUtils.startsWith(value, "'") && StringUtils.endsWith(value, "'")) || + (StringUtils.startsWith(value, "\"") && StringUtils.endsWith(value, "\""))) { + value = value.substring(1, value.length() - 1).trim(); + } + this.values.add(value); + } + + if (this.values.isEmpty()) { + throw new IllegalArgumentException("Invalid rule format: no valid values provided."); + } + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid rule format: " + e.getMessage()); + } + } + + public int getPriority() { + + return priority; + } + + public void setPriority(int priority) { + + this.priority = priority; + } + + public int getExpiryDays() { + + return expiryDays; + } + + public void setExpiryDays(int expiryDays) { + + this.expiryDays = expiryDays; + } + + public PasswordExpiryRuleAttributeEnum getAttribute() { + + return attribute; + } + + public void setAttribute(PasswordExpiryRuleAttributeEnum attribute) { + + this.attribute = attribute; + } + + public PasswordExpiryRuleOperatorEnum getOperator() { + + return operator; + } + + public void setOperator(PasswordExpiryRuleOperatorEnum operator) { + + this.operator = operator; + } + + public List getValues() { + + return values; + } + + public void setValues(List values) { + + this.values = values; + } +} diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRuleAttributeEnum.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRuleAttributeEnum.java new file mode 100644 index 0000000000..bec2369f49 --- /dev/null +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRuleAttributeEnum.java @@ -0,0 +1,50 @@ +/* + * 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.models; + +/** + * Enum for the password expiry attribute types. + */ +public enum PasswordExpiryRuleAttributeEnum { + + ROLES("roles"), + GROUPS("groups"); + + private final String value; + + PasswordExpiryRuleAttributeEnum(String value) { + + this.value = value; + } + + public String getValue() { + + return value; + } + + public static PasswordExpiryRuleAttributeEnum fromString(String text) { + + for (PasswordExpiryRuleAttributeEnum attribute : PasswordExpiryRuleAttributeEnum.values()) { + if (attribute.value.equalsIgnoreCase(text)) { + return attribute; + } + } + throw new IllegalArgumentException("No enum constant with text " + text + " found"); + } +} diff --git a/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRuleOperatorEnum.java b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRuleOperatorEnum.java new file mode 100644 index 0000000000..4c061d604f --- /dev/null +++ b/components/org.wso2.carbon.identity.password.expiry/src/main/java/org/wso2/carbon/identity/password/expiry/models/PasswordExpiryRuleOperatorEnum.java @@ -0,0 +1,50 @@ +/* + * 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.models; + +/** + * Enum for the password expiry operator types. + */ +public enum PasswordExpiryRuleOperatorEnum { + + EQ("eq"), + NE("ne"); + + private final String value; + + PasswordExpiryRuleOperatorEnum(String value) { + + this.value = value; + } + + public String getValue() { + + return value; + } + + public static PasswordExpiryRuleOperatorEnum fromString(String text) { + + for (PasswordExpiryRuleOperatorEnum operator : PasswordExpiryRuleOperatorEnum.values()) { + if (operator.value.equalsIgnoreCase(text)) { + return operator; + } + } + throw new IllegalArgumentException("No enum constant with text " + text + " found"); + } +} 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 2eec2199b8..3a361173d4 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-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 @@ -19,9 +19,13 @@ package org.wso2.carbon.identity.password.expiry.util; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; +import org.apache.commons.collections.CollectionUtils; +import org.wso2.carbon.identity.governance.bean.ConnectorConfig; import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants; import org.wso2.carbon.identity.password.expiry.internal.EnforcePasswordResetComponentDataHolder; import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; import org.wso2.carbon.identity.application.authentication.framework.exception.PostAuthenticationFailedException; import org.wso2.carbon.identity.application.common.model.Property; import org.wso2.carbon.identity.core.ServiceURLBuilder; @@ -29,18 +33,30 @@ import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRuleAttributeEnum; +import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRuleOperatorEnum; +import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRule; +import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; +import org.wso2.carbon.identity.role.v2.mgt.core.exception.IdentityRoleManagementException; +import org.wso2.carbon.identity.role.v2.mgt.core.model.RoleBasicInfo; import org.wso2.carbon.user.api.ClaimManager; import org.wso2.carbon.user.api.UserRealm; import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import org.wso2.carbon.user.core.service.RealmService; import org.wso2.carbon.user.core.util.UserCoreUtil; import org.wso2.carbon.utils.multitenancy.MultitenantConstants; +import org.wso2.carbon.user.core.common.Group; import java.util.ArrayList; +import java.util.EnumMap; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; +import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.CONNECTOR_CONFIG_NAME; import static org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants.PASSWORD_RESET_PAGE; /** @@ -48,6 +64,8 @@ */ public class PasswordPolicyUtils { + private static final Log log = LogFactory.getLog(PasswordPolicyUtils.class); + /** * Get the property names required by the password expiry policy. * @@ -58,6 +76,7 @@ public static String[] getPasswordExpiryPropertyNames() { List properties = new ArrayList<>(); properties.add(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY); properties.add(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS); + properties.add(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES); return properties.toArray(new String[0]); } @@ -78,6 +97,56 @@ public static String getPasswordExpiryConfig(String tenantDomain, String key) th return connectorConfigs[0].getValue(); } + /** + * Get password expiry rules. + * + * @param tenantDomain Tenant domain. + * @return List of password expiry rules. + * @throws PostAuthenticationFailedException If an error occurred while getting the password expiry rules. + */ + @SuppressFBWarnings("CRLF_INJECTION_LOGS") + public static List getPasswordExpiryRules(String tenantDomain) + throws PostAuthenticationFailedException { + + List passwordExpiryRules = new ArrayList<>(); + try { + IdentityGovernanceService governanceService = + EnforcePasswordResetComponentDataHolder.getInstance().getIdentityGovernanceService(); + ConnectorConfig connectorConfig = + governanceService.getConnectorWithConfigs(tenantDomain, CONNECTOR_CONFIG_NAME); + if (connectorConfig == null) { + return passwordExpiryRules; + } + Property[] properties = connectorConfig.getProperties(); + if (properties == null) { + return passwordExpiryRules; + } + + for (Property property : properties) { + if (StringUtils.startsWith(property.getName(), PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX) && + StringUtils.isNotEmpty(property.getValue())) { + try { + PasswordExpiryRule passwordExpiryRule = new PasswordExpiryRule(property.getValue()); + passwordExpiryRules.add(passwordExpiryRule); + } catch (Exception e) { + // Log and skip the rule if an error occurred while parsing the rule, without failing the + // authentication flow. + if (log.isDebugEnabled()) { + log.debug(String.format("Error parsing password expiry rule: %s. Rule will be skipped.", + property.getValue())); + } + log.error("Error parsing password expiry rule.", e); + } + } + } + } catch (IdentityGovernanceException e) { + throw new PostAuthenticationFailedException(PasswordPolicyConstants.ErrorMessages. + ERROR_WHILE_READING_SYSTEM_CONFIGURATIONS.getCode(), + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_READING_SYSTEM_CONFIGURATIONS.getMessage()); + } + return passwordExpiryRules; + } + /** * This method checks if the password has expired. * @@ -89,15 +158,160 @@ public static String getPasswordExpiryConfig(String tenantDomain, String key) th public static boolean isPasswordExpired(String tenantDomain, String tenantAwareUsername) throws PostAuthenticationFailedException { - UserRealm userRealm = getUserRealm(tenantDomain); - UserStoreManager userStoreManager = getUserStoreManager(userRealm); - String lastPasswordUpdatedTime = getLastPasswordUpdatedTime(tenantAwareUsername, userStoreManager, userRealm); - long lastPasswordUpdatedTimeInMillis = getLastPasswordUpdatedTimeInMillis(lastPasswordUpdatedTime); - int daysDifference = getDaysDifference(lastPasswordUpdatedTimeInMillis); + try { + UserRealm userRealm = getUserRealm(tenantDomain); + UserStoreManager userStoreManager = getUserStoreManager(userRealm); + String userId = ((AbstractUserStoreManager) userStoreManager).getUserIDFromUserName(tenantAwareUsername); + String lastPasswordUpdatedTime = + getLastPasswordUpdatedTime(tenantAwareUsername, userStoreManager, userRealm); + long lastPasswordUpdatedTimeInMillis = getLastPasswordUpdatedTimeInMillis(lastPasswordUpdatedTime); + int daysDifference = getDaysDifference(lastPasswordUpdatedTimeInMillis); + + List passwordExpiryRules = getPasswordExpiryRules(tenantDomain); + boolean skipIfNoApplicableRules = isSkipIfNoApplicableRulesEnabled(tenantDomain); + + // Apply default password expiry policy if no rules given. + if (CollectionUtils.isEmpty(passwordExpiryRules)) { + return isPasswordExpiredUnderDefaultPolicy(tenantDomain, daysDifference, lastPasswordUpdatedTime, + 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()); + + Map> fetchedUserAttributes = + new EnumMap<>(PasswordExpiryRuleAttributeEnum.class); + + for (PasswordExpiryRule rule : filteredRules) { + if (isRuleApplicable(rule, fetchedUserAttributes, tenantDomain, userId, userStoreManager)) { + // Skip the rule if the operator is not equals. + if (PasswordExpiryRuleOperatorEnum.NE.equals(rule.getOperator())) { + return false; + } + int expiryDays = + rule.getExpiryDays() > 0 ? rule.getExpiryDays() : getPasswordExpiryInDays(tenantDomain); + return daysDifference >= expiryDays || lastPasswordUpdatedTime == null; + } + } + // Apply default password expiry policy if no specific rule applies. + return isPasswordExpiredUnderDefaultPolicy(tenantDomain, daysDifference, lastPasswordUpdatedTime, + skipIfNoApplicableRules); + } catch (UserStoreException e) { + throw new PostAuthenticationFailedException(PasswordPolicyConstants.ErrorMessages. + ERROR_WHILE_GETTING_USER_STORE_DOMAIN.getCode(), + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_GETTING_USER_STORE_DOMAIN.getMessage()); + } + } + + /** + * Check if the given rule is applicable for the user. + * + * @param rule Password expiry rule. + * @param fetchedUserAttributes Fetched user attributes. + * @param tenantDomain Tenant domain. + * @param userId User ID. + * @param userStoreManager User store manager. + * @return true if the rule is applicable, false otherwise. + * @throws PostAuthenticationFailedException If an error occurred while checking the rule applicability. + */ + private static boolean isRuleApplicable(PasswordExpiryRule rule, + Map> fetchedUserAttributes, + String tenantDomain, String userId, + UserStoreManager userStoreManager) + throws PostAuthenticationFailedException { + + PasswordExpiryRuleAttributeEnum ruleAttribute = rule.getAttribute(); + Set userAttributeValues = + getUserAttributes(ruleAttribute, fetchedUserAttributes, tenantDomain, userId, userStoreManager); + if (CollectionUtils.isEmpty(userAttributeValues)) { + return false; + } + return userAttributeValues.containsAll(rule.getValues()); + } + + /** + * Get the user attribute values for the given password expiry rule attribute. + * + * @param attribute Password expiry rule attribute. + * @param fetchedUserAttributes Fetched user attributes. + * @param tenantDomain Tenant domain. + * @param userId User ID. + * @param userStoreManager User store manager. + * @return The user attribute values. + * @throws PostAuthenticationFailedException If an error occurred while getting the user attributes. + */ + private static Set getUserAttributes(PasswordExpiryRuleAttributeEnum attribute, + Map> fetchedUserAttributes, + String tenantDomain, String userId, + UserStoreManager userStoreManager) + throws PostAuthenticationFailedException { + + if (!fetchedUserAttributes.containsKey(attribute)) { + try { + switch (attribute) { + case ROLES: + List userRoles = getUserRoles(tenantDomain, userId); + Set userRoleIds = userRoles.stream().map(RoleBasicInfo::getId).collect(Collectors.toSet()); + fetchedUserAttributes.put(PasswordExpiryRuleAttributeEnum.ROLES, userRoleIds); + break; + case GROUPS: + List userGroups = + ((AbstractUserStoreManager) userStoreManager).getGroupListOfUser(userId, + null, null); + Set userGroupIds = userGroups.stream().map(Group::getGroupID).collect(Collectors.toSet()); + fetchedUserAttributes.put(PasswordExpiryRuleAttributeEnum.GROUPS, userGroupIds); + break; + } + } catch (UserStoreException e) { + throw new PostAuthenticationFailedException(PasswordPolicyConstants.ErrorMessages. + ERROR_WHILE_RETRIEVING_USER_GROUPS.getCode(), + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_RETRIEVING_USER_GROUPS.getMessage()); + } + } + return fetchedUserAttributes.get(attribute); + } + + /** + * Check if the password has expired according to the default password expiry policy. + * + * @param tenantDomain The tenant domain. + * @param daysDifference The number of days since the password was last updated. + * @param lastPasswordUpdatedTime The last password updated time. + * @return true if the password has expired, false otherwise. + * @throws PostAuthenticationFailedException If an error occurs while checking the password expiry. + */ + private static boolean isPasswordExpiredUnderDefaultPolicy(String tenantDomain, int daysDifference, + String lastPasswordUpdatedTime, + boolean skipIfNoApplicableRules) + throws PostAuthenticationFailedException { - // Getting the configured number of days before password expiry in days - int passwordExpiryInDays = getPasswordExpiryInDays(tenantDomain); - return (daysDifference > passwordExpiryInDays || lastPasswordUpdatedTime == null); + if (skipIfNoApplicableRules) return false; + return lastPasswordUpdatedTime == null || daysDifference >= getPasswordExpiryInDays(tenantDomain); + } + + /** + * Get the roles of a given user. + * + * @param tenantDomain The tenant domain. + * @param userId The user ID. + * @return The roles of the user. + * @throws PostAuthenticationFailedException If an error occurs while getting the user roles. + */ + public static List getUserRoles(String tenantDomain, String userId) + throws PostAuthenticationFailedException { + + try { + RoleManagementService roleManagementService = EnforcePasswordResetComponentDataHolder.getInstance() + .getRoleManagementService(); + return roleManagementService.getRoleListOfUser(userId, tenantDomain); + } catch (IdentityRoleManagementException e) { + throw new PostAuthenticationFailedException(PasswordPolicyConstants.ErrorMessages. + ERROR_WHILE_RETRIEVING_USER_ROLES.getCode(), + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_RETRIEVING_USER_ROLES.getMessage()); + } } /** @@ -185,8 +399,7 @@ public static int getPasswordExpiryInDays(String tenantDomain) throws PostAuthen private static int getDaysDifference(long passwordChangedTime) { long currentTimeMillis = System.currentTimeMillis(); - int daysDifference = (int) ((currentTimeMillis - passwordChangedTime) / (1000 * 60 * 60 * 24)); - return daysDifference; + return (int) ((currentTimeMillis - passwordChangedTime) / (1000 * 60 * 60 * 24)); } /** @@ -196,7 +409,7 @@ private static int getDaysDifference(long passwordChangedTime) { * @param userStoreManager The user store manager to retrieve the last password updated time from. * @param userRealm The user realm to retrieve the claim manager from. * @return The last password updated time. - * @throws PostAuthenticationFailedException + * @throws PostAuthenticationFailedException If an error occurs while retrieving the last password updated time. */ @SuppressFBWarnings("FORMAT_STRING_MANIPULATION") private static String getLastPasswordUpdatedTime(String tenantAwareUsername, UserStoreManager userStoreManager, @@ -267,6 +480,26 @@ public static boolean isPasswordExpiryEnabled(String tenantDomain) throws PostAu } } + /** + * This method checks if the "skip if no applicable rules" option is enabled for a given tenant domain. + * + * @param tenantDomain The tenant domain to check for the configuration. + * @return true if "skip if no applicable rules" is enabled, false otherwise. + * @throws PostAuthenticationFailedException If an error occurs while reading system configurations. + */ + public static boolean isSkipIfNoApplicableRulesEnabled(String tenantDomain) + throws PostAuthenticationFailedException { + + try { + return Boolean.parseBoolean(PasswordPolicyUtils.getPasswordExpiryConfig(tenantDomain, + PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES)); + } catch (IdentityGovernanceException e) { + throw new PostAuthenticationFailedException(PasswordPolicyConstants.ErrorMessages. + ERROR_WHILE_READING_SYSTEM_CONFIGURATIONS.getCode(), + PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_READING_SYSTEM_CONFIGURATIONS.getMessage(), e); + } + } + /** * Get Password Reset page URL. * @@ -298,4 +531,4 @@ public static String getPasswordResetPageUrl(String tenantDomain) throws PostAut PasswordPolicyConstants.ErrorMessages.ERROR_WHILE_BUILDING_PASSWORD_RESET_PAGE_URL.getMessage()); } } -} +} \ No newline at end of file diff --git a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImplTest.java b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImplTest.java index f822df8f50..10574fa8ef 100644 --- a/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImplTest.java +++ b/components/org.wso2.carbon.identity.password.expiry/src/test/java/org/wso2/carbon/identity/password/expiry/PasswordExpiryConfigImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-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 @@ -80,13 +80,16 @@ public void testGetOrder() { public void testGetPropertyNameMapping() { Map propertyNameMapping = passwordPolicyConfig.getPropertyNameMapping(); - Assert.assertEquals(propertyNameMapping.size(), 2); + Assert.assertEquals(propertyNameMapping.size(), 3); Assert.assertEquals( propertyNameMapping.get(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS), PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS_DISPLAYED_NAME); Assert.assertEquals( propertyNameMapping.get(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY), PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY_DISPLAYED_NAME); + Assert.assertEquals( + propertyNameMapping.get(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES), + PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES_DISPLAYED_NAME); } @Test @@ -100,34 +103,41 @@ public void testGetDefaultPropertyValuesWithPropertyNames() throws IdentityGover public void testGetPropertyNames() { String[] propertyNames = passwordPolicyConfig.getPropertyNames(); - Assert.assertEquals(propertyNames.length, 2); + Assert.assertEquals(propertyNames.length, 3); Assert.assertEquals(propertyNames[0], PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY); Assert.assertEquals(propertyNames[1], PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS); + Assert.assertEquals(propertyNames[2], PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES); } @Test public void testGetPropertyDescriptionMapping() { Map propertyDescriptionMapping = passwordPolicyConfig.getPropertyDescriptionMapping(); - Assert.assertEquals(propertyDescriptionMapping.size(), 2); + Assert.assertEquals(propertyDescriptionMapping.size(), 3); Assert.assertEquals( propertyDescriptionMapping.get(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS), PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS_DESCRIPTION); Assert.assertEquals( propertyDescriptionMapping.get(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY), PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY_DESCRIPTION); + Assert.assertEquals( + propertyDescriptionMapping.get(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES), + PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES_DESCRIPTION); } @Test public void testGetDefaultPropertyValues() throws IdentityGovernanceException { Properties defaultPropertyValues = passwordPolicyConfig.getDefaultPropertyValues("test.com"); - Assert.assertEquals(defaultPropertyValues.size(), 2); + Assert.assertEquals(defaultPropertyValues.size(), 3); Assert.assertEquals( defaultPropertyValues.get(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS), String.valueOf(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS_DEFAULT_VALUE)); Assert.assertEquals( defaultPropertyValues.get(PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY), PasswordPolicyConstants.FALSE); + Assert.assertEquals( + defaultPropertyValues.get(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES), + PasswordPolicyConstants.FALSE); } } 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 ba9ca1abf5..09f15c6226 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 @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-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 @@ -18,8 +18,13 @@ package org.wso2.carbon.identity.password.expiry; +import org.testng.annotations.DataProvider; import org.wso2.carbon.identity.password.expiry.constants.PasswordPolicyConstants; 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; +import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRuleOperatorEnum; +import org.wso2.carbon.identity.password.expiry.models.PasswordExpiryRule; import org.wso2.carbon.identity.password.expiry.util.PasswordPolicyUtils; import org.mockito.Mock; import org.mockito.MockedStatic; @@ -34,24 +39,38 @@ import org.wso2.carbon.identity.core.util.IdentityTenantUtil; import org.wso2.carbon.identity.governance.IdentityGovernanceException; import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; +import org.wso2.carbon.identity.role.v2.mgt.core.model.RoleBasicInfo; +import org.wso2.carbon.identity.role.v2.mgt.core.exception.IdentityRoleManagementException; import org.wso2.carbon.user.api.UserStoreException; import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.claim.ClaimManager; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; import org.wso2.carbon.user.core.service.RealmService; +import org.wso2.carbon.user.core.util.UserCoreUtil; +import org.wso2.carbon.user.core.common.Group; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.HashMap; + +import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.when; +import static org.mockito.Mockito.verify; /** * Tests for password change utils. */ public class PasswordPolicyUtilsTest { - private PasswordPolicyUtils passwordPolicyUtils; - - @Mock private IdentityGovernanceService identityGovernanceService; @@ -61,6 +80,9 @@ public class PasswordPolicyUtilsTest { @Mock private UserStoreManager userStoreManager; + @Mock + private AbstractUserStoreManager abstractUserStoreManager; + @Mock private ClaimManager claimManager; @@ -68,34 +90,55 @@ public class PasswordPolicyUtilsTest { private org.wso2.carbon.user.core.UserRealm userRealm; private MockedStatic mockedStaticIdentityTenantUtil; - private String tenantDomain = "test.com"; + @Mock + private RoleManagementService roleManagementService; + + private MockedStatic mockedStaticUserCoreUtil; + + private final String tenantDomain = "test.com"; + private final String tenantAwareUsername = "tom@gmail.com"; + private final String userId = "testUserId"; + + private static final Map ROLE_MAP = new HashMap<>(); + static { + ROLE_MAP.put("employee", "a40ac8c2-5e51-4526-b75e-11353f473ad7"); + ROLE_MAP.put("contractor", "994b309d-3724-4519-8b0c-f23671451999"); + ROLE_MAP.put("manager", "674b309d-3724-4519-8b0c-f2367145151d"); + } + + private static final Map GROUP_MAP = new HashMap<>(); + static { + GROUP_MAP.put("admin", "eea0316e-3d99-4731-b8f1-475c1af72c6d"); + } @BeforeClass public void beforeTest() { mockedStaticIdentityTenantUtil = mockStatic(IdentityTenantUtil.class); + mockedStaticUserCoreUtil = mockStatic(UserCoreUtil.class); } @AfterClass public void afterTest() { mockedStaticIdentityTenantUtil.close(); + mockedStaticUserCoreUtil.close(); } @BeforeMethod public void setUp() { MockitoAnnotations.openMocks(this); - passwordPolicyUtils = new PasswordPolicyUtils(); EnforcePasswordResetComponentDataHolder.getInstance().setIdentityGovernanceService(identityGovernanceService); EnforcePasswordResetComponentDataHolder.getInstance().setRealmService(realmService); + EnforcePasswordResetComponentDataHolder.getInstance().setRoleManagementService(roleManagementService); } @Test public void testGetPasswordExpiryPropertyNames() { String[] passwordExpiryPropertyNames = PasswordPolicyUtils.getPasswordExpiryPropertyNames(); - Assert.assertEquals(passwordExpiryPropertyNames.length, 2); + Assert.assertEquals(passwordExpiryPropertyNames.length, 3); } @Test @@ -108,29 +151,218 @@ public void testPasswordExpiryEnabled() throws PostAuthenticationFailedException properties[0] = property; when(identityGovernanceService.getConfiguration(new String[]{ PasswordPolicyConstants.CONNECTOR_CONFIG_ENABLE_PASSWORD_EXPIRY}, tenantDomain)).thenReturn(properties); - Assert.assertEquals(PasswordPolicyUtils.isPasswordExpiryEnabled(tenantDomain), false); + Assert.assertFalse(PasswordPolicyUtils.isPasswordExpiryEnabled(tenantDomain)); } @Test - private void testPasswordExpired() throws PostAuthenticationFailedException, UserStoreException, - IdentityGovernanceException { + public void testGetPasswordExpiryRules() throws PostAuthenticationFailedException, IdentityGovernanceException { + + ConnectorConfig connectorConfig = new ConnectorConfig(); + connectorConfig.setProperties(getPasswordExpiryRulesProperties()); + when(identityGovernanceService.getConnectorWithConfigs(tenantDomain, + PasswordPolicyConstants.CONNECTOR_CONFIG_NAME)).thenReturn(connectorConfig); + + List rules = PasswordPolicyUtils.getPasswordExpiryRules(tenantDomain); + + Assert.assertEquals(rules.size(), 3); + + PasswordExpiryRule rule1 = rules.get(0); + PasswordExpiryRule rule2 = rules.get(1); + PasswordExpiryRule rule3 = rules.get(2); + + Assert.assertEquals(1, rule1.getPriority()); + Assert.assertEquals(0, rule1.getExpiryDays()); + Assert.assertEquals(PasswordExpiryRuleAttributeEnum.GROUPS, rule1.getAttribute()); + Assert.assertEquals(PasswordExpiryRuleOperatorEnum.NE, rule1.getOperator()); + Assert.assertEquals(Collections.singletonList(GROUP_MAP.get("admin")), rule1.getValues()); + Assert.assertEquals(2, rule2.getPriority()); + Assert.assertEquals(40, rule2.getExpiryDays()); + Assert.assertEquals(PasswordExpiryRuleAttributeEnum.ROLES, rule2.getAttribute()); + Assert.assertEquals(PasswordExpiryRuleOperatorEnum.EQ, rule2.getOperator()); + Assert.assertEquals(Arrays.asList(ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")), rule2.getValues()); + + Assert.assertEquals(3, rule3.getPriority()); + Assert.assertEquals(60, rule3.getExpiryDays()); + Assert.assertEquals(PasswordExpiryRuleAttributeEnum.ROLES, rule3.getAttribute()); + Assert.assertEquals(PasswordExpiryRuleOperatorEnum.EQ, rule3.getOperator()); + Assert.assertEquals(Arrays.asList(ROLE_MAP.get("employee"), ROLE_MAP.get("manager")), rule3.getValues()); + } + + @Test + public void testGetUserRoles() throws PostAuthenticationFailedException, IdentityRoleManagementException { + + PasswordPolicyUtils.getUserRoles(tenantDomain, userId); + verify(roleManagementService).getRoleListOfUser(userId, tenantDomain); + } + + @DataProvider(name = "passwordExpiryWithoutRulesTestCases") + public Object[][] passwordExpiryWithoutRulesTestCases() { + return new Object[][] { + // {daysAgo, expectedExpired, description}. + {20, Boolean.FALSE, "Password should not be expired when updated 25 days ago"}, + {35, Boolean.TRUE, "Password should be expired when updated 35 days ago"}, + {null, Boolean.TRUE, "Password should be considered expired when last update time is null"} + }; + } + + @Test(dataProvider = "passwordExpiryWithoutRulesTestCases") + public void testIsPasswordExpiredWithoutRules(Integer daysAgo, boolean expectedExpired, + String testDescription) + throws IdentityGovernanceException, UserStoreException, PostAuthenticationFailedException { + + when(IdentityTenantUtil.getTenantId(anyString())).thenReturn(3); when(realmService.getTenantUserRealm(anyInt())).thenReturn(userRealm); - when(userRealm.getUserStoreManager()).thenReturn(userStoreManager); + when(userRealm.getUserStoreManager()).thenReturn(abstractUserStoreManager); + when(userRealm.getClaimManager()).thenReturn(claimManager); + when(UserCoreUtil.addDomainToName(any(), any())).thenReturn(tenantAwareUsername); + + when(abstractUserStoreManager.getUserIDFromUserName(tenantAwareUsername)).thenReturn(userId); + + // Mock last password updated time. + Long updateTime = getUpdateTime(daysAgo); + mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager); + + // Mock empty password expiry rules by returning an empty ConnectorConfig. + when(identityGovernanceService.getConnectorWithConfigs(tenantDomain, + PasswordPolicyConstants.CONNECTOR_CONFIG_NAME)).thenReturn(new 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)); + + boolean isExpired = PasswordPolicyUtils.isPasswordExpired(tenantDomain, tenantAwareUsername); + Assert.assertEquals(isExpired, expectedExpired, testDescription); + } + + @DataProvider(name = "passwordExpiryTestCases") + 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, + "Not expired: 3rd rule (60) applies"}, + {55, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("manager"), ROLE_MAP.get("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, + "Not expired: 2nd rule (40) applies"}, + {35, new String[]{ROLE_MAP.get("employee"), ROLE_MAP.get("contractor")}, new String[]{"admin"}, false, + false, "Not expired: 1st rule (skip) applies."}, + {35, new String[]{ROLE_MAP.get("employee")}, new String[]{}, false, true, + "Expired: Default expiry policy applies."}, + {35, new String[]{ROLE_MAP.get("employee")}, new String[]{}, true, false, + "Not expired: Default expiry policy applies - skip if no rules applicable."}, + }; + } + + @Test(dataProvider = "passwordExpiryTestCases") + public void testIsPasswordExpiredWithRules(int daysAgo, String[] roles, String[] groups, + boolean skipIfNoApplicableRules, boolean expectedExpired, + String description) + throws PostAuthenticationFailedException, UserStoreException, IdentityGovernanceException, 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); + when(roleManagementService.getRoleListOfUser(userId, tenantDomain)).thenReturn(getRoles(roles)); - Property property = new Property(); - property.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS); - property.setValue(String.valueOf(10)); + 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); + + // Mock last password update time. + Long updateTime = getUpdateTime(daysAgo); + mockLastPasswordUpdateTime(updateTime, abstractUserStoreManager); + + // 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(Boolean.toString(skipIfNoApplicableRules))); + + boolean isExpired = PasswordPolicyUtils.isPasswordExpired(tenantDomain, tenantAwareUsername); + Assert.assertEquals(isExpired, expectedExpired, description); + } + + private static Long getUpdateTime(Integer daysAgo) { + + return daysAgo != null ? System.currentTimeMillis() - daysAgo * 24 * 60 * 60 * 1000L : null; + } + + private List getRoles(String[] roleIds) { + + List userRoles = new ArrayList<>(); + for (String roleId : roleIds) { + RoleBasicInfo roleInfo = new RoleBasicInfo(); + roleInfo.setId(roleId); + userRoles.add(roleInfo); + } + return userRoles; + } + + private Property[] getPasswordExpiryRulesProperties() { + + Property expiryRule1 = new Property(); + Property expiryRule2 = new Property(); + Property expiryRule3 = 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,roles,eq,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("contractor"))); + expiryRule3.setName(PasswordPolicyConstants.PASSWORD_EXPIRY_RULES_PREFIX+"3"); + expiryRule3.setValue(String.format("3,60,roles,eq,%s,%s", ROLE_MAP.get("employee"), ROLE_MAP.get("manager"))); + + Property[] properties = new Property[3]; + properties[0] = expiryRule1; + properties[1] = expiryRule2; + properties[2] = expiryRule3; + + return properties; + } + + private Property[] getPasswordExpiryInDaysProperty() { + + Property property1 = new Property(); + property1.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS); + property1.setValue(String.valueOf(30)); Property[] properties = new Property[1]; - properties[0] = property; - when(identityGovernanceService.getConfiguration(new String[]{ - PasswordPolicyConstants.CONNECTOR_CONFIG_PASSWORD_EXPIRY_IN_DAYS}, tenantDomain)).thenReturn( - properties); + properties[0] = property1; + return properties; + } - Assert.assertEquals(PasswordPolicyUtils.isPasswordExpired(tenantDomain, "tom@gmail.com"), - true); + private Property[] getSkipIfNoRulesApplicableProperty(String value) { + + Property property1 = new Property(); + property1.setName(PasswordPolicyConstants.CONNECTOR_CONFIG_SKIP_IF_NO_APPLICABLE_RULES); + property1.setValue(value); + Property[] properties = new Property[1]; + properties[0] = property1; + return properties; } + 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); + } }