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');