diff --git a/.changeset/fast-penguins-reply.md b/.changeset/fast-penguins-reply.md new file mode 100644 index 00000000000..f6f55635d1f --- /dev/null +++ b/.changeset/fast-penguins-reply.md @@ -0,0 +1,5 @@ +--- +"@wso2is/identity-apps-core": patch +--- + +Enable Role Modification for Sub-Org Admins in AppPortalRoleManagementListener. diff --git a/.github/workflows/pr-builder.yml b/.github/workflows/pr-builder.yml index 393767a1222..b50ef8261c4 100644 --- a/.github/workflows/pr-builder.yml +++ b/.github/workflows/pr-builder.yml @@ -227,7 +227,7 @@ jobs: - name: ☕ Set up JDK 11 id: jdk-setup - uses: actions/setup-java@v2 + uses: actions/setup-java@v3 with: java-version: ${{ matrix.java-version }} distribution: "adopt" diff --git a/identity-apps-core/components/org.wso2.identity.apps.common/pom.xml b/identity-apps-core/components/org.wso2.identity.apps.common/pom.xml index 9f9faa8fc8c..a7ab2f957fc 100644 --- a/identity-apps-core/components/org.wso2.identity.apps.common/pom.xml +++ b/identity-apps-core/components/org.wso2.identity.apps.common/pom.xml @@ -1,7 +1,7 @@ + + org.testng + testng + test + + + org.mockito + mockito-core + test + diff --git a/identity-apps-core/components/org.wso2.identity.apps.common/src/main/java/org/wso2/identity/apps/common/listner/AppPortalRoleManagementListener.java b/identity-apps-core/components/org.wso2.identity.apps.common/src/main/java/org/wso2/identity/apps/common/listner/AppPortalRoleManagementListener.java index 6cc3e551877..a16d320c33b 100644 --- a/identity-apps-core/components/org.wso2.identity.apps.common/src/main/java/org/wso2/identity/apps/common/listner/AppPortalRoleManagementListener.java +++ b/identity-apps-core/components/org.wso2.identity.apps.common/src/main/java/org/wso2/identity/apps/common/listner/AppPortalRoleManagementListener.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 @@ -20,6 +20,8 @@ import org.apache.commons.lang.StringUtils; import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.organization.management.service.exception.OrganizationManagementException; +import org.wso2.carbon.identity.organization.management.service.util.OrganizationManagementUtil; import org.wso2.carbon.identity.role.v2.mgt.core.exception.IdentityRoleManagementException; import org.wso2.carbon.identity.role.v2.mgt.core.listener.AbstractRoleManagementListener; import org.wso2.carbon.identity.role.v2.mgt.core.model.Permission; @@ -100,6 +102,15 @@ public void preUpdateUserListOfRole(String roleId, List newUserIDList, L return; } + try { + if (OrganizationManagementUtil.isOrganization(tenantDomain)) { + return; + } + } catch (OrganizationManagementException e) { + throw new IdentityRoleManagementException("Failed to determine if the tenant is a sub-organization for " + + "tenant domain: " + tenantDomain, e); + } + String adminUserId; try { UserRealm userRealm = diff --git a/identity-apps-core/components/org.wso2.identity.apps.common/src/test/java/org/wso2/identity/apps/common/listener/AppPortalRoleManagementListenerTest.java b/identity-apps-core/components/org.wso2.identity.apps.common/src/test/java/org/wso2/identity/apps/common/listener/AppPortalRoleManagementListenerTest.java new file mode 100644 index 00000000000..c451f31c425 --- /dev/null +++ b/identity-apps-core/components/org.wso2.identity.apps.common/src/test/java/org/wso2/identity/apps/common/listener/AppPortalRoleManagementListenerTest.java @@ -0,0 +1,207 @@ +/* + * 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.identity.apps.common.listener; + +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.MockitoAnnotations; +import org.testng.annotations.AfterMethod; +import org.testng.annotations.BeforeMethod; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; +import org.wso2.carbon.base.CarbonBaseConstants; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.organization.management.service.util.OrganizationManagementUtil; +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.Role; +import org.wso2.carbon.user.core.UserRealm; +import org.wso2.carbon.user.core.common.AbstractUserStoreManager; +import org.wso2.carbon.user.core.config.RealmConfiguration; +import org.wso2.identity.apps.common.internal.AppsCommonDataHolder; +import org.wso2.identity.apps.common.listner.AppPortalRoleManagementListener; + +import java.nio.file.Paths; +import java.util.Collections; +import java.util.List; + +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotNull; +import static org.testng.Assert.fail; +import static org.wso2.carbon.base.CarbonBaseConstants.CARBON_HOME; +import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.ADMINISTRATOR; +import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.APPLICATION; +import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.ORGANIZATION; +import static org.wso2.carbon.identity.role.v2.mgt.core.RoleConstants.RoleTableColumns.ROLE_NAME; +import static org.wso2.identity.apps.common.util.AppPortalConstants.CONSOLE_APP; + +/** + * Test class for AppPortalRoleManagementListener test cases. + */ +public class AppPortalRoleManagementListenerTest { + + private final String roleId = "roleId"; + private final String deletedUserId = "deletedUserId"; + private final String adminUserId = "adminUserId"; + private final String tenantDomain = "abc.com"; + private final boolean isAdminRole = true; + private final boolean isOrganization = true; + + private AutoCloseable closeable; + + private MockedStatic privilegedCarbonContext; + private MockedStatic organizationManagementUtil; + private MockedStatic appsCommonDataHolder; + + @Mock + private UserRealm mockUserRealm; + + @Mock + private RoleManagementService roleManagementService; + + @Mock + private Role role; + + @BeforeMethod + public void setUp() { + + setUpCarbonHome(); + privilegedCarbonContext = mockStatic(PrivilegedCarbonContext.class); + organizationManagementUtil = mockStatic(OrganizationManagementUtil.class); + appsCommonDataHolder = mockStatic(AppsCommonDataHolder.class); + mockCarbonContext(privilegedCarbonContext); + closeable = MockitoAnnotations.openMocks(this); + } + + @AfterMethod + public void tearDown() throws Exception { + + privilegedCarbonContext.close(); + organizationManagementUtil.close(); + appsCommonDataHolder.close(); + closeable.close(); + } + + @DataProvider(name = "preUpdateUserListOfRoleDataProvider") + public Object[][] preUpdateUserListOfRoleDataProvider() { + return new Object[][]{ + // Test case where deletedUserIDList == null. + {roleId, Collections.emptyList(), null, tenantDomain, isAdminRole, !isOrganization, adminUserId}, + + // Test case where !isAdministratorRole. + {roleId, Collections.emptyList(), Collections.singletonList(deletedUserId), tenantDomain, !isAdminRole, + !isOrganization, adminUserId}, + + // Test case where isOrganization == true. + {roleId, Collections.emptyList(), Collections.singletonList(deletedUserId), tenantDomain, isAdminRole, + isOrganization, adminUserId}, + + // Test case where deletedUserIDList contains adminUserId. + {roleId, Collections.emptyList(), Collections.singletonList(adminUserId), tenantDomain, isAdminRole, + !isOrganization, adminUserId} + }; + } + + @Test(dataProvider = "preUpdateUserListOfRoleDataProvider") + public void testPreUpdateUserListOfRole(String roleId, List newUserIDList, List deletedUserIDList, + String tenantDomain, boolean isAdminRole, boolean isOrganization, String adminUserId) + throws Exception { + + AppPortalRoleManagementListener appPortalRoleManagementListener = spy( + new AppPortalRoleManagementListener(true)); + + // Set up the mock behaviors. + organizationManagementUtil.when(() -> + OrganizationManagementUtil.isOrganization(tenantDomain)).thenReturn(isOrganization); + AbstractUserStoreManager mockUserStoreManager = mock(AbstractUserStoreManager.class); + when(mockUserRealm.getRealmConfiguration()).thenReturn(mock(RealmConfiguration.class)); + when(mockUserRealm.getRealmConfiguration().getAdminUserName()).thenReturn(ADMINISTRATOR); + when(mockUserRealm.getUserStoreManager()).thenReturn(mockUserStoreManager); + when(mockUserStoreManager.getUserIDFromUserName(anyString())).thenReturn(adminUserId); + mockAppsCommonDataHolder(appsCommonDataHolder); + when(roleManagementService.getRole(roleId, tenantDomain)).thenReturn(role); + setRoleAttributes(isAdminRole); + + // Call the method. + if (!isAdminRole || deletedUserIDList == null || isOrganization || !deletedUserIDList.contains(adminUserId)) { + appPortalRoleManagementListener.preUpdateUserListOfRole(roleId, newUserIDList, deletedUserIDList, + tenantDomain); + } else { + Exception exception = null; + try { + appPortalRoleManagementListener.preUpdateUserListOfRole(roleId, newUserIDList, deletedUserIDList, + tenantDomain); + fail("Expected IdentityRoleManagementException was not thrown"); + } catch (IdentityRoleManagementException e) { + exception = e; + } + assertNotNull(exception); + assertEquals(exception.getMessage(), "Deleting the tenant admin from 'Administrator' role " + + "belongs to the 'Console' application is not allowed."); + } + + if (deletedUserIDList == null || !isAdminRole) { + organizationManagementUtil.verify(() -> OrganizationManagementUtil.isOrganization(tenantDomain), never()); + } else if (isOrganization) { + organizationManagementUtil.verify(() -> OrganizationManagementUtil.isOrganization(tenantDomain)); + } + } + + private void setRoleAttributes(boolean isAdminRole) { + + if (isAdminRole) { + when(role.getName()).thenReturn(ADMINISTRATOR); + when(role.getAudience()).thenReturn(APPLICATION); + when(role.getAudienceName()).thenReturn(CONSOLE_APP); + } else { + when(role.getName()).thenReturn(ROLE_NAME); + when(role.getAudience()).thenReturn(ORGANIZATION); + when(role.getAudienceName()).thenReturn("org1"); + } + } + + private static void setUpCarbonHome() { + + String carbonHome = Paths.get(System.getProperty("user.dir"), "target", "test-classes").toString(); + System.setProperty(CARBON_HOME, carbonHome); + System.setProperty(CarbonBaseConstants.CARBON_CONFIG_DIR_PATH, Paths.get(carbonHome, + "repository/conf").toString()); + } + + @SuppressWarnings("ResultOfMethodCallIgnored") + private void mockAppsCommonDataHolder(MockedStatic appsCommonDataHolder) { + + AppsCommonDataHolder mockAppsCommonDataHolder = mock(AppsCommonDataHolder.class); + appsCommonDataHolder.when(AppsCommonDataHolder::getInstance).thenReturn(mockAppsCommonDataHolder); + when(mockAppsCommonDataHolder.getRoleManagementServiceV2()).thenReturn(roleManagementService); + } + + private void mockCarbonContext(MockedStatic privilegedCarbonContext) { + + PrivilegedCarbonContext mockPrivilegedCarbonContext = mock(PrivilegedCarbonContext.class); + privilegedCarbonContext.when( + PrivilegedCarbonContext::getThreadLocalCarbonContext).thenReturn(mockPrivilegedCarbonContext); + when(PrivilegedCarbonContext.getThreadLocalCarbonContext().getUserRealm()).thenReturn(mockUserRealm); + } +} diff --git a/identity-apps-core/components/org.wso2.identity.apps.common/src/test/resources/testng.xml b/identity-apps-core/components/org.wso2.identity.apps.common/src/test/resources/testng.xml new file mode 100644 index 00000000000..ef9b924a8fc --- /dev/null +++ b/identity-apps-core/components/org.wso2.identity.apps.common/src/test/resources/testng.xml @@ -0,0 +1,26 @@ + + + + + + + + + + diff --git a/identity-apps-core/pom.xml b/identity-apps-core/pom.xml index 4f1a72fb60c..559ccc5cb66 100644 --- a/identity-apps-core/pom.xml +++ b/identity-apps-core/pom.xml @@ -1,7 +1,7 @@ + + org.testng + testng + ${testng.version} + test + + + org.mockito + mockito-core + ${mockito.version} + test + @@ -740,5 +753,9 @@ true false + + + 7.10.1 + 5.3.1