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