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/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java index 76f2c1b0fe8..90c77ce2c79 100644 --- a/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java +++ b/clients/client-java/src/test/java/org/apache/gravitino/client/integration/test/authorization/AccessControlIT.java @@ -20,8 +20,10 @@ import com.google.common.collect.Lists; import com.google.common.collect.Maps; +import java.util.Arrays; import java.util.Collections; 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 +75,27 @@ void testManageUsers() { Assertions.assertEquals(username, user.name()); Assertions.assertTrue(user.roles().isEmpty()); + // List users + String anotherUser = "another-user"; + metalake.addUser(anotherUser); + String[] usernames = metalake.listUserNames(); + Assertions.assertEquals( + Lists.newArrayList(AuthConstants.ANONYMOUS_USER, anotherUser, username), + Arrays.asList(usernames)); + User[] users = metalake.listUsers(); + Assertions.assertEquals( + Lists.newArrayList(AuthConstants.ANONYMOUS_USER, anotherUser, username), + Arrays.stream(users).map(User::name).collect(Collectors.toList())); + // 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); } @Test diff --git a/common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java b/common/src/main/java/org/apache/gravitino/dto/responses/UserListResponse.java new file mode 100644 index 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/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 8c6a73346d0..92b5b3144de 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 eef947f22e3..2bfff532a31 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,16 @@ import com.google.common.collect.Lists; import java.io.IOException; import java.time.Instant; +import java.util.Arrays; import java.util.Collections; 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 +49,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 +113,25 @@ User getUser(String metalake, String user) throws NoSuchUserException { } } + String[] listUserNames(String metalake) { + return Arrays.stream(listUsers(metalake)).map(User::name).toArray(String[]::new); + } + + 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/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/storage/relational/JDBCBackend.java b/core/src/main/java/org/apache/gravitino/storage/relational/JDBCBackend.java index b23c7667388..4164594f500 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 @@ -104,6 +104,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/UserMetaBaseSQLProvider.java b/core/src/main/java/org/apache/gravitino/storage/relational/mapper/UserMetaBaseSQLProvider.java index a5db8e0f943..cfba535dcf7 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 @@ -138,6 +138,21 @@ 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 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..b5e1dc67c0e 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 @@ -54,6 +54,9 @@ 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); + @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 2c322db86d6..6e26a6ac3f5 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 @@ -86,6 +86,10 @@ 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 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/service/UserMetaService.java b/core/src/main/java/org/apache/gravitino/storage/relational/service/UserMetaService.java index 67bbfbce3df..c3600f9e577 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,6 +30,7 @@ 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.RoleEntity; @@ -245,6 +246,24 @@ public UserEntity updateUser( return newEntity; } + public List listUsersByNamespace(Namespace namespace) { + AuthorizationUtils.checkUserNamespace(namespace); + String metalakeName = namespace.level(0); + + List userPOs = + SessionUtils.getWithoutCommit( + UserMetaMapper.class, mapper -> mapper.listUserPOsByMetalake(metalakeName)); + + return userPOs.stream() + .map( + po -> + POConverters.fromUserPO( + po, + SupplierUtils.createRolePOsSupplier(po), + AuthorizationUtils.ofUserNamespace(metalakeName))) + .collect(Collectors.toList()); + } + public int deleteUserMetasByLegacyTimeline(long legacyTimeline, int limit) { int[] userDeletedCount = new int[] {0}; int[] userRoleRelDeletedCount = new int[] {0}; 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 27e5e667c31..971d18ea8bc 100644 --- a/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java +++ b/core/src/test/java/org/apache/gravitino/authorization/TestAccessControlManager.java @@ -76,6 +76,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) {}; @@ -86,6 +95,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); @@ -162,6 +172,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 90e675e06df..d22c862305c 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 @@ -117,6 +117,37 @@ void getUserByIdentifier() throws IOException { Assertions.assertEquals(Sets.newHashSet(user2.roles()), Sets.newHashSet(actualUser.roles())); } + @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 void insertUser() throws IOException { AuditInfo auditInfo = diff --git a/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java b/server/src/main/java/org/apache/gravitino/server/web/rest/UserOperations.java index 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 b13c0175b0b..d1e01b2cbe2 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()); + } }