Skip to content

Commit

Permalink
Merge pull request kbase#405 from MrCreosote/develop
Browse files Browse the repository at this point in the history
PTV-1890: Add admin API to translate anonymous IDs to user names
  • Loading branch information
MrCreosote committed Jul 21, 2023
2 parents b3341d7 + e37629a commit 14a7008
Show file tree
Hide file tree
Showing 6 changed files with 367 additions and 1 deletion.
2 changes: 2 additions & 0 deletions build.xml
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,8 @@
<test name="us.kbase.test.auth2.providers.OrcIDIdentityProviderTest"/>
<test name="us.kbase.test.auth2.service.AuthExternalConfigTest"/>
<test name="us.kbase.test.auth2.service.LoggingFilterTest"/>
<test name="us.kbase.test.auth2.service.api.AdminTest"/>
<test name="us.kbase.test.auth2.service.api.AdminIntegrationTest"/>
<test name="us.kbase.test.auth2.service.api.APITokenTest"/>
<test name="us.kbase.test.auth2.service.api.TokenEndpointTest"/>
<test name="us.kbase.test.auth2.service.api.TestModeTest"/>
Expand Down
1 change: 0 additions & 1 deletion src/us/kbase/auth2/lib/user/AuthUser.java
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ public class AuthUser {
private final DisplayName displayName;
private final EmailAddress email;
private final UserName userName;
// TODO ANONID add admin method to translate anon IDs to IDs
// TODO ANONID docs and release notes
private final UUID anonymousID;
private final Set<Role> roles;
Expand Down
6 changes: 6 additions & 0 deletions src/us/kbase/auth2/service/api/APIPaths.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ public class APIPaths {

private static final String SEP = "/";

private static final String ADMIN = "admin";
private static final String ME = "me";
private static final String TOKEN = "token";
private static final String USERS = "users";
Expand Down Expand Up @@ -37,6 +38,11 @@ public class APIPaths {
/** The Globus user legacy endpoint location relative to the Globus root. */
public static final String LEGACY_GLOBUS_TOKEN = "goauth" + SEP + TOKEN;

/** The admin location. */
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 token introspection endpoint location. */
public static final String API_V2_TOKEN = API_V2 + SEP + TOKEN;

Expand Down
74 changes: 74 additions & 0 deletions src/us/kbase/auth2/service/api/Admin.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package us.kbase.auth2.service.api;

import static us.kbase.auth2.service.common.ServiceCommon.getToken;
import static us.kbase.auth2.service.common.ServiceCommon.nullOrEmpty;

import java.util.Collections;
import java.util.HashSet;
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.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import us.kbase.auth2.lib.Authentication;
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.NoTokenProvidedException;
import us.kbase.auth2.lib.exceptions.UnauthorizedException;
import us.kbase.auth2.lib.storage.exceptions.AuthStorageException;
import us.kbase.auth2.service.common.Fields;

@Path(APIPaths.API_V2_ADMIN)
public class Admin {

// TODO JAVADOC or better OpenAPI

private final Authentication auth;

@Inject
public Admin(final Authentication auth) {
this.auth = auth;
}

@GET
@Path(APIPaths.ANONYMOUS_ID_LOOKUP)
@Produces(MediaType.APPLICATION_JSON)
public Map<String, String> anonIDsToUserNames(
@HeaderParam(APIConstants.HEADER_TOKEN) final String token,
@QueryParam(Fields.LIST) final String anonymousIDs)
throws NoTokenProvidedException, InvalidTokenException, AuthStorageException,
DisabledUserException, IllegalParameterException, UnauthorizedException {
final Set<UUID> ids = processAnonymousIDListString(anonymousIDs);
final Map<UUID, UserName> map = auth.getUserNamesFromAnonymousIDs(getToken(token), ids);
return map.keySet().stream().collect(
Collectors.toMap(k -> k.toString(), k -> map.get(k).getName()));
}

static Set<UUID> processAnonymousIDListString(final String anonIDs)
throws IllegalParameterException {
if (nullOrEmpty(anonIDs)) {
return Collections.emptySet();
}
final Set<UUID> ids = new HashSet<>();
for (final String id: anonIDs.split(",")) {
try {
ids.add(UUID.fromString(id.trim()));
} catch (IllegalArgumentException e) {
throw new IllegalParameterException(String.format(
"Illegal anonymous user ID [%s]: %s", id.trim(), e.getMessage()));
}
}
return ids;
}

}
168 changes: 168 additions & 0 deletions src/us/kbase/test/auth2/service/api/AdminIntegrationTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,168 @@
package us.kbase.test.auth2.service.api;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static us.kbase.test.auth2.TestCommon.inst;
import static us.kbase.test.auth2.service.ServiceTestUtils.failRequestJSON;

import java.net.URI;
import java.nio.file.Path;
import java.util.Map;
import java.util.UUID;

import javax.ws.rs.client.Client;
import javax.ws.rs.client.ClientBuilder;
import javax.ws.rs.client.Invocation.Builder;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.Response;
import javax.ws.rs.core.UriBuilder;

import org.junit.AfterClass;
import org.junit.Before;
import org.junit.BeforeClass;
import org.junit.Test;

import com.google.common.collect.ImmutableMap;

import us.kbase.auth2.kbase.KBaseAuthConfig;
import us.kbase.auth2.lib.DisplayName;
import us.kbase.auth2.lib.PasswordHashAndSalt;
import us.kbase.auth2.lib.Role;
import us.kbase.auth2.lib.UserName;
import us.kbase.auth2.lib.exceptions.UnauthorizedException;
import us.kbase.auth2.lib.token.IncomingToken;
import us.kbase.auth2.lib.token.StoredToken;
import us.kbase.auth2.lib.token.TokenType;
import us.kbase.auth2.lib.user.LocalUser;
import us.kbase.test.auth2.MongoStorageTestManager;
import us.kbase.test.auth2.StandaloneAuthServer;
import us.kbase.test.auth2.TestCommon;
import us.kbase.test.auth2.StandaloneAuthServer.ServerThread;
import us.kbase.test.auth2.service.ServiceTestUtils;

public class AdminIntegrationTest {

/*
* Keep these integration tests reasonably minimal. There no need to exercise every single
* path in the call tree; unit tests are for that.
*/

private static final UUID UID = UUID.randomUUID();
private static final UUID UID2 = UUID.randomUUID();
private static final UUID UID3 = UUID.randomUUID();
private static final String DB_NAME = "test_admin_api";

private static final Client CLI = ClientBuilder.newClient();

private static MongoStorageTestManager manager = null;
private static StandaloneAuthServer server = null;
private static int port = -1;
private static String host = null;

@BeforeClass
public static void beforeClass() throws Exception {
TestCommon.stfuLoggers();
manager = new MongoStorageTestManager(DB_NAME);
final Path cfgfile = ServiceTestUtils.generateTempConfigFile(
manager, DB_NAME, "random_cookie_name");
TestCommon.getenv().put("KB_DEPLOYMENT_CONFIG", cfgfile.toString());
server = new StandaloneAuthServer(KBaseAuthConfig.class.getName());
new ServerThread(server).start();
System.out.println("Main thread waiting for server to start up");
while (server.getPort() == null) {
Thread.sleep(1000);
}
port = server.getPort();
host = "http://localhost:" + port;
}

@AfterClass
public static void afterClass() throws Exception {
if (server != null) {
server.stop();
}
if (manager != null) {
manager.destroy();
}
}

@Before
public void beforeTest() throws Exception {
ServiceTestUtils.resetServer(manager, host, "random_cookie_name");
}

@Test
public void translateAnonIDsToUserNames() throws Exception {
// Ensures mutability of map returned from the DAO to the main auth class
// as root has to be removed.
final PasswordHashAndSalt pwd = new PasswordHashAndSalt(
"foobarbazbing".getBytes(), "aa".getBytes());
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("foobar"), UID, new DisplayName("bleah"), inst(20000)).build(),
pwd);
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("yikes"), UID2, new DisplayName("bleah2"), inst(20000)).build(),
pwd);
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
UserName.ROOT, UID3, new DisplayName("r"), inst(20000))
.withRole(Role.ROOT)
.build(),
pwd);
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("admin"), UUID.randomUUID(), new DisplayName("a"), inst(20000))
.withRole(Role.ADMIN)
.build(),
pwd);
final IncomingToken token = new IncomingToken("whee");
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/anonids")
.queryParam(
"list",
String.format(" %s, %s \t , %s , %s ",
UID2, UID3, UID, UUID.randomUUID()))
.build();

