This is an automated email from the ASF dual-hosted git repository.

lahirujayathilake pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airavata-data-catalog.git


The following commit(s) were added to refs/heads/main by this push:
     new 4eefb5b  Sharing feature
4eefb5b is described below

commit 4eefb5b65c700e9e20fdc8678b60380891ea23a9
Author: Miu0703 <[email protected]>
AuthorDate: Tue Jun 10 07:17:25 2025 -0700

    Sharing feature
    
    * Added pagination feature,without sharing
    
    * Updated pagination logic
    
    * removed the commented out methods
    
    * remove comments
    
    * add if condition
    
    * sharing feature
    
    * Resolve merge conflicts with main
    
    * reduce overloading
    
    * unit tests
    
    * reverse the order of data products
    
    * changed the format
    
    * fixed some issues according to the comments, changed selectAll logic
    
    ---------
    
    Co-authored-by: Miao <[email protected]>
---
 .../airavata/datacatalog/api/model/UserEntity.java |  16 ++
 .../PostgresqlMetadataSchemaQueryWriterImpl.java   |  67 ++++--
 .../api/service/DataCatalogAPIService.java         | 223 +++++++++++++++++++
 .../api/service/impl/DataCatalogServiceImpl.java   |   2 +
 .../api/sharing/SimpleSharingManagerImpl.java      |  43 +++-
 .../api/sharing/SimpleSharingManagerImplTest.java  | 245 +++++++++++++++++++++
 .../stubs/src/main/proto/data_catalog.proto        |  40 ++++
 7 files changed, 616 insertions(+), 20 deletions(-)

diff --git 
a/data-catalog-api/server/core/src/main/java/org/apache/airavata/datacatalog/api/model/UserEntity.java
 
b/data-catalog-api/server/core/src/main/java/org/apache/airavata/datacatalog/api/model/UserEntity.java
index f9109ca..a37e600 100644
--- 
a/data-catalog-api/server/core/src/main/java/org/apache/airavata/datacatalog/api/model/UserEntity.java
+++ 
b/data-catalog-api/server/core/src/main/java/org/apache/airavata/datacatalog/api/model/UserEntity.java
@@ -12,6 +12,11 @@ import jakarta.persistence.SequenceGenerator;
 import jakarta.persistence.Table;
 import jakarta.persistence.UniqueConstraint;
 
+import java.util.List;
+import java.util.ArrayList;
+
+import jakarta.persistence.Transient;
+
 @Entity
 // 'user' is a reserved word, so naming this table 'user_table'
 @Table(name = "user_table", uniqueConstraints = { 
@UniqueConstraint(columnNames = { "tenant_id", "external_id" }) })
@@ -39,10 +44,21 @@ public class UserEntity {
     @JoinColumn(name = "tenant_id", referencedColumnName = "tenant_id", 
nullable = false, updatable = false)
     private TenantEntity tenant;
 
+    @Transient
+    private List<String> groupIds = new ArrayList<>();
+
+    public List<String> getGroupIds() {
+        return groupIds;
+    }
+
     public Long getUserId() {
         return userId;
     }
 
+    public void setGroupIds(List<String> groupIds) {
+        this.groupIds = groupIds;
+    }
+
     public void setUserId(Long userId) {
         this.userId = userId;
     }
diff --git 
a/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/query/impl/PostgresqlMetadataSchemaQueryWriterImpl.java
 
b/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/query/impl/PostgresqlMetadataSchemaQueryWriterImpl.java
index e72b298..8cd7b54 100644
--- 
a/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/query/impl/PostgresqlMetadataSchemaQueryWriterImpl.java
+++ 
b/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/query/impl/PostgresqlMetadataSchemaQueryWriterImpl.java
@@ -173,6 +173,7 @@ public class PostgresqlMetadataSchemaQueryWriterImpl 
implements MetadataSchemaQu
                 sb.append(" WHERE ");
                 sb.append(rewriteWhereClauseFilters(sqlNode, metadataSchemas, 
tableAliases));
             }
