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

yuqi4733 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 92a0ec8c1 [#3348] feat(core,server): Add the list operation of the 
user (#4055)
92a0ec8c1 is described below

commit 92a0ec8c1ee370365830a1006fe303fcbd1aca18
Author: roryqi <ror...@apache.org>
AuthorDate: Tue Sep 24 10:46:00 2024 +0800

    [#3348] feat(core,server): Add the list operation of the user (#4055)
    
    ### What changes were proposed in this pull request?
    Add the list operation of the user
    
    ### Why are the changes needed?
    
    Fix: #3348
    
    ### Does this PR introduce _any_ user-facing change?
    I will add the document later.
    
    ### How was this patch tested?
    Add the new ut.
---
 .../apache/gravitino/client/GravitinoClient.java   |  20 +++
 .../apache/gravitino/client/GravitinoMetalake.java |  41 +++++
 .../org/apache/gravitino/client/TestUserGroup.java |  54 +++++++
 .../test/authorization/AccessControlIT.java        |  36 +++++
 .../gravitino/dto/responses/UserListResponse.java  |  66 ++++++++
 .../apache/gravitino/dto/util/DTOConverters.java   |  13 ++
 .../java/org/apache/gravitino/EntityStore.java     |  33 +++-
 .../authorization/AccessControlDispatcher.java     |  18 +++
 .../authorization/AccessControlManager.java        |  19 ++-
 .../gravitino/authorization/UserGroupManager.java  |  38 +++++
 .../hook/AccessControlHookDispatcher.java          |  10 ++
 .../java/org/apache/gravitino/meta/UserEntity.java |  18 +++
 .../gravitino/storage/relational/JDBCBackend.java  |   8 +-
 .../storage/relational/RelationalBackend.java      |   8 +-
 .../storage/relational/RelationalEntityStore.java  |  12 +-
 .../relational/mapper/UserMetaBaseSQLProvider.java |  39 +++++
 .../storage/relational/mapper/UserMetaMapper.java  |   9 ++
 .../mapper/UserMetaSQLProviderFactory.java         |  11 +-
 .../relational/mapper/h2/UserMetaH2Provider.java   |  52 ++++++
 .../postgresql/UserMetaPostgreSQLProvider.java     |  26 +++
 .../storage/relational/po/ExtendedUserPO.java      |  60 +++++++
 .../relational/service/SupportsDesiredFields.java  |  40 +++++
 .../service/SupportsDesiredFieldsHandlers.java     |  50 ++++++
 .../relational/service/UserMetaService.java        |  76 +++++++++
 .../storage/relational/utils/POConverters.java     |  52 ++++++
 .../authorization/TestAccessControlManager.java    | 175 ++++++++++++++++-----
 .../storage/relational/TestJDBCBackend.java        |  22 ++-
 .../relational/service/TestUserMetaService.java    |  73 +++++++++
 .../gravitino/server/web/rest/UserOperations.java  |  33 ++++
 .../server/web/rest/TestUserOperations.java        | 101 ++++++++++++
 30 files changed, 1147 insertions(+), 66 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 9b7769200..e074770e8 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
@@ -168,6 +168,26 @@ public class GravitinoClient extends GravitinoClientBase
     return getMetalake().getUser(user);
   }
 
+  /**
+   * Lists the users.
+   *
+   * @return The User list.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   */
+  public User[] listUsers() {
+    return getMetalake().listUsers();
+  }
+
+  /**
+   * Lists the usernames.
+   *
+   * @return The username list.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   */
+  public String[] listUserNames() {
+    return getMetalake().listUserNames();
+  }
+
   /**
    * Adds a new Group.
    *
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 f13958cb5..9a13a9dd1 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
@@ -68,6 +68,7 @@ import org.apache.gravitino.dto.responses.RoleResponse;
 import org.apache.gravitino.dto.responses.SetResponse;
 import org.apache.gravitino.dto.responses.TagListResponse;
 import org.apache.gravitino.dto.responses.TagResponse;
+import org.apache.gravitino.dto.responses.UserListResponse;
 import org.apache.gravitino.dto.responses.UserResponse;
 import org.apache.gravitino.exceptions.CatalogAlreadyExistsException;
 import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
@@ -515,6 +516,46 @@ public class GravitinoMetalake extends MetalakeDTO 
implements SupportsCatalogs,
     return resp.getUser();
   }
 
+  /**
+   * Lists the users.
+   *
+   * @return The User list.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   */
+  public User[] listUsers() throws NoSuchMetalakeException {
+    Map<String, String> params = new HashMap<>();
+    params.put("details", "true");
+
+    UserListResponse resp =
+        restClient.get(
+            String.format(API_METALAKES_USERS_PATH, name(), 
BLANK_PLACE_HOLDER),
+            params,
+            UserListResponse.class,
+            Collections.emptyMap(),
+            ErrorHandlers.userErrorHandler());
+    resp.validate();
+
+    return resp.getUsers();
+  }
+
+  /**
+   * Lists the usernames.
+   *
+   * @return The username list.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   */
+  public String[] listUserNames() throws NoSuchMetalakeException {
+    NameListResponse resp =
+        restClient.get(
+            String.format(API_METALAKES_USERS_PATH, name(), 
BLANK_PLACE_HOLDER),
+            NameListResponse.class,
+            Collections.emptyMap(),
+            ErrorHandlers.userErrorHandler());
+    resp.validate();
+
+    return resp.getNames();
+  }
+
   /**
    * Adds a new Group.
    *
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 f3885a05f..67a3035ed 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
@@ -24,6 +24,8 @@ import static javax.servlet.http.HttpServletResponse.SC_OK;
 import static org.apache.hc.core5.http.HttpStatus.SC_SERVER_ERROR;
 
 import java.time.Instant;
+import java.util.Collections;
+import java.util.Map;
 import org.apache.gravitino.authorization.Group;
 import org.apache.gravitino.authorization.User;
 import org.apache.gravitino.dto.AuditDTO;
@@ -35,7 +37,9 @@ import org.apache.gravitino.dto.requests.UserAddRequest;
 import org.apache.gravitino.dto.responses.ErrorResponse;
 import org.apache.gravitino.dto.responses.GroupResponse;
 import org.apache.gravitino.dto.responses.MetalakeResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
 import org.apache.gravitino.dto.responses.RemoveResponse;
+import org.apache.gravitino.dto.responses.UserListResponse;
 import org.apache.gravitino.dto.responses.UserResponse;
 import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
 import org.apache.gravitino.exceptions.NoSuchGroupException;
@@ -175,6 +179,56 @@ public class TestUserGroup extends TestBase {
     Assertions.assertThrows(RuntimeException.class, () -> 
gravitinoClient.removeUser(username));
   }
 
+  @Test
+  public void testListUserNames() throws Exception {
+    String userPath = withSlash(String.format(API_METALAKES_USERS_PATH, 
metalakeName, ""));
+
+    NameListResponse listResponse = new NameListResponse(new String[] 
{"user1", "user2"});
+    buildMockResource(Method.GET, userPath, null, listResponse, SC_OK);
+
+    Assertions.assertArrayEquals(new String[] {"user1", "user2"}, 
gravitinoClient.listUserNames());
+
+    ErrorResponse errRespNoMetalake =
+        ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), 
"metalake not found");
+    buildMockResource(Method.GET, userPath, null, errRespNoMetalake, 
SC_NOT_FOUND);
+    Exception ex =
+        Assertions.assertThrows(
+            NoSuchMetalakeException.class, () -> 
gravitinoClient.listUserNames());
+    Assertions.assertEquals("metalake not found", ex.getMessage());
+
+    // Test RuntimeException
+    ErrorResponse errResp = ErrorResponse.internalError("internal error");
+    buildMockResource(Method.GET, userPath, null, errResp, SC_SERVER_ERROR);
+    Assertions.assertThrows(RuntimeException.class, () -> 
gravitinoClient.listUserNames());
+  }
+
+  @Test
+  public void testListUsers() throws Exception {
+    String userPath = withSlash(String.format(API_METALAKES_USERS_PATH, 
metalakeName, ""));
+    UserDTO user1 = mockUserDTO("user1");
+    UserDTO user2 = mockUserDTO("user2");
+    Map<String, String> params = Collections.singletonMap("details", "true");
+    UserListResponse listResponse = new UserListResponse(new UserDTO[] {user1, 
user2});
+    buildMockResource(Method.GET, userPath, params, null, listResponse, SC_OK);
+
+    User[] users = gravitinoClient.listUsers();
+    Assertions.assertEquals(2, users.length);
+    assertUser(user1, users[0]);
+    assertUser(user2, users[1]);
+
+    ErrorResponse errRespNoMetalake =
+        ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), 
"metalake not found");
+    buildMockResource(Method.GET, userPath, params, null, errRespNoMetalake, 
SC_NOT_FOUND);
+    Exception ex =
+        Assertions.assertThrows(NoSuchMetalakeException.class, () -> 
gravitinoClient.listUsers());
+    Assertions.assertEquals("metalake not found", ex.getMessage());
+
+    // Test RuntimeException
+    ErrorResponse errResp = ErrorResponse.internalError("internal error");
+    buildMockResource(Method.GET, userPath, params, null, errResp, 
SC_SERVER_ERROR);
+    Assertions.assertThrows(RuntimeException.class, () -> 
gravitinoClient.listUsers());
+  }
+
   @Test
   public void testAddGroups() throws Exception {
     String groupName = "group";
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 76f2c1b0f..662e2c159 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
@@ -20,8 +20,12 @@ package 
org.apache.gravitino.client.integration.test.authorization;
 
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
 import java.util.Map;
+import java.util.stream.Collectors;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.auth.AuthConstants;
 import org.apache.gravitino.authorization.Group;
@@ -73,11 +77,43 @@ public class AccessControlIT extends AbstractIT {
     Assertions.assertEquals(username, user.name());
     Assertions.assertTrue(user.roles().isEmpty());
 
+    Map<String, String> properties = Maps.newHashMap();
+    properties.put("k1", "v1");
+    SecurableObject metalakeObject =
+        SecurableObjects.ofMetalake(
+            metalakeName, 
Lists.newArrayList(Privileges.CreateCatalog.allow()));
+
+    // Test the user with the role
+    metalake.createRole("role1", properties, 
Lists.newArrayList(metalakeObject));
+    metalake.grantRolesToUser(Lists.newArrayList("role1"), username);
+
+    // List users
+    String anotherUser = "another-user";
+    metalake.addUser(anotherUser);
+    String[] usernames = metalake.listUserNames();
+    Arrays.sort(usernames);
+    Assertions.assertEquals(
+        Lists.newArrayList(AuthConstants.ANONYMOUS_USER, anotherUser, 
username),
+        Arrays.asList(usernames));
+    List<User> users =
+        Arrays.stream(metalake.listUsers())
+            .sorted(Comparator.comparing(User::name))
+            .collect(Collectors.toList());
+    Assertions.assertEquals(
+        Lists.newArrayList(AuthConstants.ANONYMOUS_USER, anotherUser, 
username),
+        users.stream().map(User::name).collect(Collectors.toList()));
+    Assertions.assertEquals(Lists.newArrayList("role1"), users.get(2).roles());
+
     // Get a not-existed user
     Assertions.assertThrows(NoSuchUserException.class, () -> 
metalake.getUser("not-existed"));
 
     Assertions.assertTrue(metalake.removeUser(username));
+
     Assertions.assertFalse(metalake.removeUser(username));
+
+    // clean up
+    metalake.removeUser(anotherUser);
+    metalake.deleteRole("role1");
   }
 
   @Test
diff --git 
a/common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java 
b/common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java
new file mode 100644
index 000000000..2b591184a
--- /dev/null
+++ 
b/common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java
@@ -0,0 +1,66 @@
+/*
+ * 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 lombok.EqualsAndHashCode;
+import lombok.Getter;
+import lombok.ToString;
+import org.apache.gravitino.dto.authorization.UserDTO;
+
+/** Represents a response containing a list of users. */
+@Getter
+@ToString
+@EqualsAndHashCode(callSuper = true)
+public class UserListResponse extends BaseResponse {
+
+  @JsonProperty("users")
+  private final UserDTO[] users;
+
+  /**
+   * Constructor for UserListResponse.
+   *
+   * @param users The array of users.
+   */
+  public UserListResponse(UserDTO[] users) {
+    super(0);
+    this.users = users;
+  }
+
+  /**
+   * This is the constructor that is used by Jackson deserializer to create an 
instance of
+   * UserListResponse.
+   */
+  public UserListResponse() {
+    super(0);
+    this.users = null;
+  }
+
+  /**
+   * Validates the response data.
+   *
+   * @throws IllegalArgumentException if users are not set.
+   */
+  @Override
+  public void validate() throws IllegalArgumentException {
+    super.validate();
+    Preconditions.checkArgument(users != null, "users 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 d83460af1..8e706c139 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
@@ -678,6 +678,19 @@ public class DTOConverters {
     return 
Arrays.stream(catalogs).map(DTOConverters::toDTO).toArray(CatalogDTO[]::new);
   }
 
+  /**
+   * Converts an array of Users to an array of UserDTOs.
+   *
+   * @param users The users to be converted.
+   * @return The array of UserDTOs.
+   */
+  public static UserDTO[] toDTOs(User[] users) {
+    if (ArrayUtils.isEmpty(users)) {
+      return new UserDTO[0];
+    }
+    return 
Arrays.stream(users).map(DTOConverters::toDTO).toArray(UserDTO[]::new);
+  }
+
   /**
    * Converts a DistributionDTO to a Distribution.
    *
diff --git a/core/src/main/java/org/apache/gravitino/EntityStore.java 
b/core/src/main/java/org/apache/gravitino/EntityStore.java
index 1112efc4b..dcb27f022 100644
--- a/core/src/main/java/org/apache/gravitino/EntityStore.java
+++ b/core/src/main/java/org/apache/gravitino/EntityStore.java
@@ -20,7 +20,9 @@ package org.apache.gravitino;
 
 import java.io.Closeable;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Function;
 import org.apache.gravitino.Entity.EntityType;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
@@ -55,15 +57,40 @@ public interface EntityStore extends Closeable {
    * <p>Note. Depends on the isolation levels provided by the underlying 
storage, the returned list
    * may not be consistent.
    *
-   * @param namespace the namespace of the entities
    * @param <E> class of the entity
+   * @param namespace the namespace of the entities
    * @param type the detailed type of the entity
    * @param entityType the general type of the entity
+   * @return the list of entities
    * @throws IOException if the list operation fails
+   */
+  default <E extends Entity & HasIdentifier> List<E> list(
+      Namespace namespace, Class<E> type, EntityType entityType) throws 
IOException {
+    return list(namespace, type, entityType, Collections.emptySet());
+  }
+
+  /**
+   * List all the entities with the specified {@link 
org.apache.gravitino.Namespace}, and
+   * deserialize them into the specified {@link Entity} object.
+   *
+   * <p>Note. Depends on the isolation levels provided by the underlying 
storage, the returned list
+   * may not be consistent.
+   *
+   * @param <E> class of the entity
+   * @param namespace the namespace of the entities
+   * @param type the detailed type of the entity
+   * @param entityType the general type of the entity
+   * @param skippingFields Some fields may have a relatively high acquisition 
cost, EntityStore
+   *     provides an optional setting to avoid fetching these high-cost fields 
to improve the
+   *     performance.
    * @return the list of entities
+   * @throws IOException if the list operation fails
    */
-  <E extends Entity & HasIdentifier> List<E> list(
-      Namespace namespace, Class<E> type, EntityType entityType) throws 
IOException;
+  default <E extends Entity & HasIdentifier> List<E> list(
+      Namespace namespace, Class<E> type, EntityType entityType, Set<Field> 
skippingFields)
+      throws IOException {
+    throw new UnsupportedOperationException("Don't support to skip fields");
+  }
 
   /**
    * Check if the entity with the specified {@link 
org.apache.gravitino.NameIdentifier} exists.
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 fabc8acaa..fbeebd944 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlDispatcher.java
@@ -71,6 +71,24 @@ public interface AccessControlDispatcher {
    */
   User getUser(String metalake, String user) throws NoSuchUserException, 
NoSuchMetalakeException;
 
+  /**
+   * Lists the users.
+   *
+   * @param metalake The Metalake of the User.
+   * @return The User list.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   */
+  User[] listUsers(String metalake) throws NoSuchMetalakeException;
+
+  /**
+   * Lists the usernames.
+   *
+   * @param metalake The Metalake of the User.
+   * @return The username list.
+   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
+   */
+  String[] listUserNames(String metalake) throws NoSuchMetalakeException;
+
   /**
    * Adds a new Group.
    *
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 aa890667d..222b1ffb5 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java
@@ -69,6 +69,15 @@ public class AccessControlManager implements 
AccessControlDispatcher {
   }
 
   @Override
+  public String[] listUserNames(String metalake) throws 
NoSuchMetalakeException {
+    return userGroupManager.listUserNames(metalake);
+  }
+
+  @Override
+  public User[] listUsers(String metalake) throws NoSuchMetalakeException {
+    return userGroupManager.listUsers(metalake);
+  }
+
   public Group addGroup(String metalake, String group)
       throws GroupAlreadyExistsException, NoSuchMetalakeException {
     return userGroupManager.addGroup(metalake, group);
@@ -130,16 +139,6 @@ public class AccessControlManager implements 
AccessControlDispatcher {
     return roleManager.getRole(metalake, role);
   }
 
-  /**
-   * Deletes a Role.
-   *
-   * @param metalake The Metalake of the Role.
-   * @param role The name of the Role.
-   * @return True if the Role was successfully deleted, false only when 
there's no such role,
-   *     otherwise it will throw an exception.
-   * @throws NoSuchMetalakeException If the Metalake with the given name does 
not exist.
-   * @throws RuntimeException If deleting the Role encounters storage issues.
-   */
   public boolean deleteRole(String metalake, String role) throws 
NoSuchMetalakeException {
     return roleManager.deleteRole(metalake, role);
   }
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 094276689..4b7b4f2d8 100644
--- 
a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
+++ 
b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java
@@ -21,13 +21,18 @@ package org.apache.gravitino.authorization;
 import com.google.common.collect.Lists;
 import java.io.IOException;
 import java.time.Instant;
+import java.util.Arrays;
 import java.util.Collections;
+import java.util.Set;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
 import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.Field;
+import org.apache.gravitino.Namespace;
 import org.apache.gravitino.exceptions.GroupAlreadyExistsException;
 import org.apache.gravitino.exceptions.NoSuchEntityException;
 import org.apache.gravitino.exceptions.NoSuchGroupException;
+import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.exceptions.NoSuchUserException;
 import org.apache.gravitino.exceptions.UserAlreadyExistsException;
 import org.apache.gravitino.meta.AuditInfo;
@@ -35,6 +40,7 @@ import org.apache.gravitino.meta.GroupEntity;
 import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.storage.IdGenerator;
 import org.apache.gravitino.utils.PrincipalUtils;
+import org.glassfish.jersey.internal.guava.Sets;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -46,6 +52,7 @@ import org.slf4j.LoggerFactory;
 class UserGroupManager {
 
   private static final Logger LOG = 
LoggerFactory.getLogger(UserGroupManager.class);
+  private static final String METALAKE_DOES_NOT_EXIST_MSG = "Metalake %s does 
not exist";
 
   private final EntityStore store;
   private final IdGenerator idGenerator;
@@ -109,6 +116,37 @@ class UserGroupManager {
     }
   }
 
+  String[] listUserNames(String metalake) {
+    Set<Field> skippingFields = Sets.newHashSet();
+    skippingFields.add(UserEntity.ROLE_NAMES);
+    skippingFields.add(UserEntity.ROLE_IDS);
+
+    return Arrays.stream(listUsersInternal(metalake, skippingFields))
+        .map(User::name)
+        .toArray(String[]::new);
+  }
+
+  User[] listUsers(String metalake) {
+    return listUsersInternal(metalake, Collections.emptySet());
+  }
+
+  private User[] listUsersInternal(String metalake, Set<Field> skippingFields) 
{
+    try {
+      AuthorizationUtils.checkMetalakeExists(metalake);
+
+      Namespace namespace = AuthorizationUtils.ofUserNamespace(metalake);
+      return store
+          .list(namespace, UserEntity.class, Entity.EntityType.USER, 
skippingFields)
+          .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);
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 44dc491a7..730563862 100644
--- 
a/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
+++ 
b/core/src/main/java/org/apache/gravitino/hook/AccessControlHookDispatcher.java
@@ -69,6 +69,16 @@ public class AccessControlHookDispatcher implements 
AccessControlDispatcher {
     return dispatcher.getUser(metalake, user);
   }
 
+  @Override
+  public User[] listUsers(String metalake) throws NoSuchMetalakeException {
+    return dispatcher.listUsers(metalake);
+  }
+
+  @Override
+  public String[] listUserNames(String metalake) throws 
NoSuchMetalakeException {
+    return dispatcher.listUserNames(metalake);
+  }
+
   @Override
   public Group addGroup(String metalake, String group)
       throws GroupAlreadyExistsException, NoSuchMetalakeException {
diff --git a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java 
b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
index c71d731a9..df47215b4 100644
--- a/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
+++ b/core/src/main/java/org/apache/gravitino/meta/UserEntity.java
@@ -23,6 +23,7 @@ import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Set;
 import lombok.ToString;
 import org.apache.gravitino.Auditable;
 import org.apache.gravitino.Entity;
@@ -30,6 +31,7 @@ import org.apache.gravitino.Field;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.Namespace;
 import org.apache.gravitino.authorization.User;
+import org.glassfish.jersey.internal.guava.Sets;
 
 /** A class representing a user metadata entity in Apache Gravitino. */
 @ToString
@@ -154,6 +156,22 @@ public class UserEntity implements User, Entity, 
Auditable, HasIdentifier {
     return roleIds;
   }
 
+  /**
+   * Get the set of all the fields.
+   *
+   * @return The set of all the fields.
+   */
+  public static Set<Field> fieldSet() {
+    Set<Field> fields = Sets.newHashSet();
+    fields.add(ID);
+    fields.add(NAME);
+    fields.add(AUDIT_INFO);
+    fields.add(ROLE_IDS);
+    fields.add(ROLE_NAMES);
+
+    return Collections.unmodifiableSet(fields);
+  }
+
   @Override
   public boolean equals(Object o) {
     if (this == o) return true;
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 b23c76673..549c5fec2 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
@@ -26,11 +26,13 @@ import com.google.common.collect.Lists;
 import java.io.IOException;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 import java.util.function.Function;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.Field;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
@@ -88,7 +90,8 @@ public class JDBCBackend implements RelationalBackend {
 
   @Override
   public <E extends Entity & HasIdentifier> List<E> list(
-      Namespace namespace, Entity.EntityType entityType) throws IOException {
+      Namespace namespace, Entity.EntityType entityType, Set<Field> 
skippingFields)
+      throws IOException {
     switch (entityType) {
       case METALAKE:
         return (List<E>) MetalakeMetaService.getInstance().listMetalakes();
@@ -104,6 +107,9 @@ public class JDBCBackend implements RelationalBackend {
         return (List<E>) 
TopicMetaService.getInstance().listTopicsByNamespace(namespace);
       case TAG:
         return (List<E>) 
TagMetaService.getInstance().listTagsByNamespace(namespace);
+      case USER:
+        return (List<E>)
+            UserMetaService.getInstance().listUsersByNamespace(namespace, 
skippingFields);
       default:
         throw new UnsupportedEntityTypeException(
             "Unsupported entity type: %s for list operation", entityType);
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
index f15060e74..fe85754b4 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalBackend.java
@@ -21,10 +21,12 @@ package org.apache.gravitino.storage.relational;
 import java.io.Closeable;
 import java.io.IOException;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Function;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
+import org.apache.gravitino.Field;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.Namespace;
@@ -48,13 +50,17 @@ public interface RelationalBackend
    *
    * @param namespace The parent namespace of these entities.
    * @param entityType The type of these entities.
+   * @param skippingFields Some fields may have a relatively high acquisition 
cost, EntityStore
+   *     provide an optional setting to avoid fetching these high-cost fields 
to improve the
+   *     performance.
    * @return The list of entities associated with the given parent namespace 
and entityType, or null
    *     if the entities does not exist.
    * @throws NoSuchEntityException If the corresponding parent entity of these 
list entities cannot
    *     be found.
    * @throws IOException If the store operation fails
    */
-  <E extends Entity & HasIdentifier> List<E> list(Namespace namespace, 
Entity.EntityType entityType)
+  <E extends Entity & HasIdentifier> List<E> list(
+      Namespace namespace, Entity.EntityType entityType, Set<Field> 
skippingFields)
       throws NoSuchEntityException, IOException;
 
   /**
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
index 7eb1432c5..d7403729f 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/RelationalEntityStore.java
@@ -22,7 +22,9 @@ import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
 
 import com.google.common.collect.ImmutableMap;
 import java.io.IOException;
+import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 import java.util.function.Function;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.Configs;
@@ -30,6 +32,7 @@ import org.apache.gravitino.Entity;
 import org.apache.gravitino.EntityAlreadyExistsException;
 import org.apache.gravitino.EntitySerDe;
 import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.Field;
 import org.apache.gravitino.HasIdentifier;
 import org.apache.gravitino.MetadataObject;
 import org.apache.gravitino.NameIdentifier;
@@ -89,7 +92,14 @@ public class RelationalEntityStore
   @Override
   public <E extends Entity & HasIdentifier> List<E> list(
       Namespace namespace, Class<E> type, Entity.EntityType entityType) throws 
IOException {
-    return backend.list(namespace, entityType);
+    return backend.list(namespace, entityType, Collections.emptySet());
+  }
+
+  @Override
+  public <E extends Entity & HasIdentifier> List<E> list(
+      Namespace namespace, Class<E> type, Entity.EntityType entityType, 
Set<Field> skippingFields)
+      throws IOException {
+    return backend.list(namespace, entityType, skippingFields);
   }
 
   @Override
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaBaseSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaBaseSQLProvider.java
index a5db8e0f9..d3a49623d 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaBaseSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaBaseSQLProvider.java
@@ -19,6 +19,7 @@
 
 package org.apache.gravitino.storage.relational.mapper;
 
+import static 
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME;
 import static 
org.apache.gravitino.storage.relational.mapper.UserMetaMapper.USER_ROLE_RELATION_TABLE_NAME;
 import static 
org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper.USER_TABLE_NAME;
 
@@ -138,6 +139,44 @@ public class UserMetaBaseSQLProvider {
         + " AND us.deleted_at = 0 AND re.deleted_at = 0";
   }
 
+  public String listUserPOsByMetalake(@Param("metalakeName") String 
metalakeName) {
+    return "SELECT ut.user_id as userId, ut.user_name as userName,"
+        + " ut.metalake_id as metalakeId,"
+        + " ut.audit_info as auditInfo,"
+        + " ut.current_version as currentVersion, ut.last_version as 
lastVersion,"
+        + " ut.deleted_at as deletedAt"
+        + " FROM "
+        + USER_TABLE_NAME
+        + " ut JOIN "
+        + MetalakeMetaMapper.TABLE_NAME
+        + " mt ON ut.metalake_id = mt.metalake_id"
+        + " WHERE mt.metalake_name = #{metalakeName}"
+        + " AND ut.deleted_at = 0 AND mt.deleted_at = 0";
+  }
+
+  public String listExtendedUserPOsByMetalakeId(@Param("metalakeId") Long 
metalakeId) {
+    return "SELECT ut.user_id as userId, ut.user_name as userName,"
+        + " ut.metalake_id as metalakeId,"
+        + " ut.audit_info as auditInfo,"
+        + " ut.current_version as currentVersion, ut.last_version as 
lastVersion,"
+        + " ut.deleted_at as deletedAt,"
+        + " JSON_ARRAYAGG(rot.role_name) as roleNames,"
+        + " JSON_ARRAYAGG(rot.role_id) as roleIds"
+        + " FROM "
+        + USER_TABLE_NAME
+        + " ut LEFT OUTER JOIN "
+        + USER_ROLE_RELATION_TABLE_NAME
+        + " rt ON rt.user_id = ut.user_id"
+        + " LEFT OUTER JOIN "
+        + ROLE_TABLE_NAME
+        + " rot ON rot.role_id = rt.role_id"
+        + " WHERE "
+        + " ut.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 ut.metalake_id = 
#{metalakeId}"
+        + " GROUP BY ut.user_id";
+  }
+
   public String deleteUserMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
     return "DELETE FROM "
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
index ad794c395..19bbb4edd 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java
@@ -20,6 +20,7 @@
 package org.apache.gravitino.storage.relational.mapper;
 
 import java.util.List;
+import org.apache.gravitino.storage.relational.po.ExtendedUserPO;
 import org.apache.gravitino.storage.relational.po.UserPO;
 import org.apache.ibatis.annotations.DeleteProvider;
 import org.apache.ibatis.annotations.InsertProvider;
@@ -54,6 +55,14 @@ public interface UserMetaMapper {
   @InsertProvider(type = UserMetaSQLProviderFactory.class, method = 
"insertUserMeta")
   void insertUserMeta(@Param("userMeta") UserPO userPO);
 
+  @SelectProvider(type = UserMetaSQLProviderFactory.class, method = 
"listUserPOsByMetalake")
+  List<UserPO> listUserPOsByMetalake(@Param("metalakeName") String 
metalakeName);
+
+  @SelectProvider(
+      type = UserMetaSQLProviderFactory.class,
+      method = "listExtendedUserPOsByMetalakeId")
+  List<ExtendedUserPO> listExtendedUserPOsByMetalakeId(@Param("metalakeId") 
Long metalakeId);
+
   @InsertProvider(
       type = UserMetaSQLProviderFactory.class,
       method = "insertUserMetaOnDuplicateKeyUpdate")
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
index ab49a4542..9d6658bd7 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaSQLProviderFactory.java
@@ -22,6 +22,7 @@ package org.apache.gravitino.storage.relational.mapper;
 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.h2.UserMetaH2Provider;
 import 
org.apache.gravitino.storage.relational.mapper.postgresql.UserMetaPostgreSQLProvider;
 import org.apache.gravitino.storage.relational.po.UserPO;
 import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper;
@@ -48,8 +49,6 @@ public class UserMetaSQLProviderFactory {
 
   static class UserMetaMySQLProvider extends UserMetaBaseSQLProvider {}
 
-  static class UserMetaH2Provider extends UserMetaBaseSQLProvider {}
-
   public static String selectUserIdByMetalakeIdAndName(
       @Param("metalakeId") Long metalakeId, @Param("userName") String 
userName) {
     return getProvider().selectUserIdByMetalakeIdAndName(metalakeId, userName);
@@ -85,6 +84,14 @@ public class UserMetaSQLProviderFactory {
     return getProvider().listUsersByRoleId(roleId);
   }
 
+  public static String listUserPOsByMetalake(@Param("metalakeName") String 
metalakeName) {
+    return getProvider().listUserPOsByMetalake(metalakeName);
+  }
+
+  public static String listExtendedUserPOsByMetalakeId(@Param("metalakeId") 
Long metalakeId) {
+    return getProvider().listExtendedUserPOsByMetalakeId(metalakeId);
+  }
+
   public static String deleteUserMetasByLegacyTimeline(
       @Param("legacyTimeline") Long legacyTimeline, @Param("limit") int limit) 
{
     return getProvider().deleteUserMetasByLegacyTimeline(legacyTimeline, 
limit);
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/h2/UserMetaH2Provider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/h2/UserMetaH2Provider.java
new file mode 100644
index 000000000..12779d2d7
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/h2/UserMetaH2Provider.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.h2;
+
+import static 
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME;
+import static 
org.apache.gravitino.storage.relational.mapper.UserMetaMapper.USER_ROLE_RELATION_TABLE_NAME;
+import static 
org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper.USER_TABLE_NAME;
+
+import org.apache.gravitino.storage.relational.mapper.UserMetaBaseSQLProvider;
+import org.apache.ibatis.annotations.Param;
+
+public class UserMetaH2Provider extends UserMetaBaseSQLProvider {
+  @Override
+  public String listExtendedUserPOsByMetalakeId(@Param("metalakeId") Long 
metalakeId) {
+    return "SELECT ut.user_id as userId, ut.user_name as userName,"
+        + " ut.metalake_id as metalakeId,"
+        + " ut.audit_info as auditInfo,"
+        + " ut.current_version as currentVersion, ut.last_version as 
lastVersion,"
+        + " ut.deleted_at as deletedAt,"
+        + " '[' || GROUP_CONCAT('\"' || rot.role_name || '\"') || ']' as 
roleNames,"
+        + " '[' || GROUP_CONCAT('\"' || rot.role_id || '\"') || ']' as roleIds"
+        + " FROM "
+        + USER_TABLE_NAME
+        + " ut LEFT OUTER JOIN "
+        + USER_ROLE_RELATION_TABLE_NAME
+        + " rt ON rt.user_id = ut.user_id"
+        + " LEFT OUTER JOIN "
+        + ROLE_TABLE_NAME
+        + " rot ON rot.role_id = rt.role_id"
+        + " WHERE "
+        + " ut.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 ut.metalake_id = 
#{metalakeId}"
+        + " GROUP BY ut.user_id";
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/postgresql/UserMetaPostgreSQLProvider.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/postgresql/UserMetaPostgreSQLProvider.java
index af7d65d2a..846880943 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/postgresql/UserMetaPostgreSQLProvider.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/postgresql/UserMetaPostgreSQLProvider.java
@@ -18,6 +18,8 @@
  */
 package org.apache.gravitino.storage.relational.mapper.postgresql;
 
+import static 
org.apache.gravitino.storage.relational.mapper.RoleMetaMapper.ROLE_TABLE_NAME;
+import static 
org.apache.gravitino.storage.relational.mapper.UserMetaMapper.USER_ROLE_RELATION_TABLE_NAME;
 import static 
org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper.USER_TABLE_NAME;
 
 import org.apache.gravitino.storage.relational.mapper.UserMetaBaseSQLProvider;
@@ -66,4 +68,28 @@ public class UserMetaPostgreSQLProvider extends 
UserMetaBaseSQLProvider {
         + " last_version = #{userMeta.lastVersion},"
         + " deleted_at = #{userMeta.deletedAt}";
   }
+
+  @Override
+  public String listExtendedUserPOsByMetalakeId(Long metalakeId) {
+    return "SELECT ut.user_id as userId, ut.user_name as userName,"
+        + " ut.metalake_id as metalakeId,"
+        + " ut.audit_info as auditInfo,"
+        + " ut.current_version as currentVersion, ut.last_version as 
lastVersion,"
+        + " ut.deleted_at as deletedAt,"
+        + " JSON_AGG(rot.role_name) as roleNames,"
+        + " JSON_AGG(rot.role_id) as roleIds"
+        + " FROM "
+        + USER_TABLE_NAME
+        + " ut LEFT OUTER JOIN "
+        + USER_ROLE_RELATION_TABLE_NAME
+        + " rt ON rt.user_id = ut.user_id"
+        + " LEFT OUTER JOIN "
+        + ROLE_TABLE_NAME
+        + " rot ON rot.role_id = rt.role_id"
+        + " WHERE "
+        + " ut.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 ut.metalake_id = 
#{metalakeId}"
+        + " GROUP BY ut.user_id";
+  }
 }
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedUserPO.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedUserPO.java
new file mode 100644
index 000000000..919056c48
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedUserPO.java
@@ -0,0 +1,60 @@
+/*
+ * 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 com.google.common.base.Objects;
+
+/**
+ * ExtendedUserPO add extra roleNames and roleIds for UserPO. 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 ExtendedUserPO extends UserPO {
+
+  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 ExtendedUserPO)) {
+      return false;
+    }
+    ExtendedUserPO extendedUserPO = (ExtendedUserPO) o;
+
+    return super.equals(o)
+        && Objects.equal(getRoleIds(), extendedUserPO.getRoleIds())
+        && Objects.equal(getRoleNames(), extendedUserPO.getRoleNames());
+  }
+
+  @Override
+  public int hashCode() {
+    return Objects.hashCode(super.hashCode(), getRoleIds(), getRoleNames());
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/SupportsDesiredFields.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SupportsDesiredFields.java
new file mode 100644
index 000000000..42978fa50
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SupportsDesiredFields.java
@@ -0,0 +1,40 @@
+/*
+ * 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.service;
+
+import java.util.Set;
+import org.apache.gravitino.Field;
+
+/** The handler supports to skip fields to acquire part desired fields. */
+interface SupportsDesiredFields<R> {
+
+  /**
+   * The fields which could be desired.
+   *
+   * @return The fields which are desired.
+   */
+  Set<Field> desiredFields();
+
+  /**
+   * The return value of the handler.
+   *
+   * @return The return value of the handler.
+   */
+  R execute();
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/SupportsDesiredFieldsHandlers.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SupportsDesiredFieldsHandlers.java
new file mode 100644
index 000000000..b5b10a7b6
--- /dev/null
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/SupportsDesiredFieldsHandlers.java
@@ -0,0 +1,50 @@
+/*
+ * 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.service;
+
+import com.google.common.collect.Lists;
+import java.util.List;
+import java.util.Set;
+import org.apache.gravitino.Field;
+
+/**
+ * This class is the collection wrapper of SupportsDesiredFields handler. The 
class will contain all
+ * the handlers can proceed the data. We can choose different handlers 
according to the desired
+ * fields to acquire better performance.
+ *
+ * @param <T> The value type which the handler will return.
+ */
+class SupportsDesiredFieldsHandlers<T> {
+  private final List<SupportsDesiredFields<T>> methods = Lists.newArrayList();
+
+  // We should put the low-cost handler into the front of the list.
+  void addHandler(SupportsDesiredFields<T> supportsSkippingFields) {
+    methods.add(supportsSkippingFields);
+  }
+
+  T execute(Set<Field> desiredFields) {
+    for (SupportsDesiredFields<T> method : methods) {
+      if (method.desiredFields().containsAll(desiredFields)) {
+        return method.execute();
+      }
+    }
+
+    throw new IllegalArgumentException("Don't support skip fields");
+  }
+}
diff --git 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
index e7d0a435a..f64b4ab40 100644
--- 
a/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
+++ 
b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java
@@ -30,8 +30,10 @@ import java.util.Set;
 import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.apache.gravitino.Entity;
+import org.apache.gravitino.Field;
 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.RoleEntity;
@@ -39,6 +41,7 @@ import org.apache.gravitino.meta.UserEntity;
 import org.apache.gravitino.storage.relational.mapper.OwnerMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.UserMetaMapper;
 import org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper;
+import org.apache.gravitino.storage.relational.po.ExtendedUserPO;
 import org.apache.gravitino.storage.relational.po.RolePO;
 import org.apache.gravitino.storage.relational.po.UserPO;
 import org.apache.gravitino.storage.relational.po.UserRoleRelPO;
@@ -246,6 +249,20 @@ public class UserMetaService {
     return newEntity;
   }
 
+  public List<UserEntity> listUsersByNamespace(Namespace namespace, Set<Field> 
skippingFields) {
+    AuthorizationUtils.checkUserNamespace(namespace);
+    String metalakeName = namespace.level(0);
+
+    SupportsDesiredFieldsHandlers<List<UserEntity>> handlers =
+        new SupportsDesiredFieldsHandlers<>();
+    handlers.addHandler(new ListDesiredRolesHandler(metalakeName));
+    handlers.addHandler(new ListAllFieldsHandler(metalakeName));
+
+    Set<Field> desiredFields = Sets.newHashSet(UserEntity.fieldSet());
+    desiredFields.removeAll(skippingFields);
+    return handlers.execute(desiredFields);
+  }
+
   public int deleteUserMetasByLegacyTimeline(long legacyTimeline, int limit) {
     int[] userDeletedCount = new int[] {0};
     int[] userRoleRelDeletedCount = new int[] {0};
@@ -265,4 +282,63 @@ public class UserMetaService {
 
     return userDeletedCount[0] + userRoleRelDeletedCount[0];
   }
+
+  private static class ListDesiredRolesHandler implements 
SupportsDesiredFields<List<UserEntity>> {
+    private final String metalakeName;
+
+    ListDesiredRolesHandler(String metalakeName) {
+      this.metalakeName = metalakeName;
+    }
+
+    @Override
+    public Set<Field> desiredFields() {
+      Set<Field> requiredFields = Sets.newHashSet(UserEntity.fieldSet());
+      requiredFields.remove(UserEntity.ROLE_IDS);
+      requiredFields.remove(UserEntity.ROLE_NAMES);
+
+      return requiredFields;
+    }
+
+    @Override
+    public List<UserEntity> execute() {
+      List<UserPO> userPOs =
+          SessionUtils.getWithoutCommit(
+              UserMetaMapper.class, mapper -> 
mapper.listUserPOsByMetalake(metalakeName));
+      return userPOs.stream()
+          .map(
+              po ->
+                  POConverters.fromUserPO(
+                      po,
+                      Collections.emptyList(),
+                      AuthorizationUtils.ofUserNamespace(metalakeName)))
+          .collect(Collectors.toList());
+    }
+  }
+
+  private static class ListAllFieldsHandler implements 
SupportsDesiredFields<List<UserEntity>> {
+    final String metalakeName;
+
+    ListAllFieldsHandler(String metalakeName) {
+      this.metalakeName = metalakeName;
+    }
+
+    @Override
+    public Set<Field> desiredFields() {
+      return UserEntity.fieldSet();
+    }
+
+    @Override
+    public List<UserEntity> execute() {
+      Long metalakeId = 
MetalakeMetaService.getInstance().getMetalakeIdByName(metalakeName);
+      List<ExtendedUserPO> userPOs =
+          SessionUtils.getWithoutCommit(
+              UserMetaMapper.class, mapper -> 
mapper.listExtendedUserPOsByMetalakeId(metalakeId));
+      return userPOs.stream()
+          .map(
+              po ->
+                  POConverters.fromExtendedUserPO(
+                      po, AuthorizationUtils.ofUserNamespace(metalakeName)))
+          .collect(Collectors.toList());
+    }
+  }
 }
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 82d739a41..da1f3d06a 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
@@ -24,6 +24,7 @@ import com.google.common.collect.Lists;
 import java.time.Instant;
 import java.util.List;
 import java.util.Map;
+import java.util.Objects;
 import java.util.stream.Collectors;
 import org.apache.commons.lang3.StringUtils;
 import org.apache.gravitino.Catalog;
@@ -48,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.ExtendedUserPO;
 import org.apache.gravitino.storage.relational.po.FilesetPO;
 import org.apache.gravitino.storage.relational.po.FilesetVersionPO;
 import org.apache.gravitino.storage.relational.po.GroupPO;
@@ -728,6 +730,56 @@ public class POConverters {
     }
   }
 
+  /**
+   * Convert {@link ExtendedUserPO} to {@link UserEntity}
+   *
+   * @param userPO CombinedUserPo object to be converted
+   * @param namespace Namespace object to be associated with the user
+   * @return UserEntity object from ExtendedUserPO object
+   */
+  public static UserEntity fromExtendedUserPO(ExtendedUserPO userPO, Namespace 
namespace) {
+    try {
+      UserEntity.Builder builder =
+          UserEntity.builder()
+              .withId(userPO.getUserId())
+              .withName(userPO.getUserName())
+              .withNamespace(namespace)
+              .withAuditInfo(
+                  JsonUtils.anyFieldMapper().readValue(userPO.getAuditInfo(), 
AuditInfo.class));
+      if (StringUtils.isNotBlank(userPO.getRoleNames())) {
+        List<String> roleNamesFromJson =
+            JsonUtils.anyFieldMapper().readValue(userPO.getRoleNames(), 
List.class);
+        List<String> roleNames =
+            
roleNamesFromJson.stream().filter(StringUtils::isNotBlank).collect(Collectors.toList());
+        if (!roleNames.isEmpty()) {
+          builder.withRoleNames(roleNames);
+        }
+      }
+
+      if (StringUtils.isNotBlank(userPO.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(userPO.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);
+    }
+  }
+
   /**
    * Convert {@link GroupPO} to {@link GroupEntity}
    *
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 fd27771a0..1c7a26dec 100644
--- 
a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
+++ 
b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java
@@ -18,7 +18,20 @@
  */
 package org.apache.gravitino.authorization;
 
+import static org.apache.gravitino.Configs.CATALOG_CACHE_EVICTION_INTERVAL_MS;
+import static org.apache.gravitino.Configs.DEFAULT_ENTITY_RELATIONAL_STORE;
+import static 
org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER;
+import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_JDBC_BACKEND_URL;
+import static org.apache.gravitino.Configs.ENTITY_RELATIONAL_STORE;
+import static org.apache.gravitino.Configs.ENTITY_STORE;
+import static org.apache.gravitino.Configs.RELATIONAL_ENTITY_STORE;
 import static org.apache.gravitino.Configs.SERVICE_ADMINS;
+import static org.apache.gravitino.Configs.STORE_DELETE_AFTER_TIME;
+import static org.apache.gravitino.Configs.STORE_TRANSACTION_MAX_SKEW_TIME;
+import static org.apache.gravitino.Configs.TREE_LOCK_CLEAN_INTERVAL;
+import static org.apache.gravitino.Configs.TREE_LOCK_MAX_NODE_IN_MEMORY;
+import static org.apache.gravitino.Configs.TREE_LOCK_MIN_NODE_IN_MEMORY;
+import static org.apache.gravitino.Configs.VERSION_RETENTION_COUNT;
 import static org.mockito.ArgumentMatchers.any;
 import static org.mockito.Mockito.mock;
 import static org.mockito.Mockito.reset;
@@ -27,13 +40,20 @@ import static org.mockito.Mockito.when;
 
 import com.google.common.collect.ImmutableMap;
 import com.google.common.collect.Lists;
+import java.io.File;
 import java.io.IOException;
 import java.time.Instant;
+import java.util.Arrays;
+import java.util.Comparator;
 import java.util.Map;
+import java.util.UUID;
 import org.apache.commons.lang3.reflect.FieldUtils;
+import org.apache.gravitino.Catalog;
 import org.apache.gravitino.Config;
 import org.apache.gravitino.EntityStore;
+import org.apache.gravitino.EntityStoreFactory;
 import org.apache.gravitino.GravitinoEnv;
+import org.apache.gravitino.Namespace;
 import org.apache.gravitino.StringIdentifier;
 import org.apache.gravitino.catalog.CatalogManager;
 import org.apache.gravitino.connector.BaseCatalog;
@@ -45,15 +65,17 @@ import org.apache.gravitino.exceptions.NoSuchRoleException;
 import org.apache.gravitino.exceptions.NoSuchUserException;
 import org.apache.gravitino.exceptions.RoleAlreadyExistsException;
 import org.apache.gravitino.exceptions.UserAlreadyExistsException;
+import org.apache.gravitino.lock.LockManager;
 import org.apache.gravitino.meta.AuditInfo;
 import org.apache.gravitino.meta.BaseMetalake;
+import org.apache.gravitino.meta.CatalogEntity;
 import org.apache.gravitino.meta.SchemaVersion;
 import org.apache.gravitino.storage.RandomIdGenerator;
-import org.apache.gravitino.storage.memory.TestMemoryEntityStore;
 import org.junit.jupiter.api.AfterAll;
 import org.junit.jupiter.api.Assertions;
 import org.junit.jupiter.api.BeforeAll;
 import org.junit.jupiter.api.Test;
+import org.mockito.Mockito;
 
 public class TestAccessControlManager {
 
@@ -62,9 +84,13 @@ public class TestAccessControlManager {
   private static EntityStore entityStore;
   private static CatalogManager catalogManager = mock(CatalogManager.class);
 
-  private static Config config;
+  private static final Config config = Mockito.mock(Config.class);
 
   private static String METALAKE = "metalake";
+  private static final String JDBC_STORE_PATH =
+      "/tmp/gravitino_jdbc_entityStore_" + 
UUID.randomUUID().toString().replace("-", "");
+  private static final String DB_DIR = JDBC_STORE_PATH + "/testdb";
+
   private static AuthorizationPlugin authorizationPlugin;
 
   private static BaseMetalake metalakeEntity =
@@ -76,16 +102,61 @@ public class TestAccessControlManager {
           .withVersion(SchemaVersion.V_0_1)
           .build();
 
+  private static BaseMetalake listMetalakeEntity =
+      BaseMetalake.builder()
+          .withId(2L)
+          .withName("metalake_list")
+          .withAuditInfo(
+              
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+          .withVersion(SchemaVersion.V_0_1)
+          .build();
+
   @BeforeAll
   public static void setUp() throws Exception {
-    config = new Config(false) {};
-    config.set(SERVICE_ADMINS, Lists.newArrayList("admin1", "admin2"));
-
-    entityStore = new TestMemoryEntityStore.InMemoryEntityStore();
+    File dbDir = new File(DB_DIR);
+    dbDir.mkdirs();
+    
Mockito.when(config.get(SERVICE_ADMINS)).thenReturn(Lists.newArrayList("admin1",
 "admin2"));
+    Mockito.when(config.get(ENTITY_STORE)).thenReturn(RELATIONAL_ENTITY_STORE);
+    
Mockito.when(config.get(ENTITY_RELATIONAL_STORE)).thenReturn(DEFAULT_ENTITY_RELATIONAL_STORE);
+    Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_URL))
+        
.thenReturn(String.format("jdbc:h2:file:%s;DB_CLOSE_DELAY=-1;MODE=MYSQL", 
DB_DIR));
+    
Mockito.when(config.get(ENTITY_RELATIONAL_JDBC_BACKEND_DRIVER)).thenReturn("org.h2.Driver");
+    
Mockito.when(config.get(STORE_TRANSACTION_MAX_SKEW_TIME)).thenReturn(1000L);
+    Mockito.when(config.get(STORE_DELETE_AFTER_TIME)).thenReturn(20 * 60 * 
1000L);
+    Mockito.when(config.get(VERSION_RETENTION_COUNT)).thenReturn(1L);
+    
Mockito.when(config.get(CATALOG_CACHE_EVICTION_INTERVAL_MS)).thenReturn(1000L);
+    Mockito.doReturn(100000L).when(config).get(TREE_LOCK_MAX_NODE_IN_MEMORY);
+    Mockito.doReturn(1000L).when(config).get(TREE_LOCK_MIN_NODE_IN_MEMORY);
+    Mockito.doReturn(36000L).when(config).get(TREE_LOCK_CLEAN_INTERVAL);
+    FieldUtils.writeField(GravitinoEnv.getInstance(), "lockManager", new 
LockManager(config), true);
+    entityStore = EntityStoreFactory.createEntityStore(config);
     entityStore.initialize(config);
-    entityStore.setSerDe(null);
 
     entityStore.put(metalakeEntity, true);
+    entityStore.put(listMetalakeEntity, true);
+
+    CatalogEntity catalogEntity =
+        CatalogEntity.builder()
+            .withId(3L)
+            .withName("catalog")
+            .withNamespace(Namespace.of("metalake"))
+            .withType(Catalog.Type.RELATIONAL)
+            .withProvider("test")
+            .withAuditInfo(
+                
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+            .build();
+    entityStore.put(catalogEntity, true);
+    CatalogEntity anotherCatalogEntity =
+        CatalogEntity.builder()
+            .withId(4L)
+            .withName("catalog")
+            .withNamespace(Namespace.of("metalake_list"))
+            .withType(Catalog.Type.RELATIONAL)
+            .withProvider("test")
+            .withAuditInfo(
+                
AuditInfo.builder().withCreator("test").withCreateTime(Instant.now()).build())
+            .build();
+    entityStore.put(anotherCatalogEntity, true);
 
     accessControlManager = new AccessControlManager(entityStore, new 
RandomIdGenerator(), config);
     FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", 
entityStore, true);
@@ -108,11 +179,11 @@ public class TestAccessControlManager {
 
   @Test
   public void testAddUser() {
-    User user = accessControlManager.addUser("metalake", "testAdd");
+    User user = accessControlManager.addUser(METALAKE, "testAdd");
     Assertions.assertEquals("testAdd", user.name());
     Assertions.assertTrue(user.roles().isEmpty());
 
-    user = accessControlManager.addUser("metalake", 
"testAddWithOptionalField");
+    user = accessControlManager.addUser(METALAKE, "testAddWithOptionalField");
 
     Assertions.assertEquals("testAddWithOptionalField", user.name());
     Assertions.assertTrue(user.roles().isEmpty());
@@ -123,99 +194,121 @@ public class TestAccessControlManager {
 
     // Test with UserAlreadyExistsException
     Assertions.assertThrows(
-        UserAlreadyExistsException.class,
-        () -> accessControlManager.addUser("metalake", "testAdd"));
+        UserAlreadyExistsException.class, () -> 
accessControlManager.addUser(METALAKE, "testAdd"));
   }
 
   @Test
   public void testGetUser() {
-    accessControlManager.addUser("metalake", "testGet");
+    accessControlManager.addUser(METALAKE, "testGet");
 
-    User user = accessControlManager.getUser("metalake", "testGet");
+    User user = accessControlManager.getUser(METALAKE, "testGet");
     Assertions.assertEquals("testGet", user.name());
 
     // Test with NoSuchMetalakeException
     Assertions.assertThrows(
-        NoSuchMetalakeException.class, () -> 
accessControlManager.addUser("no-exist", "testAdd"));
+        NoSuchMetalakeException.class, () -> 
accessControlManager.getUser("no-exist", "testAdd"));
 
     // Test to get non-existed user
     Throwable exception =
         Assertions.assertThrows(
-            NoSuchUserException.class, () -> 
accessControlManager.getUser("metalake", "not-exist"));
+            NoSuchUserException.class, () -> 
accessControlManager.getUser(METALAKE, "not-exist"));
     Assertions.assertTrue(exception.getMessage().contains("User not-exist does 
not exist"));
   }
 
   @Test
   public void testRemoveUser() {
-    accessControlManager.addUser("metalake", "testRemove");
+    accessControlManager.addUser(METALAKE, "testRemove");
 
     // Test with NoSuchMetalakeException
     Assertions.assertThrows(
-        NoSuchMetalakeException.class, () -> 
accessControlManager.addUser("no-exist", "testAdd"));
+        NoSuchMetalakeException.class,
+        () -> accessControlManager.removeUser("no-exist", "testAdd"));
 
     // Test to remove user
-    boolean removed = accessControlManager.removeUser("metalake", 
"testRemove");
+    boolean removed = accessControlManager.removeUser(METALAKE, "testRemove");
     Assertions.assertTrue(removed);
 
     // Test to remove non-existed user
-    boolean removed1 = accessControlManager.removeUser("metalake", "no-exist");
+    boolean removed1 = accessControlManager.removeUser(METALAKE, "no-exist");
     Assertions.assertFalse(removed1);
   }
 
+  @Test
+  public void testListUsers() {
+    accessControlManager.addUser("metalake_list", "testList1");
+    accessControlManager.addUser("metalake_list", "testList2");
+
+    // Test to list users
+    String[] expectUsernames = new String[] {"testList1", "testList2"};
+    String[] actualUsernames = 
accessControlManager.listUserNames("metalake_list");
+    Arrays.sort(actualUsernames);
+    Assertions.assertArrayEquals(expectUsernames, actualUsernames);
+    User[] users = accessControlManager.listUsers("metalake_list");
+    Arrays.sort(users, Comparator.comparing(User::name));
+    Assertions.assertArrayEquals(
+        expectUsernames, 
Arrays.stream(users).map(User::name).toArray(String[]::new));
+
+    // Test with NoSuchMetalakeException
+    Assertions.assertThrows(
+        NoSuchMetalakeException.class, () -> 
accessControlManager.listUserNames("no-exist"));
+    Assertions.assertThrows(
+        NoSuchMetalakeException.class, () -> 
accessControlManager.listUsers("no-exist"));
+  }
+
   @Test
   public void testAddGroup() {
-    Group group = accessControlManager.addGroup("metalake", "testAdd");
+    Group group = accessControlManager.addGroup(METALAKE, "testAdd");
     Assertions.assertEquals("testAdd", group.name());
     Assertions.assertTrue(group.roles().isEmpty());
 
-    group = accessControlManager.addGroup("metalake", 
"testAddWithOptionalField");
+    group = accessControlManager.addGroup(METALAKE, 
"testAddWithOptionalField");
 
     Assertions.assertEquals("testAddWithOptionalField", group.name());
     Assertions.assertTrue(group.roles().isEmpty());
 
     // Test with NoSuchMetalakeException
     Assertions.assertThrows(
-        NoSuchMetalakeException.class, () -> 
accessControlManager.addUser("no-exist", "testAdd"));
+        NoSuchMetalakeException.class, () -> 
accessControlManager.addGroup("no-exist", "testAdd"));
 
     // Test with GroupAlreadyExistsException
     Assertions.assertThrows(
         GroupAlreadyExistsException.class,
-        () -> accessControlManager.addGroup("metalake", "testAdd"));
+        () -> accessControlManager.addGroup(METALAKE, "testAdd"));
   }
 
   @Test
   public void testGetGroup() {
-    accessControlManager.addGroup("metalake", "testGet");
+    accessControlManager.addGroup(METALAKE, "testGet");
 
-    Group group = accessControlManager.getGroup("metalake", "testGet");
+    Group group = accessControlManager.getGroup(METALAKE, "testGet");
     Assertions.assertEquals("testGet", group.name());
 
     // Test with NoSuchMetalakeException
     Assertions.assertThrows(
-        NoSuchMetalakeException.class, () -> 
accessControlManager.addUser("no-exist", "testAdd"));
+        NoSuchMetalakeException.class, () -> 
accessControlManager.getGroup("no-exist", "testAdd"));
 
     // Test to get non-existed group
     Throwable exception =
         Assertions.assertThrows(
-            NoSuchGroupException.class,
-            () -> accessControlManager.getGroup("metalake", "not-exist"));
+            NoSuchGroupException.class, () -> 
accessControlManager.getGroup(METALAKE, "not-exist"));
     Assertions.assertTrue(exception.getMessage().contains("Group not-exist 
does not exist"));
   }
 
   @Test
   public void testRemoveGroup() {
-    accessControlManager.addGroup("metalake", "testRemove");
+    accessControlManager.addGroup(METALAKE, "testRemove");
 
     // Test with NoSuchMetalakeException
     Assertions.assertThrows(
-        NoSuchMetalakeException.class, () -> 
accessControlManager.addUser("no-exist", "testAdd"));
+        NoSuchMetalakeException.class,
+        () -> accessControlManager.removeGroup("no-exist", "testAdd"));
 
     // Test to remove group
-    boolean removed = accessControlManager.removeGroup("metalake", 
"testRemove");
+    boolean removed = accessControlManager.removeGroup(METALAKE, "testRemove");
     Assertions.assertTrue(removed);
 
     // Test to remove non-existed group
-    boolean removed1 = accessControlManager.removeUser("metalake", "no-exist");
+    boolean removed1 = accessControlManager.removeGroup(METALAKE, "no-exist");
     Assertions.assertFalse(removed1);
   }
 
@@ -233,7 +326,7 @@ public class TestAccessControlManager {
 
     Role role =
         accessControlManager.createRole(
-            "metalake",
+            METALAKE,
             "create",
             props,
             Lists.newArrayList(
@@ -248,7 +341,7 @@ public class TestAccessControlManager {
         RoleAlreadyExistsException.class,
         () ->
             accessControlManager.createRole(
-                "metalake",
+                METALAKE,
                 "create",
                 props,
                 Lists.newArrayList(
@@ -261,22 +354,22 @@ public class TestAccessControlManager {
     Map<String, String> props = ImmutableMap.of("k1", "v1");
 
     accessControlManager.createRole(
-        "metalake",
+        METALAKE,
         "loadRole",
         props,
         Lists.newArrayList(
             SecurableObjects.ofCatalog(
                 "catalog", 
Lists.newArrayList(Privileges.UseCatalog.allow()))));
 
-    Role role = accessControlManager.getRole("metalake", "loadRole");
+    Role role = accessControlManager.getRole(METALAKE, "loadRole");
 
     Assertions.assertEquals("loadRole", role.name());
     testProperties(props, role.properties());
 
-    // Test load non-existed group
+    // Test load non-existed role
     Throwable exception =
         Assertions.assertThrows(
-            NoSuchRoleException.class, () -> 
accessControlManager.getRole("metalake", "not-exist"));
+            NoSuchRoleException.class, () -> 
accessControlManager.getRole(METALAKE, "not-exist"));
     Assertions.assertTrue(exception.getMessage().contains("Role not-exist does 
not exist"));
   }
 
@@ -285,7 +378,7 @@ public class TestAccessControlManager {
     Map<String, String> props = ImmutableMap.of("k1", "v1");
 
     accessControlManager.createRole(
-        "metalake",
+        METALAKE,
         "testDrop",
         props,
         Lists.newArrayList(
@@ -294,13 +387,13 @@ public class TestAccessControlManager {
 
     // Test drop role
     reset(authorizationPlugin);
-    boolean dropped = accessControlManager.deleteRole("metalake", "testDrop");
+    boolean dropped = accessControlManager.deleteRole(METALAKE, "testDrop");
     Assertions.assertTrue(dropped);
 
     verify(authorizationPlugin).onRoleDeleted(any());
 
     // Test drop non-existed role
-    boolean dropped1 = accessControlManager.deleteRole("metalake", "no-exist");
+    boolean dropped1 = accessControlManager.deleteRole(METALAKE, "no-exist");
     Assertions.assertFalse(dropped1);
   }
 
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
index 26430d2fb..b141eb696 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/TestJDBCBackend.java
@@ -45,6 +45,7 @@ import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.Instant;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -713,24 +714,30 @@ public class TestJDBCBackend {
     backend.insert(anotherTagEntity, false);
 
     // meta data list
-    List<BaseMetalake> metaLakes = backend.list(metalake.namespace(), 
Entity.EntityType.METALAKE);
+    List<BaseMetalake> metaLakes =
+        backend.list(metalake.namespace(), Entity.EntityType.METALAKE, 
Collections.emptySet());
     assertTrue(metaLakes.contains(metalake));
 
-    List<CatalogEntity> catalogs = backend.list(catalog.namespace(), 
Entity.EntityType.CATALOG);
+    List<CatalogEntity> catalogs =
+        backend.list(catalog.namespace(), Entity.EntityType.CATALOG, 
Collections.emptySet());
     assertTrue(catalogs.contains(catalog));
 
-    List<SchemaEntity> schemas = backend.list(schema.namespace(), 
Entity.EntityType.SCHEMA);
+    List<SchemaEntity> schemas =
+        backend.list(schema.namespace(), Entity.EntityType.SCHEMA, 
Collections.emptySet());
     assertTrue(schemas.contains(schema));
 
-    List<TableEntity> tables = backend.list(table.namespace(), 
Entity.EntityType.TABLE);
+    List<TableEntity> tables =
+        backend.list(table.namespace(), Entity.EntityType.TABLE, 
Collections.emptySet());
     assertTrue(tables.contains(table));
 
-    List<FilesetEntity> filesets = backend.list(fileset.namespace(), 
Entity.EntityType.FILESET);
+    List<FilesetEntity> filesets =
+        backend.list(fileset.namespace(), Entity.EntityType.FILESET, 
Collections.emptySet());
     assertFalse(filesets.contains(fileset));
     assertTrue(filesets.contains(filesetV2));
     assertEquals("2", 
filesets.get(filesets.indexOf(filesetV2)).properties().get("version"));
 
-    List<TopicEntity> topics = backend.list(topic.namespace(), 
Entity.EntityType.TOPIC);
+    List<TopicEntity> topics =
+        backend.list(topic.namespace(), Entity.EntityType.TOPIC, 
Collections.emptySet());
     assertTrue(topics.contains(topic));
 
     RoleEntity roleEntity = backend.get(role.nameIdentifier(), 
Entity.EntityType.ROLE);
@@ -756,7 +763,8 @@ public class TestJDBCBackend {
 
     TagEntity tagEntity = backend.get(tag.nameIdentifier(), 
Entity.EntityType.TAG);
     assertEquals(tag, tagEntity);
-    List<TagEntity> tags = backend.list(tag.namespace(), 
Entity.EntityType.TAG);
+    List<TagEntity> tags =
+        backend.list(tag.namespace(), Entity.EntityType.TAG, 
Collections.emptySet());
     assertTrue(tags.contains(tag));
     assertEquals(1, tags.size());
 
diff --git 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
index 326ccfc2d..0d037317d 100644
--- 
a/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
+++ 
b/core/src/test/java/org/apache/gravitino/storage/relational/service/TestUserMetaService.java
@@ -27,6 +27,8 @@ import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.sql.Statement;
 import java.time.Instant;
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 import java.util.Optional;
 import java.util.function.Function;
@@ -118,6 +120,77 @@ class TestUserMetaService extends TestJDBCBackend {
         Sets.newHashSet(user2.roleNames()), 
Sets.newHashSet(actualUser.roleNames()));
   }
 
+  @Test
+  void testListUsers() 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);
+
+    UserEntity user1 =
+        createUserEntity(
+            RandomIdGenerator.INSTANCE.nextId(),
+            AuthorizationUtils.ofUserNamespace(metalakeName),
+            "user1",
+            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);
+
+    UserEntity user2 =
+        createUserEntity(
+            RandomIdGenerator.INSTANCE.nextId(),
+            AuthorizationUtils.ofUserNamespace("metalake"),
+            "user2",
+            auditInfo,
+            Lists.newArrayList(role1.name(), role2.name()),
+            Lists.newArrayList(role1.id(), role2.id()));
+
+    backend.insert(user1, false);
+    backend.insert(user2, false);
+
+    UserMetaService userMetaService = UserMetaService.getInstance();
+    List<UserEntity> actualUsers =
+        userMetaService.listUsersByNamespace(
+            AuthorizationUtils.ofUserNamespace(metalakeName), 
Collections.emptySet());
+    actualUsers.sort(Comparator.comparing(UserEntity::name));
+    List<UserEntity> expectUsers = Lists.newArrayList(user1, user2);
+    Assertions.assertEquals(expectUsers.size(), actualUsers.size());
+    for (int index = 0; index < expectUsers.size(); index++) {
+      Assertions.assertEquals(expectUsers.get(index).name(), 
actualUsers.get(index).name());
+      if (expectUsers.get(index).roleNames() == null) {
+        Assertions.assertNull(actualUsers.get(index).roleNames());
+      } else {
+        Assertions.assertEquals(
+            expectUsers.get(index).roleNames().size(), 
actualUsers.get(index).roleNames().size());
+        for (String roleName : expectUsers.get(index).roleNames()) {
+          
Assertions.assertTrue(actualUsers.get(index).roleNames().contains(roleName));
+        }
+      }
+    }
+  }
+
   @Test
   void insertUser() throws IOException {
     AuditInfo auditInfo =
diff --git 
a/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java 
b/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
index 1d93e0e6a..24f34d652 100644
--- 
a/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
+++ 
b/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java
@@ -22,11 +22,13 @@ 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;
@@ -34,7 +36,9 @@ import org.apache.gravitino.NameIdentifier;
 import org.apache.gravitino.authorization.AccessControlDispatcher;
 import org.apache.gravitino.authorization.AuthorizationUtils;
 import org.apache.gravitino.dto.requests.UserAddRequest;
+import org.apache.gravitino.dto.responses.NameListResponse;
 import org.apache.gravitino.dto.responses.RemoveResponse;
+import org.apache.gravitino.dto.responses.UserListResponse;
 import org.apache.gravitino.dto.responses.UserResponse;
 import org.apache.gravitino.dto.util.DTOConverters;
 import org.apache.gravitino.lock.LockType;
@@ -84,6 +88,35 @@ public class UserOperations {
     }
   }
 
+  @GET
+  @Produces("application/vnd.gravitino.v1+json")
+  @Timed(name = "list-user." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
+  @ResponseMetered(name = "list-user", absolute = true)
+  public Response listUsers(
+      @PathParam("metalake") String metalake,
+      @QueryParam("details") @DefaultValue("false") boolean verbose) {
+    try {
+      return Utils.doAs(
+          httpRequest,
+          () ->
+              TreeLockUtils.doWithTreeLock(
+                  
NameIdentifier.of(AuthorizationUtils.ofUserNamespace(metalake).levels()),
+                  LockType.READ,
+                  () -> {
+                    if (verbose) {
+                      return Utils.ok(
+                          new UserListResponse(
+                              
DTOConverters.toDTOs(accessControlManager.listUsers(metalake))));
+                    } else {
+                      return Utils.ok(
+                          new 
NameListResponse(accessControlManager.listUserNames(metalake)));
+                    }
+                  }));
+    } catch (Exception e) {
+      return ExceptionHandlers.handleUserException(OperationType.LIST, "", 
metalake, e);
+    }
+  }
+
   @POST
   @Produces("application/vnd.gravitino.v1+json")
   @Timed(name = "add-user." + MetricNames.HTTP_PROCESS_DURATION, absolute = 
true)
diff --git 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
index d3209e0e2..7f570e779 100644
--- 
a/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
+++ 
b/server/src/test/java/org/apache/gravitino/server/web/rest/TestUserOperations.java
@@ -43,7 +43,9 @@ import org.apache.gravitino.dto.authorization.UserDTO;
 import org.apache.gravitino.dto.requests.UserAddRequest;
 import org.apache.gravitino.dto.responses.ErrorConstants;
 import org.apache.gravitino.dto.responses.ErrorResponse;
+import org.apache.gravitino.dto.responses.NameListResponse;
 import org.apache.gravitino.dto.responses.RemoveResponse;
+import org.apache.gravitino.dto.responses.UserListResponse;
 import org.apache.gravitino.dto.responses.UserResponse;
 import org.apache.gravitino.exceptions.NoSuchMetalakeException;
 import org.apache.gravitino.exceptions.NoSuchUserException;
@@ -294,4 +296,103 @@ public class TestUserOperations extends JerseyTest {
     Assertions.assertEquals(ErrorConstants.INTERNAL_ERROR_CODE, 
errorResponse.getCode());
     Assertions.assertEquals(RuntimeException.class.getSimpleName(), 
errorResponse.getType());
   }
+
+  @Test
+  public void testListUsernames() {
+    when(manager.listUserNames(any())).thenReturn(new String[] {"user"});
+
+    Response resp =
+        target("/metalakes/metalake1/users/")
+            .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("user", listResponse.getNames()[0]);
+
+    // Test to throw NoSuchMetalakeException
+    doThrow(new NoSuchMetalakeException("mock 
error")).when(manager).listUserNames(any());
+    Response resp1 =
+        target("/metalakes/metalake1/users/")
+            .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).listUserNames(any());
+    Response resp3 =
+        target("/metalakes/metalake1/users")
+            .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 testListUsers() {
+    User user = buildUser("user");
+    when(manager.listUsers(any())).thenReturn(new User[] {user});
+
+    Response resp =
+        target("/metalakes/metalake1/users/")
+            .queryParam("details", "true")
+            .request(MediaType.APPLICATION_JSON_TYPE)
+            .accept("application/vnd.gravitino.v1+json")
+            .get();
+    Assertions.assertEquals(Response.Status.OK.getStatusCode(), 
resp.getStatus());
+
+    UserListResponse listResponse = resp.readEntity(UserListResponse.class);
+    Assertions.assertEquals(0, listResponse.getCode());
+
+    Assertions.assertEquals(1, listResponse.getUsers().length);
+    Assertions.assertEquals(user.name(), listResponse.getUsers()[0].name());
+    Assertions.assertEquals(user.roles(), listResponse.getUsers()[0].roles());
+
+    // Test to throw NoSuchMetalakeException
+    doThrow(new NoSuchMetalakeException("mock 
error")).when(manager).listUsers(any());
+    Response resp1 =
+        target("/metalakes/metalake1/users/")
+            .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).listUsers(any());
+    Response resp3 =
+        target("/metalakes/metalake1/users")
+            .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());
+  }
 }


Reply via email to