diff --git a/components/org.wso2.carbon.identity.organization.management.handler/pom.xml b/components/org.wso2.carbon.identity.organization.management.handler/pom.xml index f7a98876f..6510e80a7 100644 --- a/components/org.wso2.carbon.identity.organization.management.handler/pom.xml +++ b/components/org.wso2.carbon.identity.organization.management.handler/pom.xml @@ -56,7 +56,22 @@ org.wso2.carbon.identity.governance org.wso2.carbon.identity.governance - + + org.wso2.carbon.identity.organization.management + org.wso2.carbon.identity.organization.management.application + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.event + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.role.v2.mgt.core + + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.application.mgt + org.testng @@ -115,6 +130,15 @@ org.wso2.carbon.identity.event.services; version="${carbon.identity.package.import.version.range}", org.wso2.carbon.identity.recovery.*; version="${identity.governance.imp.pkg.version.range}", org.wso2.carbon.identity.governance; version="${identity.governance.imp.pkg.version.range}", + org.wso2.carbon.identity.role.v2.mgt.core.*; version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.organization.management.application.*; + version="${org.wso2.identity.organization.mgt.imp.pkg.version.range}", + org.wso2.carbon.identity.application.mgt.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.application.common.*; + version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.identity.core.util;version="${carbon.identity.package.import.version.range}", + org.wso2.carbon.context;version="${carbon.kernel.package.import.version.range}", diff --git a/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/SharedRoleMgtHandler.java b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/SharedRoleMgtHandler.java new file mode 100644 index 000000000..01fb38905 --- /dev/null +++ b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/SharedRoleMgtHandler.java @@ -0,0 +1,258 @@ +/* + * Copyright (c) 2023, 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.organization.management.handler; + +import org.apache.commons.collections.MapUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; +import org.wso2.carbon.identity.application.common.model.RoleV2; +import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; +import org.wso2.carbon.identity.event.IdentityEventConstants; +import org.wso2.carbon.identity.event.IdentityEventException; +import org.wso2.carbon.identity.event.event.Event; +import org.wso2.carbon.identity.event.handler.AbstractEventHandler; +import org.wso2.carbon.identity.organization.management.application.OrgApplicationManager; +import org.wso2.carbon.identity.organization.management.application.constant.OrgApplicationMgtConstants; +import org.wso2.carbon.identity.organization.management.application.model.SharedApplication; +import org.wso2.carbon.identity.organization.management.handler.internal.OrganizationManagementHandlerDataHolder; +import org.wso2.carbon.identity.organization.management.service.OrganizationManager; +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.RoleConstants; +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 java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; + +/** + * Event handler to manage shared roles in sub-organizations. + */ +public class SharedRoleMgtHandler extends AbstractEventHandler { + + private static final Log LOG = LogFactory.getLog(SharedRoleMgtHandler.class); + private final ExecutorService executorService = Executors.newFixedThreadPool(5); + + @Override + public void handleEvent(Event event) throws IdentityEventException { + + String eventName = event.getEventName(); + Map eventProperties = event.getEventProperties(); + switch (eventName) { + case OrgApplicationMgtConstants.EVENT_POST_SHARE_APPLICATION: + createSharedRolesOnApplicationSharing(eventProperties); + break; + case IdentityEventConstants.Event.POST_ADD_ROLE_V2_EVENT: + createSharedRolesOnNewRoleCreation(eventProperties); + break; + default: + if (LOG.isDebugEnabled()) { + LOG.debug("Unsupported event: " + eventName); + } + break; + } + } + + private void createSharedRolesOnApplicationSharing(Map eventProperties) + throws IdentityEventException { + + String parentOrganizationId = + (String) eventProperties.get(OrgApplicationMgtConstants.EVENT_PROP_PARENT_ORGANIZATION_ID); + String parentApplicationId = + (String) eventProperties.get(OrgApplicationMgtConstants.EVENT_PROP_PARENT_APPLICATION_ID); + String sharedOrganizationId = + (String) eventProperties.get(OrgApplicationMgtConstants.EVENT_PROP_SHARED_ORGANIZATION_ID); + String sharedApplicationId = + (String) eventProperties.get(OrgApplicationMgtConstants.EVENT_PROP_SHARED_APPLICATION_ID); + try { + String sharedAppTenantDomain = getOrganizationManager().resolveTenantDomain(sharedOrganizationId); + String mainAppTenantDomain = getOrganizationManager().resolveTenantDomain(parentOrganizationId); + String allowedAudienceForRoleAssociationInMainApp = + getApplicationMgtService().getAllowedAudienceForRoleAssociation(parentApplicationId, + mainAppTenantDomain); + switch (allowedAudienceForRoleAssociationInMainApp) { + case RoleConstants.APPLICATION: + // Create the roles, and add the relationship. + createSharedRolesWithAppAudience(parentApplicationId, mainAppTenantDomain, sharedApplicationId, + sharedAppTenantDomain); + break; + default: + // Create the role if not exists, and add the relationship. + List associatedRolesOfApplication = + getApplicationMgtService().getAssociatedRolesOfApplication( + parentApplicationId, mainAppTenantDomain); + createSharedRolesWithOrgAudience(associatedRolesOfApplication, mainAppTenantDomain, + sharedOrganizationId); + break; + } + } catch (OrganizationManagementException | IdentityRoleManagementException | + IdentityApplicationManagementException e) { + throw new IdentityEventException( + String.format("Error while sharing roles related to application %s.", sharedApplicationId), e); + } + } + + private void createSharedRolesWithOrgAudience(List rolesList, String mainAppTenantDomain, + String sharedAppOrgId) + throws IdentityRoleManagementException, OrganizationManagementException { + + if (rolesList == null) { + return; + } + String sharedAppTenantDomain = getOrganizationManager().resolveTenantDomain(sharedAppOrgId); + for (RoleV2 role : rolesList) { + // Check if the role exists in the application shared org. + boolean roleExistsInSharedOrg = + getRoleManagementServiceV2().isExistingRoleName(role.getName(), RoleConstants.ORGANIZATION, + sharedAppOrgId, sharedAppTenantDomain); + Map mainRoleToSharedRoleMappingInSharedOrg = + getRoleManagementServiceV2().getMainRoleToSharedRoleMappingsBySubOrg( + Collections.singletonList(role.getId()), sharedAppTenantDomain); + boolean roleRelationshipExistsInSharedOrg = + MapUtils.isNotEmpty(mainRoleToSharedRoleMappingInSharedOrg); + if (roleExistsInSharedOrg && !roleRelationshipExistsInSharedOrg) { + // Add relationship between main role and shared role. + String roleIdInSharedOrg = + getRoleManagementServiceV2().getRoleIdByName(role.getName(), RoleConstants.ORGANIZATION, + sharedAppOrgId, sharedAppTenantDomain); + getRoleManagementServiceV2().addMainRoleToSharedRoleRelationship(role.getId(), + roleIdInSharedOrg, mainAppTenantDomain, sharedAppTenantDomain); + } else if (!roleExistsInSharedOrg && !roleRelationshipExistsInSharedOrg) { + // Create the role in the shared org. + RoleBasicInfo sharedRole = + getRoleManagementServiceV2().addRole(role.getName(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), RoleConstants.ORGANIZATION, + sharedAppOrgId, sharedAppTenantDomain); + // Add relationship between main role and shared role. + getRoleManagementServiceV2().addMainRoleToSharedRoleRelationship(role.getId(), + sharedRole.getId(), mainAppTenantDomain, sharedAppTenantDomain); + } + } + } + + private void createSharedRolesWithAppAudience(String mainAppId, String mainAppTenantDomain, String sharedAppId, + String sharedAppTenantDomain) throws IdentityRoleManagementException { + + // Get parent organization's roles which has application audience. + String filter = RoleConstants.AUDIENCE_ID + " " + RoleConstants.EQ + " " + mainAppId; + List parentOrgRoles = + getRoleManagementServiceV2().getRoles(filter, null, 0, null, null, mainAppTenantDomain); + for (RoleBasicInfo parentOrgRole : parentOrgRoles) { + String parentOrgRoleName = parentOrgRole.getName(); + // Create the role in the shared org. + RoleBasicInfo subOrgRole = getRoleManagementServiceV2().addRole(parentOrgRoleName, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), RoleConstants.APPLICATION, sharedAppId, + sharedAppTenantDomain); + // Add relationship between main role and the shared role. + getRoleManagementServiceV2().addMainRoleToSharedRoleRelationship(parentOrgRole.getId(), subOrgRole.getId(), + mainAppTenantDomain, sharedAppTenantDomain); + } + } + + private void createSharedRolesOnNewRoleCreation(Map eventProperties) + throws IdentityEventException { + + try { + String mainRoleUUID = (String) eventProperties.get(IdentityEventConstants.EventProperty.ROLE_ID); + String mainRoleName = (String) eventProperties.get(IdentityEventConstants.EventProperty.ROLE_NAME); + String roleTenantDomain = (String) eventProperties.get(IdentityEventConstants.EventProperty.TENANT_DOMAIN); + String roleAudienceType = (String) eventProperties.get(IdentityEventConstants.EventProperty.AUDIENCE); + String roleAudienceId = (String) eventProperties.get(IdentityEventConstants.EventProperty.AUDIENCE_ID); + String roleOrgId = getOrganizationManager().resolveOrganizationId(roleTenantDomain); + if (OrganizationManagementUtil.isOrganization(roleTenantDomain)) { + return; + } + switch (roleAudienceType) { + case RoleConstants.APPLICATION: + /* + If the audienced application is a shared application, create the role in + the shared apps' org space. + */ + List sharedApplications = + getOrgApplicationManager().getSharedApplications(roleOrgId, roleAudienceId); + int noOfSharedApps = sharedApplications.size(); + for (int i = 0; i < noOfSharedApps; i++) { + final int taskId = i; + CompletableFuture.runAsync(() -> { + try { + String sharedApplicationId = sharedApplications.get(taskId).getSharedApplicationId(); + String sharedOrganizationId = sharedApplications.get(taskId).getOrganizationId(); + String shareAppTenantDomain = + getOrganizationManager().resolveTenantDomain(sharedOrganizationId); + RoleBasicInfo sharedRoleInfo = + getRoleManagementServiceV2().addRole(mainRoleName, Collections.emptyList(), + Collections.emptyList(), + Collections.emptyList(), RoleConstants.APPLICATION, sharedApplicationId, + shareAppTenantDomain); + // Add relationship between main role and shared role. + getRoleManagementServiceV2().addMainRoleToSharedRoleRelationship(mainRoleUUID, + sharedRoleInfo.getId(), roleTenantDomain, shareAppTenantDomain); + } catch (IdentityRoleManagementException | OrganizationManagementException e) { + LOG.error("Error occurred while creating shared role in organization with id: " + + sharedApplications.get(taskId).getOrganizationId(), e); + } + }, executorService).exceptionally(throwable -> { + LOG.error(String.format( + "Exception occurred during creating a shared role: %s in organization: %s", + mainRoleName, sharedApplications.get(taskId).getOrganizationId()), throwable); + return null; + }); + } + break; + case RoleConstants.ORGANIZATION: + /* + Organization audience roles can't be attached to an application at the same time of role creation. + Organization audience role get shared to other application only if that role is associated with any + shared application in the organization. So nothing do in this case. + */ + break; + default: + LOG.error("Unsupported audience type: " + roleAudienceType); + } + } catch (OrganizationManagementException e) { + throw new IdentityEventException("Error occurred while retrieving shared applications.", e); + } + } + + private static RoleManagementService getRoleManagementServiceV2() { + + return OrganizationManagementHandlerDataHolder.getInstance().getRoleManagementServiceV2(); + } + + private static OrganizationManager getOrganizationManager() { + + return OrganizationManagementHandlerDataHolder.getInstance().getOrganizationManager(); + } + + private static OrgApplicationManager getOrgApplicationManager() { + + return OrganizationManagementHandlerDataHolder.getInstance().getOrgApplicationManager(); + } + + private static ApplicationManagementService getApplicationMgtService() { + + return OrganizationManagementHandlerDataHolder.getInstance().getApplicationManagementService(); + } +} diff --git a/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerDataHolder.java b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerDataHolder.java index dddbba7b9..41f1d52a3 100644 --- a/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerDataHolder.java +++ b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerDataHolder.java @@ -18,9 +18,12 @@ package org.wso2.carbon.identity.organization.management.handler.internal; +import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.organization.management.application.OrgApplicationManager; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; +import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; /** * Organization management handler data holder. @@ -31,10 +34,11 @@ public class OrganizationManagementHandlerDataHolder { new OrganizationManagementHandlerDataHolder(); private IdentityEventService identityEventService; - private IdentityGovernanceService identityGovernanceService; - private OrganizationManager organizationManager; + private RoleManagementService roleManagementServiceV2; + private OrgApplicationManager orgApplicationManager; + private ApplicationManagementService applicationManagementService; public static OrganizationManagementHandlerDataHolder getInstance() { @@ -100,5 +104,66 @@ public void setOrganizationManager(OrganizationManager organizationManager) { this.organizationManager = organizationManager; } + + /** + * Set {@link RoleManagementService}. + * + * @param roleManagementServiceV2 Instance of {@link RoleManagementService}. + */ + public void setRoleManagementServiceV2(RoleManagementService roleManagementServiceV2) { + + this.roleManagementServiceV2 = roleManagementServiceV2; + } + + /** + * Get {@link RoleManagementService}. + * + * @return role management service instance {@link RoleManagementService}. + */ + public RoleManagementService getRoleManagementServiceV2() { + + return roleManagementServiceV2; + } + + /** + * Get {@link OrgApplicationManager}. + * + * @return Org application manager instance {@link OrgApplicationManager}. + */ + public OrgApplicationManager getOrgApplicationManager() { + + return orgApplicationManager; + } + + /** + * Set {@link OrgApplicationManager}. + * + * @param orgApplicationManager Instance of {@link OrgApplicationManager}. + */ + public void setOrgApplicationManager(OrgApplicationManager orgApplicationManager) { + + this.orgApplicationManager = orgApplicationManager; + } + + /** + * Get {@link ApplicationManagementService}. + * + * @return Application management instance {@link ApplicationManagementService}. + */ + public ApplicationManagementService getApplicationManagementService() { + + return applicationManagementService; + } + + /** + * Set {@link ApplicationManagementService}. + * + * @param applicationManagementService Instance of {@link ApplicationManagementService}. + */ + public void setApplicationManagementService( + ApplicationManagementService applicationManagementService) { + + this.applicationManagementService = applicationManagementService; + } } diff --git a/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerServiceComponent.java b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerServiceComponent.java index d2e77840c..742934cdf 100644 --- a/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerServiceComponent.java +++ b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/internal/OrganizationManagementHandlerServiceComponent.java @@ -27,11 +27,17 @@ import org.osgi.service.component.annotations.Reference; import org.osgi.service.component.annotations.ReferenceCardinality; import org.osgi.service.component.annotations.ReferencePolicy; +import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; +import org.wso2.carbon.identity.application.mgt.listener.ApplicationMgtListener; import org.wso2.carbon.identity.event.handler.AbstractEventHandler; import org.wso2.carbon.identity.event.services.IdentityEventService; import org.wso2.carbon.identity.governance.IdentityGovernanceService; +import org.wso2.carbon.identity.organization.management.application.OrgApplicationManager; import org.wso2.carbon.identity.organization.management.handler.GovernanceConfigUpdateHandler; +import org.wso2.carbon.identity.organization.management.handler.SharedRoleMgtHandler; +import org.wso2.carbon.identity.organization.management.handler.listener.SharedRoleMgtListener; import org.wso2.carbon.identity.organization.management.service.OrganizationManager; +import org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService; /** * Organization management handler service component. @@ -54,8 +60,9 @@ protected void activate(ComponentContext componentContext) { try { BundleContext bundleContext = componentContext.getBundleContext(); - bundleContext.registerService(AbstractEventHandler.class, new GovernanceConfigUpdateHandler(), - null); + bundleContext.registerService(AbstractEventHandler.class, new GovernanceConfigUpdateHandler(), null); + bundleContext.registerService(AbstractEventHandler.class, new SharedRoleMgtHandler(), null); + bundleContext.registerService(ApplicationMgtListener.class.getName(), new SharedRoleMgtListener(), null); LOG.debug("Organization management handler component activated successfully."); } catch (Throwable e) { LOG.error("Error while activating organization management handler module.", e); @@ -110,5 +117,59 @@ protected void unsetOrganizationManager(OrganizationManager organizationManager) OrganizationManagementHandlerDataHolder.getInstance().setOrganizationManager(null); } -} + @Reference( + name = "org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService", + service = org.wso2.carbon.identity.role.v2.mgt.core.RoleManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetRoleManagementServiceV2") + protected void setRoleManagementServiceV2(RoleManagementService roleManagementService) { + + OrganizationManagementHandlerDataHolder.getInstance().setRoleManagementServiceV2(roleManagementService); + LOG.debug("RoleManagementServiceV2 set in OrganizationManagementHandlerService bundle."); + } + + protected void unsetRoleManagementServiceV2(RoleManagementService roleManagementService) { + + OrganizationManagementHandlerDataHolder.getInstance().setRoleManagementServiceV2(null); + LOG.debug("RoleManagementServiceV2 unset in OrganizationManagementHandlerService bundle."); + } + + @Reference( + name = "org.wso2.carbon.identity.organization.management.application.OrgApplicationManager", + service = org.wso2.carbon.identity.organization.management.application.OrgApplicationManager.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetOrgApplicationManagementService") + protected void setOrgApplicationManagementService(OrgApplicationManager orgApplicationManagementService) { + + OrganizationManagementHandlerDataHolder.getInstance().setOrgApplicationManager(orgApplicationManagementService); + LOG.debug("OrgApplication management service set in OrganizationManagementHandlerService bundle."); + } + + protected void unsetOrgApplicationManagementService(OrgApplicationManager orgApplicationManagementService) { + + OrganizationManagementHandlerDataHolder.getInstance().setOrgApplicationManager(null); + LOG.debug("OrgApplication management service unset in OrganizationManagementHandlerService bundle."); + } + + @Reference( + name = "org.wso2.carbon.identity.application.mgt.ApplicationManagementService", + service = org.wso2.carbon.identity.application.mgt.ApplicationManagementService.class, + cardinality = ReferenceCardinality.MANDATORY, + policy = ReferencePolicy.DYNAMIC, + unbind = "unsetApplicationManagementService") + protected void setApplicationManagementService(ApplicationManagementService applicationManagementService) { + + OrganizationManagementHandlerDataHolder.getInstance() + .setApplicationManagementService(applicationManagementService); + LOG.debug("Application management service set in OrganizationManagementHandlerService bundle."); + } + + protected void unsetApplicationManagementService(ApplicationManagementService applicationManagementService) { + + OrganizationManagementHandlerDataHolder.getInstance().setApplicationManagementService(null); + LOG.debug("Application management service unset in OrganizationManagementHandlerService bundle."); + } +} diff --git a/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/listener/SharedRoleMgtListener.java b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/listener/SharedRoleMgtListener.java new file mode 100644 index 000000000..b873b34f9 --- /dev/null +++ b/components/org.wso2.carbon.identity.organization.management.handler/src/main/java/org/wso2/carbon/identity/organization/management/handler/listener/SharedRoleMgtListener.java @@ -0,0 +1,524 @@ +/* + * Copyright (c) 2023, 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.organization.management.handler.listener; + +import org.apache.commons.collections.CollectionUtils; +import org.apache.commons.collections.MapUtils; +import org.apache.commons.lang.StringUtils; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.wso2.carbon.context.PrivilegedCarbonContext; +import org.wso2.carbon.identity.application.common.IdentityApplicationManagementException; +import org.wso2.carbon.identity.application.common.model.RoleV2; +import org.wso2.carbon.identity.application.common.model.ServiceProvider; +import org.wso2.carbon.identity.application.mgt.ApplicationManagementService; +import org.wso2.carbon.identity.application.mgt.listener.AbstractApplicationMgtListener; +import org.wso2.carbon.identity.core.util.IdentityTenantUtil; +import org.wso2.carbon.identity.core.util.IdentityUtil; +import org.wso2.carbon.identity.organization.management.application.OrgApplicationManager; +import org.wso2.carbon.identity.organization.management.application.model.SharedApplication; +import org.wso2.carbon.identity.organization.management.handler.internal.OrganizationManagementHandlerDataHolder; +import org.wso2.carbon.identity.organization.management.service.OrganizationManager; +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.RoleConstants; +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 java.util.ArrayList; +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.stream.Collectors; + +import static org.wso2.carbon.identity.organization.management.service.constant.OrganizationManagementConstants.SUPER_ORG_ID; + +/** + * Application management listener to handle shared roles in organizations. + */ +public class SharedRoleMgtListener extends AbstractApplicationMgtListener { + + private static final Log LOG = LogFactory.getLog(SharedRoleMgtListener.class); + private static final String REMOVED_APPLICATION_AUDIENCE_ROLES = "removedApplicationAudienceRoles"; + private static final String ADDED_APPLICATION_AUDIENCE_ROLES = "addedApplicationAudienceRoles"; + private static final String REMOVED_ORGANIZATION_AUDIENCE_ROLES = "removedOrganizationAudienceRoles"; + private static final String ADDED_ORGANIZATION_AUDIENCE_ROLES = "addedOrganizationAudienceRoles"; + + ApplicationManagementService applicationManagementService = + OrganizationManagementHandlerDataHolder.getInstance().getApplicationManagementService(); + OrganizationManager organizationManager = + OrganizationManagementHandlerDataHolder.getInstance().getOrganizationManager(); + OrgApplicationManager orgApplicationManager = + OrganizationManagementHandlerDataHolder.getInstance().getOrgApplicationManager(); + RoleManagementService roleManagementService = + OrganizationManagementHandlerDataHolder.getInstance().getRoleManagementServiceV2(); + private final ExecutorService executorService = Executors.newFixedThreadPool(5); + + @Override + public int getDefaultOrderId() { + + return 49; + } + + @Override + public boolean doPreUpdateApplication(ServiceProvider serviceProvider, String tenantDomain, String userName) + throws IdentityApplicationManagementException { + + // Associated role changes on main applications in tenant need to be handled here. + try { + if (OrganizationManagementUtil.isOrganization(tenantDomain)) { + return true; + } + String applicationResourceId = serviceProvider.getApplicationResourceId(); + // Get the currently associated roles set from DB/cache. + String existingAllowedAudienceForRoleAssociation = + applicationManagementService.getAllowedAudienceForRoleAssociation(applicationResourceId, + tenantDomain); + List existingAssociatedRolesList = + applicationManagementService.getAssociatedRolesOfApplication(applicationResourceId, tenantDomain); + + String updatedAllowedAudienceForRoleAssociation = + serviceProvider.getAssociatedRolesConfig() == null ? RoleConstants.ORGANIZATION : + serviceProvider.getAssociatedRolesConfig().getAllowedAudience(); + + List updatedAssociatedRolesList; + if (serviceProvider.getAssociatedRolesConfig() != null && + serviceProvider.getAssociatedRolesConfig().getRoles() != null) { + updatedAssociatedRolesList = Arrays.asList(serviceProvider.getAssociatedRolesConfig().getRoles()); + } else { + updatedAssociatedRolesList = new ArrayList<>(); + } + + if (CollectionUtils.isEmpty(existingAssociatedRolesList) && + CollectionUtils.isEmpty(updatedAssociatedRolesList)) { + // No change in roles list. + return true; + } + + /* + if old and new audiences are equals, need to handle the role diff. + */ + if (existingAllowedAudienceForRoleAssociation.equalsIgnoreCase(updatedAllowedAudienceForRoleAssociation)) { + switch (updatedAllowedAudienceForRoleAssociation) { + case RoleConstants.APPLICATION: + List addedApplicationAudienceRoles = updatedAssociatedRolesList.stream() + .filter(updatedRole -> !existingAssociatedRolesList.contains(updatedRole)) + .collect(Collectors.toList()); + + List removedApplicationAudienceRoles = existingAssociatedRolesList.stream() + .filter(existingRole -> !updatedAssociatedRolesList.contains(existingRole)) + .collect(Collectors.toList()); + // Add to threadLocal. + IdentityUtil.threadLocalProperties.get() + .put(ADDED_APPLICATION_AUDIENCE_ROLES, addedApplicationAudienceRoles); + IdentityUtil.threadLocalProperties.get() + .put(REMOVED_APPLICATION_AUDIENCE_ROLES, removedApplicationAudienceRoles); + return true; + default: + if (existingAssociatedRolesList.equals(updatedAssociatedRolesList)) { + // Nothing to change in shared applications' organizations. + return true; + } + // Get the added roles and removed roles. + List addedOrganizationAudienceRoles = updatedAssociatedRolesList.stream() + .filter(updatedRole -> !existingAssociatedRolesList.contains(updatedRole)) + .collect(Collectors.toList()); + + List removedOrganizationAudienceRoles = existingAssociatedRolesList.stream() + .filter(existingRole -> !updatedAssociatedRolesList.contains(existingRole)) + .collect(Collectors.toList()); + // Add to threadLocal. + IdentityUtil.threadLocalProperties.get() + .put(ADDED_ORGANIZATION_AUDIENCE_ROLES, addedOrganizationAudienceRoles); + IdentityUtil.threadLocalProperties.get() + .put(REMOVED_ORGANIZATION_AUDIENCE_ROLES, removedOrganizationAudienceRoles); + return true; + } + } + + /* + If audience has changed from application to organization, all previous associated roles will be deleted. + For updated organization roles, create shared role in organizations below if applicable. + */ + if (RoleConstants.APPLICATION.equalsIgnoreCase(existingAllowedAudienceForRoleAssociation) && + RoleConstants.ORGANIZATION.equalsIgnoreCase(updatedAllowedAudienceForRoleAssociation)) { + + // Add to thread local. + IdentityUtil.threadLocalProperties.get() + .put(REMOVED_APPLICATION_AUDIENCE_ROLES, existingAssociatedRolesList); + IdentityUtil.threadLocalProperties.get() + .put(ADDED_ORGANIZATION_AUDIENCE_ROLES, updatedAssociatedRolesList); + return true; + } + + /* + If audience has changed from organization to application, need to remove the organization roles. + Nothing to handle in application audience roles because they will be added/deleted in shared orgs + based on role creation/deletion. + */ + if (RoleConstants.ORGANIZATION.equalsIgnoreCase(existingAllowedAudienceForRoleAssociation) && + RoleConstants.APPLICATION.equalsIgnoreCase(updatedAllowedAudienceForRoleAssociation)) { + + // Add to thread local. + IdentityUtil.threadLocalProperties.get() + .put(REMOVED_ORGANIZATION_AUDIENCE_ROLES, existingAssociatedRolesList); + return true; + } + } catch (OrganizationManagementException e) { + throw new IdentityApplicationManagementException( + String.format("Error while checking shared roles to be updated related to application %s update.", + serviceProvider.getApplicationID()), e); + } + return true; + } + + @Override + public boolean doPostUpdateApplication(ServiceProvider serviceProvider, String tenantDomain, String userName) + throws IdentityApplicationManagementException { + + try { + if (OrganizationManagementUtil.isOrganization(tenantDomain)) { + return true; + } + Object addedAppRoles = IdentityUtil.threadLocalProperties.get().get(ADDED_APPLICATION_AUDIENCE_ROLES); + if (addedAppRoles != null) { + List addedAppRolesList = (List) addedAppRoles; + handleAddedApplicationAudienceRolesOnAppUpdate(addedAppRolesList, serviceProvider, tenantDomain); + } + + Object removedAppRoles = IdentityUtil.threadLocalProperties.get().get(REMOVED_APPLICATION_AUDIENCE_ROLES); + if (removedAppRoles != null) { + List removedAppRolesList = (List) removedAppRoles; + handleRemovedApplicationAudienceRolesOnAppUpdate(removedAppRolesList, tenantDomain); + } + + Object addedOrgRoles = IdentityUtil.threadLocalProperties.get().get(ADDED_ORGANIZATION_AUDIENCE_ROLES); + if (addedOrgRoles != null) { + List addedOrgRolesList = (List) addedOrgRoles; + List namesResolvedAddedRolesList = addedOrgRolesList.stream() + .map(role -> { + try { + String roleName = roleManagementService.getRoleNameByRoleId(role.getId(), tenantDomain); + if (roleName != null) { + return new RoleV2(role.getId(), roleName); + } + return null; + } catch (Exception e) { + LOG.error("Failed to resolve role name of role id: " + role.getId()); + return null; + } + }) + .filter(Objects::nonNull) // Filter out null values (roles that couldn't be resolved) + .collect(Collectors.toList()); + + handleAddedOrganizationAudienceRolesOnAppUpdate(namesResolvedAddedRolesList, serviceProvider, + tenantDomain); + } + + Object removedOrgRoles = IdentityUtil.threadLocalProperties.get().get(REMOVED_ORGANIZATION_AUDIENCE_ROLES); + if (removedOrgRoles != null) { + List removedOrgRolesList = (List) removedOrgRoles; + handleRemovedOrganizationAudienceRolesOnAppUpdate(removedOrgRolesList, serviceProvider, tenantDomain); + } + } catch (OrganizationManagementException | IdentityRoleManagementException e) { + throw new IdentityApplicationManagementException( + String.format("Error while updating shared roles related to application %s update.", + serviceProvider.getApplicationID()), e); + } finally { + IdentityUtil.threadLocalProperties.get().remove(ADDED_APPLICATION_AUDIENCE_ROLES); + IdentityUtil.threadLocalProperties.get().remove(REMOVED_APPLICATION_AUDIENCE_ROLES); + IdentityUtil.threadLocalProperties.get().remove(ADDED_ORGANIZATION_AUDIENCE_ROLES); + IdentityUtil.threadLocalProperties.get().remove(REMOVED_ORGANIZATION_AUDIENCE_ROLES); + } + return true; + } + + private void handleRemovedOrganizationAudienceRolesOnAppUpdate(List removedOrgRolesList, + ServiceProvider serviceProvider, String tenantDomain) + throws OrganizationManagementException, IdentityRoleManagementException { + + if (CollectionUtils.isEmpty(removedOrgRolesList)) { + return; + } + String mainAppId = serviceProvider.getApplicationResourceId(); + String mainAppOrgId = organizationManager.resolveOrganizationId(tenantDomain); + List sharedApplications = + orgApplicationManager.getSharedApplications(mainAppOrgId, mainAppId); + if (CollectionUtils.isEmpty(sharedApplications)) { + return; + } + for (SharedApplication sharedApplication : sharedApplications) { + CompletableFuture.runAsync(() -> { + String sharedAppOrgId = sharedApplication.getOrganizationId(); + try { + handleOrganizationAudiencedSharedRoleDeletion(removedOrgRolesList, + serviceProvider.getApplicationResourceId(), + tenantDomain, sharedAppOrgId); + } catch (IdentityRoleManagementException | OrganizationManagementException e) { + LOG.error(String.format("Exception occurred during deleting roles from organization %s", + sharedApplication.getOrganizationId()), e); + } + }, executorService).exceptionally(throwable -> { + LOG.error(String.format("Exception occurred during deleting roles from organization %s", + sharedApplication.getOrganizationId()), throwable); + return null; + }); + } + } + + private void handleAddedOrganizationAudienceRolesOnAppUpdate(List addedOrgRolesList, + ServiceProvider serviceProvider, String tenantDomain) + throws OrganizationManagementException, IdentityRoleManagementException { + + if (CollectionUtils.isEmpty(addedOrgRolesList)) { + return; + } + String mainAppId = serviceProvider.getApplicationResourceId(); + String mainAppOrgId = organizationManager.resolveOrganizationId(tenantDomain); + List sharedApplications = + orgApplicationManager.getSharedApplications(mainAppOrgId, mainAppId); + if (CollectionUtils.isEmpty(sharedApplications)) { + return; + } + + for (SharedApplication sharedApplication : sharedApplications) { + CompletableFuture.runAsync(() -> { + String sharedAppOrgId = sharedApplication.getOrganizationId(); + try { + createSharedRolesWithOrgAudience(addedOrgRolesList, tenantDomain, sharedAppOrgId); + } catch (IdentityRoleManagementException | OrganizationManagementException e) { + LOG.error(String.format("Exception occurred while adding shared roles to organization: %s", + sharedApplication.getOrganizationId()), e); + } + }, executorService).exceptionally(throwable -> { + LOG.error(String.format("Exception occurred while adding shared roles to organization: %s", + sharedApplication.getOrganizationId()), throwable); + return null; + }); + } + } + + private void createSharedRolesWithOrgAudience(List rolesList, String mainAppTenantDomain, + String sharedAppOrgId) + throws IdentityRoleManagementException, OrganizationManagementException { + + if (rolesList == null) { + return; + } + String sharedAppTenantDomain = organizationManager.resolveTenantDomain(sharedAppOrgId); + for (RoleV2 role : rolesList) { + // Check if the role exists in the application shared org. + boolean roleExistsInSharedOrg = + roleManagementService.isExistingRoleName(role.getName(), RoleConstants.ORGANIZATION, + sharedAppOrgId, sharedAppTenantDomain); + Map mainRoleToSharedRoleMappingInSharedOrg = + roleManagementService.getMainRoleToSharedRoleMappingsBySubOrg( + Collections.singletonList(role.getId()), sharedAppTenantDomain); + boolean roleRelationshipExistsInSharedOrg = + MapUtils.isNotEmpty(mainRoleToSharedRoleMappingInSharedOrg); + if (roleExistsInSharedOrg && !roleRelationshipExistsInSharedOrg) { + // Add relationship between main role and shared role. + String roleIdInSharedOrg = + roleManagementService.getRoleIdByName(role.getName(), RoleConstants.ORGANIZATION, + sharedAppOrgId, sharedAppTenantDomain); + roleManagementService.addMainRoleToSharedRoleRelationship(role.getId(), + roleIdInSharedOrg, mainAppTenantDomain, sharedAppTenantDomain); + } else if (!roleExistsInSharedOrg && !roleRelationshipExistsInSharedOrg) { + // Create the role in the shared org. + RoleBasicInfo sharedRole = roleManagementService.addRole(role.getName(), Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), RoleConstants.ORGANIZATION, + sharedAppOrgId, sharedAppTenantDomain); + // Add relationship between main role and shared role. + roleManagementService.addMainRoleToSharedRoleRelationship(role.getId(), + sharedRole.getId(), mainAppTenantDomain, sharedAppTenantDomain); + } + } + } + + private void handleRemovedApplicationAudienceRolesOnAppUpdate(List removedAppRolesList, String tenantDomain) + throws IdentityRoleManagementException { + + if (CollectionUtils.isEmpty(removedAppRolesList)) { + return; + } + /* + Delete the application audience roles from parent organization. Deleting their shared roles also handled inside. + */ + for (RoleV2 removedRole : removedAppRolesList) { + roleManagementService.deleteRole(removedRole.getId(), tenantDomain); + } + } + + private void handleAddedApplicationAudienceRolesOnAppUpdate(List addedAppRolesList, + ServiceProvider serviceProvider, String tenantDomain) + throws OrganizationManagementException, IdentityRoleManagementException { + + if (CollectionUtils.isEmpty(addedAppRolesList)) { + return; + } + // Get shared applications of the given main app, and share the role. + String mainAppId = serviceProvider.getApplicationResourceId(); + String mainAppOrgId = organizationManager.resolveOrganizationId(tenantDomain); + + List sharedApplications = + orgApplicationManager.getSharedApplications(mainAppOrgId, mainAppId); + if (CollectionUtils.isEmpty(sharedApplications)) { + return; + } + + for (RoleV2 parentRole : addedAppRolesList) { + for (SharedApplication sharedApplication : sharedApplications) { + String sharedAppOrgId = sharedApplication.getOrganizationId(); + String sharedAppTenantDomain = organizationManager.resolveTenantDomain(sharedAppOrgId); + String parentAppRoleName = parentRole.getName(); + // Create the role in the shared org. + RoleBasicInfo subOrgRole = + roleManagementService.addRole(parentAppRoleName, Collections.emptyList(), + Collections.emptyList(), Collections.emptyList(), RoleConstants.APPLICATION, + sharedApplication.getSharedApplicationId(), + sharedAppTenantDomain); + // Add relationship between main role and the shared role. + roleManagementService.addMainRoleToSharedRoleRelationship(parentRole.getId(), subOrgRole.getId(), + tenantDomain, sharedAppTenantDomain); + } + } + } + + @Override + public boolean doPreDeleteApplication(String applicationName, String tenantDomain, String userName) + throws IdentityApplicationManagementException { + + try { + // If the deleting application is an application of tenant(i.e primary org) nothing to do here. + if (!OrganizationManagementUtil.isOrganization(tenantDomain)) { + return true; + } + + ServiceProvider sharedApplication = getApplicationByName(applicationName, tenantDomain); + if (sharedApplication == null) { + return false; + } + String sharedAppId = sharedApplication.getApplicationResourceId(); + String sharedAppOrgId = organizationManager.resolveOrganizationId(tenantDomain); + // Resolve the main application details. + String mainAppId = orgApplicationManager.getMainApplicationIdForGivenSharedApp(sharedAppId, sharedAppOrgId); + if (mainAppId == null) { + return false; + } + int mainAppTenantId = applicationManagementService.getTenantIdByApp(mainAppId); + String mainAppTenantDomain = IdentityTenantUtil.getTenantDomain(mainAppTenantId); + + String allowedAudienceForRoleAssociationInMainApp = + applicationManagementService.getAllowedAudienceForRoleAssociation(mainAppId, mainAppTenantDomain); + boolean hasAppAudiencedRoles = + RoleConstants.APPLICATION.equalsIgnoreCase(allowedAudienceForRoleAssociationInMainApp); + if (hasAppAudiencedRoles) { + // Handle role deletion in application deletion post actions. + return true; + } + + // Handing organization audienced roles associated case. + List associatedRolesOfMainApplication = applicationManagementService + .getAssociatedRolesOfApplication(mainAppId, mainAppTenantDomain); + handleOrganizationAudiencedSharedRoleDeletion(associatedRolesOfMainApplication, mainAppId, + mainAppTenantDomain, sharedAppOrgId); + } catch (OrganizationManagementException | IdentityRoleManagementException e) { + throw new IdentityApplicationManagementException( + "Error while deleting organization roles associated to the app.", e); + } + return super.doPreDeleteApplication(applicationName, tenantDomain, userName); + } + + private void handleOrganizationAudiencedSharedRoleDeletion(List rolesList, String mainApplicationId, + String mainApplicationTenantDomain, + String sharedAppOrgId) + throws IdentityRoleManagementException, OrganizationManagementException { + + String mainApplicationOrgId = organizationManager.resolveOrganizationId(mainApplicationTenantDomain); + if (mainApplicationOrgId == null) { + mainApplicationOrgId = SUPER_ORG_ID; + } + String sharedAppTenantDomain = organizationManager.resolveTenantDomain(sharedAppOrgId); + List mainAppRoleIds = + rolesList.stream().map(RoleV2::getId).collect(Collectors.toList()); + Map mainRoleToSharedRoleMappingsInSubOrg = + roleManagementService.getMainRoleToSharedRoleMappingsBySubOrg(mainAppRoleIds, sharedAppTenantDomain); + + // Get each role associated applications. + for (String mainAppRoleId : mainAppRoleIds) { + List associatedApplicationsIds = + roleManagementService.getAssociatedApplicationByRoleId(mainAppRoleId, + mainApplicationTenantDomain); + String sharedRoleId = mainRoleToSharedRoleMappingsInSubOrg.get(mainAppRoleId); + if (StringUtils.isBlank(sharedRoleId)) { + // There is no role available in the shared org. May be due to role creation issue. + continue; + } + /* + If this private method is called from application update post listener, the role already removed + from the application. associatedApplicationsIds is empty means there are no any other applications. + + If this private method is called from application deletion post listener, + and if the only associated application is the main app in this flow, this condition is satisfied. + Hence, deleting the shared roles. + */ + if (CollectionUtils.isEmpty(associatedApplicationsIds) || (associatedApplicationsIds.size() == 1 && + mainApplicationId.equals(associatedApplicationsIds.get(0)))) { + try { + PrivilegedCarbonContext.startTenantFlow(); + PrivilegedCarbonContext.getThreadLocalCarbonContext().setTenantDomain(sharedAppTenantDomain, true); + roleManagementService.deleteRole(sharedRoleId, sharedAppTenantDomain); + } finally { + PrivilegedCarbonContext.endTenantFlow(); + } + } else if (associatedApplicationsIds.size() > 1) { + boolean isRoleUsedByAnotherSharedApp = false; + for (String associatedApplicationId : associatedApplicationsIds) { + if (associatedApplicationId.equals(mainApplicationId)) { + continue; + } + boolean applicationSharedWithGivenOrganization = + orgApplicationManager.isApplicationSharedWithGivenOrganization(associatedApplicationId, + mainApplicationOrgId, sharedAppOrgId); + if (applicationSharedWithGivenOrganization) { + isRoleUsedByAnotherSharedApp = true; + break; + } + } + if (!isRoleUsedByAnotherSharedApp) { + // Delete the role in org. + roleManagementService.deleteRole(sharedRoleId, sharedAppTenantDomain); + break; + } + } + } + } + + private ServiceProvider getApplicationByName(String name, String tenantDomain) + throws IdentityApplicationManagementException { + + return applicationManagementService.getServiceProvider(name, tenantDomain); + } +} diff --git a/pom.xml b/pom.xml index b145e912c..8d0fb2bb9 100644 --- a/pom.xml +++ b/pom.xml @@ -240,7 +240,11 @@ org.wso2.carbon.identity.governance ${org.wso2.carbon.identity.governance.version} - + + org.wso2.carbon.identity.framework + org.wso2.carbon.identity.role.v2.mgt.core + ${carbon.identity.framework.version} + com.google.code.findbugs annotations @@ -499,7 +503,7 @@ [4.7.0,5.0.0) - 5.25.400 + 5.25.426 [5.20.0, 7.0.0)