Skip to content

Commit

Permalink
[#3348] feat(core,server): Add the list operation of the user (#4055)
Browse files Browse the repository at this point in the history
### What changes were proposed in this pull request?
Add the list operation of the user

### Why are the changes needed?

Fix: #3348 

### Does this PR introduce _any_ user-facing change?
I will add the document later.

### How was this patch tested?
Add the new ut.
  • Loading branch information
jerqi authored Sep 24, 2024
1 parent 870c569 commit 92a0ec8
Show file tree
Hide file tree
Showing 30 changed files with 1,147 additions and 66 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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<String, String> params = new HashMap<>();
params.put("details", "true");

UserListResponse resp =
restClient.get(
String.format(API_METALAKES_USERS_PATH, name(), BLANK_PLACE_HOLDER),
params,
UserListResponse.class,
Collections.emptyMap(),
ErrorHandlers.userErrorHandler());
resp.validate();

return resp.getUsers();
}

/**
* Lists the usernames.
*
* @return The username list.
* @throws NoSuchMetalakeException If the Metalake with the given name does not exist.
*/
public String[] listUserNames() throws NoSuchMetalakeException {
NameListResponse resp =
restClient.get(
String.format(API_METALAKES_USERS_PATH, name(), BLANK_PLACE_HOLDER),
NameListResponse.class,
Collections.emptyMap(),
ErrorHandlers.userErrorHandler());
resp.validate();

return resp.getNames();
}

/**
* Adds a new Group.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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;
Expand Down Expand Up @@ -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<String, String> params = Collections.singletonMap("details", "true");
UserListResponse listResponse = new UserListResponse(new UserDTO[] {user1, user2});
buildMockResource(Method.GET, userPath, params, null, listResponse, SC_OK);

User[] users = gravitinoClient.listUsers();
Assertions.assertEquals(2, users.length);
assertUser(user1, users[0]);
assertUser(user2, users[1]);

ErrorResponse errRespNoMetalake =
ErrorResponse.notFound(NoSuchMetalakeException.class.getSimpleName(), "metalake not found");
buildMockResource(Method.GET, userPath, params, null, errRespNoMetalake, SC_NOT_FOUND);
Exception ex =
Assertions.assertThrows(NoSuchMetalakeException.class, () -> gravitinoClient.listUsers());
Assertions.assertEquals("metalake not found", ex.getMessage());

// Test RuntimeException
ErrorResponse errResp = ErrorResponse.internalError("internal error");
buildMockResource(Method.GET, userPath, params, null, errResp, SC_SERVER_ERROR);
Assertions.assertThrows(RuntimeException.class, () -> gravitinoClient.listUsers());
}

@Test
public void testAddGroups() throws Exception {
String groupName = "group";
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -73,11 +77,43 @@ void testManageUsers() {
Assertions.assertEquals(username, user.name());
Assertions.assertTrue(user.roles().isEmpty());

Map<String, String> properties = Maps.newHashMap();
properties.put("k1", "v1");
SecurableObject metalakeObject =
SecurableObjects.ofMetalake(
metalakeName, Lists.newArrayList(Privileges.CreateCatalog.allow()));

// Test the user with the role
metalake.createRole("role1", properties, Lists.newArrayList(metalakeObject));
metalake.grantRolesToUser(Lists.newArrayList("role1"), username);

// List users
String anotherUser = "another-user";
metalake.addUser(anotherUser);
String[] usernames = metalake.listUserNames();
Arrays.sort(usernames);
Assertions.assertEquals(
Lists.newArrayList(AuthConstants.ANONYMOUS_USER, anotherUser, username),
Arrays.asList(usernames));
List<User> users =
Arrays.stream(metalake.listUsers())
.sorted(Comparator.comparing(User::name))
.collect(Collectors.toList());
Assertions.assertEquals(
Lists.newArrayList(AuthConstants.ANONYMOUS_USER, anotherUser, username),
users.stream().map(User::name).collect(Collectors.toList()));
Assertions.assertEquals(Lists.newArrayList("role1"), users.get(2).roles());

// Get a not-existed user
Assertions.assertThrows(NoSuchUserException.class, () -> metalake.getUser("not-existed"));

Assertions.assertTrue(metalake.removeUser(username));

Assertions.assertFalse(metalake.removeUser(username));

// clean up
metalake.removeUser(anotherUser);
metalake.deleteRole("role1");
}

@Test
Expand Down
Original file line number Diff line number Diff line change
@@ -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");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
33 changes: 30 additions & 3 deletions core/src/main/java/org/apache/gravitino/EntityStore.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -55,15 +57,40 @@ public interface EntityStore extends Closeable {
* <p>Note. Depends on the isolation levels provided by the underlying storage, the returned list
* may not be consistent.
*
* @param namespace the namespace of the entities
* @param <E> class of the entity
* @param namespace the namespace of the entities
* @param type the detailed type of the entity
* @param entityType the general type of the entity
* @return the list of entities
* @throws IOException if the list operation fails
*/
default <E extends Entity & HasIdentifier> List<E> list(
Namespace namespace, Class<E> type, EntityType entityType) throws IOException {
return list(namespace, type, entityType, Collections.emptySet());
}

/**
* List all the entities with the specified {@link org.apache.gravitino.Namespace}, and
* deserialize them into the specified {@link Entity} object.
*
* <p>Note. Depends on the isolation levels provided by the underlying storage, the returned list
* may not be consistent.
*
* @param <E> class of the entity
* @param namespace the namespace of the entities
* @param type the detailed type of the entity
* @param entityType the general type of the entity
* @param skippingFields Some fields may have a relatively high acquisition cost, EntityStore
* provides an optional setting to avoid fetching these high-cost fields to improve the
* performance.
* @return the list of entities
* @throws IOException if the list operation fails
*/
<E extends Entity & HasIdentifier> List<E> list(
Namespace namespace, Class<E> type, EntityType entityType) throws IOException;
default <E extends Entity & HasIdentifier> List<E> list(
Namespace namespace, Class<E> type, EntityType entityType, Set<Field> skippingFields)
throws IOException {
throw new UnsupportedOperationException("Don't support to skip fields");
}

/**
* Check if the entity with the specified {@link org.apache.gravitino.NameIdentifier} exists.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
Expand Down
Loading

0 comments on commit 92a0ec8

Please sign in to comment.