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

jshao pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/gravitino.git


The following commit(s) were added to refs/heads/main by this push:
     new 517f66c77 [#4873] feat(core): support list group (#4879)
517f66c77 is described below

commit 517f66c775ff20f052a32bbd91933c7e2091323d
Author: Qiang-Liu <[email protected]>
AuthorDate: Fri Sep 27 19:09:43 2024 +0800

    [#4873] feat(core): support list group (#4879)
    
    ### What changes were proposed in this pull request?
    
    support list group
    
    ### Why are the changes needed?
    
    support list group
    
    Fix: #4873
    
    ### Does this PR introduce _any_ user-facing change?
    
    no
    
    ### How was this patch tested?
    
    UT
    
    ---------
    
    Co-authored-by: Rory <[email protected]>
---
 .../apache/gravitino/client/GravitinoClient.java   |  20 +++
 .../apache/gravitino/client/GravitinoMetalake.java |  39 ++++++
 .../org/apache/gravitino/client/TestUserGroup.java |  52 ++++++++
 .../test/authorization/AccessControlIT.java        |  30 +++++
 .../gravitino/dto/responses/GroupListResponse.java |  74 ++++++++++++
 .../apache/gravitino/dto/util/DTOConverters.java   |  13 ++
 .../authorization/AccessControlDispatcher.java     |  18 +++
 .../authorization/AccessControlManager.java        |  10 ++
 .../gravitino/authorization/UserGroupManager.java  |  61 +++++++---
 .../hook/AccessControlHookDispatcher.java          |  10 ++
 .../gravitino/storage/relational/JDBCBackend.java  |   2 +
 .../storage/relational/mapper/GroupMetaMapper.java |   9 ++
 .../mapper/GroupMetaSQLProviderFactory.java        |  11 +-
 .../provider/base/GroupMetaBaseSQLProvider.java    |  36 ++++++
 .../mapper/provider/h2/GroupMetaH2Provider.java    |  52 ++++++++
 .../postgresql/GroupMetaPostgreSQLProvider.java    |  26 ++++
 .../storage/relational/po/ExtendedGroupPO.java     |  59 +++++++++
 .../relational/service/GroupMetaService.java       |  32 +++++
 .../storage/relational/utils/POConverters.java     |  54 ++++++++-
 .../authorization/TestAccessControlManager.java    |  22 ++++
 .../relational/service/TestGroupMetaService.java   |  84 ++++++++++++-
 .../gravitino/server/web/rest/GroupOperations.java |  32 +++++
 .../server/web/rest/TestGroupOperations.java       | 134 ++++++++++++++++++---
 23 files changed, 837 insertions(+), 43 deletions(-)

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 a7656fd02..a8a46ff8f 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 class GravitinoClient extends GravitinoClientBase
     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 8f98b6fd3..4905681b7 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
@@ -62,6 +62,7 @@ import org.apache.gravitino.dto.responses.DeleteResponse;
 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;
@@ -635,6 +636,44 @@ public class GravitinoMetalake extends MetalakeDTO
     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<String, String> 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 67a3035ed..ff98b2ca6 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_NOT_FOUND;
 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.authorization.UserDTO;
 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 class TestUserGroup extends TestBase {
     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<String, String> 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 d9b85bf0d..76cd938fa 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
@@ -146,8 +146,38 @@ public class AccessControlIT extends AbstractIT {
     // Get a not-existed group
     Assertions.assertThrows(NoSuchGroupException.class, () -> 
metalake.getGroup("not-existed"));
 
+    Map<String, String> 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<Group> 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 000000000..271fb9a92
--- /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 8e706c139..38224493b 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 class DTOConverters {
     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 3214c187f..a3919512b 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
@@ -129,6 +129,24 @@ public interface AccessControlDispatcher {
   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 c2f2976aa..75b0f9f1e 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -95,6 +95,16 @@ public class AccessControlManager implements 
AccessControlDispatcher {
     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<String> 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 be1b687f3..cd852ab66 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.time.Instant;
 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 @@ class UserGroupManager {
     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 @@ class UserGroupManager {
       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 65ed2c9da..e16974764 100644
--- 
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -98,6 +98,16 @@ public class AccessControlHookDispatcher implements 
AccessControlDispatcher {
     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<String> 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 42b079234..c1f72c360 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 class JDBCBackend implements RelationalBackend {
         return (List<E>) 
UserMetaService.getInstance().listUsersByNamespace(namespace, allFields);
       case ROLE:
         return (List<E>) 
RoleMetaService.getInstance().listRolesByNamespace(namespace);
+      case GROUP:
+        return (List<E>) 
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 5743095dd..ae554a2a4 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 @@ public interface GroupMetaMapper {
   GroupPO selectGroupMetaByMetalakeIdAndName(
       @Param("metalakeId") Long metalakeId, @Param("groupName") String name);
 
+  @SelectProvider(type = GroupMetaSQLProviderFactory.class, method = 
"listGroupPOsByMetalake")
+  List<GroupPO> listGroupPOsByMetalake(@Param("metalakeName") String 
metalakeName);
+
+  @SelectProvider(
+      type = GroupMetaSQLProviderFactory.class,
+      method = "listExtendedGroupPOsByMetalakeId")
+  List<ExtendedGroupPO> 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 75841e7bd..591ac2e9a 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 com.google.common.collect.ImmutableMap;
 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 class GroupMetaSQLProviderFactory {
 
   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 class GroupMetaSQLProviderFactory {
     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 0c26d7488..a52e1b861 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 @@ package 
org.apache.gravitino.storage.relational.mapper.provider.base;
 
 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 class GroupMetaBaseSQLProvider {
         + " 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 000000000..175d9d8ae
--- /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 4dddcad42..51cf47bf7 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 class GroupMetaPostgreSQLProvider extends 
GroupMetaBaseSQLProvider {
         + " 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 000000000..390a00398
--- /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 2ffc10dac..4329b3a0a 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 java.util.stream.Collectors;
 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.meta.RoleEntity;
 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 class GroupMetaService {
     return newEntity;
   }
 
+  public List<GroupEntity> listGroupsByNamespace(Namespace namespace, boolean 
allFields) {
+    AuthorizationUtils.checkGroupNamespace(namespace);
+    String metalakeName = namespace.level(0);
+
+    if (allFields) {
+      Long metalakeId = 
MetalakeMetaService.getInstance().getMetalakeIdByName(metalakeName);
+      List<ExtendedGroupPO> 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<GroupPO> 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 da1f3d06a..f6392127b 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.TagEntity;
 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 class POConverters {
   /**
    * 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 class POConverters {
     }
   }
 
+  /**
+   * 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<String> roleNamesFromJson =
+            JsonUtils.anyFieldMapper().readValue(groupPO.getRoleNames(), 
List.class);
+        List<String> 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<Object> roleIdsFromJson =
+            JsonUtils.anyFieldMapper().readValue(groupPO.getRoleIds(), 
List.class);
+        List<Long> 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 b299c15ef..a2b6dace1 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -298,6 +298,28 @@ public class TestAccessControlManager {
     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 22246ba0c..77cd9d110 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.ResultSet;
 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 @@ class TestGroupMetaService extends TestJDBCBackend {
         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<GroupEntity> actualGroups =
+        groupMetaService.listGroupsByNamespace(
+            AuthorizationUtils.ofGroupNamespace(metalakeName), true);
+    actualGroups.sort(Comparator.comparing(GroupEntity::name));
+    List<GroupEntity> 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 @@ class TestGroupMetaService extends TestJDBCBackend {
     GroupEntity group3Overwrite =
         createGroupEntity(
             group1.id(),
-            AuthorizationUtils.ofUserNamespace(metalakeName),
+            AuthorizationUtils.ofGroupNamespace(metalakeName),
             "group3Overwrite",
             auditInfo,
             Lists.newArrayList(role3.name()),
@@ -260,7 +332,7 @@ class TestGroupMetaService extends TestJDBCBackend {
     GroupEntity group4Overwrite =
         createGroupEntity(
             group1.id(),
-            AuthorizationUtils.ofUserNamespace(metalakeName),
+            AuthorizationUtils.ofGroupNamespace(metalakeName),
             "group4Overwrite",
             auditInfo);
     Assertions.assertDoesNotThrow(() -> 
groupMetaService.insertGroup(group4Overwrite, true));
@@ -779,7 +851,7 @@ class TestGroupMetaService extends TestJDBCBackend {
     GroupEntity group1 =
         createGroupEntity(
             RandomIdGenerator.INSTANCE.nextId(),
-            AuthorizationUtils.ofUserNamespace(metalakeName),
+            AuthorizationUtils.ofGroupNamespace(metalakeName),
             "group1",
             auditInfo,
             Lists.newArrayList(role1.name(), role2.name()),
@@ -787,7 +859,7 @@ class TestGroupMetaService extends TestJDBCBackend {
     GroupEntity group2 =
         createGroupEntity(
             RandomIdGenerator.INSTANCE.nextId(),
-            AuthorizationUtils.ofUserNamespace(metalakeName),
+            AuthorizationUtils.ofGroupNamespace(metalakeName),
             "group2",
             auditInfo,
             Lists.newArrayList(role1.name(), role2.name()),
@@ -795,7 +867,7 @@ class TestGroupMetaService extends TestJDBCBackend {
     GroupEntity group3 =
         createGroupEntity(
             RandomIdGenerator.INSTANCE.nextId(),
-            AuthorizationUtils.ofUserNamespace(metalakeName),
+            AuthorizationUtils.ofGroupNamespace(metalakeName),
             "group3",
             auditInfo,
             Lists.newArrayList(role1.name(), role2.name()),
@@ -803,7 +875,7 @@ class TestGroupMetaService extends TestJDBCBackend {
     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 537bafb9e..12cf76993 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.ResponseMetered;
 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 class GroupOperations {
       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 c3b34bc6b..77f0cf979 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.client.Entity;
 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.authorization.GroupDTO;
 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 class TestGroupOperations extends JerseyTest {
             .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 class TestGroupOperations extends JerseyTest {
             .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 class TestGroupOperations extends JerseyTest {
             .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 class TestGroupOperations extends JerseyTest {
             .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 class TestGroupOperations extends JerseyTest {
     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 class TestGroupOperations extends JerseyTest {
             .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 class TestGroupOperations extends JerseyTest {
             .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 class TestGroupOperations extends JerseyTest {
             .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();
+  }
 }

Reply via email to