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 <[email protected]>
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());
+ }
}