Skip to content

Commit

Permalink
support direct injection of the keycloak JSON in the client configura…
Browse files Browse the repository at this point in the history
…tion (#20)

* support direct injection of the keycloak JSON in the client configuratio

* Copyright reformatted

* Removed temporary file logic, implemented Chris suggestion.

* Log added

* Common builder authenticator function added

* Common builder authenticator function added

* Unit test function added

* brackets  added

* removed builderAuthenticator as a unit test function
  • Loading branch information
sabuz-262 authored Sep 10, 2020
1 parent 7a02c4b commit 6f44fb5
Show file tree
Hide file tree
Showing 4 changed files with 123 additions and 23 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -208,6 +208,7 @@ boolean isTokenTimeToLiveSufficient() {
public static class Builder {
private String audience;
private String configFile;
private String configString;
private BiFunction<Configuration, ClientAuthenticator, AuthzClient> clientSupplier;
private int httpMaxRetries;
private int httpInitialDelayMs;
Expand All @@ -228,6 +229,15 @@ public Builder withConfigFile(String path) {
return this;
}

/**
* Sets the Keycloak client configuration String to use.
* @param configString Keycloak OIDC JSON as a String.
*/
public Builder withConfigString(final String configString) {
this.configString = configString;
return this;
}

/**
* Sets the audience for the RPT ticket to obtain.
*
Expand Down Expand Up @@ -265,13 +275,18 @@ KeycloakAuthzClient.Builder withAuthzClientSupplier(BiFunction<Configuration, Cl
public KeycloakAuthzClient build() {
Configuration configuration;
try {
configuration = KeycloakConfigResolver.resolve(configFile).orElseThrow(() -> new KeycloakConfigurationException(
"Unable to resolve a Keycloak client configuration for Pravega authentication purposes.\n\n" +
"Use one of the following approaches to provide a client configuration (in Keycloak OIDC JSON format):\n" +
"1. Use a builder method to set the path to a file.\n" +
"2. Set the environment variable 'KEYCLOAK_SERVICE_ACCOUNT_FILE' to the path to a file.\n" +
"3. Update the classpath to contain a resource named 'keycloak.json'.\n" +
""));
String errorMessage = "Unable to resolve a Keycloak client configuration for Pravega authentication purposes.\n\n" +
"Use one of the following approaches to provide a client configuration (in Keycloak OIDC JSON format):\n" +
"1. Use a builder method to set the keycloak OIDC config as a JSON string.\n" +
"2. Use a builder method to set the path to a file.\n" +
"3. Set the environment variable 'KEYCLOAK_SERVICE_ACCOUNT_FILE' to the path to a file.\n" +
"4. Update the classpath to contain a resource named 'keycloak.json'.\n" +
"";
if (configString != null) {
configuration = KeycloakConfigResolver.resolveFromString(configString).orElseThrow(() -> new KeycloakConfigurationException(errorMessage));
} else {
configuration = KeycloakConfigResolver.resolve(configFile).orElseThrow(() -> new KeycloakConfigurationException(errorMessage));
}
} catch (IOException e) {
throw new KeycloakConfigurationException("Unexpected error in resolving or loading the Keycloak client configuration", e);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,21 @@
* 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
* http://www.apache.org/licenses/LICENSE-2.0
*/

package io.pravega.keycloak.client;

import com.google.common.base.Strings;
import org.keycloak.authorization.client.Configuration;
import org.keycloak.util.JsonSerialization;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
Expand All @@ -25,7 +28,7 @@

/**
* A resolver for Keycloak {@link Configuration} objects.
* <p>
*
* The following methods are attempted, in order, to load a Keycloak adapter configuration:
* 1. an explicit file location pointing to a Keycloak adapter configuration file
* 2. an environment variable pointing to the location of an adapter configuration file
Expand All @@ -51,8 +54,29 @@ public static Optional<Configuration> resolve() throws IOException {
* @return a {@link Configuration} if found.
*/
public static Optional<Configuration> resolve(String fileLocation) throws IOException {
return resolve(null, fileLocation);
}

/**
* Resolves a {@link Configuration} using the available methods.
*
* @param configString adapter configuration file content as a String
* @return a {@link Configuration} if found.
*/
public static Optional<Configuration> resolveFromString(String configString) throws IOException {
return resolve(configString, null);
}

Optional<InputStream> stream = open(fileLocation, System.getenv(), Thread.currentThread().getContextClassLoader());
/**
* Resolves a {@link Configuration} using the available methods.
*
* @param configString adapter configuration file content as a String
* @param fileLocation location of an adapter configuration file
* @return a {@link Configuration} if found.
*/
static Optional<Configuration> resolve(String configString, String fileLocation) throws IOException {

Optional<InputStream> stream = open(configString, fileLocation, System.getenv(), Thread.currentThread().getContextClassLoader());
if (!stream.isPresent()) {
LOG.debug("Keycloak adapter configuration not found");
return Optional.empty();
Expand All @@ -67,8 +91,10 @@ public static Optional<Configuration> resolve(String fileLocation) throws IOExce
}
}

static Optional<InputStream> open(String fileLocation, Map<String, String> envs, ClassLoader classLoader) throws IOException {
static Optional<InputStream> open(String configString, String fileLocation, Map<String, String> envs, ClassLoader classLoader) throws IOException {
Optional<InputStream> stream;
stream = string(configString);
if (stream.isPresent()) return stream;
stream = path(fileLocation);
if (stream.isPresent()) return stream;
stream = env(envs);
Expand All @@ -77,6 +103,14 @@ static Optional<InputStream> open(String fileLocation, Map<String, String> envs,
return stream;
}

static Optional<InputStream> string(String configString) {
if (!Strings.isNullOrEmpty(configString)) {
LOG.debug("Loaded configuration from string");
return Optional.of(new ByteArrayInputStream(configString.getBytes(StandardCharsets.UTF_8)));
}
return Optional.empty();
}

static Optional<InputStream> path(String fileLocation) throws IOException {
Optional<Path> path = pathIfExists(fileLocation);
if (path.isPresent()) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,8 +26,16 @@ public class PravegaKeycloakCredentials implements Credentials {

// The actual keycloak client won't be serialized.
private transient KeycloakAuthzClient kc = null;
private final String keycloakJsonString;

public PravegaKeycloakCredentials() {
init();
keycloakJsonString = null;
LOG.info("Loaded Keycloak Credentials");
}

public PravegaKeycloakCredentials(final String keycloakJsonString) {
this.keycloakJsonString = keycloakJsonString;
init();
LOG.info("Loaded Keycloak Credentials");
}
Expand All @@ -45,7 +53,11 @@ public String getAuthenticationToken() {

private void init() {
if (kc == null) {
kc = KeycloakAuthzClient.builder().build();
if (keycloakJsonString != null) {
kc = KeycloakAuthzClient.builder().withConfigString(keycloakJsonString).build();
} else {
kc = KeycloakAuthzClient.builder().build();
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
* 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
* http://www.apache.org/licenses/LICENSE-2.0
*/

package io.pravega.keycloak.client;
Expand All @@ -27,7 +27,10 @@
import org.mockito.Mockito;

import java.io.File;
import java.io.IOException;
import java.net.ConnectException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
Expand All @@ -41,6 +44,7 @@

public class KeycloakAuthzClientTest {
private static final String SVC_ACCOUNT_JSON_FILE = getResourceFile("service-account.json");
private static final String SVC_ACCOUNT_JSON_STRING = getResourceString(getResourceFile("service-account.json"));
private static final AccessTokenIssuer ISSUER = new AccessTokenIssuer();

@Test
Expand Down Expand Up @@ -78,7 +82,7 @@ public void getRPTFailsToGetAccessToken() {
try {
authzClient.getRPT();
Assert.fail();
} catch(KeycloakAuthenticationException e) {
} catch (KeycloakAuthenticationException e) {
}
verify(client, times(1)).obtainAccessToken();
}
Expand All @@ -95,7 +99,7 @@ public void getRPTCannotExchangeAccessTokenForRPT() {
try {
authzClient.getRPT();
Assert.fail();
} catch(KeycloakAuthorizationException e) {
} catch (KeycloakAuthorizationException e) {
}
verify(client.authorization(any()), times(1)).authorize(any());
}
Expand All @@ -110,7 +114,7 @@ public void getRPTWithHttp500Exception() {
try {
authzClient.getRPT();
Assert.fail();
} catch(RetriesExhaustedException e) {
} catch (RetriesExhaustedException e) {
}
verify(client, times(3)).obtainAccessToken();
}
Expand All @@ -121,11 +125,11 @@ public void getRPTWithRuntimeConnectException() {
TokenCache tokenCache = spy(new TokenCache(0));

when(client.obtainAccessToken()).thenThrow(new RuntimeException(new ConnectException()));
KeycloakAuthzClient authzClient = new KeycloakAuthzClient(client, tokenCache,3, 1);
KeycloakAuthzClient authzClient = new KeycloakAuthzClient(client, tokenCache, 3, 1);
try {
authzClient.getRPT();
Assert.fail();
} catch(RetriesExhaustedException e) {
} catch (RetriesExhaustedException e) {
}
verify(client, times(3)).obtainAccessToken();
}
Expand All @@ -140,7 +144,7 @@ public void getRPTWithRandomRuntimeException() {
try {
authzClient.getRPT();
Assert.fail();
} catch(RetriesExhaustedException e) {
} catch (RetriesExhaustedException e) {
Assert.fail();
} catch (RuntimeException e) {
}
Expand Down Expand Up @@ -174,6 +178,13 @@ public void builderDefaultAudience() {
assertEquals(DEFAULT_PRAVEGA_CONTROLLER_CLIENT_ID, supplier.configuration.getResource());
}

@Test
public void builderDefaultAudienceFromString() {
TestSupplier supplier = new TestSupplier();
KeycloakAuthzClient.builder().withAuthzClientSupplier(supplier).withConfigString(SVC_ACCOUNT_JSON_STRING).build();
assertEquals(DEFAULT_PRAVEGA_CONTROLLER_CLIENT_ID, supplier.configuration.getResource());
}

@Test
public void builderSetAudience() {
TestSupplier supplier = new TestSupplier();
Expand All @@ -182,17 +193,27 @@ public void builderSetAudience() {
assertEquals("builder_setAudience", supplier.configuration.getResource());
}

@Test
public void builderSetAudienceFromString() {
TestSupplier supplier = new TestSupplier();
KeycloakAuthzClient.builder().withAuthzClientSupplier(supplier).withConfigString(SVC_ACCOUNT_JSON_STRING)
.withAudience("builder_setAudience").build();
assertEquals("builder_setAudience", supplier.configuration.getResource());
}

@Test(expected = KeycloakConfigurationException.class)
public void builderNoConfig() {
TestSupplier supplier = new TestSupplier();
KeycloakAuthzClient.builder().withAuthzClientSupplier(supplier).build();
}

@Test
public void builderAuthenticator() {
void builderAuthenticator(boolean isFile) {
TestSupplier supplier = new TestSupplier();
KeycloakAuthzClient.builder().withAuthzClientSupplier(supplier).withConfigFile(SVC_ACCOUNT_JSON_FILE).build();

if (isFile) {
KeycloakAuthzClient.builder().withAuthzClientSupplier(supplier).withConfigFile(SVC_ACCOUNT_JSON_FILE).build();
} else {
KeycloakAuthzClient.builder().withAuthzClientSupplier(supplier).withConfigString(SVC_ACCOUNT_JSON_STRING).build();
}
Map<String, List<String>> requestParams = new HashMap<>();
Map<String, String> requestHeaders = new HashMap<>();
supplier.clientAuthenticator.configureClientCredentials(requestParams, requestHeaders);
Expand All @@ -202,6 +223,16 @@ public void builderAuthenticator() {
BasicAuthHelper.createHeader("test-client", "b3f202cb-29fe-4d13-afb8-15e787c6e56c"));
}

@Test
public void builder_authenticatorFromFile() {
builderAuthenticator(true);
}

@Test
public void builder_authenticatorFromString() {
builderAuthenticator(false);
}

@Test
public void checkDeserializeToken() {
String goodToken = token(UUID.randomUUID().toString(), false);
Expand Down Expand Up @@ -244,6 +275,14 @@ private static String getResourceFile(String resourceName) {
return new File(KeycloakAuthzClientTest.class.getClassLoader().getResource(resourceName).getFile()).getAbsolutePath();
}

private static String getResourceString(String resourceFilePath) {
try {
return new String(Files.readAllBytes(Paths.get(resourceFilePath)));
} catch (IOException e) {
throw new RuntimeException("Could not load resource path: " + resourceFilePath, e);
}
}

class TestSupplier implements BiFunction<Configuration, ClientAuthenticator, AuthzClient> {
Configuration configuration;
ClientAuthenticator clientAuthenticator;
Expand Down

0 comments on commit 6f44fb5

Please sign in to comment.