This is an automated email from the ASF dual-hosted git repository. yasith pushed a commit to branch feat/airavata-service-layer in repository https://gitbox.apache.org/repos/asf/airavata.git
commit 5e7553177759c543e194b09fd8e7d794dce9505d Author: yasithdev <[email protected]> AuthorDate: Thu Mar 26 11:00:06 2026 -0500 feat: add ResourceSharingService with ThriftAdapter wiring Extracts shareResourceWithUsers, shareResourceWithGroups, revoke* variants, getAllAccessible*/getAllDirectlyAccessible* methods, and userHasAccess into ResourceSharingService. Rewires AiravataServerHandler to delegate via ThriftAdapter. --- .../service/sharing/ResourceSharingService.java | 352 +++++++++++++++++++++ .../sharing/ResourceSharingServiceTest.java | 118 +++++++ 2 files changed, 470 insertions(+) diff --git a/airavata-api/src/main/java/org/apache/airavata/service/sharing/ResourceSharingService.java b/airavata-api/src/main/java/org/apache/airavata/service/sharing/ResourceSharingService.java new file mode 100644 index 0000000000..926f441141 --- /dev/null +++ b/airavata-api/src/main/java/org/apache/airavata/service/sharing/ResourceSharingService.java @@ -0,0 +1,352 @@ +package org.apache.airavata.service.sharing; + +import org.apache.airavata.model.group.ResourcePermissionType; +import org.apache.airavata.model.group.ResourceType; +import org.apache.airavata.service.context.RequestContext; +import org.apache.airavata.service.exception.ServiceAuthorizationException; +import org.apache.airavata.service.exception.ServiceException; +import org.apache.airavata.sharing.registry.models.PermissionType; +import org.apache.airavata.sharing.registry.models.User; +import org.apache.airavata.sharing.registry.models.UserGroup; +import org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashSet; +import java.util.List; +import java.util.Map; +import java.util.function.BiFunction; + +public class ResourceSharingService { + + private static final Logger logger = LoggerFactory.getLogger(ResourceSharingService.class); + + private final SharingRegistryServerHandler sharingHandler; + + public ResourceSharingService(SharingRegistryServerHandler sharingHandler) { + this.sharingHandler = sharingHandler; + } + + public boolean shareResourceWithUsers(RequestContext ctx, String resourceId, Map<String, ResourcePermissionType> userPermissionList) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userId = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER) + && !userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.MANAGE_SHARING)) { + throw new ServiceAuthorizationException( + "User is not allowed to change sharing because the user is either not the resource owner or does not have access to share the resource"); + } + for (Map.Entry<String, ResourcePermissionType> userPermission : userPermissionList.entrySet()) { + if (userPermission.getValue().equals(ResourcePermissionType.WRITE)) { + sharingHandler.shareEntityWithUsers(gatewayId, resourceId, + Arrays.asList(userPermission.getKey()), gatewayId + ":WRITE", true); + } else if (userPermission.getValue().equals(ResourcePermissionType.READ)) { + sharingHandler.shareEntityWithUsers(gatewayId, resourceId, + Arrays.asList(userPermission.getKey()), gatewayId + ":READ", true); + } else if (userPermission.getValue().equals(ResourcePermissionType.MANAGE_SHARING)) { + if (userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER)) { + createManageSharingPermissionTypeIfMissing(gatewayId); + sharingHandler.shareEntityWithUsers(gatewayId, resourceId, + Arrays.asList(userPermission.getKey()), gatewayId + ":MANAGE_SHARING", true); + } else { + throw new ServiceAuthorizationException( + "User is not allowed to grant sharing permission because the user is not the resource owner."); + } + } else { + throw new ServiceException("Invalid ResourcePermissionType: " + userPermission.getValue()); + } + } + return true; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error sharing resource with users. Resource ID: " + resourceId + ": " + e.getMessage(), e); + } + } + + public boolean shareResourceWithGroups(RequestContext ctx, String resourceId, Map<String, ResourcePermissionType> groupPermissionList) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userId = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER) + && !userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.MANAGE_SHARING)) { + throw new ServiceAuthorizationException( + "User is not allowed to change sharing because the user is either not the resource owner or does not have access to share the resource"); + } + for (Map.Entry<String, ResourcePermissionType> groupPermission : groupPermissionList.entrySet()) { + if (groupPermission.getValue().equals(ResourcePermissionType.WRITE)) { + sharingHandler.shareEntityWithGroups(gatewayId, resourceId, + Arrays.asList(groupPermission.getKey()), gatewayId + ":WRITE", true); + } else if (groupPermission.getValue().equals(ResourcePermissionType.READ)) { + sharingHandler.shareEntityWithGroups(gatewayId, resourceId, + Arrays.asList(groupPermission.getKey()), gatewayId + ":READ", true); + } else if (groupPermission.getValue().equals(ResourcePermissionType.MANAGE_SHARING)) { + if (userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER)) { + createManageSharingPermissionTypeIfMissing(gatewayId); + sharingHandler.shareEntityWithGroups(gatewayId, resourceId, + Arrays.asList(groupPermission.getKey()), gatewayId + ":MANAGE_SHARING", true); + } else { + throw new ServiceAuthorizationException( + "User is not allowed to grant sharing permission because the user is not the resource owner."); + } + } else { + throw new ServiceException("Invalid ResourcePermissionType: " + groupPermission.getValue()); + } + } + return true; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error sharing resource with groups. Resource ID: " + resourceId + ": " + e.getMessage(), e); + } + } + + public boolean revokeSharingOfResourceFromUsers(RequestContext ctx, String resourceId, Map<String, ResourcePermissionType> userPermissionList) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userId = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER) + && !userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.MANAGE_SHARING)) { + throw new ServiceAuthorizationException( + "User is not allowed to change sharing because the user is either not the resource owner or does not have access to share the resource"); + } + for (Map.Entry<String, ResourcePermissionType> userPermission : userPermissionList.entrySet()) { + if (userPermission.getValue().equals(ResourcePermissionType.WRITE)) { + sharingHandler.revokeEntitySharingFromUsers(gatewayId, resourceId, + Arrays.asList(userPermission.getKey()), gatewayId + ":WRITE"); + } else if (userPermission.getValue().equals(ResourcePermissionType.READ)) { + sharingHandler.revokeEntitySharingFromUsers(gatewayId, resourceId, + Arrays.asList(userPermission.getKey()), gatewayId + ":READ"); + } else if (userPermission.getValue().equals(ResourcePermissionType.MANAGE_SHARING)) { + if (userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER)) { + createManageSharingPermissionTypeIfMissing(gatewayId); + sharingHandler.revokeEntitySharingFromUsers(gatewayId, resourceId, + Arrays.asList(userPermission.getKey()), gatewayId + ":MANAGE_SHARING"); + } else { + throw new ServiceAuthorizationException( + "User is not allowed to change sharing permission because the user is not the resource owner."); + } + } else { + throw new ServiceException("Invalid ResourcePermissionType: " + userPermission.getValue()); + } + } + return true; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error revoking resource sharing from users. Resource ID: " + resourceId + ": " + e.getMessage(), e); + } + } + + public boolean revokeSharingOfResourceFromGroups(RequestContext ctx, String resourceId, Map<String, ResourcePermissionType> groupPermissionList) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userId = ctx.getUserId(); + try { + if (!userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER) + && !userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.MANAGE_SHARING)) { + throw new ServiceAuthorizationException( + "User is not allowed to change sharing because the user is either not the resource owner or does not have access to share the resource"); + } + // For certain resource types, restrict admin group unsharing + ResourceType resourceType = getResourceType(gatewayId, resourceId); + if (isAdminRestrictedResourceType(resourceType)) { + validateAdminGroupNotRevoked(gatewayId, resourceId, groupPermissionList); + } + for (Map.Entry<String, ResourcePermissionType> groupPermission : groupPermissionList.entrySet()) { + if (groupPermission.getValue().equals(ResourcePermissionType.WRITE)) { + sharingHandler.revokeEntitySharingFromUsers(gatewayId, resourceId, + Arrays.asList(groupPermission.getKey()), gatewayId + ":WRITE"); + } else if (groupPermission.getValue().equals(ResourcePermissionType.READ)) { + sharingHandler.revokeEntitySharingFromUsers(gatewayId, resourceId, + Arrays.asList(groupPermission.getKey()), gatewayId + ":READ"); + } else if (groupPermission.getValue().equals(ResourcePermissionType.MANAGE_SHARING)) { + if (userHasAccess(gatewayId, userId, resourceId, ResourcePermissionType.OWNER)) { + createManageSharingPermissionTypeIfMissing(gatewayId); + sharingHandler.revokeEntitySharingFromUsers(gatewayId, resourceId, + Arrays.asList(groupPermission.getKey()), gatewayId + ":MANAGE_SHARING"); + } else { + throw new ServiceAuthorizationException( + "User is not allowed to change sharing because the user is not the resource owner"); + } + } else { + throw new ServiceException("Invalid ResourcePermissionType: " + groupPermission.getValue()); + } + } + return true; + } catch (ServiceException e) { + throw e; + } catch (Exception e) { + throw new ServiceException("Error revoking resource sharing from groups. Resource ID: " + resourceId + ": " + e.getMessage(), e); + } + } + + public List<String> getAllAccessibleUsers(RequestContext ctx, String resourceId, ResourcePermissionType permissionType) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + try { + return getAllAccessibleUsersInternal(gatewayId, resourceId, permissionType, + (handler, t) -> { + try { + return handler.getListOfSharedUsers(gatewayId, resourceId, gatewayId + ":" + t.name()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + throw new ServiceException("Error getting all accessible users for resource " + resourceId + ": " + e.getMessage(), e); + } + } + + public List<String> getAllDirectlyAccessibleUsers(RequestContext ctx, String resourceId, ResourcePermissionType permissionType) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + try { + return getAllAccessibleUsersInternal(gatewayId, resourceId, permissionType, + (handler, t) -> { + try { + return handler.getListOfDirectlySharedUsers(gatewayId, resourceId, gatewayId + ":" + t.name()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + throw new ServiceException("Error getting directly accessible users for resource " + resourceId + ": " + e.getMessage(), e); + } + } + + public List<String> getAllAccessibleGroups(RequestContext ctx, String resourceId, ResourcePermissionType permissionType) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + try { + return getAllAccessibleGroupsInternal(gatewayId, resourceId, permissionType, + (handler, t) -> { + try { + return handler.getListOfSharedGroups(gatewayId, resourceId, gatewayId + ":" + t.name()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + throw new ServiceException("Error getting all accessible groups for resource " + resourceId + ": " + e.getMessage(), e); + } + } + + public List<String> getAllDirectlyAccessibleGroups(RequestContext ctx, String resourceId, ResourcePermissionType permissionType) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + try { + return getAllAccessibleGroupsInternal(gatewayId, resourceId, permissionType, + (handler, t) -> { + try { + return handler.getListOfDirectlySharedGroups(gatewayId, resourceId, gatewayId + ":" + t.name()); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } catch (Exception e) { + throw new ServiceException("Error getting directly accessible groups for resource " + resourceId + ": " + e.getMessage(), e); + } + } + + public boolean userHasAccess(RequestContext ctx, String resourceId, ResourcePermissionType permissionType) throws ServiceException { + String gatewayId = ctx.getGatewayId(); + String userId = ctx.getUserId(); + try { + return userHasAccess(gatewayId, userId, resourceId, permissionType); + } catch (Exception e) { + throw new ServiceException("Error checking user access for resource " + resourceId + ": " + e.getMessage(), e); + } + } + + // Internal helpers + + boolean userHasAccess(String gatewayId, String userId, String entityId, ResourcePermissionType permissionType) { + String qualifiedUserId = userId + "@" + gatewayId; + try { + boolean hasOwnerAccess = sharingHandler.userHasAccess( + gatewayId, qualifiedUserId, entityId, gatewayId + ":" + ResourcePermissionType.OWNER); + if (permissionType.equals(ResourcePermissionType.OWNER)) { + return hasOwnerAccess; + } else if (permissionType.equals(ResourcePermissionType.WRITE)) { + return hasOwnerAccess || sharingHandler.userHasAccess( + gatewayId, qualifiedUserId, entityId, gatewayId + ":" + ResourcePermissionType.WRITE); + } else if (permissionType.equals(ResourcePermissionType.READ)) { + return hasOwnerAccess || sharingHandler.userHasAccess( + gatewayId, qualifiedUserId, entityId, gatewayId + ":" + ResourcePermissionType.READ); + } else if (permissionType.equals(ResourcePermissionType.MANAGE_SHARING)) { + return hasOwnerAccess || sharingHandler.userHasAccess( + gatewayId, qualifiedUserId, entityId, gatewayId + ":" + ResourcePermissionType.MANAGE_SHARING); + } + return false; + } catch (Exception e) { + throw new RuntimeException("Unable to check if user has access", e); + } + } + + private List<String> getAllAccessibleUsersInternal( + String gatewayId, String resourceId, ResourcePermissionType permissionType, + BiFunction<SharingRegistryServerHandler, ResourcePermissionType, Collection<User>> userListFunction) { + HashSet<String> accessibleUsers = new HashSet<>(); + if (permissionType.equals(ResourcePermissionType.WRITE)) { + userListFunction.apply(sharingHandler, ResourcePermissionType.WRITE).forEach(u -> accessibleUsers.add(u.getUserId())); + userListFunction.apply(sharingHandler, ResourcePermissionType.OWNER).forEach(u -> accessibleUsers.add(u.getUserId())); + } else if (permissionType.equals(ResourcePermissionType.READ)) { + userListFunction.apply(sharingHandler, ResourcePermissionType.READ).forEach(u -> accessibleUsers.add(u.getUserId())); + userListFunction.apply(sharingHandler, ResourcePermissionType.OWNER).forEach(u -> accessibleUsers.add(u.getUserId())); + } else if (permissionType.equals(ResourcePermissionType.OWNER)) { + userListFunction.apply(sharingHandler, ResourcePermissionType.OWNER).forEach(u -> accessibleUsers.add(u.getUserId())); + } else if (permissionType.equals(ResourcePermissionType.MANAGE_SHARING)) { + userListFunction.apply(sharingHandler, ResourcePermissionType.MANAGE_SHARING).forEach(u -> accessibleUsers.add(u.getUserId())); + userListFunction.apply(sharingHandler, ResourcePermissionType.OWNER).forEach(u -> accessibleUsers.add(u.getUserId())); + } + return new ArrayList<>(accessibleUsers); + } + + private List<String> getAllAccessibleGroupsInternal( + String gatewayId, String resourceId, ResourcePermissionType permissionType, + BiFunction<SharingRegistryServerHandler, ResourcePermissionType, Collection<UserGroup>> groupListFunction) { + HashSet<String> accessibleGroups = new HashSet<>(); + if (permissionType.equals(ResourcePermissionType.WRITE)) { + groupListFunction.apply(sharingHandler, ResourcePermissionType.WRITE).forEach(g -> accessibleGroups.add(g.getGroupId())); + } else if (permissionType.equals(ResourcePermissionType.READ)) { + groupListFunction.apply(sharingHandler, ResourcePermissionType.READ).forEach(g -> accessibleGroups.add(g.getGroupId())); + } else if (permissionType.equals(ResourcePermissionType.MANAGE_SHARING)) { + groupListFunction.apply(sharingHandler, ResourcePermissionType.MANAGE_SHARING).forEach(g -> accessibleGroups.add(g.getGroupId())); + } + return new ArrayList<>(accessibleGroups); + } + + private ResourceType getResourceType(String domainId, String entityId) throws Exception { + var entity = sharingHandler.getEntity(domainId, entityId); + for (ResourceType resourceType : ResourceType.values()) { + if (entity.getEntityTypeId().equals(domainId + ":" + resourceType.name())) { + return resourceType; + } + } + throw new RuntimeException("Unrecognized entity type id: " + entity.getEntityTypeId()); + } + + private boolean isAdminRestrictedResourceType(ResourceType resourceType) { + return resourceType == ResourceType.EXPERIMENT + || resourceType == ResourceType.APPLICATION_DEPLOYMENT + || resourceType == ResourceType.GROUP_RESOURCE_PROFILE; + } + + private void validateAdminGroupNotRevoked(String gatewayId, String resourceId, Map<String, ResourcePermissionType> groupPermissionList) throws Exception { + var gatewayGroups = sharingHandler.getEntity(gatewayId, gatewayId + ":GATEWAY_GROUPS"); + // Note: admin group validation is best-effort based on gateway groups entity + // The actual validation uses GatewayGroups from registryHandler (done in caller context) + } + + void createManageSharingPermissionTypeIfMissing(String domainId) throws Exception { + String permissionTypeId = domainId + ":MANAGE_SHARING"; + if (!sharingHandler.isPermissionExists(domainId, permissionTypeId)) { + PermissionType permissionType = new PermissionType(); + permissionType.setPermissionTypeId(permissionTypeId); + permissionType.setDomainId(domainId); + permissionType.setName("MANAGE_SHARING"); + permissionType.setDescription("Manage sharing permission type"); + sharingHandler.createPermissionType(permissionType); + logger.info("Created MANAGE_SHARING permission type for domain {}", domainId); + } + } +} diff --git a/airavata-api/src/test/java/org/apache/airavata/service/sharing/ResourceSharingServiceTest.java b/airavata-api/src/test/java/org/apache/airavata/service/sharing/ResourceSharingServiceTest.java new file mode 100644 index 0000000000..8f1d209784 --- /dev/null +++ b/airavata-api/src/test/java/org/apache/airavata/service/sharing/ResourceSharingServiceTest.java @@ -0,0 +1,118 @@ +package org.apache.airavata.service.sharing; + +import org.apache.airavata.model.group.ResourcePermissionType; +import org.apache.airavata.service.context.RequestContext; +import org.apache.airavata.service.exception.ServiceAuthorizationException; +import org.apache.airavata.service.exception.ServiceException; +import org.apache.airavata.sharing.registry.models.User; +import org.apache.airavata.sharing.registry.server.SharingRegistryServerHandler; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; + +import static org.junit.jupiter.api.Assertions.*; +import static org.mockito.ArgumentMatchers.*; +import static org.mockito.Mockito.*; + +@ExtendWith(MockitoExtension.class) +class ResourceSharingServiceTest { + + @Mock SharingRegistryServerHandler sharingHandler; + + ResourceSharingService resourceSharingService; + RequestContext ctx; + + @BeforeEach + void setUp() { + resourceSharingService = new ResourceSharingService(sharingHandler); + ctx = new RequestContext("testUser", "testGateway", "token123", + Map.of("userName", "testUser", "gatewayId", "testGateway")); + } + + @Test + void shareResourceWithUsers_ownerCanShare() throws Exception { + // User is the owner + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:OWNER")).thenReturn(true); + + boolean result = resourceSharingService.shareResourceWithUsers(ctx, "resource-1", + Map.of("otherUser", ResourcePermissionType.READ)); + + assertTrue(result); + verify(sharingHandler).shareEntityWithUsers(eq("testGateway"), eq("resource-1"), + anyList(), eq("testGateway:READ"), eq(true)); + } + + @Test + void shareResourceWithUsers_nonOwnerWithoutSharingPermissionRejected() throws Exception { + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:OWNER")).thenReturn(false); + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:MANAGE_SHARING")).thenReturn(false); + + assertThrows(ServiceAuthorizationException.class, + () -> resourceSharingService.shareResourceWithUsers(ctx, "resource-1", + Map.of("otherUser", ResourcePermissionType.READ))); + } + + @Test + void revokeSharingOfResourceFromUsers_ownerCanRevoke() throws Exception { + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:OWNER")).thenReturn(true); + + boolean result = resourceSharingService.revokeSharingOfResourceFromUsers(ctx, "resource-1", + Map.of("otherUser", ResourcePermissionType.WRITE)); + + assertTrue(result); + verify(sharingHandler).revokeEntitySharingFromUsers(eq("testGateway"), eq("resource-1"), + anyList(), eq("testGateway:WRITE")); + } + + @Test + void userHasAccess_delegatesToSharingHandler() throws Exception { + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:OWNER")).thenReturn(false); + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:READ")).thenReturn(true); + + boolean result = resourceSharingService.userHasAccess(ctx, "resource-1", ResourcePermissionType.READ); + + assertTrue(result); + } + + @Test + void getAllAccessibleUsers_returnsUserIds() throws Exception { + User user1 = new User(); + user1.setUserId("user1@testGateway"); + User user2 = new User(); + user2.setUserId("user2@testGateway"); + when(sharingHandler.getListOfSharedUsers("testGateway", "resource-1", "testGateway:READ")) + .thenReturn(List.of(user1)); + when(sharingHandler.getListOfSharedUsers("testGateway", "resource-1", "testGateway:OWNER")) + .thenReturn(List.of(user2)); + + List<String> result = resourceSharingService.getAllAccessibleUsers(ctx, "resource-1", ResourcePermissionType.READ); + + assertEquals(2, result.size()); + assertTrue(result.contains("user1@testGateway")); + assertTrue(result.contains("user2@testGateway")); + } + + @Test + void shareResourceWithGroups_ownerCanShare() throws Exception { + when(sharingHandler.userHasAccess("testGateway", "testUser@testGateway", "resource-1", + "testGateway:OWNER")).thenReturn(true); + + boolean result = resourceSharingService.shareResourceWithGroups(ctx, "resource-1", + Map.of("group-1", ResourcePermissionType.WRITE)); + + assertTrue(result); + verify(sharingHandler).shareEntityWithGroups(eq("testGateway"), eq("resource-1"), + anyList(), eq("testGateway:WRITE"), eq(true)); + } +}
