Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions src/main/java/us/kbase/auth2/service/api/APIPaths.java
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,9 @@ public class APIPaths {
public static final String API_V2_ADMIN = API_V2 + SEP + ADMIN;
/** The anonymous ID lookup endpoint location relative to the admin root. */
public static final String ANONYMOUS_ID_LOOKUP = "anonids";

/** The admin user roles endpoint location relative to the admin root. */
public static final String ADMIN_USER_ROLES = USERS + SEP + "{" + USERNAME + "}" + SEP + "roles";

/** The token introspection endpoint location. */
public static final String API_V2_TOKEN = API_V2 + SEP + TOKEN;
Expand Down
104 changes: 104 additions & 0 deletions src/main/java/us/kbase/auth2/service/api/Admin.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,40 @@

import java.util.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.stream.Collectors;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
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.MediaType;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;

import us.kbase.auth2.lib.Authentication;
import us.kbase.auth2.lib.Role;
import us.kbase.auth2.lib.UserName;
import us.kbase.auth2.lib.exceptions.DisabledUserException;
import us.kbase.auth2.lib.exceptions.IllegalParameterException;
import us.kbase.auth2.lib.exceptions.InvalidTokenException;
import us.kbase.auth2.lib.exceptions.MissingParameterException;
import us.kbase.auth2.lib.exceptions.NoSuchRoleException;
import us.kbase.auth2.lib.exceptions.NoSuchUserException;
import us.kbase.auth2.lib.exceptions.NoTokenProvidedException;
import us.kbase.auth2.lib.exceptions.UnauthorizedException;
import us.kbase.auth2.lib.storage.exceptions.AuthStorageException;
import us.kbase.auth2.service.common.Fields;
import us.kbase.auth2.service.common.IncomingJSON;

@Path(APIPaths.API_V2_ADMIN)
public class Admin {
Expand Down Expand Up @@ -71,4 +83,96 @@ static Set<UUID> processAnonymousIDListString(final String anonIDs)
return ids;
}

/** Request body for updating user roles. */
public static class UpdateUserRoles extends IncomingJSON {

public final List<String> addRoles;
public final List<String> removeRoles;
public final List<String> addCustomRoles;
public final List<String> removeCustomRoles;

@JsonCreator
public UpdateUserRoles(
@JsonProperty(Fields.ADD_ROLES) final List<String> addRoles,
@JsonProperty(Fields.REMOVE_ROLES) final List<String> removeRoles,
@JsonProperty(Fields.ADD_CUSTOM_ROLES) final List<String> addCustomRoles,
@JsonProperty(Fields.REMOVE_CUSTOM_ROLES) final List<String> removeCustomRoles) {
this.addRoles = addRoles;
this.removeRoles = removeRoles;
this.addCustomRoles = addCustomRoles;
this.removeCustomRoles = removeCustomRoles;
}
}

@POST
@Path(APIPaths.ADMIN_USER_ROLES)
@Consumes(MediaType.APPLICATION_JSON)
public void updateUserRoles(
@HeaderParam(APIConstants.HEADER_TOKEN) final String token,
@PathParam(APIPaths.USERNAME) final String userName,
final UpdateUserRoles update)
throws NoTokenProvidedException, InvalidTokenException, AuthStorageException,
UnauthorizedException, NoSuchUserException, NoSuchRoleException,
IllegalParameterException, MissingParameterException, DisabledUserException {

if (update == null) {
throw new MissingParameterException("JSON body missing");
}
update.exceptOnAdditionalProperties();

final UserName user = new UserName(userName);

// Convert string lists to appropriate sets, handling nulls
final List<String> addRolesList = update.addRoles == null ?
Collections.emptyList() : update.addRoles;
final List<String> removeRolesList = update.removeRoles == null ?
Collections.emptyList() : update.removeRoles;
final List<String> addCustomList = update.addCustomRoles == null ?
Collections.emptyList() : update.addCustomRoles;
final List<String> removeCustomList = update.removeCustomRoles == null ?
Collections.emptyList() : update.removeCustomRoles;

noNulls(addRolesList, "Null item in roles");
noNulls(removeRolesList, "Null item in roles");
noNulls(addCustomList, "Null item in custom roles");
noNulls(removeCustomList, "Null item in custom roles");

final Set<Role> addRoles = toRoles(addRolesList);
final Set<Role> removeRoles = toRoles(removeRolesList);

// Update built-in roles if any specified
if (!addRoles.isEmpty() || !removeRoles.isEmpty()) {
auth.updateRoles(getToken(token), user, addRoles, removeRoles);
}

// Update custom roles if any specified
if (!addCustomList.isEmpty() || !removeCustomList.isEmpty()) {
auth.updateCustomRoles(
getToken(token), user,
new HashSet<>(addCustomList),
new HashSet<>(removeCustomList));
}
}

private Set<Role> toRoles(final List<String> roles) throws IllegalParameterException {
final Set<Role> ret = new HashSet<>();
for (final String role : roles) {
try {
ret.add(Role.getRole(role));
} catch (IllegalArgumentException e) {
throw new IllegalParameterException(e.getMessage(), e);
}
}
return ret;
}

private void noNulls(final List<String> list, final String message)
throws IllegalParameterException {
for (final String item : list) {
if (item == null) {
throw new IllegalParameterException(message);
}
}
}

}
8 changes: 8 additions & 0 deletions src/main/java/us/kbase/auth2/service/common/Fields.java
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,14 @@ public class Fields {
* names from conflicting with other items in the form.
*/
public static final String CUSTOM_ROLE_FORM_PREFIX = "crole_";
/** Roles to add to a user. */
public static final String ADD_ROLES = "addRoles";
/** Roles to remove from a user. */
public static final String REMOVE_ROLES = "removeRoles";
/** Custom roles to add to a user. */
public static final String ADD_CUSTOM_ROLES = "addCustomRoles";
/** Custom roles to remove from a user. */
public static final String REMOVE_CUSTOM_ROLES = "removeCustomRoles";

/* search */

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Entity;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
Expand All @@ -29,7 +30,10 @@
import us.kbase.auth2.lib.PasswordHashAndSalt;
import us.kbase.auth2.lib.Role;
import us.kbase.auth2.lib.UserName;
import us.kbase.auth2.lib.exceptions.IllegalParameterException;
import us.kbase.auth2.lib.exceptions.NoTokenProvidedException;
import us.kbase.auth2.lib.exceptions.UnauthorizedException;
import us.kbase.auth2.lib.user.AuthUser;
import us.kbase.auth2.lib.token.IncomingToken;
import us.kbase.auth2.lib.token.StoredToken;
import us.kbase.auth2.lib.token.TokenType;
Expand Down Expand Up @@ -165,4 +169,134 @@ public void translateAnonIDsToUserNamesFailNotAdmin() throws Exception {

failRequestJSON(req.get(), 403, "Forbidden", new UnauthorizedException());
}

/* updateUserRoles integration tests */

@Test
public void updateUserRolesSuccess() throws Exception {
final PasswordHashAndSalt pwd = new PasswordHashAndSalt(
"foobarbazbing".getBytes(), "aa".getBytes());

// Create admin user
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("admin"), UUID.randomUUID(), new DisplayName("Admin"), inst(20000))
.withRole(Role.ADMIN)
.build(),
pwd);

