From a57e4e35ff4b0aa8ccc6fee2c885c5e6772a03cc Mon Sep 17 00:00:00 2001 From: Rory Date: Wed, 3 Jul 2024 10:50:24 +0800 Subject: [PATCH] Add ListRoles --- .../client/GravitinoAdminClient.java | 45 ++++++++ .../gravitino/client/TestUserGroup.java | 56 ++++++++++ .../dto/responses/NameListResponse.java | 53 +++++++++ .../dto/responses/UserListResponse.java | 54 ++++++++++ .../gravitino/dto/util/DTOConverters.java | 13 +++ .../authorization/AccessControlManager.java | 8 ++ .../authorization/UserGroupManager.java | 34 ++++++ .../storage/relational/JDBCBackend.java | 2 + .../relational/mapper/UserMetaMapper.java | 12 +++ .../relational/service/UserMetaService.java | 36 +++++++ .../relational/utils/SessionUtils.java | 16 +++ .../TestAccessControlManager.java | 25 +++++ .../service/TestUserMetaService.java | 37 +++++++ .../integration/test/UserGroupIT.java | 57 ++++++++++ .../server/web/rest/UserOperations.java | 37 +++++++ .../server/web/rest/TestUserOperations.java | 101 ++++++++++++++++++ 16 files changed, 586 insertions(+) create mode 100644 common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java create mode 100644 common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java create mode 100644 integration-test/src/test/java/org/apache/gravitino/integration/test/UserGroupIT.java diff --git a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoAdminClient.java b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoAdminClient.java index 67d32289f06..727228dde27 100644 --- a/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoAdminClient.java +++ b/clients/client-java/src/main/java/org/apache/gravitino/client/GravitinoAdminClient.java @@ -22,6 +22,7 @@ import com.google.common.base.Preconditions; import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.stream.Collectors; @@ -45,8 +46,10 @@ import org.apache.gravitino.dto.responses.GroupResponse; import org.apache.gravitino.dto.responses.MetalakeListResponse; 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.RoleResponse; +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.MetalakeAlreadyExistsException; @@ -262,6 +265,48 @@ public User getUser(String metalake, String user) return resp.getUser(); } + /** + * 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. + */ + public String[] listUserNames(String metalake) throws NoSuchMetalakeException { + NameListResponse resp = + restClient.get( + String.format(API_METALAKES_USERS_PATH, metalake, BLANK_PLACE_HOLDER), + NameListResponse.class, + Collections.emptyMap(), + ErrorHandlers.userErrorHandler()); + resp.validate(); + + return resp.getNames(); + } + + /** + * 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. + */ + public User[] listUsers(String metalake) { + Map params = new HashMap<>(); + params.put("details", "true"); + + UserListResponse resp = + restClient.get( + String.format(API_METALAKES_USERS_PATH, metalake, BLANK_PLACE_HOLDER), + params, + UserListResponse.class, + Collections.emptyMap(), + ErrorHandlers.userErrorHandler()); + resp.validate(); + + return resp.getUsers(); + } + /** * 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 4aa2e9f7305..8f6af4cebcc 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; @@ -33,7 +35,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.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; @@ -155,6 +159,58 @@ public void testRemoveUsers() throws Exception { RuntimeException.class, () -> client.removeUser(metalakeName, 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"}, client.listUserNames(metalakeName)); + + 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, () -> client.listUserNames(metalakeName)); + 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, () -> client.listUserNames(metalakeName)); + } + + @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 = client.listUsers(metalakeName); + 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, () -> client.listUsers(metalakeName)); + 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, () -> client.listUsers(metalakeName)); + } + @Test public void testAddGroups() throws Exception { String groupName = "group"; diff --git a/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java b/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java new file mode 100644 index 00000000000..61042660c24 --- /dev/null +++ b/common/src/main/java/org/apache/gravitino/dto/responses/NameListResponse.java @@ -0,0 +1,53 @@ +/* + * 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 lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.ToString; + +/** Represents a response containing a list of names. */ +@Getter +@ToString +@EqualsAndHashCode(callSuper = true) +public class NameListResponse extends BaseResponse { + + @JsonProperty("names") + private final String[] names; + + /** + * Constructor for NameListResponse. + * + * @param names The array of names. + */ + public NameListResponse(String[] names) { + super(0); + this.names = names; + } + + /** + * This is the constructor that is used by Jackson deserializer to create an instance of + * NameListResponse. + */ + public NameListResponse() { + super(0); + this.names = null; + } +} 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..db945e41e39 --- /dev/null +++ b/common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java @@ -0,0 +1,54 @@ +/* + * 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 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; + } +} 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 5e253788a28..0a260f36c53 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 @@ -628,6 +628,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/authorization/AccessControlManager.java b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java index 2425f6d5889..92ca7e9b5a3 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/AccessControlManager.java @@ -101,6 +101,14 @@ public User getUser(String metalake, String user) return doWithNonAdminLock(() -> userGroupManager.getUser(metalake, user)); } + public String[] listUserNames(String metalake) throws NoSuchMetalakeException { + return userGroupManager.listUserNames(metalake); + } + + public User[] listUsers(String metalake) throws NoSuchMetalakeException { + return userGroupManager.listUsers(metalake); + } + /** * Adds a new Group. * 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..51216bce0c6 100644 --- a/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java +++ b/core/src/main/java/org/apache/gravitino/authorization/UserGroupManager.java @@ -25,9 +25,11 @@ import org.apache.gravitino.Entity; import org.apache.gravitino.EntityAlreadyExistsException; import org.apache.gravitino.EntityStore; +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; @@ -46,6 +48,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 +112,37 @@ User getUser(String metalake, String user) throws NoSuchUserException { } } + String[] listUserNames(String metalake) { + try { + AuthorizationUtils.checkMetalakeExists(metalake); + Namespace namespace = AuthorizationUtils.ofUserNamespace(metalake); + return store.list(namespace, UserEntity.class, Entity.EntityType.USER).stream() + .map(UserEntity::name) + .toArray(String[]::new); + } catch (NoSuchEntityException e) { + LOG.warn("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); + } + } + + User[] listUsers(String metalake) { + try { + Namespace namespace = AuthorizationUtils.ofUserNamespace(metalake); + return store.list(namespace, UserEntity.class, Entity.EntityType.USER).stream() + .map(entity -> (User) entity) + .toArray(User[]::new); + } catch (NoSuchEntityException e) { + LOG.warn("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/storage/relational/JDBCBackend.java b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java index 430fb5a55d5..ef973708148 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 @@ -100,6 +100,8 @@ 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); default: throw new UnsupportedEntityTypeException( "Unsupported entity type: %s for list operation", entityType); diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaMapper.java index e7b442c099a..4b007140db1 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 @@ -60,6 +60,18 @@ Long selectUserIdByMetalakeIdAndName( UserPO selectUserMetaByMetalakeIdAndName( @Param("metalakeId") Long metalakeId, @Param("userName") String name); + @Select( + "SELECT user_id as userId, user_name as userName," + + " metalake_id as metalakeId," + + " audit_info as auditInfo," + + " current_version as currentVersion, last_version as lastVersion," + + " deleted_at as deletedAt" + + " FROM " + + USER_TABLE_NAME + + " WHERE metalake_id = #{metalakeId}" + + " AND deleted_at = 0") + List listUserPOsByMetalakeId(@Param("metalakeId") Long metalakeId); + @Insert( "INSERT INTO " + USER_TABLE_NAME 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 a24d404b802..60c157dccdb 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 @@ -26,13 +26,16 @@ import java.util.Objects; import java.util.Optional; import java.util.Set; +import java.util.concurrent.atomic.AtomicLong; import java.util.function.Function; import org.apache.gravitino.Entity; import org.apache.gravitino.HasIdentifier; import org.apache.gravitino.NameIdentifier; +import org.apache.gravitino.Namespace; import org.apache.gravitino.authorization.AuthorizationUtils; import org.apache.gravitino.exceptions.NoSuchEntityException; import org.apache.gravitino.meta.UserEntity; +import org.apache.gravitino.storage.relational.mapper.MetalakeMetaMapper; import org.apache.gravitino.storage.relational.mapper.UserMetaMapper; import org.apache.gravitino.storage.relational.mapper.UserRoleRelMapper; import org.apache.gravitino.storage.relational.po.RolePO; @@ -221,6 +224,39 @@ public UserEntity updateUser( return newEntity; } + public List listUsersByNamespace(Namespace namespace) { + AuthorizationUtils.checkUserNamespace(namespace); + + List userEntities = Lists.newArrayList(); + String metalakeName = namespace.level(0); + List userPOs = Lists.newArrayList(); + AtomicLong metalakeId = new AtomicLong(); + SessionUtils.doMultipleWithoutCommit( + () -> { + Long id = + SessionUtils.doWithoutCommitAndFetchResult( + MetalakeMetaMapper.class, + mapper -> mapper.selectMetalakeIdMetaByName(metalakeName)); + if (id == null) { + throw new NoSuchEntityException( + NoSuchEntityException.NO_SUCH_ENTITY_MESSAGE, + Entity.EntityType.METALAKE.name().toLowerCase(), + metalakeName); + } + metalakeId.set(id); + }, + () -> + userPOs.addAll( + SessionUtils.doWithoutCommitAndFetchResult( + UserMetaMapper.class, + mapper -> mapper.listUserPOsByMetalakeId(metalakeId.get())))); + for (UserPO userPO : userPOs) { + List rolePOs = RoleMetaService.getInstance().listRolesByUserId(userPO.getUserId()); + userEntities.add(POConverters.fromUserPO(userPO, rolePOs, namespace)); + } + return userEntities; + } + public int deleteUserMetasByLegacyTimeline(long legacyTimeline, int limit) { int[] userDeletedCount = new int[] {0}; int[] userRoleRelDeletedCount = new int[] {0}; diff --git a/core/src/main/java/org/apache/gravitino/storage/relational/utils/SessionUtils.java b/core/src/main/java/org/apache/gravitino/storage/relational/utils/SessionUtils.java index 8bc18ca923d..0363c2353f3 100644 --- a/core/src/main/java/org/apache/gravitino/storage/relational/utils/SessionUtils.java +++ b/core/src/main/java/org/apache/gravitino/storage/relational/utils/SessionUtils.java @@ -145,4 +145,20 @@ public static void doMultipleWithCommit(Runnable... operations) { } } } + + /** + * This method is used to perform multiple database operations without commit. If any of the + * operations fail, we just abort the operations. + * + * @param operations the operations to be performed + */ + public static void doMultipleWithoutCommit(Runnable... operations) { + try { + Arrays.stream(operations).forEach(Runnable::run); + } catch (Throwable t) { + throw t; + } finally { + SqlSessions.closeSqlSession(); + } + } } 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 e0a6e3835f6..d1f1b06279b 100644 --- a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java +++ b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java @@ -66,6 +66,15 @@ public class TestAccessControlManager { .withVersion(SchemaVersion.V_0_1) .build(); + private static BaseMetalake listMetalakeEntity = + BaseMetalake.builder() + .withId(1L) + .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) {}; @@ -76,6 +85,7 @@ public static void setUp() throws Exception { entityStore.setSerDe(null); entityStore.put(metalakeEntity, true); + entityStore.put(listMetalakeEntity, true); accessControlManager = new AccessControlManager(entityStore, new RandomIdGenerator(), config); FieldUtils.writeField(GravitinoEnv.getInstance(), "entityStore", entityStore, true); @@ -147,6 +157,21 @@ public void testRemoveUser() { Assertions.assertFalse(removed1); } + @Test + public void testListUsers() { + accessControlManager.addUser("metalake_list", "testList1"); + accessControlManager.addUser("metalake_list", "testList2"); + + // Test to list users + Assertions.assertArrayEquals( + new String[] {"testList2", "testList1"}, + accessControlManager.listUserNames("metalake_list")); + User[] users = accessControlManager.listUsers("metalake_list"); + Assertions.assertEquals(2, users.length); + Assertions.assertEquals("testList1", users[1].name()); + Assertions.assertEquals("testList2", users[0].name()); + } + @Test public void testAddGroup() { Group group = accessControlManager.addGroup("metalake", "testAdd"); 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 4f283b60391..fc3f332fcea 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 @@ -116,6 +116,43 @@ 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); + + UserEntity user1 = + createUserEntity( + RandomIdGenerator.INSTANCE.nextId(), + AuthorizationUtils.ofUserNamespace(metalakeName), + "user1", + auditInfo); + + UserEntity user2 = + createUserEntity( + RandomIdGenerator.INSTANCE.nextId(), + AuthorizationUtils.ofUserNamespace(metalakeName), + "user2", + auditInfo); + + backend.insert(user1, false); + backend.insert(user2, false); + + UserMetaService userMetaService = UserMetaService.getInstance(); + Assertions.assertEquals( + Lists.newArrayList(user1, user2), + userMetaService.listUsersByNamespace(AuthorizationUtils.ofUserNamespace(metalakeName))); + + // Test NoSuchMetalakeException + Assertions.assertThrows( + NoSuchEntityException.class, + () -> + userMetaService.listUsersByNamespace(AuthorizationUtils.ofUserNamespace("not-exist"))); + } + @Test void insertUser() throws IOException { AuditInfo auditInfo = diff --git a/integration-test/src/test/java/org/apache/gravitino/integration/test/UserGroupIT.java b/integration-test/src/test/java/org/apache/gravitino/integration/test/UserGroupIT.java new file mode 100644 index 00000000000..86f78716fd9 --- /dev/null +++ b/integration-test/src/test/java/org/apache/gravitino/integration/test/UserGroupIT.java @@ -0,0 +1,57 @@ +/* + * 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.integration.test; + +import com.google.common.collect.Maps; +import java.util.Collections; +import java.util.Map; +import org.apache.gravitino.Configs; +import org.apache.gravitino.auth.AuthConstants; +import org.apache.gravitino.authorization.User; +import org.apache.gravitino.integration.test.util.AbstractIT; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Tag; +import org.junit.jupiter.api.Test; + +@Tag("gravitino-docker-test") +public class UserGroupIT extends AbstractIT { + + @BeforeAll + public static void startIntegrationTest() throws Exception { + Map configs = Maps.newHashMap(); + configs.put(Configs.ENABLE_AUTHORIZATION.getKey(), "true"); + configs.put(Configs.SERVICE_ADMINS.getKey(), AuthConstants.ANONYMOUS_USER); + registerCustomConfigs(configs); + AbstractIT.startIntegrationTest(); + } + + @Test + public void testListUsers() { + client.createMetalake("listMetalake", "", Collections.emptyMap()); + client.addUser("listMetalake", "user1"); + client.addUser("listMetalake", "user2"); + Assertions.assertArrayEquals( + new String[] {"user1", "user2"}, client.listUserNames("listMetalake")); + User[] users = client.listUsers("listMetalake"); + Assertions.assertEquals(2, users.length); + Assertions.assertEquals("user1", users[0].name()); + Assertions.assertEquals("user2", users[1].name()); + } +} 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 4203cd51055..b0527ea0cbc 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,19 +22,27 @@ import com.codahale.metrics.annotation.Timed; import javax.servlet.http.HttpServletRequest; import javax.ws.rs.DELETE; +import javax.ws.rs.DefaultValue; import javax.ws.rs.GET; import javax.ws.rs.POST; import javax.ws.rs.Path; import javax.ws.rs.PathParam; import javax.ws.rs.Produces; +import javax.ws.rs.QueryParam; import javax.ws.rs.core.Context; import javax.ws.rs.core.Response; import org.apache.gravitino.GravitinoEnv; +import org.apache.gravitino.NameIdentifier; import org.apache.gravitino.authorization.AccessControlManager; +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; +import org.apache.gravitino.lock.TreeLockUtils; import org.apache.gravitino.metrics.MetricNames; import org.apache.gravitino.server.authorization.NameBindings; import org.apache.gravitino.server.web.Utils; @@ -76,6 +84,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 adb521cf0ea..c7f7bb45bf5 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 testListUsers() { + 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 testListCatalogsInfo() { + 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()); + } }