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 9b7769200be..e074770e84f 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 User getUser(String user) throws NoSuchUserException, NoSuchMetalakeExcep 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 f13958cb526..9a13a9dd12f 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.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 User getUser(String user) throws NoSuchUserException, NoSuchMetalakeExcep 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 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 f3885a05f9c..67a3035ed8b 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 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.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 void testRemoveUsers() throws Exception { 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 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/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 00000000000..2b591184a24 --- /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 d83460af182..8e706c139e9 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 static CatalogDTO[] toDTOs(Catalog[] catalogs) { 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 1112efc4b8d..dcb27f022a7 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 @@ 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 { *

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 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 List list( + Namespace namespace, Class 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. + * + *

Note. Depends on the isolation levels provided by the underlying storage, the returned list + * may not be consistent. + * + * @param 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 */ - List list( - Namespace namespace, Class type, EntityType entityType) throws IOException; + default List list( + Namespace namespace, Class type, EntityType entityType, Set 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 fabc8acaaf3..fbeebd9449e 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 @@ User addUser(String metalake, String user) */ 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 aa890667dd4..222b1ffb5ae 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 User getUser(String metalake, String user) } @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 Role getRole(String metalake, String role) 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 09427668969..4b7b4f2d8c3 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 @@ 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.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 @@ 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 @@ User getUser(String metalake, String user) throws NoSuchUserException { } } + String[] listUserNames(String metalake) { + Set 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 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 44dc491a722..730563862e7 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 User getUser(String metalake, String user) 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 c71d731a99e..df47215b4b5 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.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.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 List roleIds() { return roleIds; } + /** + * Get the set of all the fields. + * + * @return The set of all the fields. + */ + public static Set fieldSet() { + Set 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 b23c7667388..549c5fec2e4 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 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 void initialize(Config config) { @Override public List list( - Namespace namespace, Entity.EntityType entityType) throws IOException { + Namespace namespace, Entity.EntityType entityType, Set skippingFields) + throws IOException { switch (entityType) { case METALAKE: return (List) MetalakeMetaService.getInstance().listMetalakes(); @@ -104,6 +107,9 @@ public List list( return (List) TopicMetaService.getInstance().listTopicsByNamespace(namespace); case TAG: return (List) TagMetaService.getInstance().listTagsByNamespace(namespace); + case USER: + return (List) + 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 f15060e7426..fe85754b4ef 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 @@ 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 */ - List list(Namespace namespace, Entity.EntityType entityType) + List list( + Namespace namespace, Entity.EntityType entityType, Set 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 7eb1432c590..d7403729f60 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 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.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 void setSerDe(EntitySerDe entitySerDe) { @Override public List list( Namespace namespace, Class type, Entity.EntityType entityType) throws IOException { - return backend.list(namespace, entityType); + return backend.list(namespace, entityType, Collections.emptySet()); + } + + @Override + public List list( + Namespace namespace, Class type, Entity.EntityType entityType, Set 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 a5db8e0f943..d3a49623d7d 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 String listUsersByRoleId(@Param("roleId") Long roleId) { + " 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 ad794c39530..19bbb4eddd2 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 @@ UserPO selectUserMetaByMetalakeIdAndName( @InsertProvider(type = UserMetaSQLProviderFactory.class, method = "insertUserMeta") void insertUserMeta(@Param("userMeta") UserPO userPO); + @SelectProvider(type = UserMetaSQLProviderFactory.class, method = "listUserPOsByMetalake") + List listUserPOsByMetalake(@Param("metalakeName") String metalakeName); + + @SelectProvider( + type = UserMetaSQLProviderFactory.class, + method = "listExtendedUserPOsByMetalakeId") + List 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 3c64f510c27..aa609c03d67 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 @@ 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.po.UserPO; import org.apache.gravitino.storage.relational.session.SqlSessionFactoryHelper; import org.apache.ibatis.annotations.Param; @@ -47,8 +48,6 @@ public static UserMetaBaseSQLProvider getProvider() { 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); @@ -84,6 +83,14 @@ public static String listUsersByRoleId(@Param("roleId") Long roleId) { 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 00000000000..12779d2d70b --- /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/po/ExtendedUserPO.java b/core/src/main/java/org/apache/gravitino/storage/relational/po/ExtendedUserPO.java new file mode 100644 index 00000000000..919056c4883 --- /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 00000000000..42978fa5013 --- /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 { + + /** + * The fields which could be desired. + * + * @return The fields which are desired. + */ + Set 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 00000000000..b5b10a7b6aa --- /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 The value type which the handler will return. + */ +class SupportsDesiredFieldsHandlers { + private final List> methods = Lists.newArrayList(); + + // We should put the low-cost handler into the front of the list. + void addHandler(SupportsDesiredFields supportsSkippingFields) { + methods.add(supportsSkippingFields); + } + + T execute(Set desiredFields) { + for (SupportsDesiredFields 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 e7d0a435a1b..f64b4ab405a 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.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.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 UserEntity updateUser( return newEntity; } + public List listUsersByNamespace(Namespace namespace, Set skippingFields) { + AuthorizationUtils.checkUserNamespace(namespace); + String metalakeName = namespace.level(0); + + SupportsDesiredFieldsHandlers> handlers = + new SupportsDesiredFieldsHandlers<>(); + handlers.addHandler(new ListDesiredRolesHandler(metalakeName)); + handlers.addHandler(new ListAllFieldsHandler(metalakeName)); + + Set 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 int deleteUserMetasByLegacyTimeline(long legacyTimeline, int limit) { return userDeletedCount[0] + userRoleRelDeletedCount[0]; } + + private static class ListDesiredRolesHandler implements SupportsDesiredFields> { + private final String metalakeName; + + ListDesiredRolesHandler(String metalakeName) { + this.metalakeName = metalakeName; + } + + @Override + public Set desiredFields() { + Set requiredFields = Sets.newHashSet(UserEntity.fieldSet()); + requiredFields.remove(UserEntity.ROLE_IDS); + requiredFields.remove(UserEntity.ROLE_NAMES); + + return requiredFields; + } + + @Override + public List execute() { + List 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> { + final String metalakeName; + + ListAllFieldsHandler(String metalakeName) { + this.metalakeName = metalakeName; + } + + @Override + public Set desiredFields() { + return UserEntity.fieldSet(); + } + + @Override + public List execute() { + Long metalakeId = MetalakeMetaService.getInstance().getMetalakeIdByName(metalakeName); + List 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 82d739a41cc..da1f3d06a3b 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 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.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 static UserEntity fromUserPO(UserPO userPO, List rolePOs, Namespa } } + /** + * 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 roleNamesFromJson = + JsonUtils.anyFieldMapper().readValue(userPO.getRoleNames(), List.class); + List 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 roleIdsFromJson = + JsonUtils.anyFieldMapper().readValue(userPO.getRoleIds(), List.class); + List roleIds = + roleIdsFromJson.stream() + .filter(Objects::nonNull) + .map(String::valueOf) + .filter(StringUtils::isNotBlank) + .map(Long::valueOf) + .collect(Collectors.toList()); + + if (!roleIds.isEmpty()) { + builder.withRoleIds(roleIds); + } + } + return builder.build(); + } catch (JsonProcessingException e) { + throw new RuntimeException("Failed to deserialize json object:", e); + } + } + /** * 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 fd27771a088..1c7a26dec53 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 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.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 static void tearDown() throws IOException { @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 void testAddUser() { // 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 void testCreateRole() { Role role = accessControlManager.createRole( - "metalake", + METALAKE, "create", props, Lists.newArrayList( @@ -248,7 +341,7 @@ public void testCreateRole() { RoleAlreadyExistsException.class, () -> accessControlManager.createRole( - "metalake", + METALAKE, "create", props, Lists.newArrayList( @@ -261,22 +354,22 @@ public void testLoadRole() { Map 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 void testDropRole() { Map props = ImmutableMap.of("k1", "v1"); accessControlManager.createRole( - "metalake", + METALAKE, "testDrop", props, Lists.newArrayList( @@ -294,13 +387,13 @@ public void testDropRole() { // 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 26430d2fb5e..b141eb6963b 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.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 void testMetaLifeCycleFromCreationToDeletion() throws IOException { backend.insert(anotherTagEntity, false); // meta data list - List metaLakes = backend.list(metalake.namespace(), Entity.EntityType.METALAKE); + List metaLakes = + backend.list(metalake.namespace(), Entity.EntityType.METALAKE, Collections.emptySet()); assertTrue(metaLakes.contains(metalake)); - List catalogs = backend.list(catalog.namespace(), Entity.EntityType.CATALOG); + List catalogs = + backend.list(catalog.namespace(), Entity.EntityType.CATALOG, Collections.emptySet()); assertTrue(catalogs.contains(catalog)); - List schemas = backend.list(schema.namespace(), Entity.EntityType.SCHEMA); + List schemas = + backend.list(schema.namespace(), Entity.EntityType.SCHEMA, Collections.emptySet()); assertTrue(schemas.contains(schema)); - List tables = backend.list(table.namespace(), Entity.EntityType.TABLE); + List tables = + backend.list(table.namespace(), Entity.EntityType.TABLE, Collections.emptySet()); assertTrue(tables.contains(table)); - List filesets = backend.list(fileset.namespace(), Entity.EntityType.FILESET); + List 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 topics = backend.list(topic.namespace(), Entity.EntityType.TOPIC); + List 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 void testMetaLifeCycleFromCreationToDeletion() throws IOException { TagEntity tagEntity = backend.get(tag.nameIdentifier(), Entity.EntityType.TAG); assertEquals(tag, tagEntity); - List tags = backend.list(tag.namespace(), Entity.EntityType.TAG); + List 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 326ccfc2da3..0d037317d90 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.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 @@ void getUserByIdentifier() throws IOException { 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 actualUsers = + userMetaService.listUsersByNamespace( + AuthorizationUtils.ofUserNamespace(metalakeName), Collections.emptySet()); + actualUsers.sort(Comparator.comparing(UserEntity::name)); + List 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/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java b/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java index 62366d07504..2345dbcdda9 100644 --- a/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java +++ b/integration-test/src/test/java/org/apache/gravitino/integration/test/authorization/AccessControlIT.java @@ -20,8 +20,12 @@ 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 @@ void testManageUsers() { Assertions.assertEquals(username, user.name()); Assertions.assertTrue(user.roles().isEmpty()); + Map 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 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/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 1d93e0e6afa..24f34d652ab 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.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.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 Response getUser(@PathParam("metalake") String metalake, @PathParam("user } } + @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 d3209e0e22c..7f570e779f4 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.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 void testRemoveUser() { 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()); + } }