+            sb.append(" ORDER BY data_product_id DESC ");
         } else if (sqlNode instanceof SqlBasicCall unionNode &&
                 ((SqlBasicCall) sqlNode).getOperator().getKind() == 
SqlKind.UNION) {
 
@@ -196,12 +197,47 @@ public class PostgresqlMetadataSchemaQueryWriterImpl 
implements MetadataSchemaQu
 
     String writeCommonTableExpressions(UserEntity userEntity, 
Collection<MetadataSchemaEntity> metadataSchemas) {
         StringBuilder sb = new StringBuilder();
-        List<String> commonTableExpressions = new ArrayList<>();
+        List<String> ctes = new ArrayList<>();
+        long userDbId = userEntity.getUserId(); // userEntity -> user -> 
getUserId()
+        List<String> groupIds = userEntity.getGroupIds();
+        // user_group_union CTE
+        // (simple_data_product_sharing_view, integer)
+        //union (simple_group_sharing, varchar)
+        StringBuilder unionCte = new StringBuilder();
+        unionCte.append("user_group_union AS (");
+
+        //user_id = userDbId, permission_id in (0,1) => OWNER=0,READ=1
+        unionCte.append(" SELECT data_product_id FROM ")
+                .append(sharingManager.getDataProductSharingView()) // 
"simple_data_product_sharing_view"
+                .append(" WHERE user_id = ")
+                .append(userDbId)
+                .append(" AND permission_id IN (")
+                .append(Permission.OWNER.getNumber())
+                .append(",")
+                .append(Permission.READ.getNumber())
+                .append(")");
+
+        if (groupIds != null && !groupIds.isEmpty()) {
+            unionCte.append(" UNION SELECT sgs.data_product_id ")
+                    .append(" FROM simple_group_sharing sgs ")
+                    .append(" JOIN simple_group g ON g.simple_group_id = 
sgs.simple_group_id ")
+                    .append(" WHERE sgs.permission_id IN ('READ') ")
+                    .append("   AND g.external_id IN (");
+            for (int i = 0; i < groupIds.size(); i++) {
+                if (i > 0) unionCte.append(",");
+                unionCte.append("'").append(groupIds.get(i).replace("'", 
"''")).append("'");
+            }
+            unionCte.append(")");
+        }
+        unionCte.append(")"); // end of user_group_union
+
+        ctes.add(unionCte.toString());
         for (MetadataSchemaEntity metadataSchema : metadataSchemas) {
-            commonTableExpressions.add(writeCommonTableExpression(userEntity, 
metadataSchema));
+            String cteForSchema = writeCommonTableExpression(userEntity, 
metadataSchema);
+            ctes.add(cteForSchema);
         }
         sb.append("WITH ");
-        sb.append(String.join(", ", commonTableExpressions));
+        sb.append(String.join(", ", ctes));
         return sb.toString();
     }
 
@@ -219,18 +255,21 @@ public class PostgresqlMetadataSchemaQueryWriterImpl 
implements MetadataSchemaQu
         sb.append("from data_product dp_ ");
         sb.append("inner join data_product_metadata_schema dpms_ on 
dpms_.data_product_id = dp_.data_product_id ");
         sb.append("inner join metadata_schema ms_ on ms_.metadata_schema_id = 
dpms_.metadata_schema_id ");
-        sb.append("inner join ");
-        sb.append(sharingManager.getDataProductSharingView());
-        sb.append(" dpsv_ on dpsv_.data_product_id = dp_.data_product_id ");
-        sb.append("and dpsv_.user_id = ");
+        //sb.append("inner join ");
+        //sb.append(sharingManager.getDataProductSharingView());
+        //sb.append(" dpsv_ on dpsv_.data_product_id = dp_.data_product_id ");
+        //sb.append("and dpsv_.user_id = ");
         // TODO: change these to be bound parameters
-        sb.append(userEntity.getUserId());
-        sb.append(" and dpsv_.permission_id in (");
-        sb.append(Permission.OWNER.getNumber());
-        sb.append(",");
-        sb.append(Permission.READ_METADATA.getNumber());
-        sb.append(") ");
-        sb.append("where ms_.metadata_schema_id = " + 
metadataSchemaEntity.getMetadataSchemaId());
+        //sb.append(userEntity.getUserId());
+        //sb.append(" and dpsv_.permission_id in (");
+        // sb.append(Permission.OWNER.getNumber());
+        //sb.append(",");
+        //sb.append(Permission.READ.getNumber());
+        //sb.append(") ");
+        //sb.append("where ms_.metadata_schema_id = " + 
metadataSchemaEntity.getMetadataSchemaId());
+        sb.append("   WHERE dp_.data_product_id IN (SELECT data_product_id 
FROM user_group_union)");
+        sb.append("     AND ms_.metadata_schema_id = ");
+        sb.append(metadataSchemaEntity.getMetadataSchemaId());
         sb.append(")");
         return sb.toString();
     }
diff --git 
a/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/DataCatalogAPIService.java
 
b/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/DataCatalogAPIService.java
index f53b5bf..f7b8b28 100644
--- 
a/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/DataCatalogAPIService.java
+++ 
b/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/DataCatalogAPIService.java
@@ -2,6 +2,10 @@ package org.apache.airavata.datacatalog.api.service;
 
 import java.util.List;
 
+import jakarta.persistence.EntityManager;
+import jakarta.persistence.PersistenceContext;
+import jakarta.persistence.Query;
+
 import org.apache.airavata.datacatalog.api.DataCatalogAPIServiceGrpc;
 import org.apache.airavata.datacatalog.api.DataProduct;
 import 
org.apache.airavata.datacatalog.api.DataProductAddToMetadataSchemaRequest;
@@ -50,10 +54,21 @@ import org.lognet.springboot.grpc.GRpcService;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.transaction.annotation.Transactional;
 
 import io.grpc.Status;
 import io.grpc.stub.StreamObserver;
 
+import org.apache.airavata.datacatalog.api.GrantPermissionToUserRequest;
+import org.apache.airavata.datacatalog.api.GrantPermissionToUserResponse;
+import org.apache.airavata.datacatalog.api.GrantPermissionToGroupRequest;
+import org.apache.airavata.datacatalog.api.GrantPermissionToGroupResponse;
+import org.apache.airavata.datacatalog.api.GrantPermissionToUserOnAllRequest;
+import org.apache.airavata.datacatalog.api.GrantPermissionToUserOnAllResponse;
+import org.apache.airavata.datacatalog.api.GrantPermissionToGroupOnAllRequest;
+import org.apache.airavata.datacatalog.api.GrantPermissionToGroupOnAllResponse;
+
+
 @GRpcService
 public class DataCatalogAPIService extends 
DataCatalogAPIServiceGrpc.DataCatalogAPIServiceImplBase {
 
@@ -65,6 +80,9 @@ public class DataCatalogAPIService extends 
DataCatalogAPIServiceGrpc.DataCatalog
     @Autowired
     SharingManager sharingManager;
 
+    @PersistenceContext
+    private EntityManager entityManager;
+
     @Override
     public void createDataProduct(DataProductCreateRequest request,
             StreamObserver<DataProductCreateResponse> responseObserver) {
@@ -336,4 +354,209 @@ public class DataCatalogAPIService extends 
DataCatalogAPIServiceGrpc.DataCatalog
         return false;
     }
 
+    @Override
+    public void grantPermissionToUser(GrantPermissionToUserRequest request,
+                                      
StreamObserver<GrantPermissionToUserResponse> responseObserver) {
+
+        try {
+
+            DataProduct dataProduct = 
dataCatalogService.getDataProduct(request.getDataProductId());
+
+            sharingManager.grantPermissionToUser(
+                    request.getTargetUser(),
+                    dataProduct,
+                    request.getPermission(),
+                    request.getUserInfo()
+            );
+            
responseObserver.onNext(GrantPermissionToUserResponse.getDefaultInstance());
+            responseObserver.onCompleted();
+        } catch (EntityNotFoundException e) {
+            logger.error("grantPermissionToUser: dataProduct({}) not found, 
sharedByUser={}",
+                    request.getDataProductId(), request.getUserInfo(), e);
+            responseObserver.onError(Status.NOT_FOUND
+                    .withDescription(e.getMessage()).asException());
+        } catch (Exception e) {
+            logger.error("Failed to grant permission to user", e);
+            responseObserver.onError(Status.INTERNAL
+                    .withDescription(e.getMessage()).asException());
+        }
+    }
+
+    @Override
+    public void grantPermissionToGroup(GrantPermissionToGroupRequest request,
+                                       
StreamObserver<GrantPermissionToGroupResponse> responseObserver) {
+
+        try {
+            DataProduct dataProduct = 
dataCatalogService.getDataProduct(request.getDataProductId());
+
+            if (!checkHasPermission(request.getUserInfo(), dataProduct, 
Permission.OWNER, responseObserver)) {
+                return;
+            }
+
+            sharingManager.grantPermissionToGroup(
+                    request.getTargetGroup(),
+                    dataProduct,
+                    request.getPermission(),
+                    request.getUserInfo()
+            );
+
+            
responseObserver.onNext(GrantPermissionToGroupResponse.getDefaultInstance());
+            responseObserver.onCompleted();
+        } catch (EntityNotFoundException e) {
+            logger.error("grantPermissionToGroup: dataProduct({}) not found, 
sharedByGroup={}",
+                    request.getDataProductId(), request.getTargetGroup(), e);
+            responseObserver.onError(Status.NOT_FOUND
+                    .withDescription(e.getMessage()).asException());
+        } catch (Exception e) {
+            logger.error("Failed to grant permission to group", e);
+            responseObserver.onError(Status.INTERNAL
+                    .withDescription(e.getMessage()).asException());
+        }
+    }
+
+    @Override
+    @Transactional
+    public void grantPermissionToUserOnAll(GrantPermissionToUserOnAllRequest 
req,
+                                           
StreamObserver<GrantPermissionToUserOnAllResponse> resp) {
+
+        try {
+            long actingUserId = 
sharingManager.resolveUser(req.getUserInfo()).getUserId();
+            int permId = req.getPermission().getNumber();
+            int ownerId = Permission.OWNER.getNumber();
+
+            String permName = req.getPermission().name();
+
+            entityManager.createNativeQuery(
+                    "CREATE TEMP SEQUENCE IF NOT EXISTS 
temp_sharing_id_seq").executeUpdate();
+
+
+            entityManager.createNativeQuery("""
+                        SELECT setval(
+                               'temp_sharing_id_seq',
+                              CAST(extract(epoch from clock_timestamp())*1000 
AS bigint),
+                               false)                    
+                    """).getSingleResult();
+            String sql = """
+                    INSERT INTO simple_user_sharing (sharing_id,
+                                                     data_product_id,
+                                                     permission_id,
+                                                     shared_by_user_id,
+                                                     simple_user_id)
+                    SELECT nextval('temp_sharing_id_seq'),
+                           v.data_product_id,
+                           CAST(:perm AS varchar),
+                           :actingSid,
+                           su.simple_user_id
+                    FROM (
+                    
+                        SELECT DISTINCT data_product_id
+                        FROM simple_data_product_sharing_view
+                        WHERE user_id       = :actingUid
+                          AND permission_id IN (:owner, :read)
+                    ) v
+                    
+                    JOIN simple_user   su ON su.external_id   = :userExt
+                    JOIN simple_tenant t  ON t.simple_tenant_id = 
su.simple_tenant_id
+                                         AND t.external_id      = :tenantExt
+                    
+                    
+                    ON CONFLICT (simple_user_id, data_product_id, 
permission_id) DO NOTHING
+                    """;
+
+
+            Query q = entityManager.createNativeQuery(sql)
+                    .setParameter("perm", permName)
+                    .setParameter("actingSid", actingUserId)
+                    .setParameter("actingUid", actingUserId)
+                    .setParameter("owner", ownerId)
+                    .setParameter("read", permId)
+                    .setParameter("userExt", req.getTargetUser().getUserId())
+                    .setParameter("tenantExt", 
req.getTargetUser().getTenantId());
+
+            int inserted = q.executeUpdate();
+
+            logger.info("bulk-shared {} products with user {}",
+                    inserted, req.getTargetUser().getUserId());
+
+            
resp.onNext(GrantPermissionToUserOnAllResponse.getDefaultInstance());
+            resp.onCompleted();
+
+        } catch (SharingException ex) {
+            logger.error("share-all to user failed", ex);
+            resp.onError(Status.INTERNAL.withDescription(ex.getMessage())
+                    .asRuntimeException());
+        }
+    }
+
+
+    @Override
+    @Transactional
+    public void grantPermissionToGroupOnAll(GrantPermissionToGroupOnAllRequest 
req,
+                                            
StreamObserver<GrantPermissionToGroupOnAllResponse> resp) {
+
+        try {
+            long actingUserId = 
sharingManager.resolveUser(req.getUserInfo()).getUserId();
+            int ownerId = Permission.OWNER.getNumber();
+
+            String permName = req.getPermission().name();
+            entityManager.createNativeQuery(
+                            "CREATE TEMP SEQUENCE IF NOT EXISTS 
temp_sharing_id_seq")
+                    .executeUpdate();
+
+            entityManager.createNativeQuery("""
+                        SELECT setval(
+                               'temp_sharing_id_seq',
+                               CAST(extract(epoch from clock_timestamp())*1000 
AS bigint),
+                               false)
+                    """).getSingleResult();
+
+            String sql = """
+                    INSERT INTO simple_group_sharing (sharing_id, 
data_product_id, permission_id,
+                                                      shared_by_user_id, 
simple_group_id)
+                    
+                    SELECT nextval('temp_sharing_id_seq'),
+                           v.data_product_id, :perm, :actingSid, 
g.simple_group_id
+                    FROM (
+                        SELECT DISTINCT data_product_id
+                        FROM   simple_data_product_sharing_view
+                        WHERE  user_id       = :actingUid
+                          AND  permission_id IN (:owner, :read)     
+                    ) v
+                    JOIN simple_group  g ON g.external_id     = :groupExt
+                    JOIN simple_tenant t ON t.simple_tenant_id = 
g.simple_tenant_id
+                                        AND t.external_id      = :tenantExt
+                    
+                    WHERE NOT EXISTS (
+                          SELECT 1 FROM simple_group_sharing s
+                           WHERE s.simple_group_id = g.simple_group_id
+                             AND s.data_product_id = v.data_product_id
+                             AND s.permission_id   = :perm
+                    )
+                    """;
+
+            Query q = entityManager.createNativeQuery(sql)
+                    .setParameter("perm", permName)
+                    .setParameter("actingSid", actingUserId)
+                    .setParameter("actingUid", actingUserId)
+                    .setParameter("owner", ownerId)
+                    .setParameter("read", req.getPermission().getNumber())
+                    .setParameter("groupExt", 
req.getTargetGroup().getGroupId())
+                    .setParameter("tenantExt", 
req.getTargetGroup().getTenantId());
+            int inserted = q.executeUpdate();
+
+            logger.info("bulk-shared {} products with group {}", inserted,
+                    req.getTargetGroup().getGroupId());
+
+            
resp.onNext(GrantPermissionToGroupOnAllResponse.getDefaultInstance());
+            resp.onCompleted();
+
+        } catch (SharingException ex) {
+            logger.error("share-all to group failed", ex);
+            resp.onError(Status.INTERNAL
+                    .withDescription(ex.getMessage())
+                    .asRuntimeException());
+        }
+    }
+
+
 }
\ No newline at end of file
diff --git 
a/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/impl/DataCatalogServiceImpl.java
 
b/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/impl/DataCatalogServiceImpl.java
index c4acf80..d164d92 100644
--- 
a/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/impl/DataCatalogServiceImpl.java
+++ 
b/data-catalog-api/server/service/src/main/java/org/apache/airavata/datacatalog/api/service/impl/DataCatalogServiceImpl.java
@@ -127,6 +127,8 @@ public class DataCatalogServiceImpl implements 
DataCatalogService {
             throws MetadataSchemaSqlParseException, 
MetadataSchemaSqlValidateException {
         try {
             UserEntity userEntity = sharingManager.resolveUser(userInfo);
+            userEntity.getGroupIds().clear();
+            userEntity.getGroupIds().addAll(userInfo.getGroupIdsList());
             return metadataSchemaQueryExecutor.execute(userEntity, sql, page, 
pageSize);
         } catch (SharingException e) {
             throw new RuntimeException("Unable to resolve " + userInfo, e);
diff --git 
a/data-catalog-api/server/simple-sharing/src/main/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImpl.java
 
b/data-catalog-api/server/simple-sharing/src/main/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImpl.java
index 293ae6e..52f9936 100644
--- 
a/data-catalog-api/server/simple-sharing/src/main/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImpl.java
+++ 
b/data-catalog-api/server/simple-sharing/src/main/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImpl.java
@@ -80,13 +80,44 @@ public class SimpleSharingManagerImpl implements 
SharingManager {
             throws SharingException {
         UserEntity user = resolveUser(userInfo);
         DataProductEntity dataProductEntity = resolveDataProduct(dataProduct);
-        Query query = entityManager.createNativeQuery("select 1 from " + 
getDataProductSharingView()
-                + " where user_id = :user_id and data_product_id = 
:data_product_id and permission_id in :permission_id");
-        query.setParameter("user_id", user.getUserId());
-        query.setParameter("data_product_id", 
dataProductEntity.getDataProductId());
-        query.setParameter("permission_id", 
Arrays.asList(permission.getNumber(), Permission.OWNER.getNumber()));
 
-        return query.getResultList().size() > 0;
+        Query userQuery = entityManager.createNativeQuery(
+                "SELECT 1 " +
+                        "  FROM " + getDataProductSharingView() +
+                        " WHERE user_id = :user_id " +
+                        "   AND data_product_id = :dp_id " +
+                        "   AND permission_id IN (:perm_ids_int)"
+        );
+        userQuery.setParameter("user_id", user.getUserId());
+        userQuery.setParameter("dp_id", dataProductEntity.getDataProductId());
+        userQuery.setParameter("perm_ids_int",
+                Arrays.asList(permission.getNumber(), 
Permission.OWNER.getNumber())
+        );
+
+        boolean userHasPerm = !userQuery.getResultList().isEmpty();
+        if (userHasPerm) {
+            return true;
+        }
+
+        if (userInfo.getGroupIdsCount() > 0) {
+            Query groupQuery = entityManager.createNativeQuery(
+                    "SELECT 1 " +
+                            "  FROM simple_group_sharing sgs " +
+                            "  JOIN simple_group g ON sgs.simple_group_id = 
g.simple_group_id " +
+                            " WHERE sgs.data_product_id = :dp_id " +
+                            "   AND sgs.permission_id IN (:perm_ids_str) " +
+                            "   AND g.external_id IN (:group_external_ids)"
+            );
+            groupQuery.setParameter("dp_id", 
dataProductEntity.getDataProductId());
+            groupQuery.setParameter("perm_ids_str",
+                    Arrays.asList(permission.name(), Permission.OWNER.name())
+            );
+            groupQuery.setParameter("group_external_ids", 
userInfo.getGroupIdsList());
+
+            boolean groupHasPerm = !groupQuery.getResultList().isEmpty();
+            return groupHasPerm;
+        }
+        return false;
     }
 
     @Override
diff --git 
a/data-catalog-api/server/simple-sharing/src/test/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImplTest.java
 
b/data-catalog-api/server/simple-sharing/src/test/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImplTest.java
index 956c504..d96d4c3 100644
--- 
a/data-catalog-api/server/simple-sharing/src/test/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImplTest.java
+++ 
b/data-catalog-api/server/simple-sharing/src/test/java/org/apache/airavata/datacatalog/api/sharing/SimpleSharingManagerImplTest.java
@@ -473,4 +473,249 @@ public class SimpleSharingManagerImplTest {
 
     }
 
+    /**
+     * This test ensures a user who is the sole owner of a data product always 
has OWNER permission,
+     * even if they belong to no groups.
+     */
+    @Test
+    public void testOwnerHasAccessEvenWithoutGroups() throws SharingException {
+        UserInfo userA = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("ownerUser")
+                .build();
+        UserEntity ownerUserEntity = 
simpleSharingManagerImpl.resolveUser(userA);
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setOwner(ownerUserEntity);
+        dataProductEntity.setName("Owner-only data product");
+        dataProductRepository.save(dataProductEntity);
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+        simpleSharingManagerImpl.grantPermissionToUser(userA, dataProduct, 
Permission.OWNER, userA);
+        boolean hasOwnerAccess = simpleSharingManagerImpl.userHasAccess(userA, 
dataProduct, Permission.OWNER);
+        assertTrue(hasOwnerAccess, "Owner should have OWNER permission even 
with no groups in the test scenario");
+    }
+
+
+    /**
+     * This test checks that a user who is not directly granted permissions 
but is in a group
+     * that has READ permission can access a data product.
+     */
+    @Test
+    public void testUserInGroupHasReadAccess() throws SharingException {
+        UserInfo userA = 
UserInfo.newBuilder().setTenantId("tenantId").setUserId("ownerUser").build();
+        simpleSharingManagerImpl.resolveUser(userA);
+        UserInfo userB = 
UserInfo.newBuilder().setTenantId("tenantId").setUserId("groupMember").build();
+        simpleSharingManagerImpl.resolveUser(userB);
+        GroupInfo testGroup = 
GroupInfo.newBuilder().setGroupId("readersGroup").setTenantId("tenantId").build();
+        SimpleGroupEntity testGroupEntity = new SimpleGroupEntity();
+        testGroupEntity.setName(testGroup.getGroupId());
+        testGroupEntity.setExternalId(testGroup.getGroupId());
+        Optional<SimpleUserEntity> userBEntity = 
simpleUserRepository.findByExternalIdAndSimpleTenant_ExternalId(
+                userB.getUserId(), userB.getTenantId());
+        assertTrue(userBEntity.isPresent());
+        testGroupEntity.getMemberUsers().add(userBEntity.get());
+        testGroupEntity.setSimpleTenant(userBEntity.get().getSimpleTenant());
+        simpleGroupRepository.save(testGroupEntity);
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setName("Group accessible data product");
+        
dataProductEntity.setOwner(userRepository.findByExternalIdAndTenant_ExternalId(userA.getUserId(),
+                userA.getTenantId()).orElseThrow());
+        dataProductRepository.save(dataProductEntity);
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+        simpleSharingManagerImpl.grantPermissionToGroup(testGroup, 
dataProduct, Permission.READ, userA);
+        assertTrue(simpleSharingManagerImpl.userHasAccess(userB, dataProduct, 
Permission.READ),
+                "User in group should have READ access via group's READ 
permission");
+    }
+
+    /**
+     * This test verifies that a user who only has READ permission through a 
group
+     * does NOT have OWNER or WRITE_METADATA permission to the data product.
+     */
+    @Test
+    public void testUserInGroupHasOnlyReadNotOwner() throws SharingException {
+        UserInfo userA = 
UserInfo.newBuilder().setTenantId("tenantId").setUserId("ownerUser").build();
+        simpleSharingManagerImpl.resolveUser(userA);
+        UserInfo userB = 
UserInfo.newBuilder().setTenantId("tenantId").setUserId("readOnlyUser").build();
+        simpleSharingManagerImpl.resolveUser(userB);
+        GroupInfo readGroup = 
GroupInfo.newBuilder().setGroupId("readonlyGroup").setTenantId("tenantId").build();
+        SimpleGroupEntity groupEntity = new SimpleGroupEntity();
+        groupEntity.setExternalId(readGroup.getGroupId());
+        groupEntity.setName(readGroup.getGroupId());
+        Optional<SimpleUserEntity> userBEntity = 
simpleUserRepository.findByExternalIdAndSimpleTenant_ExternalId(
+                userB.getUserId(), userB.getTenantId());
+        assertTrue(userBEntity.isPresent());
+        groupEntity.getMemberUsers().add(userBEntity.get());
+        groupEntity.setSimpleTenant(userBEntity.get().getSimpleTenant());
+        simpleGroupRepository.save(groupEntity);
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setName("Read-only data product");
+        
dataProductEntity.setOwner(userRepository.findByExternalIdAndTenant_ExternalId(userA.getUserId(),
+                userA.getTenantId()).orElseThrow());
+        dataProductRepository.save(dataProductEntity);
+
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+        simpleSharingManagerImpl.grantPermissionToGroup(readGroup, 
dataProduct, Permission.READ, userA);
+        assertTrue(simpleSharingManagerImpl.userHasAccess(userB, dataProduct, 
Permission.READ),
+                "User should have READ permission");
+        assertFalse(simpleSharingManagerImpl.userHasAccess(userB, dataProduct, 
Permission.OWNER),
+                "User should not have OWNER permission");
+        assertFalse(simpleSharingManagerImpl.userHasAccess(userB, dataProduct, 
Permission.WRITE_METADATA),
+                "User should not have WRITE_METADATA permission");
+    }
+
+    /**
+     * This test checks that if the user is neither the owner nor part of any 
group
+     * that has permissions, they cannot access the data product.
+     */
+    @Test
+    public void testUserWithoutAnyPermissionCannotAccess() throws 
SharingException {
+        UserInfo userA = 
UserInfo.newBuilder().setTenantId("tenantId").setUserId("ownerUser").build();
+        UserEntity ownerUserEntity = 
simpleSharingManagerImpl.resolveUser(userA);
+        UserInfo userB = 
UserInfo.newBuilder().setTenantId("tenantId").setUserId("noPermUser").build();
+        simpleSharingManagerImpl.resolveUser(userB);
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setOwner(ownerUserEntity);
+        dataProductEntity.setName("No-permission data product");
+        dataProductRepository.save(dataProductEntity);
+        // Verify userB cannot READ
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+        assertFalse(simpleSharingManagerImpl.userHasAccess(userB, dataProduct, 
Permission.READ),
+                "User with no group or direct permission should not have READ 
access");
+    }
+
+    /**
+     * Test that a user with only userId (i.e. no groupIds provided)
+     * can see the data product they own.
+     */
+    @Test
+    public void testSearchDataProductByOwner() throws SharingException {
+        UserInfo ownerOnly = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("ownerOnly")
+                .build();
+        UserEntity ownerEntity = 
simpleSharingManagerImpl.resolveUser(ownerOnly);
+
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setOwner(ownerEntity);
+        dataProductEntity.setName("Owned Data Product");
+        dataProductRepository.save(dataProductEntity);
+
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+
+        simpleSharingManagerImpl.grantPermissionToUser(ownerOnly, dataProduct, 
Permission.OWNER, ownerOnly);
+        //READ is in OWNER
+        assertTrue(simpleSharingManagerImpl.userHasAccess(ownerOnly, 
dataProduct, Permission.READ),
+                "User with only userId should see data product they own");
+    }
+
+    /**
+     * Test that when UserInfo explicitly includes a groupId,
+     * the user can see the data product that has been shared with that group.
+     */
+    @Test
+    public void testSearchDataProductByGroupMembershipWithExplicitGroupId() 
throws SharingException {
+        UserInfo owner = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("ownerUser_explicitGroup")
+                .build();
+        UserEntity ownerEntity = simpleSharingManagerImpl.resolveUser(owner);
+
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setOwner(ownerEntity);
+        dataProductEntity.setName("Group Shared Data Product");
+        dataProductRepository.save(dataProductEntity);
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+        UserInfo groupUser = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("groupUser_explicitGroup")
+                .build();
+        simpleSharingManagerImpl.resolveUser(groupUser);
+        Optional<SimpleUserEntity> groupUserEntityOpt = simpleUserRepository
+                
.findByExternalIdAndSimpleTenant_ExternalId(groupUser.getUserId(), 
groupUser.getTenantId());
+        assertTrue(groupUserEntityOpt.isPresent());
+        SimpleGroupEntity groupEntity = new SimpleGroupEntity();
+        groupEntity.setName("explicitGroup");
+        groupEntity.setExternalId("explicitGroup");
+        groupEntity.getMemberUsers().add(groupUserEntityOpt.get());
+        
groupEntity.setSimpleTenant(groupUserEntityOpt.get().getSimpleTenant());
+        simpleGroupRepository.save(groupEntity);
+        simpleSharingManagerImpl.grantPermissionToGroup(
+                
GroupInfo.newBuilder().setGroupId("explicitGroup").setTenantId("tenantId").build(),
+                dataProduct,
+                Permission.READ,
+                owner);
+        UserInfo groupUserWithGroup = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("groupUser_explicitGroup")
+                .addGroupIds("explicitGroup")
+                .build();
+        assertTrue(simpleSharingManagerImpl.userHasAccess(groupUserWithGroup, 
dataProduct, Permission.READ),
+                "User with explicit groupIds should see group shared data 
product");
+    }
+
+    /**
+     * Test that if a user is not a member of the group (i.e. their UserInfo 
does not include the shared group),
+     * then even if a group sharing exists, they cannot access the data 
product.
+     */
+    @Test
+    public void testGroupAccessNotAppliedWhenUserIsNotMember() throws 
SharingException {
+        UserInfo owner = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("ownerUser_nonMember")
+                .build();
+        UserEntity ownerEntity = simpleSharingManagerImpl.resolveUser(owner);
+
+        DataProductEntity dataProductEntity = new DataProductEntity();
+        dataProductEntity.setExternalId(UUID.randomUUID().toString());
+        dataProductEntity.setOwner(ownerEntity);
+        dataProductEntity.setName("Group Shared Data Product NonMember");
+        dataProductRepository.save(dataProductEntity);
+        DataProduct dataProduct = DataProduct.newBuilder()
+                .setDataProductId(dataProductEntity.getExternalId())
+                .build();
+        UserInfo groupUser = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("groupUser_nonMember")
+                .build();
+        simpleSharingManagerImpl.resolveUser(groupUser);
+        Optional<SimpleUserEntity> groupUserEntityOpt = simpleUserRepository
+                
.findByExternalIdAndSimpleTenant_ExternalId(groupUser.getUserId(), 
groupUser.getTenantId());
+        assertTrue(groupUserEntityOpt.isPresent());
+        SimpleGroupEntity groupEntity = new SimpleGroupEntity();
+        groupEntity.setName("nonMemberGroup");
+        groupEntity.setExternalId("nonMemberGroup");
+        groupEntity.getMemberUsers().add(groupUserEntityOpt.get());
+        
groupEntity.setSimpleTenant(groupUserEntityOpt.get().getSimpleTenant());
+        simpleGroupRepository.save(groupEntity);
+        simpleSharingManagerImpl.grantPermissionToGroup(
+                
GroupInfo.newBuilder().setGroupId("nonMemberGroup").setTenantId("tenantId").build(),
+                dataProduct,
+                Permission.READ,
+                owner);
+        UserInfo nonMemberUser = UserInfo.newBuilder()
+                .setTenantId("tenantId")
+                .setUserId("nonMemberUser")
+                .build();
+        assertFalse(simpleSharingManagerImpl.userHasAccess(nonMemberUser, 
dataProduct, Permission.READ),
+                "User not a member of the group should not see group shared 
data product");
+    }
+
+
 }
diff --git a/data-catalog-api/stubs/src/main/proto/data_catalog.proto 
b/data-catalog-api/stubs/src/main/proto/data_catalog.proto
index 9ed5620..f3d2c3c 100644
--- a/data-catalog-api/stubs/src/main/proto/data_catalog.proto
+++ b/data-catalog-api/stubs/src/main/proto/data_catalog.proto
@@ -32,6 +32,7 @@ message UserInfo {
      * is integrated with.
      */
     optional string tenant_id = 2;
+    repeated string group_ids = 3;
 }
 
 message GroupInfo {
@@ -219,4 +220,43 @@ service DataCatalogAPIService {
     rpc addDataProductToMetadataSchema(DataProductAddToMetadataSchemaRequest) 
returns (DataProductAddToMetadataSchemaResponse){}
     rpc 
removeDataProductFromMetadataSchema(DataProductRemoveFromMetadataSchemaRequest) 
returns (DataProductRemoveFromMetadataSchemaResponse){}
     rpc searchDataProducts(DataProductSearchRequest) returns 
(DataProductSearchResponse){}
+
+    rpc GrantPermissionToUser (GrantPermissionToUserRequest) returns 
(GrantPermissionToUserResponse) {}
+    rpc GrantPermissionToGroup (GrantPermissionToGroupRequest) returns 
(GrantPermissionToGroupResponse) {}
+
+    rpc GrantPermissionToUserOnAll (GrantPermissionToUserOnAllRequest)
+        returns (GrantPermissionToUserOnAllResponse);
+    rpc GrantPermissionToGroupOnAll (GrantPermissionToGroupOnAllRequest)
+        returns (GrantPermissionToGroupOnAllResponse);
+}
+message GrantPermissionToUserRequest {
+    UserInfo user_info = 1;
+    UserInfo target_user = 2;
+    string data_product_id = 3;
+    Permission permission = 4;
 }
+message GrantPermissionToUserResponse {}
+
+message GrantPermissionToGroupRequest {
+    UserInfo user_info = 1;
+    GroupInfo target_group = 2;
+    string data_product_id = 3;
+    Permission permission = 4;
+}
+message GrantPermissionToGroupResponse {}
+
+message GrantPermissionToUserOnAllRequest {
+    UserInfo user_info = 1;
+    UserInfo target_user = 2;
+    Permission permission = 3;
+}
+
+message GrantPermissionToUserOnAllResponse {}
+
+message GrantPermissionToGroupOnAllRequest {
+    UserInfo user_info = 1;
+    GroupInfo target_group = 2;
+    Permission permission = 3;
+}
+
+message GrantPermissionToGroupOnAllResponse {}
\ No newline at end of file


Reply via email to