From 3ccc9d5507543fb8f33ad23a6e402500d2e42a9b Mon Sep 17 00:00:00 2001 From: Rory Date: Tue, 10 Sep 2024 19:19:43 +0800 Subject: [PATCH] Supports to list roles by object --- .../gravitino/client/GravitinoClient.java | 14 ++ .../gravitino/client/GravitinoMetalake.java | 23 +++ .../org/apache/gravitino/client/TestRole.java | 34 ++++ .../test/authorization/AccessControlIT.java | 6 + .../gravitino/SupportsRelationOperations.java | 21 ++- .../AccessControlDispatcher.java | 14 ++ .../authorization/AccessControlManager.java | 10 +- .../authorization/FutureGrantManager.java | 2 +- .../gravitino/authorization/RoleManager.java | 33 ++++ .../hook/AccessControlHookDispatcher.java | 8 + .../storage/relational/JDBCBackend.java | 13 +- .../relational/RelationalEntityStore.java | 6 +- .../relational/service/RoleMetaService.java | 121 ++++++++------- .../TestAccessControlManager.java | 29 ++++ .../service/TestRoleMetaService.java | 2 +- .../server/web/rest/ObjectRoleOperations.java | 85 ++++++++++ .../web/rest/TestObjectRoleOperations.java | 146 ++++++++++++++++++ .../server/web/rest/TestRoleOperations.java | 34 ++-- 18 files changed, 514 insertions(+), 87 deletions(-) create mode 100644 server/src/main/java/org/apache/gravitino/server/web/rest/ObjectRoleOperations.java create mode 100644 server/src/test/java/org/apache/gravitino/server/web/rest/TestObjectRoleOperations.java diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java index a7656fd02de..ef50159579a 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoClient.java @@ -369,6 +369,20 @@ public String[] listRoleNames() throws NoSuchMetalakeException { return getMetalake().listRoleNames(); } + /** + * Lists the role names associated with a metadata object. + * + * @param object The object associated with the role. + * @return The role name list. + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + * @throws NoSuchMetadataObjectException If the Metadata object with the given name does not + * exist. + */ + public String[] listRoleNamesByObject(MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException { + return getMetalake().listRoleNamesByObject(object); + } + /** * Creates a new builder for constructing a GravitinoClient. * diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java index 58973b4cf63..8ba9aef5445 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoMetalake.java @@ -719,6 +719,29 @@ public String[] listRoleNames() { return resp.getNames(); } + /** + * Lists the role names associated with a metadata object. + * + * @param object The object associated with the role. + * @return The role name list. + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + * @throws NoSuchMetadataObjectException If the Metadata object with the given name does not + * exist. + */ + public String[] listRoleNamesByObject(MetadataObject object) { + NameListResponse resp = + restClient.get( + String.format( + "api/metalakes/%s/objects/%s/%s/roles", + this.name(), object.type(), object.fullName()), + NameListResponse.class, + Collections.emptyMap(), + ErrorHandlers.roleErrorHandler()); + resp.validate(); + + return resp.getNames(); + } + /** * Grant roles to a user. * diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/TestRole.java b/clients/client-java/src/test/java/org/apache/gravitino/client/TestRole.java index 3d0771fc5f0..daf8279730c 100644 --- a/clients/client-java/src/test/java/org/apache/gravitino/client/TestRole.java +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/TestRole.java @@ -27,6 +27,8 @@ import com.google.common.collect.ImmutableMap; import com.google.common.collect.Lists; import java.time.Instant; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; import org.apache.gravitino.authorization.Privileges; import org.apache.gravitino.authorization.Role; import org.apache.gravitino.authorization.SecurableObject; @@ -235,6 +237,38 @@ public void testListRoleNames() throws Exception { Assertions.assertThrows(RuntimeException.class, () -> gravitinoClient.listRoleNames()); } + @Test + public void testListRoleNamesByObject() throws Exception { + String rolePath = + withSlash( + String.format( + "api/metalakes/%s/objects/%s/%s/roles", + metalakeName, MetadataObject.Type.CATALOG.name(), "catalog")); + + NameListResponse listResponse = new NameListResponse(new String[] {"role1", "role2"}); + buildMockResource(Method.GET, rolePath, null, listResponse, SC_OK); + MetadataObject metadataObject = + MetadataObjects.of(null, "catalog", MetadataObject.Type.CATALOG); + + Assertions.assertArrayEquals( + new String[] {"role1", "role2"}, gravitinoClient.listRoleNamesByObject(metadataObject)); + + ErrorResponse errRespNoMetalake = + ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), "metalake not found"); + buildMockResource(Method.GET, rolePath, null, errRespNoMetalake, SC_NOT_FOUND); + Exception ex = + Assertions.assertThrows( + NoSuchMetalakeException.class, + () -> gravitinoClient.listRoleNamesByObject(metadataObject)); + Assertions.assertEquals("metalake not found", ex.getMessage()); + + // Test RuntimeException + ErrorResponse errResp = ErrorResponse.internalError("internal error"); + buildMockResource(Method.GET, rolePath, null, errResp, SC_SERVER_ERROR); + Assertions.assertThrows( + RuntimeException.class, () -> gravitinoClient.listRoleNamesByObject(metadataObject)); + } + private RoleDTO mockRoleDTO(String name) { SecurableObject securableObject = SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java index 965c31fdf45..86ec1fb7d08 100644 --- a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java @@ -184,6 +184,12 @@ void testManageRoles() { String[] roleNames = metalake.listRoleNames(); Arrays.sort(roleNames); + Assertions.assertEquals( + Lists.newArrayList(anotherRoleName, roleName), Arrays.asList(roleNames)); + + // List roles by the object + roleNames = metalake.listRoleNamesByObject(metalakeObject); + Arrays.sort(roleNames); Assertions.assertEquals( Lists.newArrayList(anotherRoleName, roleName), Arrays.asList(roleNames)); diff --git a/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java b/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java index 617f72ab95d..584445588c4 100644 --- a/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java +++ b/core/src/main/java/org/apache/gravitino/SupportsRelationOperations.java @@ -48,8 +48,27 @@ enum Type { * @return The list of entities * @throws IOException When occurs storage issues, it will throw IOException. */ + default List listEntitiesByRelation( + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType) throws IOException { + return listEntitiesByRelation(relType, nameIdentifier, identType, true /* allFields*/); + } + + /** + * List the entities according to a give entity in a specific relation. + * + * @param relType The type of relation. + * @param nameIdentifier The given entity identifier + * @param identType The given entity type. + * @param allFields Some fields may have a relatively high acquisition cost, EntityStore provide + * an optional setting to avoid fetching these high-cost fields to improve the performance. If + * true, the method will fetch all the fields, Otherwise, the method will fetch all the fields + * except for high-cost fields. + * @return The list of entities + * @throws IOException When occurs storage issues, it will throw IOException. + */ List listEntitiesByRelation( - Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType) throws IOException; + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType, boolean allFields) + throws IOException; /** * insert a relation between two entities diff --git a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java index 95cb304de26..3214c187fc4 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java @@ -20,8 +20,10 @@ import java.util.List; import java.util.Map; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; @@ -246,4 +248,16 @@ Role createRole( * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. */ String[] listRoleNames(String metalake) throws NoSuchMetalakeException; + + /** + * Lists the role names associated the metadata object. + * + * @param metalake The Metalake of the Role. + * @return The role list. + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + * @throws NoSuchMetadataObjectException If the Metadata object with the given name does not + * exist. + */ + String[] listRoleNamesByObject(String metalake, MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException; } diff --git a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java index 8872afade70..c2f2976aa3c 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java @@ -18,14 +18,15 @@ */ package org.apache.gravitino.authorization; -import com.google.common.annotations.VisibleForTesting; import java.util.List; import java.util.Map; import org.apache.gravitino.Config; import org.apache.gravitino.Configs; import org.apache.gravitino.EntityStore; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; @@ -148,8 +149,9 @@ public String[] listRoleNames(String metalake) throws NoSuchMetalakeException { return roleManager.listRoleNames(metalake); } - @VisibleForTesting - RoleManager getRoleManager() { - return roleManager; + @Override + public String[] listRoleNamesByObject(String metalake, MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException { + return roleManager.listRoleNamesByObject(metalake, object); } } diff --git a/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java b/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java index c24817ea5eb..b838e195686 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/FutureGrantManager.java @@ -20,6 +20,7 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import com.google.common.collect.Sets; import java.io.IOException; import java.util.List; import java.util.Map; @@ -37,7 +38,6 @@ import org.apache.gravitino.meta.GroupEntity; import org.apache.gravitino.meta.RoleEntity; import org.apache.gravitino.meta.UserEntity; -import org.glassfish.jersey.internal.guava.Sets; /** * FutureGrantManager is responsible for granting privileges to future object. When you grant a diff --git a/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java b/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java index 8b195894f4a..dc675fdcef5 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/RoleManager.java @@ -27,15 +27,19 @@ import org.apache.gravitino.Entity; import org.apache.gravitino.EntityAlreadyExistsException; import org.apache.gravitino.EntityStore; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.Namespace; +import org.apache.gravitino.SupportsRelationOperations; import org.apache.gravitino.exceptions.NoSuchEntityException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.RoleAlreadyExistsException; import org.apache.gravitino.meta.AuditInfo; import org.apache.gravitino.meta.RoleEntity; import org.apache.gravitino.storage.IdGenerator; +import org.apache.gravitino.utils.MetadataObjectUtil; import org.apache.gravitino.utils.PrincipalUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -148,6 +152,35 @@ String[] listRoleNames(String metalake) { } } + String[] listRoleNamesByObject(String metalake, MetadataObject object) { + try { + AuthorizationUtils.checkMetalakeExists(metalake); + + return store.relationOperations() + .listEntitiesByRelation( + SupportsRelationOperations.Type.METADATA_OBJECT_ROLE_REL, + MetadataObjectUtil.toEntityIdent(metalake, object), + MetadataObjectUtil.toEntityType(object), + false /* allFields */) + .stream() + .map(entity -> ((RoleEntity) entity).name()) + .toArray(String[]::new); + + } catch (NoSuchEntityException nse) { + LOG.error("Metadata object {} (type {}) doesn't exist", object.fullName(), object.type()); + throw new NoSuchMetadataObjectException( + "Metadata object %s (type %s) doesn't exist", object.fullName(), object.type()); + } catch (IOException ioe) { + LOG.error( + "Listing roles under metalake {} by object full name {} and type {} failed due to storage issues", + metalake, + object.fullName(), + object.type(), + ioe); + throw new RuntimeException(ioe); + } + } + private RoleEntity getRoleEntity(NameIdentifier identifier) { try { return store.get(identifier, Entity.EntityType.ROLE, RoleEntity.class); diff --git a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java index 7882e9c8a5e..65ed2c9da09 100644 --- a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java +++ b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java @@ -22,6 +22,7 @@ import java.util.Map; import org.apache.gravitino.Entity; import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.MetadataObject; import org.apache.gravitino.authorization.AccessControlDispatcher; import org.apache.gravitino.authorization.AuthorizationUtils; import org.apache.gravitino.authorization.Group; @@ -32,6 +33,7 @@ import org.apache.gravitino.authorization.User; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; +import org.apache.gravitino.exceptions.NoSuchMetadataObjectException; import org.apache.gravitino.exceptions.NoSuchMetalakeException; import org.apache.gravitino.exceptions.NoSuchRoleException; import org.apache.gravitino.exceptions.NoSuchUserException; @@ -162,4 +164,10 @@ public boolean deleteRole(String metalake, String role) throws NoSuchMetalakeExc public String[] listRoleNames(String metalake) throws NoSuchMetalakeException { return dispatcher.listRoleNames(metalake); } + + @Override + public String[] listRoleNamesByObject(String metalake, MetadataObject object) + throws NoSuchMetalakeException, NoSuchMetadataObjectException { + return dispatcher.listRoleNamesByObject(metalake, object); + } } diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java index 2b9a6d0e4ed..a77b824af9a 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java @@ -369,9 +369,7 @@ public List associateTagsWithMetadataObject( @Override public List listEntitiesByRelation( - SupportsRelationOperations.Type relType, - NameIdentifier nameIdentifier, - Entity.EntityType identType) { + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType, boolean allFields) { switch (relType) { case OWNER_REL: List list = Lists.newArrayList(); @@ -382,20 +380,23 @@ public List listEntitiesByRelation( case METADATA_OBJECT_ROLE_REL: return (List) RoleMetaService.getInstance() - .listRolesByMetadataObjectIdentAndType(nameIdentifier, identType); + .listRolesByMetadataObjectIdentAndType(nameIdentifier, identType, allFields); case ROLE_GROUP_REL: if (identType == Entity.EntityType.ROLE) { return (List) GroupMetaService.getInstance().listGroupsByRoleIdent(nameIdentifier); } else { throw new IllegalArgumentException( - String.format("ROLE_GROUP_REL doesn't support type %s", identType.name())); + String.format( + "ROLE_GROUP_REL doesn't support type %s or loading all fields", + identType.name())); } case ROLE_USER_REL: if (identType == Entity.EntityType.ROLE) { return (List) UserMetaService.getInstance().listUsersByRoleIdent(nameIdentifier); } else { throw new IllegalArgumentException( - String.format("ROLE_USER_REL doesn't support type %s", identType.name())); + String.format( + "ROLE_USER_REL doesn't support type %s or loading all fields", identType.name())); } default: throw new IllegalArgumentException( diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java index c95db1a0710..a337e7a785e 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java @@ -188,11 +188,9 @@ public List associateTagsWithMetadataObject( @Override public List listEntitiesByRelation( - SupportsRelationOperations.Type relType, - NameIdentifier nameIdentifier, - Entity.EntityType identType) + Type relType, NameIdentifier nameIdentifier, Entity.EntityType identType, boolean allFields) throws IOException { - return backend.listEntitiesByRelation(relType, nameIdentifier, identType); + return backend.listEntitiesByRelation(relType, nameIdentifier, identType, allFields); } @Override diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java index 1e914f59ad4..6c03e89917f 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/service/RoleMetaService.java @@ -57,21 +57,6 @@ public static RoleMetaService getInstance() { private RoleMetaService() {} - private RolePO getRolePOByMetalakeIdAndName(Long metalakeId, String roleName) { - RolePO rolePO = - SessionUtils.getWithoutCommit( - RoleMetaMapper.class, - mapper -> mapper.selectRoleMetaByMetalakeIdAndName(metalakeId, roleName)); - - if (rolePO == null) { - throw new NoSuchEntityException( - NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE, - Entity.EntityType.ROLE.name().toLowerCase(), - roleName); - } - return rolePO; - } - public Long getRoleIdByMetalakeIdAndName(Long metalakeId, String roleName) { Long roleId = SessionUtils.getWithoutCommit( @@ -93,7 +78,7 @@ public List listRolesByUserId(Long userId) { } public List listRolesByMetadataObjectIdentAndType( - NameIdentifier metadataObjectIdent, Entity.EntityType metadataObjectType) { + NameIdentifier metadataObjectIdent, Entity.EntityType metadataObjectType, boolean allFields) { String metalake = NameIdentifierUtil.getMetalake(metadataObjectIdent); long metalakeId = MetalakeMetaService.getInstance().getMetalakeIdByName(metalake); MetadataObject metadataObject = @@ -101,41 +86,33 @@ public List listRolesByMetadataObjectIdentAndType( long metadataObjectId = MetadataObjectService.getMetadataObjectId( metalakeId, metadataObject.fullName(), metadataObject.type()); - List rolePOs = - SessionUtils.getWithoutCommit( - RoleMetaMapper.class, - mapper -> - mapper.listRolesByMetadataObjectIdAndType( - metadataObjectId, metadataObject.type().name())); - return rolePOs.stream() - .map( - po -> - POConverters.fromRolePO( - po, listSecurableObjects(po), AuthorizationUtils.ofRoleNamespace(metalake))) - .collect(Collectors.toList()); - } - - private List listSecurableObjects(RolePO po) { - List securableObjectPOs = listSecurableObjectsByRoleId(po.getRoleId()); - List securableObjects = Lists.newArrayList(); - - for (SecurableObjectPO securableObjectPO : securableObjectPOs) { - String fullName = - MetadataObjectService.getMetadataObjectFullName( - securableObjectPO.getType(), securableObjectPO.getMetadataObjectId()); - if (fullName != null) { - securableObjects.add( - POConverters.fromSecurableObjectPO( - fullName, securableObjectPO, getType(securableObjectPO.getType()))); - } else { - LOG.info( - "The securable object {} {} may be deleted", - securableObjectPO.getMetadataObjectId(), - securableObjectPO.getType()); - } + if (allFields) { + List rolePOs = + SessionUtils.getWithoutCommit( + RoleMetaMapper.class, + mapper -> + mapper.listRolesByMetadataObjectIdAndType( + metadataObjectId, metadataObject.type().name())); + return rolePOs.stream() + .map( + po -> + POConverters.fromRolePO( + po, listSecurableObjects(po), AuthorizationUtils.ofRoleNamespace(metalake))) + .collect(Collectors.toList()); + } else { + List rolePOs = + SessionUtils.getWithoutCommit( + RoleMetaMapper.class, + mapper -> + mapper.listRolesByMetadataObjectIdAndType( + metadataObjectId, metadataObject.type().name())); + return rolePOs.stream() + .map( + po -> + POConverters.fromRolePO( + po, Collections.emptyList(), AuthorizationUtils.ofRoleNamespace(metalake))) + .collect(Collectors.toList()); } - - return securableObjects; } public List listRolesByGroupId(Long groupId) { @@ -234,7 +211,7 @@ public boolean deleteRole(NameIdentifier identifier) { return true; } - private List listSecurableObjectsByRoleId(Long roleId) { + private static List listSecurableObjectsByRoleId(Long roleId) { return SessionUtils.getWithoutCommit( SecurableObjectMapper.class, mapper -> mapper.listSecurableObjectsByRoleId(roleId)); } @@ -291,11 +268,49 @@ public int deleteRoleMetasByLegacyTimeline(long legacyTimeline, int limit) { + securableObjectsCount[0]; } - private MetadataObject.Type getType(String type) { + private static List listSecurableObjects(RolePO po) { + List securableObjectPOs = listSecurableObjectsByRoleId(po.getRoleId()); + List securableObjects = Lists.newArrayList(); + + for (SecurableObjectPO securableObjectPO : securableObjectPOs) { + String fullName = + MetadataObjectService.getMetadataObjectFullName( + securableObjectPO.getType(), securableObjectPO.getMetadataObjectId()); + if (fullName != null) { + securableObjects.add( + POConverters.fromSecurableObjectPO( + fullName, securableObjectPO, getType(securableObjectPO.getType()))); + } else { + LOG.info( + "The securable object {} {} may be deleted", + securableObjectPO.getMetadataObjectId(), + securableObjectPO.getType()); + } + } + + return securableObjects; + } + + private static RolePO getRolePOByMetalakeIdAndName(Long metalakeId, String roleName) { + RolePO rolePO = + SessionUtils.getWithoutCommit( + RoleMetaMapper.class, + mapper -> mapper.selectRoleMetaByMetalakeIdAndName(metalakeId, roleName)); + + if (rolePO == null) { + throw new NoSuchEntityException( + NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE, + Entity.EntityType.ROLE.name().toLowerCase(), + roleName); + } + return rolePO; + } + + private static MetadataObject.Type getType(String type) { return MetadataObject.Type.valueOf(type); } - private String getEntityType(SecurableObject securableObject) { + private static String getEntityType(SecurableObject securableObject) { return securableObject.type().name(); } } diff --git a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java index 6dfaf54fecf..b299c15ef97 100644 --- a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java +++ b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java @@ -115,6 +115,7 @@ public class TestAccessControlManager { public static void setUp() throws Exception { File dbDir = new File(DB_DIR); dbDir.mkdirs(); + Mockito.when(config.get(SERVICE_ADMINS)).thenReturn(Lists.newArrayList("admin1", "admin2")); Mockito.when(config.get(ENTITY_STORE)).thenReturn(RELATIONAL_ENTITY_STORE); Mockito.when(config.get(ENTITY_RELATIONAL_STORE)).thenReturn(DEFAULT_ENTITY_RELATIONAL_STORE); @@ -125,10 +126,12 @@ public static void setUp() throws Exception { Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 1000L); Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L); Mockito.when(config.get(CATALOG_CACHE_EVICTION_INTERVAL_MS)).thenReturn(1000L); + Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY); Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY); Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL); FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new LockManager(config), true); + entityStore = EntityStoreFactory.createEntityStore(config); entityStore.initialize(config); @@ -146,6 +149,7 @@ public static void setUp() throws Exception { AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build()) .build(); entityStore.put(catalogEntity, true); + CatalogEntity anotherCatalogEntity = CatalogEntity.builder() .withId(4L) @@ -421,6 +425,31 @@ public void testListRoles() { String[] actualRoles = accessControlManager.listRoleNames("metalake_list"); Arrays.sort(actualRoles); Assertions.assertArrayEquals(new String[] {"testList1", "testList2"}, actualRoles); + + accessControlManager.deleteRole("metalake_list", "testList1"); + accessControlManager.deleteRole("metalake_list", "testList2"); + } + + @Test + public void testListRolesByObject() { + Map props = ImmutableMap.of("k1", "v1"); + SecurableObject catalogObject = + SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); + + accessControlManager.createRole( + "metalake_list", "testList1", props, Lists.newArrayList(catalogObject)); + + accessControlManager.createRole( + "metalake_list", "testList2", props, Lists.newArrayList(catalogObject)); + + // Test to list roles + String[] listedRoles = + accessControlManager.listRoleNamesByObject("metalake_list", catalogObject); + Arrays.sort(listedRoles); + Assertions.assertArrayEquals(new String[] {"testList1", "testList2"}, listedRoles); + + accessControlManager.deleteRole("metalake_list", "testList1"); + accessControlManager.deleteRole("metalake_list", "testList2"); } private void testProperties(Map expectedProps, Map testProps) { diff --git a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java index 4a781f01861..1f818b11253 100644 --- a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java +++ b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestRoleMetaService.java @@ -441,7 +441,7 @@ void listRolesBySecurableObject() throws IOException { List roleEntities = roleMetaService.listRolesByMetadataObjectIdentAndType( - catalog.nameIdentifier(), catalog.type()); + catalog.nameIdentifier(), catalog.type(), true); roleEntities.sort(Comparator.comparing(RoleEntity::name)); Assertions.assertEquals(Lists.newArrayList(role1, role2), roleEntities); } diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/ObjectRoleOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/ObjectRoleOperations.java new file mode 100644 index 00000000000..395fc5eb44d --- /dev/null +++ b/server/src/main/java/org/apache/gravitino/server/web/rest/ObjectRoleOperations.java @@ -0,0 +1,85 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.server.web.rest; + +import com.codahale.metrics.annotation.ResponseMetered; +import com.codahale.metrics.annotation.Timed; +import java.util.Locale; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.Response; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.MetadataObject; +import org.apache.gravitino.MetadataObjects; +import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.authorization.AccessControlDispatcher; +import org.apache.gravitino.dto.responses.NameListResponse; +import org.apache.gravitino.lock.LockType; +import org.apache.gravitino.lock.TreeLockUtils; +import org.apache.gravitino.metrics.MetricNames; +import org.apache.gravitino.server.authorization.NameBindings; +import org.apache.gravitino.server.web.Utils; + +@NameBindings.AccessControlInterfaces +@Path("/metalakes/{metalake}/objects/{type}/{fullName}/roles") +public class ObjectRoleOperations { + + private final AccessControlDispatcher accessControlManager; + + @Context private HttpServletRequest httpRequest; + + public ObjectRoleOperations() { + // Because accessControlManager may be null when Gravitino doesn't enable authorization, + // and Jersey injection doesn't support null value. So ObjectRoleOperations chooses to retrieve + // accessControlManager from GravitinoEnv instead of injection here. + this.accessControlManager = GravitinoEnv.getInstance().accessControlDispatcher(); + } + + @GET + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "list-role-by-object." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "list-role-by-object", absolute = true) + public Response listRoles( + @PathParam("metalake") String metalake, + @PathParam("type") String type, + @PathParam("fullName") String fullName) { + try { + MetadataObject object = + MetadataObjects.parse( + fullName, MetadataObject.Type.valueOf(type.toUpperCase(Locale.ROOT))); + + return Utils.doAs( + httpRequest, + () -> + TreeLockUtils.doWithTreeLock( + NameIdentifier.of(metalake), + LockType.READ, + () -> { + String[] names = accessControlManager.listRoleNamesByObject(metalake, object); + return Utils.ok(new NameListResponse(names)); + })); + } catch (Exception e) { + return ExceptionHandlers.handleRoleException(OperationType.LIST, "", metalake, e); + } + } +} diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestObjectRoleOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestObjectRoleOperations.java new file mode 100644 index 00000000000..92fb4ad64e7 --- /dev/null +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestObjectRoleOperations.java @@ -0,0 +1,146 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.gravitino.server.web.rest; + +import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL; +import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY; +import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import java.io.IOException; +import javax.servlet.http.HttpServletRequest; +import javax.ws.rs.core.Application; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.Response; +import org.apache.commons.lang3.reflect.FieldUtils; +import org.apache.gravitino.Config; +import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.authorization.AccessControlManager; +import org.apache.gravitino.dto.responses.ErrorConstants; +import org.apache.gravitino.dto.responses.ErrorResponse; +import org.apache.gravitino.dto.responses.NameListResponse; +import org.apache.gravitino.exceptions.NoSuchMetalakeException; +import org.apache.gravitino.lock.LockManager; +import org.apache.gravitino.rest.RESTUtils; +import org.glassfish.hk2.utilities.binding.AbstractBinder; +import org.glassfish.jersey.server.ResourceConfig; +import org.glassfish.jersey.test.JerseyTest; +import org.glassfish.jersey.test.TestProperties; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.Mockito; + +public class TestObjectRoleOperations extends JerseyTest { + + private static final AccessControlManager manager = mock(AccessControlManager.class); + + private static class MockServletRequestFactory extends ServletRequestFactoryBase { + @Override + public HttpServletRequest get() { + HttpServletRequest request = mock(HttpServletRequest.class); + when(request.getRemoteUser()).thenReturn(null); + return request; + } + } + + @BeforeAll + public static void setup() throws IllegalAccessException { + Config config = mock(Config.class); + Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY); + Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY); + Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL); + FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new LockManager(config), true); + FieldUtils.writeField(GravitinoEnv.getInstance(), "accessControlDispatcher", manager, true); + } + + @Override + protected Application configure() { + try { + forceSet( + TestProperties.CONTAINER_PORT, String.valueOf(RESTUtils.findAvailablePort(2000, 3000))); + } catch (IOException e) { + throw new RuntimeException(e); + } + + ResourceConfig resourceConfig = new ResourceConfig(); + resourceConfig.register(ObjectRoleOperations.class); + resourceConfig.register( + new AbstractBinder() { + @Override + protected void configure() { + bindFactory(MockServletRequestFactory.class).to(HttpServletRequest.class); + } + }); + + return resourceConfig; + } + + @Test + public void testListRoleNames() { + when(manager.listRoleNamesByObject(any(), any())).thenReturn(new String[] {"role"}); + + Response resp = + target("/metalakes/metalake1/objects/metalake/metalake1/roles/") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + + NameListResponse listResponse = resp.readEntity(NameListResponse.class); + Assertions.assertEquals(0, listResponse.getCode()); + + Assertions.assertEquals(1, listResponse.getNames().length); + Assertions.assertEquals("role", listResponse.getNames()[0]); + + // Test to throw NoSuchMetalakeException + doThrow(new NoSuchMetalakeException("mock error")) + .when(manager) + .listRoleNamesByObject(any(), any()); + Response resp1 = + target("/metalakes/metalake1/objects/metalake/metalake1/roles/") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); + + ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.NOT_FOUND_CODE, errorResponse.getCode()); + Assertions.assertEquals(NoSuchMetalakeException.class.getSimpleName(), errorResponse.getType()); + + // Test to throw internal RuntimeException + doThrow(new RuntimeException("mock error")).when(manager).listRoleNamesByObject(any(), any()); + Response resp3 = + target("/metalakes/metalake1/objects/metalake/metalake1/roles") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + + Assertions.assertEquals( + Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + + ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class); + Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); + Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); + } +} diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java index eb365d1ac69..a2f0c4847d6 100644 --- a/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestRoleOperations.java @@ -334,23 +334,6 @@ public void testGetRole() { Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); } - private Role buildRole(String role) { - SecurableObject catalog = - SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); - SecurableObject anotherSecurableObject = - SecurableObjects.ofCatalog( - "another_catalog", Lists.newArrayList(Privileges.CreateSchema.deny())); - - return RoleEntity.builder() - .withId(1L) - .withName(role) - .withProperties(Collections.emptyMap()) - .withSecurableObjects(Lists.newArrayList(catalog, anotherSecurableObject)) - .withAuditInfo( - AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) - .build(); - } - @Test public void testDeleteRole() { when(manager.deleteRole(any(), any())).thenReturn(true); @@ -502,4 +485,21 @@ public void testListRoleNames() { Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); } + + private Role buildRole(String role) { + SecurableObject catalog = + SecurableObjects.ofCatalog("catalog", Lists.newArrayList(Privileges.UseCatalog.allow())); + SecurableObject anotherSecurableObject = + SecurableObjects.ofCatalog( + "another_catalog", Lists.newArrayList(Privileges.CreateSchema.deny())); + + return RoleEntity.builder() + .withId(1L) + .withName(role) + .withProperties(Collections.emptyMap()) + .withSecurableObjects(Lists.newArrayList(catalog, anotherSecurableObject)) + .withAuditInfo( + AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) + .build(); + } }