final Builder req = CLI.target(target).request().header("authorization", token.getToken());

final Response res = req.get();

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

@SuppressWarnings("unchecked")
final Map<String, Object> response = res.readEntity(Map.class);

assertThat("incorrect users", response, is(ImmutableMap.of(
UID.toString(), "foobar", UID2.toString(), "yikes")));
}

@Test
public void translateAnonIDsToUserNamesFailNotAdmin() throws Exception {
manager.storage.createLocalUser(LocalUser.getLocalUserBuilder(
new UserName("admin"), UUID.randomUUID(), new DisplayName("a"), inst(20000))
.withRole(Role.CREATE_ADMIN)
.build(),
new PasswordHashAndSalt("foobarbazbing".getBytes(), "aa".getBytes()));
final IncomingToken token = new IncomingToken("whee");
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/anonids")
.queryParam(
"list",
String.format(" %s, %s \t , %s , %s ",
UID2, UID3, UID, UUID.randomUUID()))
.build();

final Builder req = CLI.target(target).request()
// GDI, Jersey adds a default accept header and I can't figure out how to stop it
// http://stackoverflow.com/questions/40900870/how-do-i-get-jersey-test-client-to-not-fill-in-a-default-accept-header
.header("accept", MediaType.APPLICATION_JSON)
.header("authorization", token.getToken());

failRequestJSON(req.get(), 403, "Forbidden", new UnauthorizedException());
}
}
117 changes: 117 additions & 0 deletions src/us/kbase/test/auth2/service/api/AdminTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
package us.kbase.test.auth2.service.api;

