diff --git a/src/main/java/us/kbase/auth/AuthConfig.java b/src/main/java/us/kbase/auth/AuthConfig.java new file mode 100644 index 0000000..3b855a4 --- /dev/null +++ b/src/main/java/us/kbase/auth/AuthConfig.java @@ -0,0 +1,120 @@ +package us.kbase.auth; + +import java.net.MalformedURLException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URL; + +/** The configuration class for {@link ConfigurableAuthService}. + * + * Changes to the config have no effect after {@link ConfigurableAuthService} is instantiated. + * + * @author gaprice@lbl.gov + * + */ +public class AuthConfig { + + private static final String DEFAULT_KBASE_AUTH_SERVER_URL = + "https://ci.kbase.us/services/auth/"; + + private static final String LEGACY_PATH = "api/legacy/KBase/"; + private static final String LOGIN_LOC = "Sessions/Login/"; + + private URI authServerURL; + private boolean allowInsecureURLs = false; + + /** Get the default authorization URL. + * @return the default authorization URL. + */ + public static URL getDefaultAuthURL() { + try { + return new URL(DEFAULT_KBASE_AUTH_SERVER_URL); + } catch (MalformedURLException e) { + throw new RuntimeException("The impossible just happened"); + } + } + + /** + * Create a configuration object with default settings. + */ + public AuthConfig() { + try { + authServerURL = new URI(DEFAULT_KBASE_AUTH_SERVER_URL); + } catch (URISyntaxException use) { + throw new RuntimeException( + "This cannot occur. Please check with your local deity for an explanation."); + } + } + + /** Set the URL of the KBase authorization server. Note that to maintain + * compatibility with previous versions of this client, URLs ending in + * Sessions/Login, api/legacy/KBase/, or api/legacy/KBase/Sessions/Login + * with or without trailing slashes will have that portion of the URL removed. + * @param authServer the URL of the KBase authorization server. + * @return this + * @throws URISyntaxException if the URL is not a valid URI. In general + * this should never happen. + */ + public AuthConfig withKBaseAuthServerURL(URL authServer) + throws URISyntaxException { + if (authServer == null) { + throw new NullPointerException("authServer cannot be null"); + } + if (!authServer.toString().endsWith("/")) { + try { + authServer = new URL(authServer.toString() + "/"); + } catch (MalformedURLException e) { + throw new RuntimeException("This can't happen", e); + } + } + authServer = stripURLSuffix(authServer, LOGIN_LOC); + authServer = stripURLSuffix(authServer, LEGACY_PATH); + authServerURL = authServer.toURI(); + return this; + } + + private URL stripURLSuffix(URL rui, final String suffix) { + if (rui.getPath().endsWith(suffix)) { + final int index = rui.toString().lastIndexOf(suffix); + try { + rui = new URL(rui.toString().substring(0, index)); + } catch (MalformedURLException e) { + throw new RuntimeException( + "The impossible just occured. Congratulations.", e); + } + } + return rui; + } + + /** Allow insecure http URLs rather than https URLs. Only use this setting + * for tests, never in production. + * + * When using insecure URLs, you must call this method *before* + * initializing the auth client. + * @param insecure true to allow insecure URLs. + * @return this + */ + public AuthConfig withAllowInsecureURLs(final boolean insecure) { + this.allowInsecureURLs = insecure; + return this; + } + + /** Returns the configured KBase authorization service URL. + * @return the authorization service URL. + */ + public URL getAuthServerURL() { + try { + return authServerURL.toURL(); + } catch (MalformedURLException e) { + throw new RuntimeException("This should never happen"); + } + } + + /** Returns true if insecure URLs are allowed, false otherwise. + * @return whether insecure URLs are allowed. + */ + public boolean isInsecureURLsAllowed() { + return allowInsecureURLs; + } + +} diff --git a/src/main/java/us/kbase/auth/ConfigurableAuthService.java b/src/main/java/us/kbase/auth/ConfigurableAuthService.java new file mode 100644 index 0000000..abbdd51 --- /dev/null +++ b/src/main/java/us/kbase/auth/ConfigurableAuthService.java @@ -0,0 +1,89 @@ +package us.kbase.auth; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.util.List; +import java.util.Map; + +import us.kbase.auth.client.AuthClient; + +/** + * This is a shim around the {@link AuthClient} for backwards compatibility purposes. + * + * Only one instance of a client should be created per application if at all possible. + * + * @author wjriehl + * @author gaprice@lbl.gov + */ +public class ConfigurableAuthService { + + private final AuthClient client; + + /** Create an authorization service client with the default configuration. + * @throws IOException if an IO error occurs. + */ + public ConfigurableAuthService() throws IOException { + this(new AuthConfig()); + } + + /** Create an authorization service client with a custom configuration. + * @param config the configuration for the auth client. + * @throws IOException if an IO error occurs. + */ + public ConfigurableAuthService(final AuthConfig config) throws IOException { + if (config == null) { + throw new NullPointerException("config cannot be null"); + } + if (!config.isInsecureURLsAllowed() && + !"https".equals(config.getAuthServerURL().getProtocol())) { + throw new IllegalArgumentException(String.format( + "The URL %s is insecure and insecure URLs are not allowed", + config.getAuthServerURL())); + } + try { + client = AuthClient.from(config.getAuthServerURL().toURI()); + } catch (AuthException e) { + // can't break backwards compatibility by throwing AuthException + throw new IOException(e.getMessage(), e); + } catch (URISyntaxException e) { + throw new RuntimeException("this should be impossible - checked in AuthConfig", e); + } + } + + /** Get the auth client underlying this client. + * @return the client. + */ + public AuthClient getClient() { + return client; + } + + /** + * Checks whether strings are a valid user names. + * @param usernames the usernames + * @param token a valid token + * @return a mapping of username to validity. + * @throws AuthException if the credentials are invalid + * @throws IOException if there is a problem communicating with the server. + * @throws IllegalArgumentException if a username is invalid. + */ + public Map isValidUserName( + final List usernames, + final AuthToken token) + throws IOException, AuthException { + return client.isValidUserName(usernames, token.getToken()); + } + + /** + * Validates a token and returns a validated token. + * + * @param tokenStr the token string to validate. + * @return a validated token + * @throws IOException if there is a problem communicating with the server. + * @throws AuthException if the token is invalid. + */ + public AuthToken validateToken(final String tokenStr) + throws IOException, AuthException { + return client.validateToken(tokenStr); + } + +} diff --git a/src/main/java/us/kbase/auth/client/AuthClient.java b/src/main/java/us/kbase/auth/client/AuthClient.java index 9cf1877..5e69cb9 100644 --- a/src/main/java/us/kbase/auth/client/AuthClient.java +++ b/src/main/java/us/kbase/auth/client/AuthClient.java @@ -152,6 +152,13 @@ private String readResponse( } return restext.toString(); } + + /** Get the auth service URI. + * @return the URI. + */ + public URI getURI() { + return rootURI; + } /** Get the version of the auth server with which this client communicates. * @return the server version. diff --git a/src/main/java/us/kbase/auth/client/cache/TokenCache.java b/src/main/java/us/kbase/auth/client/cache/TokenCache.java index 2d4fe18..1864695 100644 --- a/src/main/java/us/kbase/auth/client/cache/TokenCache.java +++ b/src/main/java/us/kbase/auth/client/cache/TokenCache.java @@ -127,7 +127,6 @@ class UserDate { long date; UserDate(String user) { - super(); this.user = user; this.date = new Date().getTime(); } diff --git a/src/test/java/us/kbase/test/auth/AuthConfigTest.java b/src/test/java/us/kbase/test/auth/AuthConfigTest.java new file mode 100644 index 0000000..cc53d0c --- /dev/null +++ b/src/test/java/us/kbase/test/auth/AuthConfigTest.java @@ -0,0 +1,68 @@ +package us.kbase.test.auth; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.net.URL; + +import org.junit.Test; + +import us.kbase.auth.AuthConfig; +import us.kbase.test.common.TestCommon; + +public class AuthConfigTest { + + @Test + public void defaultURL() throws Exception { + assertThat("incorrect default URL", AuthConfig.getDefaultAuthURL(), + is(new URL("https://ci.kbase.us/services/auth/"))); + } + + @Test + public void buildMinimal() throws Exception { + final AuthConfig c = new AuthConfig(); + + assertThat("incorrect auth URL", c.getAuthServerURL(), + is(new URL("https://ci.kbase.us/services/auth/"))); + assertThat("incorrect allow insecure", c.isInsecureURLsAllowed(), is(false)); + } + + @Test + public void buildMaximal() throws Exception { + final AuthConfig c = new AuthConfig() + .withKBaseAuthServerURL(new URL("https://vegtableexcitement.com")) + .withAllowInsecureURLs(true); + + assertThat("incorrect auth URL", c.getAuthServerURL(), + is(new URL("https://vegtableexcitement.com/"))); + assertThat("incorrect allow insecure", c.isInsecureURLsAllowed(), is(true)); + } + + @Test + public void buildAndStripURLs() throws Exception { + // this one seems unlikely to be seen in the wild + buildAndStripURLs("https://ci.kbase.us/services/auth/Sessions/Login"); + buildAndStripURLs("https://ci.kbase.us/services/auth/api/legacy/KBase/"); + buildAndStripURLs("https://ci.kbase.us/services/auth/api/legacy/KBase/Sessions/Login"); + } + + private void buildAndStripURLs(final String url) throws Exception { + final AuthConfig c = new AuthConfig().withKBaseAuthServerURL(new URL(url)); + + assertThat("incorrect auth URL", c.getAuthServerURL(), + is(new URL("https://ci.kbase.us/services/auth/"))); + } + + @Test + public void withKBaseAuthServerURLFail() throws Exception { + try { + new AuthConfig().withKBaseAuthServerURL(null); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect( + got, new NullPointerException("authServer cannot be null")); + } + } + +} diff --git a/src/test/java/us/kbase/test/auth/ConfigurableAuthServiceTest.java b/src/test/java/us/kbase/test/auth/ConfigurableAuthServiceTest.java new file mode 100644 index 0000000..b05baab --- /dev/null +++ b/src/test/java/us/kbase/test/auth/ConfigurableAuthServiceTest.java @@ -0,0 +1,111 @@ +package us.kbase.test.auth; + +import static org.hamcrest.CoreMatchers.is; +import static org.junit.Assert.assertThat; +import static org.junit.Assert.fail; + +import java.io.IOException; +import java.net.URI; +import java.net.URL; +import java.util.Arrays; +import java.util.HashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import us.kbase.auth.AuthConfig; +import us.kbase.auth.AuthToken; +import us.kbase.auth.ConfigurableAuthService; +import us.kbase.test.common.TestCommon; + +public class ConfigurableAuthServiceTest { + + + @Test + public void buildFailBadArgs() throws Exception { + buildFail(null, new NullPointerException("config cannot be null")); + + final AuthConfig c = new AuthConfig() + .withKBaseAuthServerURL(new URL("http://ci.kbase.us")); + + buildFail(c, new IllegalArgumentException( + "The URL http://ci.kbase.us/ is insecure and insecure URLs are not allowed")); + } + + @Test + public void buildFailBadURL() throws Exception { + final AuthConfig c = new AuthConfig() + .withKBaseAuthServerURL(new URL("https://ci.kbase.us/services/auth/fakeapi")); + + buildFail(c, new IOException( + "Auth service returned an error: HTTP 404 Not Found")); + } + + private void buildFail(final AuthConfig cfg, final Exception expected) { + try { + new ConfigurableAuthService(cfg); + fail("expected exception"); + } catch (Exception got) { + TestCommon.assertExceptionCorrect(got, expected); + } + } + + @Test + public void buildNoArgs() throws Exception { + // not sure what else to test here + assertThat("incorrect build", new ConfigurableAuthService().getClient().getURI(), + is(new URI("https://ci.kbase.us/services/auth/"))); + } + + @Test + public void buildLenient() throws Exception { + final ConfigurableAuthService c = new ConfigurableAuthService(new AuthConfig() + .withAllowInsecureURLs(true) + .withKBaseAuthServerURL(new URL("https://ci.kbase.us/services/auth/"))); + // not sure what else to test here + assertThat("incorrect build", c.getClient().getURI(), + is(new URI("https://ci.kbase.us/services/auth/"))); + } + + @Test + public void validateToken() throws Exception { + // most of the logic is tested in the auth client tests, just do a minimal test here + final String token1 = TestCommon.getAuthToken1(); + final String user1 = TestCommon.getAuthUser1(); + + final AuthToken t = new ConfigurableAuthService( + new AuthConfig().withKBaseAuthServerURL(new URL(TestCommon.getAuthURI())) + ).validateToken(token1); + + assertThat("incorrect user", t.getUserName(), is(user1)); // for easier debugging + assertThat("incorrect auth token", t, is(new AuthToken(token1, user1))); + } + + @Test + public void isValidUserName() throws Exception { + // most of the logic is tested in the auth client tests, just do a minimal test her + final String token1 = TestCommon.getAuthToken1(); + final List goodUsers = TestCommon.getGoodUsers(); + + final ConfigurableAuthService c = new ConfigurableAuthService( + new AuthConfig().withKBaseAuthServerURL(new URL(TestCommon.getAuthURI()))); + + final List badUsers = Arrays.asList( + "superfakeuserthatdoesntexistihope", + "anothersuperfakeuserrighthereimfake"); + + final List allUsers = new LinkedList<>(badUsers); + allUsers.addAll(goodUsers); + + final Map expected = new HashMap<>(); + goodUsers.stream().forEach(u -> expected.put(u.trim(), true)); + badUsers.stream().forEach(u -> expected.put(u, false)); + + final Map res = c.isValidUserName( + allUsers, new AuthToken(token1, "fakeuser")); + + assertThat("incorrect users", res, is(expected)); + } +} diff --git a/src/test/java/us/kbase/test/auth/client/AuthClientTest.java b/src/test/java/us/kbase/test/auth/client/AuthClientTest.java index 69679b6..a083f3b 100644 --- a/src/test/java/us/kbase/test/auth/client/AuthClientTest.java +++ b/src/test/java/us/kbase/test/auth/client/AuthClientTest.java @@ -118,6 +118,12 @@ private void failConstruct(final URI uri, final Exception err) { TestCommon.assertExceptionCorrect(got, err); } } + + @Test + public void constructAndGetURI() throws Exception { + final AuthClient c = AuthClient.from(new URI("https://ci.kbase.us/services/auth")); + assertThat("incorrect uri", c.getURI(), is(new URI("https://ci.kbase.us/services/auth/"))); + } @Test public void getServerVersion() throws Exception {