diff --git a/components/org.wso2.carbon.identity.governance/pom.xml b/components/org.wso2.carbon.identity.governance/pom.xml index 6fcdc19f64..6686144da6 100644 --- a/components/org.wso2.carbon.identity.governance/pom.xml +++ b/components/org.wso2.carbon.identity.governance/pom.xml @@ -48,6 +48,11 @@ org.apache.axis2.wso2 axis2 + + com.h2database + h2 + test + org.testng testng diff --git a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreService.java b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreService.java index a1e0e9333a..c288c51f0a 100644 --- a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreService.java +++ b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreService.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-2025, 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 @@ -24,6 +24,7 @@ import org.wso2.carbon.user.core.UserStoreManager; import org.wso2.carbon.user.core.model.ExpressionCondition; +import java.util.Collections; import java.util.List; import java.util.Map; @@ -128,6 +129,58 @@ List getUserNamesMoreThanProvidedClaimValue(String claimURI, String clai List getUserNamesBetweenProvidedClaimValues(String claimURI, String startValue, String endValue, int tenantId) throws IdentityException; + /** + * Get the list of usernames who have the claim value less than the provided claim value for a given claim URI + * and include or exclude the users with the boolean isIncluded + * based on the nested claim value for a given nested claim URI. + * + * @param claimURI Claim URI. + * @param claimValue Claim value. + * @param nestedClaimURI Nested claim URI. + * @param nestedClaimValue Nested claim value. + * @param tenantId Tenant ID. + * @param isIncluded Include or exclude the users based on the nested claim. + * @return List of usernames. + * @throws IdentityException Identity exception. + */ + default List getUserNamesLessThanClaimWithNestedClaim(String claimURI, + String claimValue, + String nestedClaimURI, + String nestedClaimValue, + int tenantId, + boolean isIncluded) + throws IdentityException { + + return Collections.emptyList(); + } + + /** + * Get the list of usernames who have the claim value between the provided claim values for a given claim URI + * and include or exclude the users with the boolean isIncluded + * based on the nested claim value for a given nested claim URI. + * + * @param claimURI Claim URI. + * @param startValue Start value. + * @param endValue End value. + * @param nestedClaimURI Nested claim URI. + * @param nestedClaimValue Nested claim value. + * @param tenantId Tenant ID. + * @param isIncluded Include or exclude the users based on the nested claim. + * @return List of usernames. + * @throws IdentityException Identity exception. + */ + default List getUserNamesBetweenGivenClaimsWithNestedClaim(String claimURI, + String startValue, + String endValue, + String nestedClaimURI, + String nestedClaimValue, + int tenantId, + boolean isIncluded) + throws IdentityException { + + return Collections.emptyList(); + } + /** * Check whether the identity data store is user store based. * diff --git a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreServiceImpl.java b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreServiceImpl.java index 17386d45a5..5973f8df0a 100644 --- a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreServiceImpl.java +++ b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/service/IdentityDataStoreServiceImpl.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-2025, 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 @@ -179,6 +179,31 @@ public List getUserNamesBetweenProvidedClaimValues(String claimURI, Stri return identityDataStore.getUserNamesBetweenProvidedClaimValues(claimURI, startValue, endValue, tenantId); } + @Override + public List getUserNamesLessThanClaimWithNestedClaim(String claimURI, + String claimValue, + String nestedClaimURI, + String nestedClaimValue, + int tenantId, + boolean isIncluded) + throws IdentityException { + + return identityDataStore.getUserNamesLessThanClaimWithNestedClaim(claimURI, claimValue, + nestedClaimURI, nestedClaimValue, tenantId, isIncluded); + } + + @Override + public List getUserNamesBetweenGivenClaimsWithNestedClaim(String claimURI, String startValue, + String endValue, + String nestedClaimURI, + String nestedClaimValue, int tenantId, + boolean isIncluded) + throws IdentityException { + + return identityDataStore.getUserNamesBetweenGivenClaimsWithNestedClaim(claimURI, startValue, + endValue, nestedClaimURI, nestedClaimValue, tenantId, isIncluded); + } + @Override public boolean isUserStoreBasedIdentityDataStore() { diff --git a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStore.java b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStore.java index a836d819b8..c739f8cd43 100644 --- a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStore.java +++ b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStore.java @@ -1,17 +1,19 @@ /* - * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2016-2025, WSO2 LLC. (http://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package org.wso2.carbon.identity.governance.store; @@ -463,6 +465,82 @@ public List getUserNamesBetweenProvidedClaimValues(String claimURI, Str } } + @Override + public List getUserNamesLessThanClaimWithNestedClaim(String claimURI, String claimValue, + String nestedClaimURI, + String nestedClaimValue, int tenantId, + boolean isIncluded) + throws IdentityException { + + String sqlStmt = SQLQuery.FILTER_USERS_BY_DATA_KEY_LESS_THAN_DATA_VALUE; + String subSqlStmt = SQLQuery.LIST_USERS_FROM_CLAIM; + if (isIncluded) { + sqlStmt = sqlStmt + " AND USER_NAME IN (" + subSqlStmt + ")"; + } else { + sqlStmt = sqlStmt + " AND USER_NAME NOT IN (" + subSqlStmt + ")"; + } + + List userNames = new ArrayList<>(); + try (Connection connection = IdentityDatabaseUtil.getDBConnection(false)) { + try (PreparedStatement prepStmt = connection.prepareStatement(sqlStmt)) { + prepStmt.setString(1, claimURI); + prepStmt.setInt(2, tenantId); + prepStmt.setString(3, claimValue); + prepStmt.setString(4, nestedClaimURI); + prepStmt.setString(5, nestedClaimValue); + prepStmt.setInt(6, tenantId); + prepStmt.setString(7, "%"); + try (ResultSet resultSet = prepStmt.executeQuery()) { + while (resultSet.next()) { + String username = resultSet.getString(1); + userNames.add(username); + } + } + return userNames; + } + } catch (SQLException e) { + throw new IdentityException("Error occurred while retrieving users from Identity Store.", e); + } + } + + @Override + public List getUserNamesBetweenGivenClaimsWithNestedClaim(String claimURI, String startValue, + String endValue, + String nestedClaimURI, + String nestedClaimValue, int tenantId, + boolean isIncluded) + throws IdentityException { + + String sqlStmt = SQLQuery.FILTER_USERS_BY_DATA_KEY_LESS_THAN_AND_GREATER_THAN_DATA_VALUES; + String subSqlStmt = SQLQuery.LIST_USERS_FROM_CLAIM; + if (isIncluded) { + sqlStmt = sqlStmt + " AND USER_NAME IN (" + subSqlStmt + ")"; + } else { + sqlStmt = sqlStmt + " AND USER_NAME NOT IN (" + subSqlStmt + ")"; + } + List userNames = new ArrayList<>(); + try (Connection connection = IdentityDatabaseUtil.getDBConnection(false)) { + try (PreparedStatement prepStmt = connection.prepareStatement(sqlStmt)) { + prepStmt.setString(1, claimURI); + prepStmt.setInt(2, tenantId); + prepStmt.setString(3, endValue); + prepStmt.setString(4, startValue); + prepStmt.setString(5, nestedClaimURI); + prepStmt.setString(6, nestedClaimValue); + prepStmt.setInt(7, tenantId); + prepStmt.setString(8, "%"); + try (ResultSet resultSet = prepStmt.executeQuery()) { + while (resultSet.next()) { + String username = resultSet.getString(1); + userNames.add(username); + } + } + return userNames; + } + } catch (SQLException e) { + throw new IdentityException("Error occurred while retrieving users from Identity Store.", e); + } + } private void populatePrepareStatement(SqlBuilder sqlBuilder, PreparedStatement prepStmt, int startIndex, int endIndex) throws SQLException { diff --git a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/UserIdentityDataStore.java b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/UserIdentityDataStore.java index 96b5af2c15..6f38ceba6b 100644 --- a/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/UserIdentityDataStore.java +++ b/components/org.wso2.carbon.identity.governance/src/main/java/org/wso2/carbon/identity/governance/store/UserIdentityDataStore.java @@ -1,17 +1,19 @@ /* - * Copyright (c) 2016, WSO2 Inc. (http://www.wso2.org) All Rights Reserved. + * Copyright (c) 2016-2025, WSO2 LLC. (http://www.wso2.com). * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. + * WSO2 LLC. licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except + * in compliance with the License. * You may obtain a copy of the License at * - * http://www.apache.org/licenses/LICENSE-2.0 + * http://www.apache.org/licenses/LICENSE-2.0 * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. */ package org.wso2.carbon.identity.governance.store; @@ -161,4 +163,56 @@ public List getUserNamesBetweenProvidedClaimValues(String claimURI, Stri // Return an immutable empty list if subclasses do not have any overrides. return Collections.emptyList(); } + + /** + * Get the list of usernames who have the claim value less than the provided claim value for a given claim URI + * and include or exclude the users with the boolean isIncluded + * based on the nested claim value for a given nested claim URI. + * + * @param claimURI Claim URI. + * @param claimValue Claim value. + * @param nestedClaimURI Nested claim URI. + * @param nestedClaimValue Nested claim value. + * @param tenantId Tenant ID. + * @param isIncluded Include or exclude the users based on the nested claim. + * @return List of usernames. + * @throws IdentityException Identity exception. + */ + public List getUserNamesLessThanClaimWithNestedClaim(String claimURI, + String claimValue, + String nestedClaimURI, + String nestedClaimValue, + int tenantId, + boolean isIncluded) throws IdentityException { + + // Return an immutable empty list if subclasses do not have any overrides. + return Collections.emptyList(); + } + + /** + * Get the list of usernames who have the claim value between the provided claim values for a given claim URI + * and include or exclude the users with the boolean isIncluded + * based on the nested claim value for a given nested claim URI. + * + * @param claimURI Claim URI. + * @param startValue Start value. + * @param endValue End value. + * @param nestedClaimURI Nested claim URI. + * @param nestedClaimValue Nested claim value. + * @param tenantId Tenant ID. + * @param isIncluded Include or exclude the users based on the nested claim. + * @return List of usernames. + * @throws IdentityException Identity exception. + */ + public List getUserNamesBetweenGivenClaimsWithNestedClaim(String claimURI, + String startValue, + String endValue, + String nestedClaimURI, + String nestedClaimValue, + int tenantId, + boolean isIncluded) throws IdentityException { + + // Return an immutable empty list if subclasses do not have any overrides. + return Collections.emptyList(); + } } diff --git a/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStoreTest.java b/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStoreTest.java new file mode 100644 index 0000000000..82fa52f38e --- /dev/null +++ b/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/store/JDBCIdentityDataStoreTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (c) 2025, 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.governance.store; + +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.identity.core.util.IdentityDatabaseUtil; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.governance.service.IdentityDataStoreService; +import org.wso2.carbon.identity.governance.service.IdentityDataStoreServiceImpl; +import org.wso2.carbon.identity.governance.store.Utils.TestUtils; +import org.wso2.carbon.user.core.UserRealm; +import org.wso2.carbon.user.core.UserStoreManager; + +import java.sql.Connection; +import java.util.List; + +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; +import static org.testng.Assert.assertEquals; + +public class JDBCIdentityDataStoreTest { + + private static final int TENANT_ID = 3; + private static final String IDENTITY_DATA_STORE_TYPE = "org.wso2.carbon.identity." + + "governance.store.JDBCIdentityDataStore"; + private static final String CLAIM_URI = "http://wso2.org/claims/identity/lastLogonTime"; + private static final String CLAIM_VALUE_1 = "1680000000000"; + private static final String CLAIM_VALUE_2 = "1673000000000"; + private static final String NESTED_CLAIM_URI = "http://wso2.org/claims/identity/accountState"; + private static final String NESTED_CLAIM_VALUE = "DISABLED"; + + private MockedStatic mockedIdentityDatabaseUtils; + private MockedStatic mockedIdentityTenantUtil; + private MockedStatic mockedCarbonContext; + private MockedStatic mockedIdentityUtil; + + private UserStoreManager userStoreManager; + IdentityDataStoreService identityDataStoreService; + + @BeforeMethod + public void setUp() throws Exception { + + TestUtils.initiateH2Base(); + TestUtils.mockDataSource(); + + Connection connection = TestUtils.getConnection(); + mockedIdentityDatabaseUtils = Mockito.mockStatic(IdentityDatabaseUtil.class); + mockedIdentityDatabaseUtils.when(() -> IdentityDatabaseUtil.getDBConnection(anyBoolean())) + .thenReturn(connection); + + mockedIdentityTenantUtil = Mockito.mockStatic(IdentityTenantUtil.class); + mockedIdentityTenantUtil.when(() -> IdentityTenantUtil.getTenantId(anyString())) + .thenReturn(TENANT_ID); + + UserRealm userRealm = mock(UserRealm.class); + userStoreManager = mock(UserStoreManager.class); + + mockedCarbonContext = Mockito.mockStatic(CarbonContext.class); + CarbonContext carbonContext = mock(CarbonContext.class); + mockedCarbonContext.when(CarbonContext::getThreadLocalCarbonContext).thenReturn(carbonContext); + mockedCarbonContext.when(carbonContext::getUserRealm).thenReturn(userRealm); + mockedCarbonContext.when(userRealm::getUserStoreManager).thenReturn(userStoreManager); + mockedCarbonContext.when(() -> + userStoreManager.getSecondaryUserStoreManager(anyString())).thenReturn(userStoreManager); + + mockedIdentityUtil = Mockito.mockStatic(IdentityUtil.class); + mockedIdentityUtil.when(() -> IdentityUtil.getProperty(anyString())).thenReturn + (IDENTITY_DATA_STORE_TYPE); + identityDataStoreService = spy(new IdentityDataStoreServiceImpl()); + } + + @AfterMethod + public void tearDown() throws Exception { + + mockedIdentityDatabaseUtils.close(); + mockedIdentityTenantUtil.close(); + mockedCarbonContext.close(); + mockedIdentityUtil.close(); + TestUtils.closeH2Base(); + } + + @DataProvider + public Object[] getIsIncludedValues() { + + return new Object[]{true, false}; + } + + @DataProvider + Object[][] testDataForNestedLessThan() { + return new Object[][] { + { true, 3 }, + { false, 2 } + }; + } + + @DataProvider + Object[][] testDataForNestedBetween() { + return new Object[][] { + { true, 2 }, + { false, 2 } + }; + } + + @Test(dataProvider = "testDataForNestedLessThan") + public void testGetUserNamesLessThanClaimWithNestedClaim(boolean isIncluded, int expected) throws Exception { + + List userNames = + identityDataStoreService.getUserNamesLessThanClaimWithNestedClaim(CLAIM_URI, CLAIM_VALUE_1, + NESTED_CLAIM_URI, NESTED_CLAIM_VALUE, TENANT_ID, isIncluded); + + assertEquals(userNames.size(), expected); + } + + @Test(dataProvider = "testDataForNestedBetween") + public void testGetUserNamesBetweenGivenClaimsWithNestedClaim(boolean isIncluded, int expected) throws Exception { + + List userNames = + identityDataStoreService.getUserNamesBetweenGivenClaimsWithNestedClaim(CLAIM_URI, CLAIM_VALUE_2, + CLAIM_VALUE_1, NESTED_CLAIM_URI, NESTED_CLAIM_VALUE, TENANT_ID, isIncluded); + + assertEquals(userNames.size(), expected); + } +} diff --git a/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/store/Utils/TestUtils.java b/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/store/Utils/TestUtils.java new file mode 100644 index 0000000000..ed3f8d7e45 --- /dev/null +++ b/components/org.wso2.carbon.identity.governance/src/test/java/org/wso2/carbon/identity/governance/store/Utils/TestUtils.java @@ -0,0 +1,109 @@ +/* + * Copyright (c) 2025, 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.governance.store.Utils; + +import org.apache.commons.dbcp.BasicDataSource; +import org.apache.commons.lang.StringUtils; +import org.wso2.carbon.base.CarbonBaseConstants; +import org.wso2.carbon.context.CarbonContext; +import org.wso2.carbon.context.internal.CarbonContextDataHolder; +import org.wso2.carbon.user.api.UserRealm; +import org.wso2.carbon.user.core.util.DatabaseUtil; + +import java.lang.reflect.Field; +import java.nio.file.Paths; +import java.sql.Connection; +import java.sql.SQLException; +import java.util.HashMap; +import java.util.Map; + +import javax.sql.DataSource; + +import static org.mockito.Mockito.mock; + +public class TestUtils { + + public static final String DB_NAME = "test_db"; + public static final String H2_SCRIPT_NAME = "h2.sql"; + public static Map dataSourceMap = new HashMap<>(); + + public static String getFilePath(String fileName) { + + if (StringUtils.isNotBlank(fileName)) { + return Paths.get(System.getProperty("user.dir"), "src", "test", "resources", "dbscripts", + fileName).toString(); + } + throw new IllegalArgumentException("DB Script file name cannot be empty."); + } + + public static void initiateH2Base() throws Exception { + + BasicDataSource dataSource = new BasicDataSource(); + dataSource.setDriverClassName("org.h2.Driver"); + dataSource.setUsername("username"); + dataSource.setPassword("password"); + dataSource.setUrl("jdbc:h2:mem:test" + DB_NAME); + dataSource.setTestOnBorrow(true); + dataSource.setValidationQuery("select 1"); + try (Connection connection = dataSource.getConnection()) { + connection.createStatement().executeUpdate("RUNSCRIPT FROM '" + getFilePath(H2_SCRIPT_NAME) + "'"); + } + dataSourceMap.put(DB_NAME, dataSource); + } + + public static void closeH2Base() throws Exception { + + BasicDataSource dataSource = dataSourceMap.remove(DB_NAME); + if (dataSource != null) { + dataSource.close(); + } + } + + public static Connection getConnection() throws SQLException { + + if (dataSourceMap.get(DB_NAME) != null) { + return dataSourceMap.get(DB_NAME).getConnection(); + } + throw new RuntimeException("No datasource initiated for database: " + DB_NAME); + } + + public static void mockDataSource() throws Exception { + + String carbonHome = Paths.get(System.getProperty("user.dir"), "target", "test-classes").toString(); + System.setProperty(CarbonBaseConstants.CARBON_HOME, carbonHome); + System.setProperty(CarbonBaseConstants.CARBON_CONFIG_DIR_PATH, Paths.get(carbonHome, + "repository/conf").toString()); + + DataSource dataSource = dataSourceMap.get(DB_NAME); + + setStatic(DatabaseUtil.class.getDeclaredField("dataSource"), dataSource); + + Field carbonContextHolderField = + CarbonContext.getThreadLocalCarbonContext().getClass().getDeclaredField("carbonContextHolder"); + carbonContextHolderField.setAccessible(true); + CarbonContextDataHolder carbonContextHolder + = (CarbonContextDataHolder) carbonContextHolderField.get(CarbonContext.getThreadLocalCarbonContext()); + carbonContextHolder.setUserRealm(mock(UserRealm.class)); + } + + private static void setStatic(Field field, Object newValue) throws Exception { + field.setAccessible(true); + field.set(null, newValue); + } +} diff --git a/components/org.wso2.carbon.identity.governance/src/test/resources/dbscripts/h2.sql b/components/org.wso2.carbon.identity.governance/src/test/resources/dbscripts/h2.sql new file mode 100644 index 0000000000..877b4ac834 --- /dev/null +++ b/components/org.wso2.carbon.identity.governance/src/test/resources/dbscripts/h2.sql @@ -0,0 +1,20 @@ +-- ----------------------------------------------------- +-- Table IDN_IDENTITY_USER_DATA +-- ----------------------------------------------------- +CREATE TABLE IDN_IDENTITY_USER_DATA ( + TENANT_ID INTEGER DEFAULT -1234, + USER_NAME VARCHAR(255) NOT NULL, + DATA_KEY VARCHAR(255) NOT NULL, + DATA_VALUE VARCHAR(2048), + PRIMARY KEY (TENANT_ID, USER_NAME, DATA_KEY) +); + +INSERT INTO IDN_IDENTITY_USER_DATA (TENANT_ID, USER_NAME, DATA_KEY, DATA_VALUE) VALUES +(3, 'DEFAULT/sampleUser1@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1672704000000'), +(3, 'DEFAULT/sampleUser2@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1673481600000'), +(3, 'DEFAULT/sampleUser3@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674000000000'), +(3, 'DEFAULT/sampleUser4@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674518400000'), +(3, 'DEFAULT/sampleUser5@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674950400000'), +(3, 'DEFAULT/sampleUser1@xmail.com', 'http://wso2.org/claims/identity/accountState', 'DISABLED'), +(3, 'DEFAULT/sampleUser3@xmail.com', 'http://wso2.org/claims/identity/accountState', 'DISABLED'), +(3, 'DEFAULT/sampleUser5@xmail.com', 'http://wso2.org/claims/identity/accountState', 'DISABLED'); diff --git a/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/constants/IdleAccIdentificationConstants.java b/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/constants/IdleAccIdentificationConstants.java index 4337713e32..c0557655bb 100644 --- a/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/constants/IdleAccIdentificationConstants.java +++ b/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/constants/IdleAccIdentificationConstants.java @@ -1,5 +1,5 @@ /* - * Copyright (c) 2023, WSO2 LLC. (http://www.wso2.com). + * Copyright (c) 2023-2025, 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 @@ -26,6 +26,8 @@ public class IdleAccIdentificationConstants { public static final String IDLE_ACC_IDENTIFICATION_SERVICE_ERROR_PREFIX = "IDLE_ACC-"; public static final String LAST_LOGIN_TIME_CLAIM = "http://wso2.org/claims/identity/lastLogonTime"; + public static final String ACCOUNT_STATE_CLAIM_URI = "http://wso2.org/claims/identity/accountState"; + public static final String ACCOUNT_STATE_DISABLED = "DISABLED"; /** * Class containing SQL queries. diff --git a/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/IdleAccountIdentificationService.java b/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/IdleAccountIdentificationService.java index f3adfc460f..54de111072 100644 --- a/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/IdleAccountIdentificationService.java +++ b/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/IdleAccountIdentificationService.java @@ -4,6 +4,7 @@ import org.wso2.carbon.identity.idle.account.identification.models.InactiveUserModel; import java.time.LocalDateTime; +import java.util.Collections; import java.util.List; /** @@ -38,4 +39,28 @@ List getInactiveUsersFromSpecificDate(LocalDateTime inactiveA */ List getLimitedInactiveUsersFromSpecificDate(LocalDateTime inactiveAfter, LocalDateTime excludeBefore, String tenantDomain) throws IdleAccountIdentificationException; + + /** + * Get inactive users from a specific date or from a specific date excluding the oldest inactive users while + * filtering the disabled users based on the value provided for the isDisabled. + * If isDisabled is true, the method will return inactive and disabled users. + * If isDisabled is false, the method will return inactive and non-disabled users filtering disabled users. + * (Example: If isDisabled is true, the method will return all the inactive users who have not logged in since + * 2023-01-31 00:00:00.000 excluding users who have not logged in since 2023-01-01 00:00:00.000 and who are + * in the state of DISABLED) + * + * @param inactiveAfter Inactive after date. + * @param excludeBefore Exclude before date. + * @param tenantDomain Tenant domain. + * @param isDisabled Filter based on the state DISABLED. + * @return List of inactive users. + * @throws IdleAccountIdentificationException Idle account identification exception. + */ + default List filterInactiveUsersIfDisabled(LocalDateTime inactiveAfter, + LocalDateTime excludeBefore, String tenantDomain, + boolean isDisabled) + throws IdleAccountIdentificationException { + + return Collections.emptyList(); + } } diff --git a/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/impl/IdleAccountIdentificationServiceImpl.java b/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/impl/IdleAccountIdentificationServiceImpl.java index fb381e8f7a..610bfd3b84 100644 --- a/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/impl/IdleAccountIdentificationServiceImpl.java +++ b/components/org.wso2.carbon.identity.idle.account.identification/src/main/java/org/wso2/carbon/identity/idle/account/identification/services/impl/IdleAccountIdentificationServiceImpl.java @@ -92,6 +92,51 @@ public List getLimitedInactiveUsersFromSpecificDate(LocalDate return inactiveUsers; } + /** + * Retrieve inactive users if the account is disabled or non-disabled depending on the value for isDisabled. + * + * @param inactiveAfter Inactive after date. + * @param excludeBefore Exclude before date. + * @param tenantDomain Tenant domain. + * @param isDisabled isDisabled. + * @return List of inactive users. + * @throws IdleAccountIdentificationException Idle account identification exception. + */ + public List filterInactiveUsersIfDisabled(LocalDateTime inactiveAfter, + LocalDateTime excludeBefore, String tenantDomain, + boolean isDisabled) + throws IdleAccountIdentificationException { + + List inactiveUsers = new ArrayList<>(); + int tenantId = IdentityTenantUtil.getTenantId(tenantDomain); + String lastLoginTime = Long.toString(inactiveAfter.toEpochSecond(ZoneOffset.UTC)); + List usernames; + try { + if (excludeBefore == null) { + usernames = IdleAccountIdentificationDataHolder.getInstance().getIdentityDataStoreService() + .getUserNamesLessThanClaimWithNestedClaim( + IdleAccIdentificationConstants.LAST_LOGIN_TIME_CLAIM, lastLoginTime, + IdleAccIdentificationConstants.ACCOUNT_STATE_CLAIM_URI, + IdleAccIdentificationConstants.ACCOUNT_STATE_DISABLED, tenantId, isDisabled); + } else { + String excludeDateEpoch = Long.toString(excludeBefore.toEpochSecond(ZoneOffset.UTC)); + usernames = IdleAccountIdentificationDataHolder.getInstance().getIdentityDataStoreService() + .getUserNamesBetweenGivenClaimsWithNestedClaim( + IdleAccIdentificationConstants.LAST_LOGIN_TIME_CLAIM, excludeDateEpoch, lastLoginTime, + IdleAccIdentificationConstants.ACCOUNT_STATE_CLAIM_URI, + IdleAccIdentificationConstants.ACCOUNT_STATE_DISABLED, tenantId, isDisabled); + } + if (!usernames.isEmpty()) { + inactiveUsers = buildInactiveUsers(usernames); + } + } catch (IdentityException e) { + IdleAccIdentificationConstants.ErrorMessages errorEnum = + IdleAccIdentificationConstants.ErrorMessages.ERROR_RETRIEVE_INACTIVE_USERS_FROM_DB; + throw new IdleAccountIdentificationServerException(errorEnum.getCode(), errorEnum.getMessage()); + } + return inactiveUsers; + } + /** * Build a list of inactive users. * diff --git a/components/org.wso2.carbon.identity.idle.account.identification/src/test/java/org/wso2/carbon/identity/idle/account/identification/IdleAccountIdentificationServiceImplTest.java b/components/org.wso2.carbon.identity.idle.account.identification/src/test/java/org/wso2/carbon/identity/idle/account/identification/IdleAccountIdentificationServiceImplTest.java index 9eb8273877..eb3ca8112d 100644 --- a/components/org.wso2.carbon.identity.idle.account.identification/src/test/java/org/wso2/carbon/identity/idle/account/identification/IdleAccountIdentificationServiceImplTest.java +++ b/components/org.wso2.carbon.identity.idle.account.identification/src/test/java/org/wso2/carbon/identity/idle/account/identification/IdleAccountIdentificationServiceImplTest.java @@ -157,4 +157,29 @@ public void testGetLimitedInactiveUsersFromSpecificDate(LocalDateTime inactiveAf assertEquals(inactiveUsers.size(), expected); } + + @DataProvider + public Object[][] getDatesAndFilter() { + + return new Object[][]{ + {LocalDate.parse("2023-01-31").atStartOfDay(), null, true, 3}, + {LocalDate.parse("2023-01-31").atStartOfDay(), null, false, 2}, + {LocalDate.parse("2023-01-31").atStartOfDay(), LocalDate.parse("2023-01-15").atStartOfDay(), true, 2}, + {LocalDate.parse("2023-01-31").atStartOfDay(), LocalDate.parse("2023-01-15").atStartOfDay(), false, 1} + }; + } + + @Test(dataProvider = "getDatesAndFilter") + public void testFilterInactiveUsersIfDisabled(LocalDateTime inactiveAfter, LocalDateTime excludeBefore, + boolean isDisabled, int expected) throws Exception { + + IdleAccountIdentificationServiceImpl idleAccountIdentificationService = + spy(IdleAccountIdentificationServiceImpl.class); + doReturn(SAMPLE_USER_ID).when(idleAccountIdentificationService).fetchUserId(anyString()); + + List inactiveUsers = idleAccountIdentificationService. + filterInactiveUsersIfDisabled(inactiveAfter, excludeBefore, TENANT_DOMAIN, isDisabled); + + assertEquals(inactiveUsers.size(), expected); + } } diff --git a/components/org.wso2.carbon.identity.idle.account.identification/src/test/resources/dbscripts/h2.sql b/components/org.wso2.carbon.identity.idle.account.identification/src/test/resources/dbscripts/h2.sql index 9e8e487883..877b4ac834 100644 --- a/components/org.wso2.carbon.identity.idle.account.identification/src/test/resources/dbscripts/h2.sql +++ b/components/org.wso2.carbon.identity.idle.account.identification/src/test/resources/dbscripts/h2.sql @@ -14,4 +14,7 @@ INSERT INTO IDN_IDENTITY_USER_DATA (TENANT_ID, USER_NAME, DATA_KEY, DATA_VALUE) (3, 'DEFAULT/sampleUser2@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1673481600000'), (3, 'DEFAULT/sampleUser3@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674000000000'), (3, 'DEFAULT/sampleUser4@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674518400000'), -(3, 'DEFAULT/sampleUser5@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674950400000'); +(3, 'DEFAULT/sampleUser5@xmail.com', 'http://wso2.org/claims/identity/lastLogonTime', '1674950400000'), +(3, 'DEFAULT/sampleUser1@xmail.com', 'http://wso2.org/claims/identity/accountState', 'DISABLED'), +(3, 'DEFAULT/sampleUser3@xmail.com', 'http://wso2.org/claims/identity/accountState', 'DISABLED'), +(3, 'DEFAULT/sampleUser5@xmail.com', 'http://wso2.org/claims/identity/accountState', 'DISABLED');