// Create target user with DevToken role (to be removed)
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("targetuser"), UUID.randomUUID(), new DisplayName("Target"), inst(20000))
.withRole(Role.DEV_TOKEN)
.build(),
pwd);

final IncomingToken token = new IncomingToken("admintoken");
manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(),
new UserName("admin")).withLifeTime(inst(10000), inst(1000000000000000L)).build(),
token.getHashedToken().getTokenHash());

final URI target = UriBuilder.fromUri(host)
.path("/api/V2/admin/users/targetuser/roles")
.build();

final String body = "{\"addRoles\": [\"ServToken\"], \"removeRoles\": [\"DevToken\"]}";

final Response res = CLI.target(target).request()
.header("authorization", token.getToken())
.header("content-type", MediaType.APPLICATION_JSON)
.post(Entity.json(body));

assertThat("incorrect response code", res.getStatus(), is(204));

// Verify roles were updated
final AuthUser user = manager.storage.getUser(new UserName("targetuser"));
assertThat("should have ServToken", user.hasRole(Role.SERV_TOKEN), is(true));
assertThat("should not have DevToken", user.hasRole(Role.DEV_TOKEN), is(false));
}

@Test
public void updateUserRolesFailNotAdmin() throws Exception {
final PasswordHashAndSalt pwd = new PasswordHashAndSalt(
"foobarbazbing".getBytes(), "aa".getBytes());

// Create non-admin user
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("nonadmin"), UUID.randomUUID(), new DisplayName("NonAdmin"), inst(20000))
.build(),
pwd);

