forked from kbase/auth2
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge pull request kbase#405 from MrCreosote/develop
PTV-1890: Add admin API to translate anonymous IDs to user names
- Loading branch information
Showing
6 changed files
with
367 additions
and
1 deletion.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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
168
src/us/kbase/test/auth2/service/api/AdminIntegrationTest.java
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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()); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
} | ||
} | ||
} |