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..a8a46ff8f47 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 @@ -227,6 +227,26 @@ public Group getGroup(String group) throws NoSuchGroupException, NoSuchMetalakeE return getMetalake().getGroup(group); } + /** + * List the groups. + * + * @return The Group list + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + */ + public Group[] listGroups() throws NoSuchMetalakeException { + return getMetalake().listGroups(); + } + + /** + * List the group names. + * + * @return The group names list. + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + */ + public String[] listGroupNames() throws NoSuchMetalakeException { + return getMetalake().listGroupNames(); + } + /** * Gets a Role. * 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..3354bf2a6c6 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 @@ -60,6 +60,7 @@ import org.apache.gravitino.dto.responses.DropResponse; import org.apache.gravitino.dto.responses.EntityListResponse; import org.apache.gravitino.dto.responses.ErrorResponse; +import org.apache.gravitino.dto.responses.GroupListResponse; import org.apache.gravitino.dto.responses.GroupResponse; import org.apache.gravitino.dto.responses.NameListResponse; import org.apache.gravitino.dto.responses.OwnerResponse; @@ -623,6 +624,44 @@ public Group getGroup(String group) throws NoSuchGroupException, NoSuchMetalakeE return resp.getGroup(); } + /** + * Lists the groups + * + * @return The Group list + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + */ + public Group[] listGroups() throws NoSuchMetalakeException { + Map params = new HashMap<>(); + params.put("details", "true"); + + GroupListResponse resp = + restClient.get( + String.format(API_METALAKES_GROUPS_PATH, name(), BLANK_PLACEHOLDER), + params, + GroupListResponse.class, + Collections.emptyMap(), + ErrorHandlers.groupErrorHandler()); + resp.validate(); + return resp.getGroups(); + } + + /** + * Lists the group names + * + * @return The Group Name List + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + */ + public String[] listGroupNames() throws NoSuchMetalakeException { + NameListResponse resp = + restClient.get( + String.format(API_METALAKES_GROUPS_PATH, name(), BLANK_PLACEHOLDER), + NameListResponse.class, + Collections.emptyMap(), + ErrorHandlers.groupErrorHandler()); + resp.validate(); + return resp.getNames(); + } + /** * Gets a Role. * diff --git a/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java b/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java index 67a3035ed8b..ff98b2ca6c7 100644 --- a/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/TestUserGroup.java @@ -23,8 +23,10 @@ import static javax.servlet.http.HttpServletResponse.SC_OK; import static org.apache.hc.core5.http.HttpStatus.SC_SERVER_ERROR; +import com.fasterxml.jackson.core.JsonProcessingException; import java.time.Instant; import java.util.Collections; +import java.util.HashMap; import java.util.Map; import org.apache.gravitino.authorization.Group; import org.apache.gravitino.authorization.User; @@ -35,6 +37,7 @@ import org.apache.gravitino.dto.requests.GroupAddRequest; import org.apache.gravitino.dto.requests.UserAddRequest; import org.apache.gravitino.dto.responses.ErrorResponse; +import org.apache.gravitino.dto.responses.GroupListResponse; import org.apache.gravitino.dto.responses.GroupResponse; import org.apache.gravitino.dto.responses.MetalakeResponse; import org.apache.gravitino.dto.responses.NameListResponse; @@ -327,6 +330,55 @@ public void testRemoveGroups() throws Exception { Assertions.assertThrows(RuntimeException.class, () -> gravitinoClient.removeGroup(groupName)); } + @Test + public void testListGroupNames() throws JsonProcessingException { + String groupPath = withSlash(String.format(API_METALAKES_GROUPS_PATH, metalakeName, "")); + NameListResponse listResponse = new NameListResponse(new String[] {"group1", "group2"}); + buildMockResource(Method.GET, groupPath, null, listResponse, SC_OK); + Assertions.assertArrayEquals( + new String[] {"group1", "group2"}, gravitinoClient.listGroupNames()); + ErrorResponse errRespNoMetaLake = + ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), "metalake not found"); + buildMockResource(Method.GET, groupPath, null, errRespNoMetaLake, SC_NOT_FOUND); + Exception ex = + Assertions.assertThrows( + NoSuchMetalakeException.class, () -> gravitinoClient.listGroupNames()); + Assertions.assertEquals("metalake not found", ex.getMessage()); + + // Test RuntimeException + ErrorResponse errResp = ErrorResponse.internalError("internal error"); + buildMockResource(Method.GET, groupPath, null, errResp, SC_SERVER_ERROR); + + Assertions.assertThrows(RuntimeException.class, () -> gravitinoClient.listGroupNames()); + } + + @Test + public void testListGroups() throws JsonProcessingException { + String groupPath = withSlash(String.format(API_METALAKES_GROUPS_PATH, metalakeName, "")); + GroupDTO group1 = mockGroupDTO("group1"); + GroupDTO group2 = mockGroupDTO("group2"); + GroupDTO group3 = mockGroupDTO("group3"); + Map params = new HashMap<>(); + GroupListResponse listResponse = new GroupListResponse(new GroupDTO[] {group1, group2, group3}); + buildMockResource(Method.GET, groupPath, params, null, listResponse, SC_OK); + + Group[] groups = gravitinoClient.listGroups(); + Assertions.assertEquals(3, groups.length); + assertGroup(group1, groups[0]); + assertGroup(group2, groups[1]); + assertGroup(group3, groups[2]); + ErrorResponse errResNoMetaLake = + ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), "metalake not found"); + buildMockResource(Method.GET, groupPath, params, null, errResNoMetaLake, SC_NOT_FOUND); + Exception ex = + Assertions.assertThrows(NoSuchMetalakeException.class, () -> gravitinoClient.listGroups()); + Assertions.assertEquals("metalake not found", ex.getMessage()); + // Test RuntimeException + ErrorResponse errResp = ErrorResponse.internalError("internal error"); + buildMockResource(Method.GET, groupPath, params, null, errResp, SC_SERVER_ERROR); + Assertions.assertThrows(RuntimeException.class, () -> gravitinoClient.listGroups()); + } + private UserDTO mockUserDTO(String name) { return UserDTO.builder() .withName(name) 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..742eae25981 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 @@ -133,8 +133,38 @@ void testManageGroups() { // Get a not-existed group Assertions.assertThrows(NoSuchGroupException.class, () -> metalake.getGroup("not-existed")); + Map properties = Maps.newHashMap(); + properties.put("k1", "v1"); + SecurableObject metalakeObject = + SecurableObjects.ofMetalake( + metalakeName, Lists.newArrayList(Privileges.CreateCatalog.allow())); + + // Test the group with the role + metalake.createRole("role2", properties, Lists.newArrayList(metalakeObject)); + metalake.grantRolesToGroup(Lists.newArrayList("role2"), groupName); + + // List groups + String anotherGroup = "group2#456"; + metalake.addGroup(anotherGroup); + String[] groupNames = metalake.listGroupNames(); + Arrays.sort(groupNames); + Assertions.assertEquals(Lists.newArrayList(groupName, anotherGroup), Arrays.asList(groupNames)); + + List groups = + Arrays.stream(metalake.listGroups()) + .sorted(Comparator.comparing(Group::name)) + .collect(Collectors.toList()); + Assertions.assertEquals( + Lists.newArrayList(groupName, anotherGroup), + groups.stream().map(Group::name).collect(Collectors.toList())); + Assertions.assertEquals(Lists.newArrayList("role2"), groups.get(0).roles()); + Assertions.assertTrue(metalake.removeGroup(groupName)); Assertions.assertFalse(metalake.removeGroup(groupName)); + + // clean up + metalake.removeGroup(anotherGroup); + metalake.deleteRole("role2"); } @Test diff --git a/common/src/main/java/org/apache/gravitino/dto/responses/GroupListResponse.java b/common/src/main/java/org/apache/gravitino/dto/responses/GroupListResponse.java new file mode 100644 index 00000000000..271fb9a92ba --- /dev/null +++ b/common/src/main/java/org/apache/gravitino/dto/responses/GroupListResponse.java @@ -0,0 +1,74 @@ +/* + * 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.dto.responses; + +import com.fasterxml.jackson.annotation.JsonProperty; +import com.google.common.base.Preconditions; +import java.util.Arrays; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; +import org.apache.commons.lang3.StringUtils; +import org.apache.gravitino.dto.authorization.GroupDTO; + +/** Represents a response for a list of groups. */ +@Getter +@ToString +@EqualsAndHashCode(callSuper = true) +public class GroupListResponse extends BaseResponse { + + @JsonProperty("groups") + private final GroupDTO[] groups; + + /** + * Constructor for GroupListResponse. + * + * @param groups The array of group DTOs. + */ + public GroupListResponse(GroupDTO[] groups) { + super(0); + this.groups = groups; + } + + /** Default constructor for GroupListResponse. (Used for Jackson deserialization.) */ + public GroupListResponse() { + super(); + this.groups = null; + } + + /** + * Validates the response data. + * + * @throws IllegalArgumentException if the name or audit is not set. + */ + @Override + public void validate() throws IllegalArgumentException { + super.validate(); + + Preconditions.checkArgument(groups != null, "groups must not be null"); + Arrays.stream(groups) + .forEach( + group -> { + Preconditions.checkArgument( + StringUtils.isNotBlank(group.name()), "group 'name' must not be blank"); + Preconditions.checkArgument( + group.auditInfo() != null, "group 'auditInfo' must not be null"); + }); + } +} diff --git a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java index 8e706c139e9..38224493b71 100644 --- a/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java +++ b/common/src/main/java/org/apache/gravitino/dto/util/DTOConverters.java @@ -691,6 +691,19 @@ public static UserDTO[] toDTOs(User[] users) { return Arrays.stream(users).map(DTOConverters::toDTO).toArray(UserDTO[]::new); } + /** + * Converts an array of Groups to an array of GroupDTOs. + * + * @param groups The groups to be converted. + * @return The array of GroupDTOs. + */ + public static GroupDTO[] toDTOs(Group[] groups) { + if (ArrayUtils.isEmpty(groups)) { + return new GroupDTO[0]; + } + return Arrays.stream(groups).map(DTOConverters::toDTO).toArray(GroupDTO[]::new); + } + /** * Converts a DistributionDTO to a Distribution. * 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..0dc65909a5c 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java @@ -127,6 +127,24 @@ Group addGroup(String metalake, String group) Group getGroup(String metalake, String group) throws NoSuchGroupException, NoSuchMetalakeException; + /** + * List groups + * + * @param metalake The Metalake of the Group. + * @return The list of groups + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + */ + Group[] listGroups(String metalake); + + /** + * List group names + * + * @param metalake The Metalake of the Group. + * @return The list of group names + * @throws NoSuchMetalakeException If the Metalake with the given name does not exist. + */ + String[] listGroupNames(String metalake); + /** * Grant roles to a user. * 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..067d3845613 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java @@ -94,6 +94,16 @@ public Group getGroup(String metalake, String group) return userGroupManager.getGroup(metalake, group); } + @Override + public Group[] listGroups(String metalake) throws NoSuchMetalakeException { + return userGroupManager.listGroups(metalake); + } + + @Override + public String[] listGroupNames(String metalake) throws NoSuchMetalakeException { + return userGroupManager.listGroupNames(metalake); + } + @Override public User grantRolesToUser(String metalake, List roles, String user) throws NoSuchUserException, NoSuchRoleException, NoSuchMetalakeException { diff --git a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java index be1b687f3e4..cd852ab66a7 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java @@ -24,6 +24,7 @@ import java.util.Arrays; import java.util.Collections; import org.apache.gravitino.Entity; +import org.apache.gravitino.Entity.EntityType; import org.apache.gravitino.EntityAlreadyExistsException; import org.apache.gravitino.EntityStore; import org.apache.gravitino.Namespace; @@ -124,23 +125,6 @@ User[] listUsers(String metalake) { return listUsersInternal(metalake, true /* allFields */); } - private User[] listUsersInternal(String metalake, boolean allFields) { - try { - AuthorizationUtils.checkMetalakeExists(metalake); - - Namespace namespace = AuthorizationUtils.ofUserNamespace(metalake); - return store - .list(namespace, UserEntity.class, Entity.EntityType.USER, allFields) - .toArray(new User[0]); - } catch (NoSuchEntityException e) { - LOG.error("Metalake {} does not exist", metalake, e); - throw new NoSuchMetalakeException(METALAKE_DOES_NOT_EXIST_MSG, metalake); - } catch (IOException ioe) { - LOG.error("Listing user under metalake {} failed due to storage issues", metalake, ioe); - throw new RuntimeException(ioe); - } - } - Group addGroup(String metalake, String group) throws GroupAlreadyExistsException { try { AuthorizationUtils.checkMetalakeExists(metalake); @@ -197,4 +181,47 @@ Group getGroup(String metalake, String group) { throw new RuntimeException(ioe); } } + + Group[] listGroups(String metalake) { + return listGroupInternal(metalake, true); + } + + String[] listGroupNames(String metalake) { + return Arrays.stream(listGroupInternal(metalake, false)) + .map(Group::name) + .toArray(String[]::new); + } + + private User[] listUsersInternal(String metalake, boolean allFields) { + try { + AuthorizationUtils.checkMetalakeExists(metalake); + + Namespace namespace = AuthorizationUtils.ofUserNamespace(metalake); + return store + .list(namespace, UserEntity.class, Entity.EntityType.USER, allFields) + .toArray(new User[0]); + } catch (NoSuchEntityException e) { + LOG.error("Metalake {} does not exist", metalake, e); + throw new NoSuchMetalakeException(METALAKE_DOES_NOT_EXIST_MSG, metalake); + } catch (IOException ioe) { + LOG.error("Listing user under metalake {} failed due to storage issues", metalake, ioe); + throw new RuntimeException(ioe); + } + } + + private Group[] listGroupInternal(String metalake, boolean allFields) { + try { + AuthorizationUtils.checkMetalakeExists(metalake); + Namespace namespace = AuthorizationUtils.ofGroupNamespace(metalake); + return store + .list(namespace, GroupEntity.class, EntityType.GROUP, allFields) + .toArray(new Group[0]); + } catch (NoSuchEntityException e) { + LOG.error("Metalake {} does not exist", metalake, e); + throw new NoSuchMetalakeException(METALAKE_DOES_NOT_EXIST_MSG, metalake); + } catch (IOException ioe) { + LOG.error("Listing group under metalake {} failed due to storage issues", metalake, ioe); + throw new RuntimeException(ioe); + } + } } 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..a057af86691 100644 --- a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java +++ b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java @@ -96,6 +96,16 @@ public Group getGroup(String metalake, String group) return dispatcher.getGroup(metalake, group); } + @Override + public Group[] listGroups(String metalake) throws NoSuchMetalakeException { + return dispatcher.listGroups(metalake); + } + + @Override + public String[] listGroupNames(String metalake) throws NoSuchMetalakeException { + return dispatcher.listGroupNames(metalake); + } + @Override public User grantRolesToUser(String metalake, List roles, String user) throws NoSuchUserException, NoSuchRoleException, NoSuchMetalakeException { 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..fb1084d8e48 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 @@ -108,6 +108,8 @@ public List list( return (List) UserMetaService.getInstance().listUsersByNamespace(namespace, allFields); case ROLE: return (List) RoleMetaService.getInstance().listRolesByNamespace(namespace); + case GROUP: + return (List) GroupMetaService.getInstance().listGroupsByNamespace(namespace, allFields); default: throw new UnsupportedEntityTypeException( "Unsupported entity type: %s for list operation", entityType); diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java index 5743095dd72..ae554a2a436 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaMapper.java @@ -20,6 +20,7 @@ package org.apache.gravitino.storage.relational.mapper; import java.util.List; +import org.apache.gravitino.storage.relational.po.ExtendedGroupPO; import org.apache.gravitino.storage.relational.po.GroupPO; import org.apache.ibatis.annotations.DeleteProvider; import org.apache.ibatis.annotations.InsertProvider; @@ -51,6 +52,14 @@ Long selectGroupIdBySchemaIdAndName( GroupPO selectGroupMetaByMetalakeIdAndName( @Param("metalakeId") Long metalakeId, @Param("groupName") String name); + @SelectProvider(type = GroupMetaSQLProviderFactory.class, method = "listGroupPOsByMetalake") + List listGroupPOsByMetalake(@Param("metalakeName") String metalakeName); + + @SelectProvider( + type = GroupMetaSQLProviderFactory.class, + method = "listExtendedGroupPOsByMetalakeId") + List listExtendedGroupPOsByMetalakeId(Long metalakeId); + @InsertProvider(type = GroupMetaSQLProviderFactory.class, method = "insertGroupMeta") void insertGroupMeta(@Param("groupMeta") GroupPO groupPO); diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java index 75841e7bd90..591ac2e9a1c 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/GroupMetaSQLProviderFactory.java @@ -22,6 +22,7 @@ import java.util.Map; import org.apache.gravitino.storage.relational.JDBCBackend.JDBCBackendType; import org.apache.gravitino.storage.relational.mapper.provider.base.GroupMetaBaseSQLProvider; +import org.apache.gravitino.storage.relational.mapper.provider.h2.GroupMetaH2Provider; import org.apache.gravitino.storage.relational.mapper.provider.postgresql.GroupMetaPostgreSQLProvider; import org.apache.gravitino.storage.relational.po.GroupPO; import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper; @@ -47,8 +48,6 @@ public static GroupMetaBaseSQLProvider getProvider() { static class GroupMetaMySQLProvider extends GroupMetaBaseSQLProvider {} - static class GroupMetaH2Provider extends GroupMetaBaseSQLProvider {} - public static String selectGroupIdBySchemaIdAndName( @Param("metalakeId") Long metalakeId, @Param("groupName") String name) { return getProvider().selectGroupIdBySchemaIdAndName(metalakeId, name); @@ -84,6 +83,14 @@ public static String listGroupsByRoleId(@Param("roleId") Long roleId) { return getProvider().listGroupsByRoleId(roleId); } + public static String listGroupPOsByMetalake(@Param("metalakeName") String metalakeName) { + return getProvider().listGroupPOsByMetalake(metalakeName); + } + + public static String listExtendedGroupPOsByMetalakeId(@Param("metalakeId") Long metalakeId) { + return getProvider().listExtendedGroupPOsByMetalakeId(metalakeId); + } + public static String deleteGroupMetasByLegacyTimeline( @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) { return getProvider().deleteGroupMetasByLegacyTimeline(legacyTimeline, limit); diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java index 0c26d74885a..a52e1b86144 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/base/GroupMetaBaseSQLProvider.java @@ -20,7 +20,9 @@ import static org.apache.gravitino.storage.relational.mapper.GroupMetaMapper.GROUP_TABLE_NAME; import static org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.GROUP_ROLE_RELATION_TABLE_NAME; +import static org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME; +import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper; import org.apache.gravitino.storage.relational.po.GroupPO; import org.apache.ibatis.annotations.Param; @@ -34,6 +36,40 @@ public String selectGroupIdBySchemaIdAndName( + " AND deleted_at = 0"; } + public String listGroupPOsByMetalake(@Param("metalakeName") String metalakeName) { + return "SELECT gt.group_id as groupId, gt.group_name as groupName, gt.metalake_id as metalakeId," + + " gt.audit_info as auditInfo, gt.current_version as currentVersion, gt.last_version as lastVersion," + + " gt.deleted_at as deletedAt FROM " + + GROUP_TABLE_NAME + + " gt JOIN " + + MetalakeMetaMapper.TABLE_NAME + + " mt ON gt.metalake_id = mt.metalake_id WHERE mt.metalake_name = #{metalakeName}" + + " AND gt.deleted_at = 0 AND mt.deleted_at = 0"; + } + + public String listExtendedGroupPOsByMetalakeId(Long metalakeId) { + return "SELECT gt.group_id as groupId, gt.group_name as groupName," + + " gt.metalake_id as metalakeId," + + " gt.audit_info as auditInfo," + + " gt.current_version as currentVersion, gt.last_version as lastVersion," + + " gt.deleted_at as deletedAt," + + " JSON_ARRAYAGG(rot.role_name) as roleNames," + + " JSON_ARRAYAGG(rot.role_id) as roleIds" + + " FROM " + + GROUP_TABLE_NAME + + " gt LEFT OUTER JOIN " + + GROUP_ROLE_RELATION_TABLE_NAME + + " rt ON rt.group_id = gt.group_id" + + " LEFT OUTER JOIN " + + ROLE_TABLE_NAME + + " rot ON rot.role_id = rt.role_id" + + " WHERE " + + " gt.deleted_at = 0 AND" + + " (rot.deleted_at = 0 OR rot.deleted_at is NULL) AND" + + " (rt.deleted_at = 0 OR rt.deleted_at is NULL) AND gt.metalake_id = #{metalakeId}" + + " GROUP BY gt.group_id"; + } + public String selectGroupMetaByMetalakeIdAndName( @Param("metalakeId") Long metalakeId, @Param("groupName") String name) { return "SELECT group_id as groupId, group_name as groupName," diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java new file mode 100644 index 00000000000..175d9d8ae9a --- /dev/null +++ b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/h2/GroupMetaH2Provider.java @@ -0,0 +1,52 @@ +/* + * 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.storage.relational.mapper.provider.h2; + +import static org.apache.gravitino.storage.relational.mapper.GroupMetaMapper.GROUP_TABLE_NAME; +import static org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.GROUP_ROLE_RELATION_TABLE_NAME; +import static org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME; + +import org.apache.gravitino.storage.relational.mapper.provider.base.GroupMetaBaseSQLProvider; +import org.apache.ibatis.annotations.Param; + +public class GroupMetaH2Provider extends GroupMetaBaseSQLProvider { + @Override + public String listExtendedGroupPOsByMetalakeId(@Param("metalakeId") Long metalakeId) { + return "SELECT gt.group_id as groupId, gt.group_name as groupName," + + " gt.metalake_id as metalakeId," + + " gt.audit_info as auditInfo," + + " gt.current_version as currentVersion, gt.last_version as lastVersion," + + " gt.deleted_at as deletedAt," + + " '[' || GROUP_CONCAT('\"' || rot.role_name || '\"') || ']' as roleNames," + + " '[' || GROUP_CONCAT('\"' || rot.role_id || '\"') || ']' as roleIds" + + " FROM " + + GROUP_TABLE_NAME + + " gt LEFT OUTER JOIN " + + GROUP_ROLE_RELATION_TABLE_NAME + + " rt ON rt.group_id = gt.group_id" + + " LEFT OUTER JOIN " + + ROLE_TABLE_NAME + + " rot ON rot.role_id = rt.role_id" + + " WHERE " + + " gt.deleted_at = 0 AND" + + " (rot.deleted_at = 0 OR rot.deleted_at is NULL) AND" + + " (rt.deleted_at = 0 OR rt.deleted_at is NULL) AND gt.metalake_id = #{metalakeId}" + + " GROUP BY gt.group_id"; + } +} diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java index 4dddcad42be..51cf47bf7d7 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/provider/postgresql/GroupMetaPostgreSQLProvider.java @@ -19,6 +19,8 @@ package org.apache.gravitino.storage.relational.mapper.provider.postgresql; import static org.apache.gravitino.storage.relational.mapper.GroupMetaMapper.GROUP_TABLE_NAME; +import static org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.GROUP_ROLE_RELATION_TABLE_NAME; +import static org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME; import org.apache.gravitino.storage.relational.mapper.provider.base.GroupMetaBaseSQLProvider; import org.apache.gravitino.storage.relational.po.GroupPO; @@ -66,4 +68,28 @@ public String insertGroupMetaOnDuplicateKeyUpdate(GroupPO groupPO) { + " last_version = #{groupMeta.lastVersion}," + " deleted_at = #{groupMeta.deletedAt}"; } + + @Override + public String listExtendedGroupPOsByMetalakeId(Long metalakeId) { + return "SELECT gt.group_id as groupId, gt.group_name as groupName," + + " gt.metalake_id as metalakeId," + + " gt.audit_info as auditInfo," + + " gt.current_version as currentVersion, gt.last_version as lastVersion," + + " gt.deleted_at as deletedAt," + + " JSON_AGG(rot.role_name) as roleNames," + + " JSON_AGG(rot.role_id) as roleIds" + + " FROM " + + GROUP_TABLE_NAME + + " gt LEFT OUTER JOIN " + + GROUP_ROLE_RELATION_TABLE_NAME + + " rt ON rt.group_id = gt.group_id" + + " LEFT OUTER JOIN " + + ROLE_TABLE_NAME + + " rot ON rot.role_id = rt.role_id" + + " WHERE " + + " gt.deleted_at = 0 AND" + + " (rot.deleted_at = 0 OR rot.deleted_at is NULL) AND" + + " (rt.deleted_at = 0 OR rt.deleted_at is NULL) AND gt.metalake_id = #{metalakeId}" + + " GROUP BY gt.group_id"; + } } diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedGroupPO.java b/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedGroupPO.java new file mode 100644 index 00000000000..390a0039833 --- /dev/null +++ b/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedGroupPO.java @@ -0,0 +1,59 @@ +/* + * 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.storage.relational.po; + +import java.util.Objects; + +/** + * ExtendedGroupPO add extra roleNames and roleIds for GroupPO. This PO is only used for reading the + * data from multiple joined tables. The PO won't be written to database. So we don't need the inner + * class Builder. + */ +public class ExtendedGroupPO extends GroupPO { + + private String roleNames; + private String roleIds; + + public String getRoleNames() { + return roleNames; + } + + public String getRoleIds() { + return roleIds; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + + if (!(o instanceof ExtendedGroupPO)) { + return false; + } + ExtendedGroupPO that = (ExtendedGroupPO) o; + return Objects.equals(getRoleIds(), that.getRoleIds()) + && Objects.equals(getRoleNames(), that.getRoleNames()); + } + + @Override + public int hashCode() { + return Objects.hash(super.hashCode(), getRoleIds(), getRoleNames()); + } +} diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java index 2ffc10dac59..4329b3a0a10 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/service/GroupMetaService.java @@ -32,6 +32,7 @@ import org.apache.gravitino.Entity; import org.apache.gravitino.HasIdentifier; import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.Namespace; import org.apache.gravitino.authorization.AuthorizationUtils; import org.apache.gravitino.exceptions.NoSuchEntityException; import org.apache.gravitino.meta.GroupEntity; @@ -39,6 +40,7 @@ import org.apache.gravitino.storage.relational.mapper.GroupMetaMapper; import org.apache.gravitino.storage.relational.mapper.GroupRoleRelMapper; import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper; +import org.apache.gravitino.storage.relational.po.ExtendedGroupPO; import org.apache.gravitino.storage.relational.po.GroupPO; import org.apache.gravitino.storage.relational.po.GroupRoleRelPO; import org.apache.gravitino.storage.relational.po.RolePO; @@ -249,6 +251,36 @@ public GroupEntity updateGroup( return newEntity; } + public List listGroupsByNamespace(Namespace namespace, boolean allFields) { + AuthorizationUtils.checkGroupNamespace(namespace); + String metalakeName = namespace.level(0); + + if (allFields) { + Long metalakeId = MetalakeMetaService.getInstance().getMetalakeIdByName(metalakeName); + List groupPOs = + SessionUtils.getWithoutCommit( + GroupMetaMapper.class, mapper -> mapper.listExtendedGroupPOsByMetalakeId(metalakeId)); + return groupPOs.stream() + .map( + po -> + POConverters.fromExtendedGroupPO( + po, AuthorizationUtils.ofGroupNamespace(metalakeName))) + .collect(Collectors.toList()); + } else { + List groupPOs = + SessionUtils.getWithoutCommit( + GroupMetaMapper.class, mapper -> mapper.listGroupPOsByMetalake(metalakeName)); + return groupPOs.stream() + .map( + po -> + POConverters.fromGroupPO( + po, + Collections.emptyList(), + AuthorizationUtils.ofGroupNamespace(metalakeName))) + .collect(Collectors.toList()); + } + } + public int deleteGroupMetasByLegacyTimeline(long legacyTimeline, int limit) { int[] groupDeletedCount = new int[] {0}; int[] groupRoleRelDeletedCount = new int[] {0}; diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java index da1f3d06a3b..f6392127b36 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/utils/POConverters.java @@ -49,6 +49,7 @@ import org.apache.gravitino.meta.TopicEntity; import org.apache.gravitino.meta.UserEntity; import org.apache.gravitino.storage.relational.po.CatalogPO; +import org.apache.gravitino.storage.relational.po.ExtendedGroupPO; import org.apache.gravitino.storage.relational.po.ExtendedUserPO; import org.apache.gravitino.storage.relational.po.FilesetPO; import org.apache.gravitino.storage.relational.po.FilesetVersionPO; @@ -733,7 +734,7 @@ public static UserEntity fromUserPO(UserPO userPO, List rolePOs, Namespa /** * Convert {@link ExtendedUserPO} to {@link UserEntity} * - * @param userPO CombinedUserPo object to be converted + * @param userPO ExtendedUserPO object to be converted * @param namespace Namespace object to be associated with the user * @return UserEntity object from ExtendedUserPO object */ @@ -814,6 +815,57 @@ public static GroupEntity fromGroupPO( } } + /** + * Convert {@link ExtendedGroupPO} to {@link GroupEntity} + * + * @param groupPO ExtendedGroupPO object to be converted + * @param namespace Namespace object to be associated with the user + * @return GroupEntity object from ExtendedGroupPO object + */ + public static GroupEntity fromExtendedGroupPO(ExtendedGroupPO groupPO, Namespace namespace) { + try { + GroupEntity.Builder builder = + GroupEntity.builder() + .withId(groupPO.getGroupId()) + .withName(groupPO.getGroupName()) + .withNamespace(namespace) + .withAuditInfo( + JsonUtils.anyFieldMapper().readValue(groupPO.getAuditInfo(), AuditInfo.class)); + + if (StringUtils.isNotBlank(groupPO.getRoleNames())) { + List roleNamesFromJson = + JsonUtils.anyFieldMapper().readValue(groupPO.getRoleNames(), List.class); + List roleNames = + roleNamesFromJson.stream().filter(StringUtils::isNotBlank).collect(Collectors.toList()); + if (!roleNames.isEmpty()) { + builder.withRoleNames(roleNames); + } + } + + if (StringUtils.isNotBlank(groupPO.getRoleIds())) { + // Different JSON AGG from backends will produce different types data, we + // can only use Object. PostSQL produces the data with type Long. H2 produces + // the data with type String. + List roleIdsFromJson = + JsonUtils.anyFieldMapper().readValue(groupPO.getRoleIds(), List.class); + List roleIds = + roleIdsFromJson.stream() + .filter(Objects::nonNull) + .map(String::valueOf) + .filter(StringUtils::isNotBlank) + .map(Long::valueOf) + .collect(Collectors.toList()); + + if (!roleIds.isEmpty()) { + builder.withRoleIds(roleIds); + } + } + return builder.build(); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize json object:", e); + } + } + /** * Initialize UserRoleRelPO * 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..ff0f5a58e1a 100644 --- a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java +++ b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java @@ -294,6 +294,28 @@ public void testGetGroup() { Assertions.assertTrue(exception.getMessage().contains("Group not-exist does not exist")); } + @Test + public void testListGroupss() { + accessControlManager.addGroup("metalake_list", "testList1"); + accessControlManager.addGroup("metalake_list", "testList2"); + + // Test to list groups + String[] expectGroupNames = new String[] {"testList1", "testList2"}; + String[] actualGroupNames = accessControlManager.listGroupNames("metalake_list"); + Arrays.sort(actualGroupNames); + Assertions.assertArrayEquals(expectGroupNames, actualGroupNames); + Group[] groups = accessControlManager.listGroups("metalake_list"); + Arrays.sort(groups, Comparator.comparing(Group::name)); + Assertions.assertArrayEquals( + expectGroupNames, Arrays.stream(groups).map(Group::name).toArray(String[]::new)); + + // Test with NoSuchMetalakeException + Assertions.assertThrows( + NoSuchMetalakeException.class, () -> accessControlManager.listGroupNames("no-exist")); + Assertions.assertThrows( + NoSuchMetalakeException.class, () -> accessControlManager.listGroups("no-exist")); + } + @Test public void testRemoveGroup() { accessControlManager.addGroup(METALAKE, "testRemove"); diff --git a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java index 22246ba0cf3..77cd9d110bc 100644 --- a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java +++ b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestGroupMetaService.java @@ -27,6 +27,7 @@ import java.sql.SQLException; import java.sql.Statement; import java.time.Instant; +import java.util.Comparator; import java.util.List; import java.util.Optional; import java.util.function.Function; @@ -119,6 +120,77 @@ void getGroupByIdentifier() throws IOException { Sets.newHashSet(group2.roleNames()), Sets.newHashSet(actualGroup.roleNames())); } + @Test + void testListGroups() throws IOException { + AuditInfo auditInfo = + AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build(); + BaseMetalake metalake = + createBaseMakeLake(RandomIdGenerator.INSTANCE.nextId(), metalakeName, auditInfo); + backend.insert(metalake, false); + + CatalogEntity catalog = + createCatalog( + RandomIdGenerator.INSTANCE.nextId(), Namespace.of(metalakeName), "catalog", auditInfo); + backend.insert(catalog, false); + + GroupEntity group1 = + createGroupEntity( + RandomIdGenerator.INSTANCE.nextId(), + AuthorizationUtils.ofGroupNamespace(metalakeName), + "group1", + auditInfo); + + RoleEntity role1 = + createRoleEntity( + RandomIdGenerator.INSTANCE.nextId(), + AuthorizationUtils.ofRoleNamespace("metalake"), + "role1", + auditInfo, + "catalog"); + backend.insert(role1, false); + + RoleEntity role2 = + createRoleEntity( + RandomIdGenerator.INSTANCE.nextId(), + AuthorizationUtils.ofRoleNamespace("metalake"), + "role2", + auditInfo, + "catalog"); + backend.insert(role2, false); + + GroupEntity group2 = + createGroupEntity( + RandomIdGenerator.INSTANCE.nextId(), + AuthorizationUtils.ofGroupNamespace("metalake"), + "group2", + auditInfo, + Lists.newArrayList(role1.name(), role2.name()), + Lists.newArrayList(role1.id(), role2.id())); + + backend.insert(group1, false); + backend.insert(group2, false); + + GroupMetaService groupMetaService = GroupMetaService.getInstance(); + List actualGroups = + groupMetaService.listGroupsByNamespace( + AuthorizationUtils.ofGroupNamespace(metalakeName), true); + actualGroups.sort(Comparator.comparing(GroupEntity::name)); + List expectGroups = Lists.newArrayList(group1, group2); + Assertions.assertEquals(expectGroups.size(), actualGroups.size()); + for (int index = 0; index < expectGroups.size(); index++) { + Assertions.assertEquals(expectGroups.get(index).name(), actualGroups.get(index).name()); + if (expectGroups.get(index).roleNames() == null) { + Assertions.assertNull(actualGroups.get(index).roleNames()); + } else { + Assertions.assertEquals( + expectGroups.get(index).roleNames().size(), actualGroups.get(index).roleNames().size()); + for (String roleName : expectGroups.get(index).roleNames()) { + Assertions.assertTrue(actualGroups.get(index).roleNames().contains(roleName)); + } + } + } + } + @Test void insertGroup() throws IOException { AuditInfo auditInfo = @@ -243,7 +315,7 @@ void insertGroup() throws IOException { GroupEntity group3Overwrite = createGroupEntity( group1.id(), - AuthorizationUtils.ofUserNamespace(metalakeName), + AuthorizationUtils.ofGroupNamespace(metalakeName), "group3Overwrite", auditInfo, Lists.newArrayList(role3.name()), @@ -260,7 +332,7 @@ void insertGroup() throws IOException { GroupEntity group4Overwrite = createGroupEntity( group1.id(), - AuthorizationUtils.ofUserNamespace(metalakeName), + AuthorizationUtils.ofGroupNamespace(metalakeName), "group4Overwrite", auditInfo); Assertions.assertDoesNotThrow(() -> groupMetaService.insertGroup(group4Overwrite, true)); @@ -779,7 +851,7 @@ void deleteGroupMetasByLegacyTimeline() throws IOException { GroupEntity group1 = createGroupEntity( RandomIdGenerator.INSTANCE.nextId(), - AuthorizationUtils.ofUserNamespace(metalakeName), + AuthorizationUtils.ofGroupNamespace(metalakeName), "group1", auditInfo, Lists.newArrayList(role1.name(), role2.name()), @@ -787,7 +859,7 @@ void deleteGroupMetasByLegacyTimeline() throws IOException { GroupEntity group2 = createGroupEntity( RandomIdGenerator.INSTANCE.nextId(), - AuthorizationUtils.ofUserNamespace(metalakeName), + AuthorizationUtils.ofGroupNamespace(metalakeName), "group2", auditInfo, Lists.newArrayList(role1.name(), role2.name()), @@ -795,7 +867,7 @@ void deleteGroupMetasByLegacyTimeline() throws IOException { GroupEntity group3 = createGroupEntity( RandomIdGenerator.INSTANCE.nextId(), - AuthorizationUtils.ofUserNamespace(metalakeName), + AuthorizationUtils.ofGroupNamespace(metalakeName), "group3", auditInfo, Lists.newArrayList(role1.name(), role2.name()), @@ -803,7 +875,7 @@ void deleteGroupMetasByLegacyTimeline() throws IOException { GroupEntity group4 = createGroupEntity( RandomIdGenerator.INSTANCE.nextId(), - AuthorizationUtils.ofUserNamespace(metalakeName), + AuthorizationUtils.ofGroupNamespace(metalakeName), "group4", auditInfo, Lists.newArrayList(role1.name(), role2.name()), diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java index 537bafb9e78..12cf769932e 100644 --- a/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java +++ b/server/src/main/java/org/apache/gravitino/server/web/rest/GroupOperations.java @@ -22,19 +22,24 @@ import com.codahale.metrics.annotation.Timed; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import org.apache.gravitino.GravitinoEnv; import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.Namespace; import org.apache.gravitino.authorization.AccessControlDispatcher; import org.apache.gravitino.authorization.AuthorizationUtils; import org.apache.gravitino.dto.requests.GroupAddRequest; +import org.apache.gravitino.dto.responses.GroupListResponse; import org.apache.gravitino.dto.responses.GroupResponse; +import org.apache.gravitino.dto.responses.NameListResponse; import org.apache.gravitino.dto.responses.RemoveResponse; import org.apache.gravitino.dto.util.DTOConverters; import org.apache.gravitino.lock.LockType; @@ -134,4 +139,31 @@ public Response removeGroup( return ExceptionHandlers.handleGroupException(OperationType.REMOVE, group, metalake, e); } } + + @GET + @Produces("application/vnd.gravitino.v1+json") + @Timed(name = "list-group." + MetricNames.HTTP_PROCESS_DURATION, absolute = true) + @ResponseMetered(name = "list-group", absolute = true) + public Response listGroups( + @PathParam("metalake") String metalake, + @QueryParam("details") @DefaultValue("false") boolean verbose) { + LOG.info("Received list groups request."); + try { + return Utils.doAs( + httpRequest, + () -> { + if (verbose) { + return Utils.ok( + new GroupListResponse( + DTOConverters.toDTOs(accessControlManager.listGroups(metalake)))); + } else { + return Utils.ok(new NameListResponse(accessControlManager.listGroupNames(metalake))); + } + }); + + } catch (Exception e) { + return ExceptionHandlers.handleGroupException( + OperationType.LIST, Namespace.empty().toString(), metalake, e); + } + } } diff --git a/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java b/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java index c3b34bc6bff..77f0cf97988 100644 --- a/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java +++ b/server/src/test/java/org/apache/gravitino/server/web/rest/TestGroupOperations.java @@ -34,6 +34,7 @@ import javax.ws.rs.core.Application; import javax.ws.rs.core.MediaType; import javax.ws.rs.core.Response; +import javax.ws.rs.core.Response.Status; import org.apache.commons.lang3.reflect.FieldUtils; import org.apache.gravitino.Config; import org.apache.gravitino.GravitinoEnv; @@ -43,7 +44,9 @@ import org.apache.gravitino.dto.requests.GroupAddRequest; import org.apache.gravitino.dto.responses.ErrorConstants; import org.apache.gravitino.dto.responses.ErrorResponse; +import org.apache.gravitino.dto.responses.GroupListResponse; import org.apache.gravitino.dto.responses.GroupResponse; +import org.apache.gravitino.dto.responses.NameListResponse; import org.apache.gravitino.dto.responses.RemoveResponse; import org.apache.gravitino.exceptions.GroupAlreadyExistsException; import org.apache.gravitino.exceptions.NoSuchGroupException; @@ -119,7 +122,7 @@ public void testAddGroup() { .accept("application/vnd.gravitino.v1+json") .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); - Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(Status.OK.getStatusCode(), resp.getStatus()); Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp.getMediaType()); GroupResponse groupResponse = resp.readEntity(GroupResponse.class); @@ -138,7 +141,7 @@ public void testAddGroup() { .accept("application/vnd.gravitino.v1+json") .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); - Assertions.assertEquals(Response.Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); + Assertions.assertEquals(Status.NOT_FOUND.getStatusCode(), resp1.getStatus()); Assertions.assertEquals(MediaType.APPLICATION_JSON_TYPE, resp1.getMediaType()); ErrorResponse errorResponse = resp1.readEntity(ErrorResponse.class); @@ -153,7 +156,7 @@ public void testAddGroup() { .accept("application/vnd.gravitino.v1+json") .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); - Assertions.assertEquals(Response.Status.CONFLICT.getStatusCode(), resp2.getStatus()); + Assertions.assertEquals(Status.CONFLICT.getStatusCode(), resp2.getStatus()); ErrorResponse errorResponse1 = resp2.readEntity(ErrorResponse.class); Assertions.assertEquals(ErrorConstants.ALREADY_EXISTS_CODE, errorResponse1.getCode()); @@ -168,8 +171,7 @@ public void testAddGroup() { .accept("application/vnd.gravitino.v1+json") .post(Entity.entity(req, MediaType.APPLICATION_JSON_TYPE)); - Assertions.assertEquals( - Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + Assertions.assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); ErrorResponse errorResponse2 = resp3.readEntity(ErrorResponse.class); Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse2.getCode()); @@ -241,14 +243,103 @@ public void testGetGroup() { Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse2.getType()); } - private Group buildGroup(String group) { - return GroupEntity.builder() - .withId(1L) - .withName(group) - .withRoleNames(Collections.emptyList()) - .withAuditInfo( - AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) - .build(); + @Test + public void testListGroupNames() { + when(manager.listGroupNames(any())).thenReturn(new String[] {"group"}); + + Response resp = + target("/metalakes/metalake1/groups/") + .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("group", listResponse.getNames()[0]); + + // Test to throw NoSuchMetalakeException + doThrow(new NoSuchMetalakeException("mock error")).when(manager).listGroupNames(any()); + Response resp1 = + target("/metalakes/metalake1/groups/") + .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).listGroupNames(any()); + Response resp3 = + target("/metalakes/metalake1/groups") + .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()); + } + + @Test + public void testListGroups() { + Group group = buildGroup("group"); + when(manager.listGroups(any())).thenReturn(new Group[] {group}); + + Response resp = + target("/metalakes/metalake1/groups/") + .queryParam("details", "true") + .request(MediaType.APPLICATION_JSON_TYPE) + .accept("application/vnd.gravitino.v1+json") + .get(); + Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + + GroupListResponse listResponse = resp.readEntity(GroupListResponse.class); + Assertions.assertEquals(0, listResponse.getCode()); + + Assertions.assertEquals(1, listResponse.getGroups().length); + Assertions.assertEquals(group.name(), listResponse.getGroups()[0].name()); + Assertions.assertEquals(group.roles(), listResponse.getGroups()[0].roles()); + + // Test to throw NoSuchMetalakeException + doThrow(new NoSuchMetalakeException("mock error")).when(manager).listGroups(any()); + Response resp1 = + target("/metalakes/metalake1/groups/") + .queryParam("details", "true") + .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).listGroups(any()); + Response resp3 = + target("/metalakes/metalake1/groups") + .queryParam("details", "true") + .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()); } @Test @@ -261,7 +352,7 @@ public void testRemoveGroup() { .accept("application/vnd.gravitino.v1+json") .delete(); - Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp.getStatus()); + Assertions.assertEquals(Status.OK.getStatusCode(), resp.getStatus()); RemoveResponse removeResponse = resp.readEntity(RemoveResponse.class); Assertions.assertEquals(0, removeResponse.getCode()); Assertions.assertTrue(removeResponse.removed()); @@ -274,7 +365,7 @@ public void testRemoveGroup() { .accept("application/vnd.gravitino.v1+json") .delete(); - Assertions.assertEquals(Response.Status.OK.getStatusCode(), resp2.getStatus()); + Assertions.assertEquals(Status.OK.getStatusCode(), resp2.getStatus()); RemoveResponse removeResponse2 = resp2.readEntity(RemoveResponse.class); Assertions.assertEquals(0, removeResponse2.getCode()); Assertions.assertFalse(removeResponse2.removed()); @@ -286,11 +377,20 @@ public void testRemoveGroup() { .accept("application/vnd.gravitino.v1+json") .delete(); - Assertions.assertEquals( - Response.Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); + Assertions.assertEquals(Status.INTERNAL_SERVER_ERROR.getStatusCode(), resp3.getStatus()); ErrorResponse errorResponse = resp3.readEntity(ErrorResponse.class); Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, errorResponse.getCode()); Assertions.assertEquals(RuntimeException.class.getSimpleName(), errorResponse.getType()); } + + private Group buildGroup(String group) { + return GroupEntity.builder() + .withId(1L) + .withName(group) + .withRoleNames(Collections.emptyList()) + .withAuditInfo( + AuditInfo.builder().withCreator("creator").withCreateTime(Instant.now()).build()) + .build(); + } }