import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.junit.Assert.fail;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static us.kbase.test.auth2.TestCommon.set;

import java.util.Collections;
import java.util.Map;
import java.util.UUID;

import org.junit.Test;

import com.google.common.collect.ImmutableMap;

import us.kbase.auth2.lib.Authentication;
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.token.IncomingToken;
import us.kbase.auth2.service.api.Admin;
import us.kbase.test.auth2.TestCommon;

public class AdminTest {

private static final UUID UID1 = UUID.randomUUID();
private static final UUID UID2 = UUID.randomUUID();

@Test
public void anonIDsToUserNamesNullAndEmpty() throws Exception {
anonIdsToUserNamesNullAndEmpty(null);
anonIdsToUserNamesNullAndEmpty(" \t \n ");
}

private void anonIdsToUserNamesNullAndEmpty(final String anonIDs) throws Exception {
final Authentication auth = mock(Authentication.class);

final Admin admin = new Admin(auth);

when(auth.getUserNamesFromAnonymousIDs(new IncomingToken("whee"), set())).thenReturn(
Collections.emptyMap());

assertThat("incorrect users", admin.anonIDsToUserNames("whee", anonIDs),
is(Collections.emptyMap()));

// if the when above doesn't match it still returns an empty map so we verify here
verify(auth).getUserNamesFromAnonymousIDs(new IncomingToken("whee"), set());
}

@Test
public void anonIDsToUserNames() throws Exception {
final Authentication auth = mock(Authentication.class);

final Admin admin = new Admin(auth);

when(auth.getUserNamesFromAnonymousIDs(new IncomingToken("whee"), set(UID2, UID1)))
.thenReturn(ImmutableMap.of(UID1, new UserName("bar"), UID2, new UserName("foo")));

final Map<String, String> ret = admin.anonIDsToUserNames(
"whee", String.format(" \t %s , %s \n ", UID1, UID2));

assertThat("incorrect users", ret,
is(ImmutableMap.of(UID1.toString(), "bar", UID2.toString(), "foo")));
}

@Test
public void anonIDsToUserNamesFailInputs() throws Exception {
final Authentication auth = mock(Authentication.class);
final Admin admin = new Admin(auth);

final String t = "token";
final String a = "b8e62d05-1968-4aa0-916d-8815ab69ea15";

anonIDsToUserNamesFail(admin, t, a + ", foobar, " + a, new IllegalParameterException(
"Illegal anonymous user ID [foobar]: Invalid UUID string: foobar"));
// error message is different for java 8 & 11. When 8 is gone switch back to exact test
anonIDsToUserNamesFailContains(admin, t, a + "x",
"Illegal anonymous user ID [b8e62d05-1968-4aa0-916d-8815ab69ea15x]: ");
anonIDsToUserNamesFail(admin, t, a + ", , " + a, new IllegalParameterException(
"Illegal anonymous user ID []: Invalid UUID string: "));

anonIDsToUserNamesFail(admin, null, a, new NoTokenProvidedException(
"No user token provided"));
anonIDsToUserNamesFail(admin, " \n \t ", a, new NoTokenProvidedException(
"No user token provided"));
}

private void anonIDsToUserNamesFail(
final Admin admin,
final String token,
final String anonIDs,
final Exception expected) {
try {
admin.anonIDsToUserNames(token, anonIDs);
fail("expected exception");
} catch (Exception got) {
TestCommon.assertExceptionCorrect(got, expected);
}
}

private void anonIDsToUserNamesFailContains(
final Admin admin,
final String token,
final String anonIDs,
final String expected)
throws Exception {
try {
admin.anonIDsToUserNames(token, anonIDs);
fail("expected exception");
} catch (IllegalParameterException got) {
TestCommon.assertExceptionMessageContains(got, expected);
}
}
}

0 comments on commit 14a7008

Please sign in to comment.