// Create target user
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("targetuser"), UUID.randomUUID(), new DisplayName("Target"), inst(20000))
.build(),
pwd);

final IncomingToken token = new IncomingToken("usertoken");
manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(),
new UserName("nonadmin")).withLifeTime(inst(10000), inst(1000000000000000L)).build(),
token.getHashedToken().getTokenHash());

final URI target = UriBuilder.fromUri(host)
.path("/api/V2/admin/users/targetuser/roles")
.build();

final Response res = CLI.target(target).request()
.header("accept", MediaType.APPLICATION_JSON)
.header("authorization", token.getToken())
.header("content-type", MediaType.APPLICATION_JSON)
.post(Entity.json("{\"addRoles\": [\"Admin\"]}"));

// Non-admin users cannot grant Admin role - the error message includes the user and role
failRequestJSON(res, 403, "Forbidden", new UnauthorizedException(
"User nonadmin is not authorized to grant role(s): Administrator"));
}

@Test
public void updateUserRolesFailNoToken() throws Exception {
final URI target = UriBuilder.fromUri(host)
.path("/api/V2/admin/users/testuser/roles")
.build();

final Response res = CLI.target(target).request()
.header("accept", MediaType.APPLICATION_JSON)
.header("content-type", MediaType.APPLICATION_JSON)
.post(Entity.json("{\"addRoles\": [\"Admin\"]}"));

// NoTokenProvidedException extends AuthException (not AuthenticationException)
// so it maps to 400 Bad Request
failRequestJSON(res, 400, "Bad Request",
new NoTokenProvidedException("No user token provided"));
}

@Test
public void updateUserRolesFailInvalidRole() throws Exception {
final PasswordHashAndSalt pwd = new PasswordHashAndSalt(
"foobarbazbing".getBytes(), "aa".getBytes());

// Create admin user
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("admin"), UUID.randomUUID(), new DisplayName("Admin"), inst(20000))
.withRole(Role.ADMIN)
.build(),
pwd);

final IncomingToken token = new IncomingToken("admintoken");
manager.storage.storeToken(StoredToken.getBuilder(TokenType.LOGIN, UUID.randomUUID(),
new UserName("admin")).withLifeTime(inst(10000), inst(1000000000000000L)).build(),
token.getHashedToken().getTokenHash());

final URI target = UriBuilder.fromUri(host)
.path("/api/V2/admin/users/admin/roles")
.build();

final Response res = CLI.target(target).request()
.header("accept", MediaType.APPLICATION_JSON)
.header("authorization", token.getToken())
.header("content-type", MediaType.APPLICATION_JSON)
.post(Entity.json("{\"addRoles\": [\"NotARealRole\"]}"));

failRequestJSON(res, 400, "Bad Request",
new IllegalParameterException("Invalid role id: NotARealRole"));
}
}
Loading
Loading