From 67b7b100ed17947d06da3b91e8f246e39657e9e6 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 24 Jul 2024 09:00:15 +0100 Subject: [PATCH 01/28] Add OIDC client credentials implementation. --- pom.xml | 14 +++ .../relations/client/CDIManagedClients.java | 11 +++ .../relations/client/Config.java | 30 ++++++- .../client/RelationsGrpcClientsManager.java | 53 ++++++++++-- .../OIDCClientCredentialsCallCredentials.java | 77 +++++++++++++++++ .../client/OIDCClientCredentialsMinter.java | 85 +++++++++++++++++++ .../NimbusOIDCClientCredentialsMinter.java | 70 +++++++++++++++ .../client/CDIManagedClientsTest.java | 6 ++ .../relations/client/ConfigTest.java | 32 +++++++ 9 files changed, 371 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java create mode 100644 src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java create mode 100644 src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java create mode 100644 src/test/java/org/project_kessel/relations/client/ConfigTest.java diff --git a/pom.xml b/pom.xml index 757a109..f53e8fa 100644 --- a/pom.xml +++ b/pom.xml @@ -87,6 +87,20 @@ pgv-java-stub 1.0.4 + + + com.nimbusds + oauth2-oidc-sdk + 11.13 + provided + + + + io.quarkus + quarkus-core + 3.12.3 + provided + org.apache.tomcat annotations-api diff --git a/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java b/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java index 74fff25..2b0e7e2 100644 --- a/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java +++ b/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java @@ -15,11 +15,22 @@ public class CDIManagedClients { RelationsGrpcClientsManager getManager(Config config) { var isSecureClients = config.isSecureClients(); var targetUrl = config.targetUrl(); + var authnEnabled = config.authenticationConfig().map(t -> !t.mode().equals(Config.AuthMode.DISABLED)).orElse(false); + + if(authnEnabled && config.authenticationConfig().isEmpty()) { + throw new RuntimeException("Authentication mode enabled but no authentication config provided."); + } if (isSecureClients) { + if(authnEnabled) { + return RelationsGrpcClientsManager.forSecureClients(targetUrl, config.authenticationConfig().get()); + } return RelationsGrpcClientsManager.forSecureClients(targetUrl); } + if(authnEnabled) { + return RelationsGrpcClientsManager.forInsecureClients(targetUrl, config.authenticationConfig().get()); + } return RelationsGrpcClientsManager.forInsecureClients(targetUrl); } diff --git a/src/main/java/org/project_kessel/relations/client/Config.java b/src/main/java/org/project_kessel/relations/client/Config.java index 893b195..c9ff098 100644 --- a/src/main/java/org/project_kessel/relations/client/Config.java +++ b/src/main/java/org/project_kessel/relations/client/Config.java @@ -2,6 +2,9 @@ import io.smallrye.config.ConfigMapping; import io.smallrye.config.WithDefault; +import io.smallrye.config.WithName; + +import java.util.Optional; /** * Interface for injecting config into container managed beans. @@ -11,7 +14,32 @@ */ @ConfigMapping(prefix = "relations-api") public interface Config { + enum AuthMode { + DISABLED, + OIDC_CLIENT_CREDENTIALS + } + @WithDefault("false") boolean isSecureClients(); String targetUrl(); -} + + @WithName("authn") + Optional authenticationConfig(); + + interface AuthenticationConfig { + @WithDefault("disabled") + AuthMode mode(); + @WithName("client") + Optional clientCredentialsConfig(); + } + + interface OIDCClientCredentialsConfig { + String issuer(); + @WithName("id") + String clientId(); + @WithName("secret") + String clientSecret(); + Optional scope(); + Optional OIDCClientCredentialsMinterImplementation(); + } +} \ No newline at end of file diff --git a/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java b/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java index 4268805..347431f 100644 --- a/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java +++ b/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java @@ -1,6 +1,7 @@ package org.project_kessel.relations.client; import io.grpc.*; +import org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsCallCredentials; import java.util.HashMap; @@ -18,6 +19,21 @@ public static synchronized RelationsGrpcClientsManager forInsecureClients(String return insecureManagers.get(targetUrl); } + public static synchronized RelationsGrpcClientsManager forInsecureClients(String targetUrl, Config.AuthenticationConfig authnConfig) throws RuntimeException { + if (!insecureManagers.containsKey(targetUrl)) { + try { + // For now, the only client authn scheme supported is OIDC client credentials + var manager = new RelationsGrpcClientsManager(targetUrl, + InsecureChannelCredentials.create(), + new OIDCClientCredentialsCallCredentials(authnConfig)); + insecureManagers.put(targetUrl, manager); + } catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + throw new RuntimeException(e); + } + } + return insecureManagers.get(targetUrl); + } + public static synchronized RelationsGrpcClientsManager forSecureClients(String targetUrl) { if (!secureManagers.containsKey(targetUrl)) { var tlsChannelCredentials = TlsChannelCredentials.create(); @@ -27,6 +43,22 @@ public static synchronized RelationsGrpcClientsManager forSecureClients(String t return secureManagers.get(targetUrl); } + public static synchronized RelationsGrpcClientsManager forSecureClients(String targetUrl, Config.AuthenticationConfig authnConfig) { + if (!secureManagers.containsKey(targetUrl)) { + var tlsChannelCredentials = TlsChannelCredentials.create(); + try { + // For now, the only client authn scheme supported is OIDC client credentials + var manager = new RelationsGrpcClientsManager(targetUrl, + tlsChannelCredentials, + new OIDCClientCredentialsCallCredentials(authnConfig)); + secureManagers.put(targetUrl, manager); + } catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + throw new RuntimeException(e); + } + } + return secureManagers.get(targetUrl); + } + public static synchronized void shutdownAll() { for (var manager : insecureManagers.values()) { manager.closeClientChannel(); @@ -60,14 +92,23 @@ public static synchronized void shutdownManager(RelationsGrpcClientsManager mana } /** - * - * Bearer token and other things can be added to ChannelCredentials. New static factory methods can be added. - * Config management also required. + * Create a manager for a grpc channel with server credentials. + * @param targetUrl + * @param serverCredentials authenticates the server for TLS or are InsecureChannelCredentials + */ + private RelationsGrpcClientsManager(String targetUrl, ChannelCredentials serverCredentials) { + this.channel = Grpc.newChannelBuilder(targetUrl, serverCredentials).build(); + } + + /** + * Create a manager for a grpc channel with server credentials and credentials for per-rpc client authentication. * @param targetUrl - * @param credentials + * @param serverCredentials authenticates the server for TLS or are InsecureChannelCredentials + * @param authnCredentials authenticates the client on each rpc */ - private RelationsGrpcClientsManager(String targetUrl, ChannelCredentials credentials) { - this.channel = Grpc.newChannelBuilder(targetUrl, credentials).build(); + private RelationsGrpcClientsManager(String targetUrl, ChannelCredentials serverCredentials, CallCredentials authnCredentials) { + this.channel = Grpc.newChannelBuilder(targetUrl, + CompositeChannelCredentials.create(serverCredentials, authnCredentials)).build(); } private void closeClientChannel() { diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java new file mode 100644 index 0000000..8a5a9f3 --- /dev/null +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java @@ -0,0 +1,77 @@ +package org.project_kessel.relations.client.authn.oidc.client; + +import io.grpc.Metadata; +import io.grpc.Status; +import org.project_kessel.relations.client.Config; + +import java.util.Optional; +import java.util.concurrent.Executor; +import java.util.concurrent.atomic.AtomicReference; + +import static org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsMinter.forName; + +public class OIDCClientCredentialsCallCredentials extends io.grpc.CallCredentials { + private static final Metadata.Key authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + + private final Config.OIDCClientCredentialsConfig clientCredentialsConfig; + private final OIDCClientCredentialsMinter minter; + + private final AtomicReference storedBearerHeaderRef = new AtomicReference<>(); + + public OIDCClientCredentialsCallCredentials(Config.AuthenticationConfig authnConfig) throws OIDCClientCredentialsCallCredentialsException { + if(authnConfig.clientCredentialsConfig().isEmpty()) { + throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig is required for OIDC client credentials authentication method."); + } + this.clientCredentialsConfig = authnConfig.clientCredentialsConfig().get(); + + Optional minterImpl = clientCredentialsConfig.OIDCClientCredentialsMinterImplementation(); + try { + if(minterImpl.isPresent()) { + this.minter = OIDCClientCredentialsMinter.forName(minterImpl.get()); + } else { + this.minter = OIDCClientCredentialsMinter.forDefaultImplementation(); + } + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + throw new OIDCClientCredentialsCallCredentialsException("Couldn't create GrpcCallCredentials because minter impl not instantiated.", e); + } + } + + @Override + public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { + appExecutor.execute(() -> { + try { + synchronized (storedBearerHeaderRef) { + if (storedBearerHeaderRef.get() == null || storedBearerHeaderRef.get().isExpired()) { + storedBearerHeaderRef.set(minter.authenticateAndRetrieveAuthorizationHeader(clientCredentialsConfig)); + } + + Metadata headers = new Metadata(); + headers.put(authorizationKey, storedBearerHeaderRef.get().getAuthorizationHeader()); + applier.apply(headers); + } + } catch (Throwable e) { + applier.fail(Status.UNAUTHENTICATED.withCause(e)); + } + }); + } + + /** + * For + */ + public void flushStoredCredentials() { + synchronized (storedBearerHeaderRef) { + storedBearerHeaderRef.set(null); + } + } + + public static class OIDCClientCredentialsCallCredentialsException extends Exception { + public OIDCClientCredentialsCallCredentialsException(String message) { + super(message); + } + + public OIDCClientCredentialsCallCredentialsException(String message, Throwable cause) { + super(message, cause); + } + } + +} diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java new file mode 100644 index 0000000..55fca8e --- /dev/null +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java @@ -0,0 +1,85 @@ +package org.project_kessel.relations.client.authn.oidc.client; + +import org.project_kessel.relations.client.Config; + +import java.lang.reflect.Constructor; +import java.lang.reflect.InvocationTargetException; +import java.time.LocalDateTime; +import java.util.Optional; + +public abstract class OIDCClientCredentialsMinter { + private static final Class defaultMinter = org.project_kessel.relations.client.authn.oidc.client.nimbus.NimbusOIDCClientCredentialsMinter.class; + + public static OIDCClientCredentialsMinter forDefaultImplementation() throws OIDCClientCredentialsMinterException { + return forClass(defaultMinter); + } + + public static OIDCClientCredentialsMinter forClass(Class minterClass) throws OIDCClientCredentialsMinterException { + try { + Constructor constructor = defaultMinter.getConstructor(); + return (OIDCClientCredentialsMinter)constructor.newInstance(); + } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { + throw new OIDCClientCredentialsMinterException("Can't create instance of OIDC client credentials minter", e); + } + } + + public static OIDCClientCredentialsMinter forName(String name) throws OIDCClientCredentialsMinterException { + try { + Class minterImplClass = Class.forName(name); + return forClass(minterImplClass); + } catch(ClassNotFoundException e) { + throw new OIDCClientCredentialsMinterException("Can't find the specified OIDC client credentials minter implementation", e); + } + } + + public abstract BearerHeader authenticateAndRetrieveAuthorizationHeader(Config.OIDCClientCredentialsConfig clientConfig) throws OIDCClientCredentialsMinterException; + + public static class BearerHeader { + private final String authorizationHeader; + private final Optional expiry; + + public BearerHeader(String authorizationHeader, Optional expiry) { + this.authorizationHeader = authorizationHeader; + this.expiry = expiry; + } + + public String getAuthorizationHeader() { + return authorizationHeader; + } + + public boolean isExpired() { + return expiry.map(t -> t.isBefore(LocalDateTime.now())).orElse(true); + } + } + + /** + * Utility method to derive an expiry dateTime from a just granted token with expires_in set. + * @param expiresIn 0 is expected if expiresIn is not set or otherwise not applicable. + * @return + */ + public static Optional getExpiryDateFromExpiresIn(long expiresIn) { + Optional expiryTime; + if (expiresIn != 0) { + // this processing happens some time after token is granted with lifetime so subtract buffer from lifetime + long bufferSeconds = 60; + if(expiresIn < bufferSeconds) { + expiryTime = Optional.empty(); + } else { + expiryTime = Optional.of(LocalDateTime.now().plusSeconds(expiresIn).minusSeconds(bufferSeconds)); + } + } else { + expiryTime = Optional.empty(); + } + + return expiryTime; + } + + public static class OIDCClientCredentialsMinterException extends Exception { + public OIDCClientCredentialsMinterException(String message) { + super(message); + } + public OIDCClientCredentialsMinterException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java new file mode 100644 index 0000000..c7caf6e --- /dev/null +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java @@ -0,0 +1,70 @@ +package org.project_kessel.relations.client.authn.oidc.client.nimbus; + +import com.nimbusds.oauth2.sdk.*; +import com.nimbusds.oauth2.sdk.auth.ClientAuthentication; +import com.nimbusds.oauth2.sdk.auth.ClientSecretBasic; +import com.nimbusds.oauth2.sdk.auth.Secret; +import com.nimbusds.oauth2.sdk.id.ClientID; +import com.nimbusds.oauth2.sdk.id.Issuer; +import com.nimbusds.oauth2.sdk.token.BearerAccessToken; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponse; +import com.nimbusds.openid.connect.sdk.OIDCTokenResponseParser; +import com.nimbusds.openid.connect.sdk.op.OIDCProviderMetadata; +import io.quarkus.runtime.annotations.RegisterForReflection; +import org.project_kessel.relations.client.Config; +import org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsMinter; + +import java.io.IOException; +import java.net.URI; +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * Implementation pulled in by reflection in Vanilla java and registered for reflection if Quarkus native is used. + */ +@RegisterForReflection +public class NimbusOIDCClientCredentialsMinter extends OIDCClientCredentialsMinter { + @Override + public BearerHeader authenticateAndRetrieveAuthorizationHeader(Config.OIDCClientCredentialsConfig authnConfig) throws OIDCClientCredentialsMinterException { + var config = (Config.OIDCClientCredentialsConfig) authnConfig; + Issuer issuer = new Issuer(config.issuer()); + ClientID clientID = new ClientID(config.clientId()); + Secret clientSecret = new Secret(config.clientSecret()); + Optional scope = config.scope().map(Scope::new); + AuthorizationGrant clientGrant = new ClientCredentialsGrant(); + + try { + OIDCProviderMetadata providerMetadata = OIDCProviderMetadata.resolve(issuer); + URI tokenEndpoint = providerMetadata.getTokenEndpointURI(); + ClientAuthentication clientAuth = new ClientSecretBasic(clientID, clientSecret); + // Make the token request + TokenRequest request; + if(scope.isPresent()) { + request = new TokenRequest(tokenEndpoint, clientAuth, clientGrant, scope.get()); + } else { + request = new TokenRequest(tokenEndpoint, clientAuth, clientGrant); + } + + TokenResponse tokenResponse = OIDCTokenResponseParser.parse(request.toHTTPRequest().send()); + if (!tokenResponse.indicatesSuccess()) { + TokenErrorResponse errorResponse = tokenResponse.toErrorResponse(); + String code = errorResponse.getErrorObject().getCode(); + String message = errorResponse.getErrorObject().getDescription(); + throw new OIDCClientCredentialsMinterException( + "Error requesting token from endpoint. TokenErrorResponse: code: " + code + ", message: " + message); + } + + OIDCTokenResponse successResponse = (OIDCTokenResponse)tokenResponse.toSuccessResponse(); + BearerAccessToken bearerAccessToken = successResponse.getOIDCTokens().getBearerAccessToken(); + + // Capture expiry if its exists in the token + long lifetime = bearerAccessToken.getLifetime(); + Optional expiryTime = getExpiryDateFromExpiresIn(lifetime); + + return new BearerHeader(bearerAccessToken.toAuthorizationHeader(), expiryTime); + } + catch(IOException | GeneralException e) { + throw new OIDCClientCredentialsMinterException("Failed to retrieve and parse OIDC well-known configuration from provider.", e); + } + } +} diff --git a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java index 96854cf..7028ab0 100644 --- a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java +++ b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java @@ -15,6 +15,7 @@ import org.junit.jupiter.api.Test; import java.io.IOException; +import java.util.Optional; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -115,5 +116,10 @@ public boolean isSecureClients() { public String targetUrl() { return "0.0.0.0:" + String.valueOf(testServerPort); } + + @Override + public Optional authenticationConfig() { + return Optional.empty(); + } } } diff --git a/src/test/java/org/project_kessel/relations/client/ConfigTest.java b/src/test/java/org/project_kessel/relations/client/ConfigTest.java new file mode 100644 index 0000000..55a48e8 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/ConfigTest.java @@ -0,0 +1,32 @@ +package org.project_kessel.relations.client; + +import io.smallrye.config.SmallRyeConfig; +import io.smallrye.config.SmallRyeConfigBuilder; + +import io.smallrye.config.common.MapBackedConfigSource; +import org.junit.jupiter.api.Test; + +import java.util.HashMap; + +public class ConfigTest { + + @Test + public void canLoadBasicConfig() { + /* Should always be able to build a Config from a ConfigSource with just a target url (i.e. minimal config for + * now). Also tests whether the mapping annotations in Config are valid beyond static type checking. */ + SmallRyeConfig config = new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("test", new HashMap<>(), Integer.MAX_VALUE) { + @Override + public String getValue(String propertyName) { + if("relations-api.target-url".equals(propertyName)) { + return "http://localhost:8080"; + } + return null; + } + } + ) + .withMapping(Config.class) + .build(); + } + +} From fb5b155fdb1779a516f21c3ddee17ab580b4dbf0 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 24 Jul 2024 11:55:08 +0100 Subject: [PATCH 02/28] Small fixes. --- src/main/java/org/project_kessel/relations/client/Config.java | 2 +- .../authn/oidc/client/OIDCClientCredentialsCallCredentials.java | 2 +- .../client/authn/oidc/client/OIDCClientCredentialsMinter.java | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/main/java/org/project_kessel/relations/client/Config.java b/src/main/java/org/project_kessel/relations/client/Config.java index c9ff098..16cff43 100644 --- a/src/main/java/org/project_kessel/relations/client/Config.java +++ b/src/main/java/org/project_kessel/relations/client/Config.java @@ -42,4 +42,4 @@ interface OIDCClientCredentialsConfig { Optional scope(); Optional OIDCClientCredentialsMinterImplementation(); } -} \ No newline at end of file +} diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java index 8a5a9f3..9c7f231 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java @@ -56,7 +56,7 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, } /** - * For + * For unusual cases where stored credentials (i.e. token), which may be long-lived, is bad and needs to be flushed. */ public void flushStoredCredentials() { synchronized (storedBearerHeaderRef) { diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java index 55fca8e..237eb45 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java @@ -16,7 +16,7 @@ public static OIDCClientCredentialsMinter forDefaultImplementation() throws OIDC public static OIDCClientCredentialsMinter forClass(Class minterClass) throws OIDCClientCredentialsMinterException { try { - Constructor constructor = defaultMinter.getConstructor(); + Constructor constructor = minterClass.getConstructor(); return (OIDCClientCredentialsMinter)constructor.newInstance(); } catch (InvocationTargetException | NoSuchMethodException | InstantiationException | IllegalAccessException e) { throw new OIDCClientCredentialsMinterException("Can't create instance of OIDC client credentials minter", e); From 8271cdda65b69b7137495b620cbe898707716965 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 24 Jul 2024 12:57:36 +0100 Subject: [PATCH 03/28] Abstract CallCredential creation into factory. --- .../client/RelationsGrpcClientsManager.java | 12 +++--- .../client/authn/CallCredentialsFactory.java | 37 +++++++++++++++++++ 2 files changed, 42 insertions(+), 7 deletions(-) create mode 100644 src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java diff --git a/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java b/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java index 347431f..4629ab4 100644 --- a/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java +++ b/src/main/java/org/project_kessel/relations/client/RelationsGrpcClientsManager.java @@ -1,7 +1,7 @@ package org.project_kessel.relations.client; import io.grpc.*; -import org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsCallCredentials; +import org.project_kessel.relations.client.authn.CallCredentialsFactory; import java.util.HashMap; @@ -22,12 +22,11 @@ public static synchronized RelationsGrpcClientsManager forInsecureClients(String public static synchronized RelationsGrpcClientsManager forInsecureClients(String targetUrl, Config.AuthenticationConfig authnConfig) throws RuntimeException { if (!insecureManagers.containsKey(targetUrl)) { try { - // For now, the only client authn scheme supported is OIDC client credentials var manager = new RelationsGrpcClientsManager(targetUrl, InsecureChannelCredentials.create(), - new OIDCClientCredentialsCallCredentials(authnConfig)); + CallCredentialsFactory.create(authnConfig)); insecureManagers.put(targetUrl, manager); - } catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + } catch (CallCredentialsFactory.CallCredentialsCreationException e) { throw new RuntimeException(e); } } @@ -47,12 +46,11 @@ public static synchronized RelationsGrpcClientsManager forSecureClients(String t if (!secureManagers.containsKey(targetUrl)) { var tlsChannelCredentials = TlsChannelCredentials.create(); try { - // For now, the only client authn scheme supported is OIDC client credentials var manager = new RelationsGrpcClientsManager(targetUrl, tlsChannelCredentials, - new OIDCClientCredentialsCallCredentials(authnConfig)); + CallCredentialsFactory.create(authnConfig)); secureManagers.put(targetUrl, manager); - } catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + } catch (CallCredentialsFactory.CallCredentialsCreationException e) { throw new RuntimeException(e); } } diff --git a/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java b/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java new file mode 100644 index 0000000..f5f9113 --- /dev/null +++ b/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java @@ -0,0 +1,37 @@ +package org.project_kessel.relations.client.authn; + +import io.grpc.CallCredentials; +import org.project_kessel.relations.client.Config; +import org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsCallCredentials; + +public class CallCredentialsFactory { + + public static CallCredentials create(Config.AuthenticationConfig authnConfig) throws CallCredentialsCreationException { + if (authnConfig == null) { + throw new CallCredentialsCreationException("AuthenticationConfig is required to create CallCredentials and must not be null."); + } + + try { + switch (authnConfig.mode()) { + case DISABLED: + return null; + case OIDC_CLIENT_CREDENTIALS: + return new OIDCClientCredentialsCallCredentials(authnConfig); + } + } catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + throw new CallCredentialsCreationException("Failed to create OIDCClientCredentialsCallCredentials.", e); + } + + return null; + } + + public static class CallCredentialsCreationException extends Exception { + public CallCredentialsCreationException(String message) { + super(message); + } + + public CallCredentialsCreationException(String message, Throwable cause) { + super(message, cause); + } + } +} From 264dd205987adc7d2ebbdc3a02759d37788ce7e4 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 24 Jul 2024 16:55:08 +0100 Subject: [PATCH 04/28] Linting changes and other small changes. --- .../relations/client/CDIManagedClients.java | 4 --- .../relations/client/Config.java | 2 +- .../client/authn/CallCredentialsFactory.java | 10 +++--- .../OIDCClientCredentialsCallCredentials.java | 6 ++-- .../NimbusOIDCClientCredentialsMinter.java | 3 +- .../client/CDIManagedClientsTest.java | 4 +-- .../relations/client/ConfigTest.java | 34 +++++++++++-------- .../RelationsGrpcClientsManagerTest.java | 16 ++++----- 8 files changed, 40 insertions(+), 39 deletions(-) diff --git a/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java b/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java index 2b0e7e2..dd9c5d1 100644 --- a/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java +++ b/src/main/java/org/project_kessel/relations/client/CDIManagedClients.java @@ -17,10 +17,6 @@ RelationsGrpcClientsManager getManager(Config config) { var targetUrl = config.targetUrl(); var authnEnabled = config.authenticationConfig().map(t -> !t.mode().equals(Config.AuthMode.DISABLED)).orElse(false); - if(authnEnabled && config.authenticationConfig().isEmpty()) { - throw new RuntimeException("Authentication mode enabled but no authentication config provided."); - } - if (isSecureClients) { if(authnEnabled) { return RelationsGrpcClientsManager.forSecureClients(targetUrl, config.authenticationConfig().get()); diff --git a/src/main/java/org/project_kessel/relations/client/Config.java b/src/main/java/org/project_kessel/relations/client/Config.java index 16cff43..96b3ecd 100644 --- a/src/main/java/org/project_kessel/relations/client/Config.java +++ b/src/main/java/org/project_kessel/relations/client/Config.java @@ -40,6 +40,6 @@ interface OIDCClientCredentialsConfig { @WithName("secret") String clientSecret(); Optional scope(); - Optional OIDCClientCredentialsMinterImplementation(); + Optional oidcClientCredentialsMinterImplementation(); } } diff --git a/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java b/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java index f5f9113..cd07d4e 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java +++ b/src/main/java/org/project_kessel/relations/client/authn/CallCredentialsFactory.java @@ -6,6 +6,10 @@ public class CallCredentialsFactory { + private CallCredentialsFactory() { + + } + public static CallCredentials create(Config.AuthenticationConfig authnConfig) throws CallCredentialsCreationException { if (authnConfig == null) { throw new CallCredentialsCreationException("AuthenticationConfig is required to create CallCredentials and must not be null."); @@ -13,10 +17,8 @@ public static CallCredentials create(Config.AuthenticationConfig authnConfig) th try { switch (authnConfig.mode()) { - case DISABLED: - return null; - case OIDC_CLIENT_CREDENTIALS: - return new OIDCClientCredentialsCallCredentials(authnConfig); + case DISABLED: return null; + case OIDC_CLIENT_CREDENTIALS: return new OIDCClientCredentialsCallCredentials(authnConfig); } } catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { throw new CallCredentialsCreationException("Failed to create OIDCClientCredentialsCallCredentials.", e); diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java index 9c7f231..87c8587 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java @@ -8,8 +8,6 @@ import java.util.concurrent.Executor; import java.util.concurrent.atomic.AtomicReference; -import static org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsMinter.forName; - public class OIDCClientCredentialsCallCredentials extends io.grpc.CallCredentials { private static final Metadata.Key authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); @@ -24,7 +22,7 @@ public OIDCClientCredentialsCallCredentials(Config.AuthenticationConfig authnCon } this.clientCredentialsConfig = authnConfig.clientCredentialsConfig().get(); - Optional minterImpl = clientCredentialsConfig.OIDCClientCredentialsMinterImplementation(); + Optional minterImpl = clientCredentialsConfig.oidcClientCredentialsMinterImplementation(); try { if(minterImpl.isPresent()) { this.minter = OIDCClientCredentialsMinter.forName(minterImpl.get()); @@ -49,7 +47,7 @@ public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, headers.put(authorizationKey, storedBearerHeaderRef.get().getAuthorizationHeader()); applier.apply(headers); } - } catch (Throwable e) { + } catch (Exception e) { applier.fail(Status.UNAUTHENTICATED.withCause(e)); } }); diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java index c7caf6e..378ea43 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinter.java @@ -25,8 +25,7 @@ @RegisterForReflection public class NimbusOIDCClientCredentialsMinter extends OIDCClientCredentialsMinter { @Override - public BearerHeader authenticateAndRetrieveAuthorizationHeader(Config.OIDCClientCredentialsConfig authnConfig) throws OIDCClientCredentialsMinterException { - var config = (Config.OIDCClientCredentialsConfig) authnConfig; + public BearerHeader authenticateAndRetrieveAuthorizationHeader(Config.OIDCClientCredentialsConfig config) throws OIDCClientCredentialsMinterException { Issuer issuer = new Issuer(config.issuer()); ClientID clientID = new ClientID(config.clientId()); Secret clientSecret = new Secret(config.clientSecret()); diff --git a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java index 7028ab0..47f07f6 100644 --- a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java +++ b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java @@ -23,7 +23,7 @@ * Use Weld as a test container to check CDI functionality. */ @EnableWeld -public class CDIManagedClientsTest { +class CDIManagedClientsTest { @WeldSetup public WeldInitiator weld = WeldInitiator.from(new Weld().setBeanDiscoveryMode(BeanDiscoveryMode.ALL).addBeanClass(TestConfig.class)).build(); @@ -92,7 +92,7 @@ static void tearDown() { } @Test - public void basicCDIWiringTest() { + void basicCDIWiringTest() { /* Make some calls to dummy services in test grpc server to test injected clients */ var checkResponse = checkClient.check(CheckRequest.getDefaultInstance()); var relationTuplesResponse = relationTuplesClient.readTuples(ReadTuplesRequest.getDefaultInstance()); diff --git a/src/test/java/org/project_kessel/relations/client/ConfigTest.java b/src/test/java/org/project_kessel/relations/client/ConfigTest.java index 55a48e8..5a2f67b 100644 --- a/src/test/java/org/project_kessel/relations/client/ConfigTest.java +++ b/src/test/java/org/project_kessel/relations/client/ConfigTest.java @@ -1,6 +1,5 @@ package org.project_kessel.relations.client; -import io.smallrye.config.SmallRyeConfig; import io.smallrye.config.SmallRyeConfigBuilder; import io.smallrye.config.common.MapBackedConfigSource; @@ -8,25 +7,32 @@ import java.util.HashMap; -public class ConfigTest { +import static org.junit.jupiter.api.Assertions.fail; + +class ConfigTest { @Test - public void canLoadBasicConfig() { + void canLoadBasicConfig() { /* Should always be able to build a Config from a ConfigSource with just a target url (i.e. minimal config for * now). Also tests whether the mapping annotations in Config are valid beyond static type checking. */ - SmallRyeConfig config = new SmallRyeConfigBuilder() - .withSources(new MapBackedConfigSource("test", new HashMap<>(), Integer.MAX_VALUE) { - @Override - public String getValue(String propertyName) { - if("relations-api.target-url".equals(propertyName)) { - return "http://localhost:8080"; + try { + new SmallRyeConfigBuilder() + .withSources(new MapBackedConfigSource("test", new HashMap<>(), Integer.MAX_VALUE) { + @Override + public String getValue(String propertyName) { + if ("relations-api.target-url".equals(propertyName)) { + return "http://localhost:8080"; + } + return null; } - return null; } - } - ) - .withMapping(Config.class) - .build(); + ) + .withMapping(Config.class) + .build(); + } + catch (Exception e) { + fail("Generating a config objective with minimal config should not fail."); + } } } diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index 621466f..5157c16 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -16,22 +16,22 @@ import static io.smallrye.common.constraint.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.*; -public class RelationsGrpcClientsManagerTest { +class RelationsGrpcClientsManagerTest { @BeforeAll - public static void testSetup() { + static void testSetup() { /* Make sure all client managers shutdown/removed before tests */ RelationsGrpcClientsManager.shutdownAll(); } @AfterEach - public void testTeardown() { + void testTeardown() { /* Make sure all client managers shutdown/removed after each test */ RelationsGrpcClientsManager.shutdownAll(); } @Test - public void testManagerReusePatterns() { + void testManagerReusePatterns() { var one = RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); var two = RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); // same as one var three = RelationsGrpcClientsManager.forInsecureClients("localhost1:8080"); @@ -52,7 +52,7 @@ public void testManagerReusePatterns() { } @Test - public void testThreadingChaos() { + void testThreadingChaos() { /* Basic testing to ensure that we don't get ConcurrentModificationExceptions, or any other exceptions, when * creating and destroying managers on different threads. */ @@ -116,7 +116,7 @@ public void testThreadingChaos() { } @Test - public void testManagerReuseInternal() throws Exception { + void testManagerReuseInternal() throws Exception { RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); // same as one RelationsGrpcClientsManager.forInsecureClients("localhost1:8080"); @@ -136,7 +136,7 @@ public void testManagerReuseInternal() throws Exception { } @Test - public void testSameChannelUsedByClientsInternal() throws Exception { + void testSameChannelUsedByClientsInternal() throws Exception { var manager = RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); var checkClient = manager.getCheckClient(); var relationTuplesClient = manager.getRelationTuplesClient(); @@ -157,7 +157,7 @@ public void testSameChannelUsedByClientsInternal() throws Exception { } @Test - public void testCreateAndShutdownPatternsInternal() throws Exception { + void testCreateAndShutdownPatternsInternal() throws Exception { var insecureField = RelationsGrpcClientsManager.class.getDeclaredField("insecureManagers"); insecureField.setAccessible(true); var insecureManagersSize = ((HashMap)insecureField.get(null)).size(); From 929ec1849c3111a2aafcb36cda19e429baef7684 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Tue, 30 Jul 2024 14:54:44 +0100 Subject: [PATCH 05/28] Added GrpcServerSpy, FakeIdp, fake cert and key to enable TLS and OIDC tests. --- .../RelationsGrpcClientsManagerTest.java | 85 +++++++++ .../relations/client/fake/FakeIdp.java | 80 ++++++++ .../relations/client/fake/GrpcServerSpy.java | 172 ++++++++++++++++++ .../relations/client/util/CertUtil.java | 105 +++++++++++ src/test/resources/certs/test.key | 28 +++ 5 files changed, 470 insertions(+) create mode 100644 src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java create mode 100644 src/test/java/org/project_kessel/relations/client/fake/GrpcServerSpy.java create mode 100644 src/test/java/org/project_kessel/relations/client/util/CertUtil.java create mode 100644 src/test/resources/certs/test.key diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index 5157c16..bc947b8 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -1,27 +1,36 @@ package org.project_kessel.relations.client; +import io.grpc.Metadata; +import org.junit.jupiter.api.AfterAll; +import org.project_kessel.api.relations.v1beta1.CheckRequest; import org.project_kessel.api.relations.v1beta1.KesselCheckServiceGrpc; import org.project_kessel.api.relations.v1beta1.KesselLookupServiceGrpc; import org.project_kessel.api.relations.v1beta1.KesselTupleServiceGrpc; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; +import org.project_kessel.relations.client.fake.GrpcServerSpy; import java.util.HashMap; import java.util.Hashtable; +import java.util.Optional; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import static io.smallrye.common.constraint.Assert.assertNotNull; import static org.junit.jupiter.api.Assertions.*; +import static org.project_kessel.relations.client.util.CertUtil.*; class RelationsGrpcClientsManagerTest { + private static final Metadata.Key authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); @BeforeAll static void testSetup() { /* Make sure all client managers shutdown/removed before tests */ RelationsGrpcClientsManager.shutdownAll(); + /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ + addTestCACertToTrustStore(); } @AfterEach @@ -30,6 +39,12 @@ void testTeardown() { RelationsGrpcClientsManager.shutdownAll(); } + @AfterAll + static void removeTestSetup() { + /* Remove self-signed cert */ + removeTestCACertFromKeystore(); + } + @Test void testManagerReusePatterns() { var one = RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); @@ -115,6 +130,37 @@ void testThreadingChaos() { } } + @Test + void testManagersHoldIntendedCredentialsInChannel() throws Exception { + Config.AuthenticationConfig authnConfig = dummyNonDisabledAuthenticationConfig(); + var manager = RelationsGrpcClientsManager.forInsecureClients("localhost:7000"); + var manager2 = RelationsGrpcClientsManager.forInsecureClients("localhost:7001", authnConfig); + var manager3 = RelationsGrpcClientsManager.forSecureClients("localhost:7002"); + var manager4 = RelationsGrpcClientsManager.forSecureClients("localhost:7003", authnConfig); + + var checkClient = manager.getCheckClient(); + var checkClient2 = manager2.getCheckClient(); + var checkClient3 = manager3.getCheckClient(); + var checkClient4 = manager4.getCheckClient(); + + var cd1 = GrpcServerSpy.runAgainstTemporaryServerWithDummyServices(7000, () -> checkClient.check(CheckRequest.getDefaultInstance())); + var cd2 = GrpcServerSpy.runAgainstTemporaryServerWithDummyServices(7001, () -> checkClient2.check(CheckRequest.getDefaultInstance())); + var cd3 = GrpcServerSpy.runAgainstTemporaryTlsServerWithDummyServices(7002, () -> checkClient3.check(CheckRequest.getDefaultInstance())); + var cd4 = GrpcServerSpy.runAgainstTemporaryTlsServerWithDummyServices(7003, () -> checkClient4.check(CheckRequest.getDefaultInstance())); + + assertNull(cd1.getMetadata().get(authorizationKey)); + assertEquals("NONE", cd1.getCall().getSecurityLevel().toString()); + + assertNotNull(cd2.getMetadata().get(authorizationKey)); + assertEquals("NONE", cd2.getCall().getSecurityLevel().toString()); + + assertNull(cd3.getMetadata().get(authorizationKey)); + assertEquals("PRIVACY_AND_INTEGRITY", cd3.getCall().getSecurityLevel().toString()); + + assertNotNull(cd4.getMetadata().get(authorizationKey)); + assertEquals("PRIVACY_AND_INTEGRITY", cd4.getCall().getSecurityLevel().toString()); + } + @Test void testManagerReuseInternal() throws Exception { RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); @@ -190,4 +236,43 @@ void testCreateAndShutdownPatternsInternal() throws Exception { insecureManagersSize = ((HashMap)insecureField.get(null)).size(); assertEquals(0, insecureManagersSize); } + + Config.AuthenticationConfig dummyNonDisabledAuthenticationConfig() { + return new Config.AuthenticationConfig() { + @Override + public Config.AuthMode mode() { + return Config.AuthMode.OIDC_CLIENT_CREDENTIALS; // any non-disabled value + } + + @Override + public Optional clientCredentialsConfig() { + return Optional.of(new Config.OIDCClientCredentialsConfig() { + @Override + public String issuer() { + return "http://localhost:8090"; + } + + @Override + public String clientId() { + return "test"; + } + + @Override + public String clientSecret() { + return "test"; + } + + @Override + public Optional scope() { + return Optional.empty(); + } + + @Override + public Optional oidcClientCredentialsMinterImplementation() { + return Optional.empty(); + } + }); + } + }; + } } \ No newline at end of file diff --git a/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java b/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java new file mode 100644 index 0000000..ede3896 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java @@ -0,0 +1,80 @@ +package org.project_kessel.relations.client.fake; + +import com.sun.net.httpserver.HttpExchange; +import com.sun.net.httpserver.HttpHandler; +import com.sun.net.httpserver.HttpServer; + +import java.io.IOException; +import java.io.OutputStream; +import java.net.InetSocketAddress; + +/** + * Super-fake Idp that supports a hard-coded well-known discovery endpoint and a corresponding fake token endpoint. + * Does not use TLS. + */ +public class FakeIdp { + private final int port; + HttpServer server = null; + + public FakeIdp(int port) { + this.port = port; + } + + public void start() { + try { + server = HttpServer.create(new InetSocketAddress(port), 0); + } catch (IOException e) { + throw new RuntimeException(e); + } + server.createContext("/.well-known/openid-configuration", new WellKnownHandler()); + server.createContext("/token", new TokenHandler()); + server.setExecutor(null); // creates a default executor + server.start(); + } + + public void stop() { + server.stop(0); + } + + static class TokenHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = "{\n" + + " \"iss\": \"http://localhost:8090/\",\n" + + " \"aud\": \"us\",\n" + + " \"sub\": \"usr_123\",\n" + + " \"scope\": \"read write\",\n" + + " \"iat\": 1458785796,\n" + + " \"exp\": 1458872196,\n" + + " \"token_type\": \"Bearer\",\n" + + " \"access_token\": \"blah\"\n" + + "}"; + t.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8"); + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + + static class WellKnownHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = "{\n" + + "\t\"issuer\":\"http://localhost:8090\",\n" + + "\t\"authorization_endpoint\":\"http://localhost:8090/protocol/openid-connect/auth\",\n" + + "\t\"token_endpoint\":\"http://localhost:8090/token\",\n" + + "\t\"introspection_endpoint\":\"http://localhost:8090/token/introspect\",\n" + + "\t\"jwks_uri\":\"http://localhost:8090/certs\",\n" + + "\t\"response_types_supported\":[\"code\",\"none\",\"id_token\",\"token\",\"id_token token\",\"code id_token\",\"code token\",\"code id_token token\"],\n" + + "\t\"token_endpoint_auth_methods_supported\":[\"private_key_jwt\",\"client_secret_basic\",\"client_secret_post\",\"tls_client_auth\",\"client_secret_jwt\"],\n" + + "\t\"subject_types_supported\":[\"public\",\"pairwise\"]\n" + + "}"; + t.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8"); + t.sendResponseHeaders(200, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } +} diff --git a/src/test/java/org/project_kessel/relations/client/fake/GrpcServerSpy.java b/src/test/java/org/project_kessel/relations/client/fake/GrpcServerSpy.java new file mode 100644 index 0000000..e7b0e95 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/fake/GrpcServerSpy.java @@ -0,0 +1,172 @@ +package org.project_kessel.relations.client.fake; + +import io.grpc.*; +import io.grpc.stub.StreamObserver; +import org.project_kessel.api.relations.v1beta1.*; + +import java.io.File; +import java.io.IOException; +import java.net.URL; +import java.util.Objects; +import java.util.concurrent.TimeUnit; + +public class GrpcServerSpy extends Server { + private final Server server; + + public GrpcServerSpy(int port, boolean tlsEnabled, ServerInterceptor interceptor, BindableService... services) { + ServerBuilder serverBuilder = ServerBuilder.forPort(port); + if (tlsEnabled) { + URL certsUrl = Thread.currentThread().getContextClassLoader().getResource("certs/test.crt"); + URL keyUrl = Thread.currentThread().getContextClassLoader().getResource("certs/test.key"); + File certFile = new File(Objects.requireNonNull(certsUrl).getPath()); + File keyFile = new File(Objects.requireNonNull(keyUrl).getPath()); + serverBuilder.useTransportSecurity(certFile, keyFile); + } + if (interceptor != null) { + serverBuilder.intercept(interceptor); + } + for (BindableService service : services) { + serverBuilder.addService(service); + } + server = serverBuilder.build(); + } + + public static ServerCallDetails runAgainstTemporaryServerWithDummyServices(int port, Call grpcCallFunction) { + return runAgainstTemporaryServerWithDummyServicesTlsSelect(port, false, grpcCallFunction); + } + + public static ServerCallDetails runAgainstTemporaryTlsServerWithDummyServices(int port, Call grpcCallFunction) { + return runAgainstTemporaryServerWithDummyServicesTlsSelect(port, true, grpcCallFunction); + } + + private static ServerCallDetails runAgainstTemporaryServerWithDummyServicesTlsSelect(int port, boolean tlsEnabled, Call grpcCallFunction) { + var dummyCheckService = new KesselCheckServiceGrpc.KesselCheckServiceImplBase() { + @Override + public void check(CheckRequest request, StreamObserver responseObserver) { + responseObserver.onNext(CheckResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }; + var dummyTupleService = new KesselTupleServiceGrpc.KesselTupleServiceImplBase() { + @Override + public void readTuples(ReadTuplesRequest request, StreamObserver responseObserver) { + responseObserver.onNext(ReadTuplesResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }; + var dummyLookupService = new KesselLookupServiceGrpc.KesselLookupServiceImplBase() { + @Override + public void lookupSubjects(LookupSubjectsRequest request, StreamObserver responseObserver) { + responseObserver.onNext(LookupSubjectsResponse.getDefaultInstance()); + responseObserver.onCompleted(); + } + }; + + return runAgainstTemporaryServerTlsSelect(port, tlsEnabled, grpcCallFunction, dummyCheckService, dummyTupleService, dummyLookupService); + } + + public static ServerCallDetails runAgainstTemporaryServer(int port, Call grpcCallFunction, BindableService... services) { + return runAgainstTemporaryServerTlsSelect(port, false, grpcCallFunction, services); + } + + public static ServerCallDetails runAgainstTemporaryTlsServer(int port, Call grpcCallFunction, BindableService... services) { + return runAgainstTemporaryServerTlsSelect(port, true, grpcCallFunction, services); + } + + private static ServerCallDetails runAgainstTemporaryServerTlsSelect(int port, boolean tlsEnabled, Call grpcCallFunction, BindableService... services) { + final ServerCallDetails serverCallDetails = new ServerCallDetails(); + + var spyInterceptor = new ServerInterceptor() { + @Override + public ServerCall.Listener interceptCall(ServerCall call, Metadata headers, ServerCallHandler next) { + serverCallDetails.setCall(call); + serverCallDetails.setMetadata(headers); + return next.startCall(call, headers); + } + }; + + FakeIdp fakeIdp = new FakeIdp(8090); + var serverSpy = new GrpcServerSpy(port, tlsEnabled, spyInterceptor, services); + + try { + fakeIdp.start(); + serverSpy.start(); + grpcCallFunction.call(); + serverSpy.shutdown(); + fakeIdp.stop(); + + return serverCallDetails; + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + serverSpy.shutdown(); + fakeIdp.stop(); + } + } + + @Override + public Server start() throws IOException { + server.start(); + return this; + } + + @Override + public Server shutdown() { + server.shutdown(); + return this; + } + + @Override + public Server shutdownNow() { + server.shutdownNow(); + return this; + } + + @Override + public boolean isShutdown() { + return server.isShutdown(); + } + + @Override + public boolean isTerminated() { + return server.isTerminated(); + } + + @Override + public boolean awaitTermination(long timeout, TimeUnit unit) throws InterruptedException { + return server.awaitTermination(timeout, unit); + } + + @Override + public void awaitTermination() throws InterruptedException { + server.awaitTermination(); + } + + public interface Call { + void call(); + } + + public static class ServerCallDetails { + private ServerCall call; + private Metadata metadata; + + public ServerCallDetails() { + } + + public ServerCall getCall() { + return call; + } + + public Metadata getMetadata() { + return metadata; + } + + public void setCall(ServerCall call) { + this.call = call; + } + + public void setMetadata(Metadata metadata) { + this.metadata = metadata; + } + } +} diff --git a/src/test/java/org/project_kessel/relations/client/util/CertUtil.java b/src/test/java/org/project_kessel/relations/client/util/CertUtil.java new file mode 100644 index 0000000..bc3d085 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/util/CertUtil.java @@ -0,0 +1,105 @@ +package org.project_kessel.relations.client.util; + +import javax.net.ssl.SSLContext; +import javax.net.ssl.TrustManager; +import javax.net.ssl.TrustManagerFactory; +import javax.net.ssl.X509TrustManager; +import java.io.*; +import java.security.*; +import java.security.cert.Certificate; +import java.security.cert.CertificateException; +import java.security.cert.CertificateFactory; + +public class CertUtil { + private static final char[] passphrase = "changeit".toCharArray(); + private static final String selfSignedAlias = "selfsigned"; + private static final String certFileName = "certs/test.crt"; + + public static void addTestCACertToTrustStore() { + final char sep = File.separatorChar; + File dir = new File(System.getProperty("java.home") + sep + "lib" + sep + "security"); + File file = new File(dir, "cacerts"); + try { + InputStream localCertIn = new FileInputStream(file); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(localCertIn, passphrase); + localCertIn.close(); + if (keystore.containsAlias(selfSignedAlias)) { + return; + } + + InputStream certIn = Thread.currentThread().getContextClassLoader().getResourceAsStream(certFileName); + BufferedInputStream bis = new BufferedInputStream(certIn); + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + while (bis.available() > 0) { + Certificate cert = cf.generateCertificate(bis); + keystore.setCertificateEntry(selfSignedAlias, cert); + } + certIn.close(); + + OutputStream out = new FileOutputStream(file); + keystore.store(out, passphrase); + out.close(); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keystore); + TrustManager[] trustManagers = tmf.getTrustManagers(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, null); + + SSLContext.setDefault(sslContext); + } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | + NullPointerException | KeyManagementException e) { + throw new RuntimeException(e); + } + } + + public static void dumpCerts() { + try { + TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + trustManagerFactory.init((KeyStore) null); + TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); + + X509TrustManager manager = (X509TrustManager) trustManagers[0]; + + for (java.security.cert.X509Certificate x509Certificate : manager.getAcceptedIssuers()) { + if ((x509Certificate.getSubjectDN().getName().startsWith("EMAILADDRESS=test@localhost"))) { + System.out.println("\n\n>>>>>> " + x509Certificate.getSubjectX500Principal() + "\n\n"); + } else { + System.out.println(x509Certificate.getSubjectX500Principal()); + } + } + } catch (NoSuchAlgorithmException | KeyStoreException e) { + throw new RuntimeException(e); + } + + } + + public static void removeTestCACertFromKeystore() { + final char sep = File.separatorChar; + File dir = new File(System.getProperty("java.home") + sep + "lib" + sep + "security"); + File file = new File(dir, "cacerts"); + try { + InputStream localCertIn = new FileInputStream(file); + KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); + keystore.load(localCertIn, passphrase); + keystore.deleteEntry(selfSignedAlias); + OutputStream out = new FileOutputStream(file); + keystore.store(out, passphrase); + out.close(); + + TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); + tmf.init(keystore); + TrustManager[] trustManagers = tmf.getTrustManagers(); + + SSLContext sslContext = SSLContext.getInstance("TLS"); + sslContext.init(null, trustManagers, null); + + SSLContext.setDefault(sslContext); + } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | + KeyManagementException e) { + throw new RuntimeException(e); + } + } +} diff --git a/src/test/resources/certs/test.key b/src/test/resources/certs/test.key new file mode 100644 index 0000000..01c3b8a --- /dev/null +++ b/src/test/resources/certs/test.key @@ -0,0 +1,28 @@ +-----BEGIN PRIVATE KEY----- +MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDCDQtt4lq66UMz +htpeyz+C1TFlAcYa2qkX1kuC7MImYhHety8yYeKXEUplhaIUF8We0TyOe5gW08dn +/rSoj+1AAKsoKOD6gJR0P0r61JfxxwAuxldhlNUd8nNlDL6PT43HGjDrLc32rRK0 +E3I7v8f4N5O4oe4J/opjFLU0Mz/BF7mx1ZcjHyaSTy7by8h7F5V/LYco4wx7eDKH +PUaKKGPOQoMciciLv/RRiZ4qvyl/xoS7RLjdAQ9+FW7WyhxZfmD6UMdu4++dkHjI +FatU7Zf9QTSMNs9YIKWom05Jsa8ypNcX5JBV/WVuTNqI+f8Kbli+391GDdKfi0mw +eg+464o1AgMBAAECggEAB8ySawYv81cwtj9zQ2AT22RKKe8aUTX8mdKpEkgFvZkv +cq4WHYZFnyC4xr/KNdiGvs1WC7v37uLwHsPj6upt9KaSOnj6IddNICbzEoW83aDL +5xVdqbRbpGp8LNGITyY5Yokw8iLTJljqaYZmjvQ0S0ugikkn0gsC8NyCM+jjZRVH +DFL/09sc0zzZpJi+nM173NB5XxyrtVO4OOxcA7HFEN29EGEufZXjHG3pvfURA5XC +TzoDUzSIE2U7JtfLunsGUfc/W34hEF6Zt/Pv/klpBaGHeSeRJ+F6YSctxPTpQc0C +49idJruRQdSuvL7rx0pKd6Fgi15auWtXiEGtgzsKgQKBgQDym10CNLXiRV1nZqWV +gfU59oA3IHTUXh+fA7ksP25l2VLnda3YYJ/Q4v/6/wpven8xPZKRmQX3klkPPhbr +YbIhOdeoIf3DljQniZX6pVWr+RDwpXogSOtdaRyu/XCpcomWqBwCsoZ/ozrp7lwG +ElbU5QKsyWRmh9RAPbJNYHmAZQKBgQDMw3c00ul9/2r8xZXZy9WDKJ63SiCU1azJ +fT1BvvzJBfaczn0woOJB6zsXOphAqdgmfj9qNWqLMitP2Cum7jMxdzvDewRw/FBB +1ZD+zitUfgMPvZ8bk+irtbinD1i3WHmnYBY1c/cwsEBXfLcw7ivy2a9MZ8aSBNqE +JFO8bP79kQKBgQCBBRjYhHm6BNOgmtkygnOMyMf1CUC4c/nzEgLXQkCOz52kVFQI +v5Ief6pMrHe7Q3UDFdCtt6iRufW9AnMj6MfXnbBPzQvsiSPhZu0o5+aA16snn4ks +RDtPaQgFE+lnY+9B/NMwqAqZNJCvOcEcxYICJGxgwZWwZUn+hBEfz0+udQKBgFvk +QIP5PwXncTj85vH18tzIhunUn2iLt944kRwHPORuA619UVtYaBGTIlKbXiZu0mz7 +7TOZwzWyjxNm/LgOX/UMAEsK0wRthwr0b/yZw4JIhtEylMvIhftBMxvt3C9zyiye +B3l3kHBOOKHKe1+/EwQKQwwz6j4vZW017Eo8U/axAoGBAJlcNL0xtM8+b/he5aDC +n8dmPItc1CwAq+i/be3oLgKnIQdhN4C7FACyepPJjF8qkxR7+oyKoZe8bhaGl2Zw +vMWPlRPtpbAtBQZevk64u/ixtTmyK1UiQLK1+ICg8w1ZE/1oe9FsDfbYjW34PB14 +oS2Rm0tk1c/5JI0RBWeyRDOQ +-----END PRIVATE KEY----- From d642704f12499db5563ffee844f0ba454f5d7770 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Tue, 30 Jul 2024 17:51:28 +0100 Subject: [PATCH 06/28] Disable test requiring write on keystore for now. --- .../client/RelationsGrpcClientsManagerTest.java | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index bc947b8..220fc04 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -29,8 +29,6 @@ class RelationsGrpcClientsManagerTest { static void testSetup() { /* Make sure all client managers shutdown/removed before tests */ RelationsGrpcClientsManager.shutdownAll(); - /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ - addTestCACertToTrustStore(); } @AfterEach @@ -41,8 +39,7 @@ void testTeardown() { @AfterAll static void removeTestSetup() { - /* Remove self-signed cert */ - removeTestCACertFromKeystore(); + } @Test @@ -130,8 +127,11 @@ void testThreadingChaos() { } } - @Test + // TODO: This test will not run github actions as is because keystore is read onlu @Test void testManagersHoldIntendedCredentialsInChannel() throws Exception { + /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ + addTestCACertToTrustStore(); + Config.AuthenticationConfig authnConfig = dummyNonDisabledAuthenticationConfig(); var manager = RelationsGrpcClientsManager.forInsecureClients("localhost:7000"); var manager2 = RelationsGrpcClientsManager.forInsecureClients("localhost:7001", authnConfig); @@ -159,6 +159,9 @@ void testManagersHoldIntendedCredentialsInChannel() throws Exception { assertNotNull(cd4.getMetadata().get(authorizationKey)); assertEquals("PRIVACY_AND_INTEGRITY", cd4.getCall().getSecurityLevel().toString()); + + /* Remove self-signed cert */ + removeTestCACertFromKeystore(); } @Test From 7d2cb90f6ca28c9577e6ca7d6633a4aa88f6b0c6 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 10:39:13 +0100 Subject: [PATCH 07/28] Testing workflow. --- .github/workflows/maven.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 664aba9..1bf0304 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -25,5 +25,16 @@ jobs: java-version: '17' distribution: 'temurin' cache: maven + - name: testing + run: ls -R ${{ github.workspace }} + - name: testing2 + run: ls -R ${{ github.RUNNER_TEMP }} + - name: testing3 + run: ls -R ${{ github.GITHUB_ACTION_PATH }} + - name: execute a keytool import function + shell: bash + env: + KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} + run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore ${{ steps.java.outputs.path }}/lib/security/cacerts -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From 8fc20a486972d2facb4c98b6d78011d26be32959 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 10:50:09 +0100 Subject: [PATCH 08/28] Testing github workflow. --- .github/workflows/maven.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 1bf0304..91ad3d2 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -31,6 +31,8 @@ jobs: run: ls -R ${{ github.RUNNER_TEMP }} - name: testing3 run: ls -R ${{ github.GITHUB_ACTION_PATH }} + - name: testing4 + run: ls -R ${{ steps.java.outputs.path }} - name: execute a keytool import function shell: bash env: From f39422e5aeefa2a47e4bacc8f92c89a16bf6de53 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 10:58:14 +0100 Subject: [PATCH 09/28] Testing github workflow. --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 91ad3d2..b769f7e 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -37,6 +37,6 @@ jobs: shell: bash env: KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} - run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore ${{ steps.java.outputs.path }}/lib/security/cacerts -storepass "$KEYSTORE_PASS" + run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From a976a6031013123824fab9b17633d60408656028 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:11:28 +0100 Subject: [PATCH 10/28] Testing github workflow. --- .github/workflows/maven.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index b769f7e..f30771a 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,6 +16,8 @@ on: jobs: build: + permissions: + contents: write runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 7cc8a001b5eb5a9b0f08826b0363b9e26ca3bade Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:17:05 +0100 Subject: [PATCH 11/28] Testing github workflow. --- .github/workflows/maven.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index f30771a..013f4aa 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,8 +16,7 @@ on: jobs: build: - permissions: - contents: write + permissions: write-all runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From d7e2e7749f443b6783aadb030d0fb22880f86af9 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:26:31 +0100 Subject: [PATCH 12/28] Testing github workflow. --- .github/workflows/maven.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 013f4aa..9ea77cf 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -38,6 +38,8 @@ jobs: shell: bash env: KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} - run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" + run: | + chmod +w "$JAVA_HOME/lib/security/cacerts" + keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From 6fd4a7d6f0e6745b5c876e80076bf257824624aa Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:37:09 +0100 Subject: [PATCH 13/28] Testing github workflow. --- .github/workflows/maven.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 9ea77cf..4c7fae7 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -23,8 +23,8 @@ jobs: - name: Set up JDK 17 uses: actions/setup-java@v4 with: - java-version: '17' - distribution: 'temurin' + java-version: '21' + distribution: 'zulu' cache: maven - name: testing run: ls -R ${{ github.workspace }} @@ -38,8 +38,6 @@ jobs: shell: bash env: KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} - run: | - chmod +w "$JAVA_HOME/lib/security/cacerts" - keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" + run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From 384270d6f3751cd5b8c3f1fec9b1c507e21c12b9 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:39:10 +0100 Subject: [PATCH 14/28] Testing github workflow. --- .github/workflows/maven.yml | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 4c7fae7..a1c1db8 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -20,20 +20,12 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Set up JDK 17 + - name: Set up Zulu JDK 21 uses: actions/setup-java@v4 with: java-version: '21' distribution: 'zulu' cache: maven - - name: testing - run: ls -R ${{ github.workspace }} - - name: testing2 - run: ls -R ${{ github.RUNNER_TEMP }} - - name: testing3 - run: ls -R ${{ github.GITHUB_ACTION_PATH }} - - name: testing4 - run: ls -R ${{ steps.java.outputs.path }} - name: execute a keytool import function shell: bash env: From 304c2e729b776273c39d9bd4cd26ba790ef2f174 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:40:15 +0100 Subject: [PATCH 15/28] Testing github workflow. --- .github/workflows/maven.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index a1c1db8..2acbbea 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -16,7 +16,6 @@ on: jobs: build: - permissions: write-all runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 From 5e2bce92d797ac2f7d734d66c5418dc680c7e119 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:43:09 +0100 Subject: [PATCH 16/28] Testing github workflow. --- .github/workflows/maven.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 2acbbea..94f3159 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -25,10 +25,10 @@ jobs: java-version: '21' distribution: 'zulu' cache: maven - - name: execute a keytool import function - shell: bash - env: - KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} +# - name: execute a keytool import function +# shell: bash +# env: +# KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From 9e724f51b41134cb77cf41604c977782346ba9ba Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 11:44:35 +0100 Subject: [PATCH 17/28] Testing github workflow. --- .github/workflows/maven.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 94f3159..01ed744 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -29,6 +29,6 @@ jobs: # shell: bash # env: # KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} - run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" +# run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From eb5606e05533c33f2e37ffaf07d0f296fff87813 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 12:24:43 +0100 Subject: [PATCH 18/28] Re add test. --- .../relations/client/RelationsGrpcClientsManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index 220fc04..de91f69 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -127,7 +127,7 @@ void testThreadingChaos() { } } - // TODO: This test will not run github actions as is because keystore is read onlu @Test + @Test void testManagersHoldIntendedCredentialsInChannel() throws Exception { /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ addTestCACertToTrustStore(); From 33476f19e67fc5f63e4cc7c60ad9bef53147d248 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 12:26:20 +0100 Subject: [PATCH 19/28] Testing breaking. --- .../relations/client/RelationsGrpcClientsManagerTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index de91f69..2d1c0fa 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -130,7 +130,7 @@ void testThreadingChaos() { @Test void testManagersHoldIntendedCredentialsInChannel() throws Exception { /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ - addTestCACertToTrustStore(); + //addTestCACertToTrustStore(); Config.AuthenticationConfig authnConfig = dummyNonDisabledAuthenticationConfig(); var manager = RelationsGrpcClientsManager.forInsecureClients("localhost:7000"); From 69c53bdae9232ba12d218c0de3862680c189635b Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 12:29:06 +0100 Subject: [PATCH 20/28] Testing working. --- .../client/RelationsGrpcClientsManagerTest.java | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index 2d1c0fa..bc947b8 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -29,6 +29,8 @@ class RelationsGrpcClientsManagerTest { static void testSetup() { /* Make sure all client managers shutdown/removed before tests */ RelationsGrpcClientsManager.shutdownAll(); + /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ + addTestCACertToTrustStore(); } @AfterEach @@ -39,7 +41,8 @@ void testTeardown() { @AfterAll static void removeTestSetup() { - + /* Remove self-signed cert */ + removeTestCACertFromKeystore(); } @Test @@ -129,9 +132,6 @@ void testThreadingChaos() { @Test void testManagersHoldIntendedCredentialsInChannel() throws Exception { - /* Add self-signed cert to keystore, trust manager and SSL context for TLS testing. */ - //addTestCACertToTrustStore(); - Config.AuthenticationConfig authnConfig = dummyNonDisabledAuthenticationConfig(); var manager = RelationsGrpcClientsManager.forInsecureClients("localhost:7000"); var manager2 = RelationsGrpcClientsManager.forInsecureClients("localhost:7001", authnConfig); @@ -159,9 +159,6 @@ void testManagersHoldIntendedCredentialsInChannel() throws Exception { assertNotNull(cd4.getMetadata().get(authorizationKey)); assertEquals("PRIVACY_AND_INTEGRITY", cd4.getCall().getSecurityLevel().toString()); - - /* Remove self-signed cert */ - removeTestCACertFromKeystore(); } @Test From e7b5d5a61c0a6a7fc341b85a55e2aab6045e720a Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 12:30:46 +0100 Subject: [PATCH 21/28] Clean up workflow. --- .github/workflows/maven.yml | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.github/workflows/maven.yml b/.github/workflows/maven.yml index 01ed744..2a18e43 100644 --- a/.github/workflows/maven.yml +++ b/.github/workflows/maven.yml @@ -25,10 +25,5 @@ jobs: java-version: '21' distribution: 'zulu' cache: maven -# - name: execute a keytool import function -# shell: bash -# env: -# KEYSTORE_PASS: ${{ secrets.KEYSTORE_PASS }} -# run: keytool -import -noprompt -trustcacerts -alias selfsigned -file ${{ github.workspace }}/src/test/resources/certs/test.crt -keystore "$JAVA_HOME/lib/security/cacerts" -storepass "$KEYSTORE_PASS" - name: Build and test with Maven run: ./mvnw -B package --file pom.xml From e5c792c17e7c83e986a35a5b953dbfbf71dd9a40 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 31 Jul 2024 12:35:17 +0100 Subject: [PATCH 22/28] Tidy up CertUtil. --- .../relations/client/util/CertUtil.java | 48 +------------------ 1 file changed, 2 insertions(+), 46 deletions(-) diff --git a/src/test/java/org/project_kessel/relations/client/util/CertUtil.java b/src/test/java/org/project_kessel/relations/client/util/CertUtil.java index bc3d085..73aec1d 100644 --- a/src/test/java/org/project_kessel/relations/client/util/CertUtil.java +++ b/src/test/java/org/project_kessel/relations/client/util/CertUtil.java @@ -1,9 +1,5 @@ package org.project_kessel.relations.client.util; -import javax.net.ssl.SSLContext; -import javax.net.ssl.TrustManager; -import javax.net.ssl.TrustManagerFactory; -import javax.net.ssl.X509TrustManager; import java.io.*; import java.security.*; import java.security.cert.Certificate; @@ -40,42 +36,12 @@ public static void addTestCACertToTrustStore() { OutputStream out = new FileOutputStream(file); keystore.store(out, passphrase); out.close(); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keystore); - TrustManager[] trustManagers = tmf.getTrustManagers(); - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagers, null); - - SSLContext.setDefault(sslContext); } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | - NullPointerException | KeyManagementException e) { + NullPointerException e) { throw new RuntimeException(e); } } - public static void dumpCerts() { - try { - TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - trustManagerFactory.init((KeyStore) null); - TrustManager[] trustManagers = trustManagerFactory.getTrustManagers(); - - X509TrustManager manager = (X509TrustManager) trustManagers[0]; - - for (java.security.cert.X509Certificate x509Certificate : manager.getAcceptedIssuers()) { - if ((x509Certificate.getSubjectDN().getName().startsWith("EMAILADDRESS=test@localhost"))) { - System.out.println("\n\n>>>>>> " + x509Certificate.getSubjectX500Principal() + "\n\n"); - } else { - System.out.println(x509Certificate.getSubjectX500Principal()); - } - } - } catch (NoSuchAlgorithmException | KeyStoreException e) { - throw new RuntimeException(e); - } - - } - public static void removeTestCACertFromKeystore() { final char sep = File.separatorChar; File dir = new File(System.getProperty("java.home") + sep + "lib" + sep + "security"); @@ -88,17 +54,7 @@ public static void removeTestCACertFromKeystore() { OutputStream out = new FileOutputStream(file); keystore.store(out, passphrase); out.close(); - - TrustManagerFactory tmf = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm()); - tmf.init(keystore); - TrustManager[] trustManagers = tmf.getTrustManagers(); - - SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init(null, trustManagers, null); - - SSLContext.setDefault(sslContext); - } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException | - KeyManagementException e) { + } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } } From 784cd1b8b3da9bf72e5828976f313624de89367a Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Fri, 2 Aug 2024 14:42:23 +0100 Subject: [PATCH 23/28] Added OIDCClientCredentialsCallCredentialsTest and NimbusOIDCClientCredentialsMinterTest. --- .../OIDCClientCredentialsCallCredentials.java | 30 +- .../client/OIDCClientCredentialsMinter.java | 4 + .../RelationsGrpcClientsManagerTest.java | 12 +- ...CClientCredentialsCallCredentialsTest.java | 261 ++++++++++++++++++ ...NimbusOIDCClientCredentialsMinterTest.java | 47 ++++ .../relations/client/fake/FakeIdp.java | 27 +- 6 files changed, 373 insertions(+), 8 deletions(-) create mode 100644 src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentialsTest.java create mode 100644 src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java index 87c8587..8c05648 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentials.java @@ -9,7 +9,7 @@ import java.util.concurrent.atomic.AtomicReference; public class OIDCClientCredentialsCallCredentials extends io.grpc.CallCredentials { - private static final Metadata.Key authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); + static final Metadata.Key authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); private final Config.OIDCClientCredentialsConfig clientCredentialsConfig; private final OIDCClientCredentialsMinter minter; @@ -17,10 +17,7 @@ public class OIDCClientCredentialsCallCredentials extends io.grpc.CallCredential private final AtomicReference storedBearerHeaderRef = new AtomicReference<>(); public OIDCClientCredentialsCallCredentials(Config.AuthenticationConfig authnConfig) throws OIDCClientCredentialsCallCredentialsException { - if(authnConfig.clientCredentialsConfig().isEmpty()) { - throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig is required for OIDC client credentials authentication method."); - } - this.clientCredentialsConfig = authnConfig.clientCredentialsConfig().get(); + this.clientCredentialsConfig = validateAndExtractConfig(authnConfig); Optional minterImpl = clientCredentialsConfig.oidcClientCredentialsMinterImplementation(); try { @@ -34,6 +31,11 @@ public OIDCClientCredentialsCallCredentials(Config.AuthenticationConfig authnCon } } + OIDCClientCredentialsCallCredentials(Config.OIDCClientCredentialsConfig clientCredentialsConfig, OIDCClientCredentialsMinter minter) { + this.clientCredentialsConfig = clientCredentialsConfig; + this.minter = minter; + } + @Override public void applyRequestMetadata(RequestInfo requestInfo, Executor appExecutor, MetadataApplier applier) { appExecutor.execute(() -> { @@ -62,6 +64,24 @@ public void flushStoredCredentials() { } } + /* We don't know that smallrye config validation will be used by clients, so do some validation here. */ + static Config.OIDCClientCredentialsConfig validateAndExtractConfig(Config.AuthenticationConfig authnConfig) throws OIDCClientCredentialsCallCredentialsException { + if (authnConfig.clientCredentialsConfig().isEmpty()) { + throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig is required for OIDC client credentials authentication method."); + } + if(authnConfig.clientCredentialsConfig().get().issuer() == null) { + throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig Issuer must not be null."); + } + if(authnConfig.clientCredentialsConfig().get().clientId() == null) { + throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig Client id must not be null."); + } + if(authnConfig.clientCredentialsConfig().get().clientSecret() == null) { + throw new OIDCClientCredentialsCallCredentialsException("ClientCredentialsConfig Client secret must not be null."); + } + + return authnConfig.clientCredentialsConfig().get(); + } + public static class OIDCClientCredentialsCallCredentialsException extends Exception { public OIDCClientCredentialsCallCredentialsException(String message) { super(message); diff --git a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java index 237eb45..10dfc9a 100644 --- a/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java +++ b/src/main/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinter.java @@ -74,6 +74,10 @@ public static Optional getExpiryDateFromExpiresIn(long expiresIn) return expiryTime; } + public static Class getDefaultMinterImplementation() { + return defaultMinter; + } + public static class OIDCClientCredentialsMinterException extends Exception { public OIDCClientCredentialsMinterException(String message) { super(message); diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index bc947b8..a210d04 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -22,7 +22,7 @@ import static org.junit.jupiter.api.Assertions.*; import static org.project_kessel.relations.client.util.CertUtil.*; -class RelationsGrpcClientsManagerTest { +public class RelationsGrpcClientsManagerTest { private static final Metadata.Key authorizationKey = Metadata.Key.of("Authorization", Metadata.ASCII_STRING_MARSHALLER); @BeforeAll @@ -130,6 +130,10 @@ void testThreadingChaos() { } } + /* + End-to-end tests against fake IdP and/or fake grpc relations-api + */ + @Test void testManagersHoldIntendedCredentialsInChannel() throws Exception { Config.AuthenticationConfig authnConfig = dummyNonDisabledAuthenticationConfig(); @@ -161,6 +165,10 @@ void testManagersHoldIntendedCredentialsInChannel() throws Exception { assertEquals("PRIVACY_AND_INTEGRITY", cd4.getCall().getSecurityLevel().toString()); } + /* + Tests relying on reflection. Maybe be brittle and could be removed in future. + */ + @Test void testManagerReuseInternal() throws Exception { RelationsGrpcClientsManager.forInsecureClients("localhost:8080"); @@ -237,7 +245,7 @@ void testCreateAndShutdownPatternsInternal() throws Exception { assertEquals(0, insecureManagersSize); } - Config.AuthenticationConfig dummyNonDisabledAuthenticationConfig() { + public static Config.AuthenticationConfig dummyNonDisabledAuthenticationConfig() { return new Config.AuthenticationConfig() { @Override public Config.AuthMode mode() { diff --git a/src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentialsTest.java b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentialsTest.java new file mode 100644 index 0000000..0d92453 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsCallCredentialsTest.java @@ -0,0 +1,261 @@ +package org.project_kessel.relations.client.authn.oidc.client; + +import io.grpc.CallCredentials; +import io.grpc.Metadata; +import io.grpc.Status; +import io.grpc.netty.shaded.io.netty.util.concurrent.DefaultEventExecutor; +import org.junit.jupiter.api.Test; +import org.project_kessel.relations.client.Config; + +import java.time.LocalDateTime; +import java.util.Optional; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.atomic.AtomicReference; + +import static org.junit.jupiter.api.Assertions.*; +import static org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsMinter.getDefaultMinterImplementation; + +public class OIDCClientCredentialsCallCredentialsTest { + + @Test + void initializationShouldFailWithNullIssuer() { + try { + var authConfig = makeAuthConfig(null, "some", "some"); + new OIDCClientCredentialsCallCredentials(authConfig); + } + catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + return; // expected + } + + fail("Issuer should not be null"); + } + + @Test + void initializationShouldFailWithNullClientId() { + try { + var authConfig = makeAuthConfig("some", null, "some"); + new OIDCClientCredentialsCallCredentials(authConfig); + } + catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + return; // expected + } + + fail("Client id should not be null"); + } + + @Test + void initializationShouldFailWithNullClientSecret() { + try { + var authConfig = makeAuthConfig("some", "some", null); + new OIDCClientCredentialsCallCredentials(authConfig); + } + catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + return; // expected + } + + fail("Client secret should not be null"); + } + + @Test + void unknownSpecifiedMinterShouldThrowException() { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.of("one.bogus.clazz")); + try { + new OIDCClientCredentialsCallCredentials(authConfig); + } + catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + return; // expected + } + + fail("Shouldn't be able to instantiate OIDCClientCredentialsCallCredentials with a bogus minter."); + } + + @Test + void knownSpecifiedMinterShouldNotThrowException() { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.of(getDefaultMinterImplementation().getName())); + try { + new OIDCClientCredentialsCallCredentials(authConfig); + } + catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + fail("Should be able create default minter with no problems."); + } + } + + @Test + void unspecifiedMinterShouldUseDefaultAndNotThrowException() { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.empty()); + try { + new OIDCClientCredentialsCallCredentials(authConfig); + } + catch (OIDCClientCredentialsCallCredentials.OIDCClientCredentialsCallCredentialsException e) { + fail("Should be able create default minter with no problems."); + } + } + + @Test + void shouldApplyBearerMetadata() throws InterruptedException { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.empty()); + var oidcClientCredentialsConfig = authConfig.clientCredentialsConfig().orElse(null); + var minter = makeFakeMinter(true, 0); + var callCreds = new OIDCClientCredentialsCallCredentials(oidcClientCredentialsConfig, minter); + final AtomicReference metaDataRef = new AtomicReference<>(); + final AtomicReference statusRef = new AtomicReference<>(); + var latch = new CountDownLatch(1); + var metaDataApplier = makeFakeMetadataApplier(metaDataRef, statusRef, latch); + + callCreds.applyRequestMetadata(null, new DefaultEventExecutor(), metaDataApplier); + + latch.await(); + assertEquals("token0", metaDataRef.get().get(OIDCClientCredentialsCallCredentials.authorizationKey)); + assertNull(statusRef.get()); + } + + @Test + void shouldApplyPreviouslyObtainedTokenWhenInLifetime() throws InterruptedException { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.empty()); + var oidcClientCredentialsConfig = authConfig.clientCredentialsConfig().orElse(null); + var minter = makeFakeMinter(true, 100000); // big lifetime + var callCreds = new OIDCClientCredentialsCallCredentials(oidcClientCredentialsConfig, minter); + final AtomicReference metaDataRef = new AtomicReference<>(); + final AtomicReference statusRef = new AtomicReference<>(); + var latch = new CountDownLatch(1); + var metaDataApplier = makeFakeMetadataApplier(metaDataRef, statusRef, latch); + + callCreds.applyRequestMetadata(null, new DefaultEventExecutor(), metaDataApplier); + + latch.await(); + var latch2 = new CountDownLatch(1); + var metaDataApplier2 = makeFakeMetadataApplier(metaDataRef, statusRef, latch2); + + callCreds.applyRequestMetadata(null, new DefaultEventExecutor(), metaDataApplier2); + + latch2.await(); + // token0 is the original minted token -- shows there was no second authentication and new token + assertEquals("token0", metaDataRef.get().get(OIDCClientCredentialsCallCredentials.authorizationKey)); + assertNull(statusRef.get()); + } + + @Test + void shouldApplyNewTokenWhenOutOfLifetime() throws InterruptedException { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.empty()); + var oidcClientCredentialsConfig = authConfig.clientCredentialsConfig().orElse(null); + var minter = makeFakeMinter(true, 0); // zero lifetime forces new auth token + var callCreds = new OIDCClientCredentialsCallCredentials(oidcClientCredentialsConfig, minter); + final AtomicReference metaDataRef = new AtomicReference<>(); + final AtomicReference statusRef = new AtomicReference<>(); + var latch = new CountDownLatch(1); + var metaDataApplier = makeFakeMetadataApplier(metaDataRef, statusRef, latch); + + callCreds.applyRequestMetadata(null, new DefaultEventExecutor(), metaDataApplier); + + latch.await(); + var latch2 = new CountDownLatch(1); + var metaDataApplier2 = makeFakeMetadataApplier(metaDataRef, statusRef, latch2); + + callCreds.applyRequestMetadata(null, new DefaultEventExecutor(), metaDataApplier2); + + latch2.await(); + // token1 is the second minted token -- shows that when out of lifetime there is a second authn and new token + assertEquals("token1", metaDataRef.get().get(OIDCClientCredentialsCallCredentials.authorizationKey)); + assertNull(statusRef.get()); + } + + @Test + void shouldApplyUnauthenticatedWhenAuthnFails() throws InterruptedException { + var authConfig = makeAuthConfig("some", "some", "some", Optional.empty(), + Optional.empty()); + var oidcClientCredentialsConfig = authConfig.clientCredentialsConfig().orElse(null); + var minter = makeFakeMinter(false, 0); + var callCreds = new OIDCClientCredentialsCallCredentials(oidcClientCredentialsConfig, minter); + final AtomicReference metaDataRef = new AtomicReference<>(); + final AtomicReference statusRef = new AtomicReference<>(); + var latch = new CountDownLatch(1); + var metaDataApplier = makeFakeMetadataApplier(metaDataRef, statusRef, latch); + + callCreds.applyRequestMetadata(null, new DefaultEventExecutor(), metaDataApplier); + + latch.await(); + assertNull(metaDataRef.get()); + assertEquals(Status.Code.UNAUTHENTICATED, statusRef.get().getCode()); + } + + static OIDCClientCredentialsMinter makeFakeMinter(boolean alwaysSucceedsOrFails, long tokensExpireIn) { + return new OIDCClientCredentialsMinter() { + int mintedNumber = 0; + + @Override + public BearerHeader authenticateAndRetrieveAuthorizationHeader(Config.OIDCClientCredentialsConfig clientConfig) throws OIDCClientCredentialsMinterException { + if (!alwaysSucceedsOrFails) { + throw new OIDCClientCredentialsMinterException("Authentication failed."); + } + + Optional expiry = Optional.of(LocalDateTime.now().plusSeconds(tokensExpireIn)); + return new BearerHeader("token" + mintedNumber++, expiry); + } + }; + } + + static CallCredentials.MetadataApplier makeFakeMetadataApplier(AtomicReference metaDataRef, AtomicReference statusRef, CountDownLatch latch) { + return new CallCredentials.MetadataApplier() { + @Override + public void apply(Metadata headers) { + metaDataRef.set(headers); + latch.countDown(); + } + + @Override + public void fail(Status status) { + statusRef.set(status); + latch.countDown(); + } + }; + } + + public static Config.AuthenticationConfig makeAuthConfig(String issuer, String clientId, String clientSecret) { + return makeAuthConfig(issuer, clientId, clientSecret, Optional.empty(), Optional.empty()); + } + + public static Config.AuthenticationConfig makeAuthConfig(String issuer, String clientId, String clientSecret, Optional scope, Optional minterImpl) { + return new Config.AuthenticationConfig() { + @Override + public Config.AuthMode mode() { + return null; + } + + @Override + public Optional clientCredentialsConfig() { + return Optional.of(new Config.OIDCClientCredentialsConfig() { + @Override + public String issuer() { + return issuer; + } + + @Override + public String clientId() { + return clientId; + } + + @Override + public String clientSecret() { + return clientSecret; + } + + @Override + public Optional scope() { + return scope; + } + + @Override + public Optional oidcClientCredentialsMinterImplementation() { + return minterImpl; + } + }); + } + }; + } +} diff --git a/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java new file mode 100644 index 0000000..c60932d --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java @@ -0,0 +1,47 @@ +package org.project_kessel.relations.client.authn.oidc.client.nimbus; + +import org.junit.jupiter.api.Test; +import org.project_kessel.relations.client.RelationsGrpcClientsManagerTest; +import org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsMinter; +import org.project_kessel.relations.client.fake.FakeIdp; + +import static org.junit.jupiter.api.Assertions.*; + +public class NimbusOIDCClientCredentialsMinterTest { + + @Test + void shouldReturnBearerHeaderWhenIdPAuthenticates() { + var minter = new NimbusOIDCClientCredentialsMinter(); + var config = RelationsGrpcClientsManagerTest.dummyNonDisabledAuthenticationConfig().clientCredentialsConfig(); + OIDCClientCredentialsMinter.BearerHeader bearerHeader = null; + try { + FakeIdp fakeIdp = new FakeIdp(8090); + fakeIdp.start(); + bearerHeader = minter.authenticateAndRetrieveAuthorizationHeader(config.get()); + fakeIdp.stop(); + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + fail("Should not throw exception if authn is successful."); + } + + assertNotNull(bearerHeader); + assertEquals("Bearer blah", bearerHeader.getAuthorizationHeader()); + } + + @Test + void shouldThrowExceptionWhenIdPAuthenticationFails() { + var minter = new NimbusOIDCClientCredentialsMinter(); + var config = RelationsGrpcClientsManagerTest.dummyNonDisabledAuthenticationConfig().clientCredentialsConfig(); + FakeIdp fakeIdp = new FakeIdp(8090, false); + try { + fakeIdp.start(); + minter.authenticateAndRetrieveAuthorizationHeader(config.get()); + fail("Should throw exception if authn is not successful."); + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + // success + } catch(Exception e) { + fail("OIDCClientCredentialsMinterException expected."); + } finally { + fakeIdp.stop(); + } + } +} diff --git a/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java b/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java index ede3896..b481455 100644 --- a/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java +++ b/src/test/java/org/project_kessel/relations/client/fake/FakeIdp.java @@ -14,10 +14,16 @@ */ public class FakeIdp { private final int port; + private final boolean alwaysSucceedOrFailAuthn; HttpServer server = null; public FakeIdp(int port) { + this(port, true); + } + + public FakeIdp(int port, boolean alwaysSucceedOrFailAuthn) { this.port = port; + this.alwaysSucceedOrFailAuthn = alwaysSucceedOrFailAuthn; } public void start() { @@ -27,7 +33,12 @@ public void start() { throw new RuntimeException(e); } server.createContext("/.well-known/openid-configuration", new WellKnownHandler()); - server.createContext("/token", new TokenHandler()); + if(alwaysSucceedOrFailAuthn) { + server.createContext("/token", new TokenHandler()); + } else { + server.createContext("/token", new UnauthorizedHandler()); + } + server.setExecutor(null); // creates a default executor server.start(); } @@ -57,6 +68,20 @@ public void handle(HttpExchange t) throws IOException { } } + static class UnauthorizedHandler implements HttpHandler { + @Override + public void handle(HttpExchange t) throws IOException { + String response = "{\"error_description\":\"Access denied by resource owner or authorization server\",\"error\":\"access_denied\"}"; + + // https://openid.net/specs/openid-connect-core-1_0.html#TokenEndpoint (3.1.3.4. Token Error Response) + t.getResponseHeaders().set("Content-Type", "application/json; charset=UTF-8"); + t.sendResponseHeaders(400, response.length()); + OutputStream os = t.getResponseBody(); + os.write(response.getBytes()); + os.close(); + } + } + static class WellKnownHandler implements HttpHandler { @Override public void handle(HttpExchange t) throws IOException { From d7ee09ff6f4d64fe810a70c3baf7639375f90615 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Fri, 2 Aug 2024 17:18:34 +0100 Subject: [PATCH 24/28] Added OIDCClientCredentialsMinterTest. --- .../OIDCClientCredentialsMinterTest.java | 97 +++++++++++++++++++ 1 file changed, 97 insertions(+) create mode 100644 src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinterTest.java diff --git a/src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinterTest.java b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinterTest.java new file mode 100644 index 0000000..8bc9d10 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/OIDCClientCredentialsMinterTest.java @@ -0,0 +1,97 @@ +package org.project_kessel.relations.client.authn.oidc.client; + +import org.junit.jupiter.api.Test; +import org.project_kessel.relations.client.Config; + +import java.time.LocalDateTime; +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.*; +import static org.project_kessel.relations.client.authn.oidc.client.OIDCClientCredentialsMinter.getExpiryDateFromExpiresIn; + +class OIDCClientCredentialsMinterTest { + + @Test + void testCreateDefaultMinter() { + Class defaultMinterClass = OIDCClientCredentialsMinter.getDefaultMinterImplementation(); + try { + var minter = OIDCClientCredentialsMinter.forClass(defaultMinterClass); + assertInstanceOf(defaultMinterClass, minter); + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + fail("Creating minter from default implementation name should not throw an OIDCClientCredentialsMinterException"); + } + } + + @Test + void testCreateMinterFromClass() { + Class testMinterClass = TestMinter.class; + try { + var minter = OIDCClientCredentialsMinter.forClass(testMinterClass); + assertInstanceOf(testMinterClass, minter); + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + fail("Creating minter from test implementation name should not throw an OIDCClientCredentialsMinterException"); + } + } + + @Test + void testCreateMinterFromName() { + String testMinterName = TestMinter.class.getName(); + try { + OIDCClientCredentialsMinter.forName(testMinterName); + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + fail("Creating minter from test implementation name should not throw an OIDCClientCredentialsMinterException"); + } + } + + @Test + void testCreateMinterFromFakeImplNameThrowsException() { + String defaultMinterName = "absolutely.not.a.valid.Implementation"; + try { + OIDCClientCredentialsMinter.forName(defaultMinterName); + } catch (OIDCClientCredentialsMinter.OIDCClientCredentialsMinterException e) { + return; + } + fail("Creating minter from not existent implementation name should throw an OIDCClientCredentialsMinterException"); + } + + @Test + void testGetExpiryDateFromExpiresInLongLived() { + LocalDateTime someTimeBefore = LocalDateTime.now().plusSeconds(9000); + LocalDateTime someTimeAfter = LocalDateTime.now().plusSeconds(11000); + Optional expiryDate = getExpiryDateFromExpiresIn(10000); + assertTrue(expiryDate.isPresent()); + assertTrue(someTimeBefore.isBefore(expiryDate.get())); + assertTrue(someTimeAfter.isAfter(expiryDate.get())); + } + + @Test + void testGetAbsentExpiryDateFromExpiresInShortLived() { + Optional expiryDate = getExpiryDateFromExpiresIn(0); + assertTrue(expiryDate.isEmpty()); + } + + @Test + void bearerHeaderExpiryScenarios() { + Optional someTimeInTheFuture = Optional.of(LocalDateTime.now().plusSeconds(10000)); + var bearerHeader = new OIDCClientCredentialsMinter.BearerHeader("header", someTimeInTheFuture); + assertFalse(bearerHeader.isExpired()); + + Optional someTimeInThePast = Optional.of(LocalDateTime.now().minusSeconds(10000)); + bearerHeader = new OIDCClientCredentialsMinter.BearerHeader("header", someTimeInThePast); + assertTrue(bearerHeader.isExpired()); + + Optional noExpiryTime = Optional.empty(); + bearerHeader = new OIDCClientCredentialsMinter.BearerHeader("header", noExpiryTime); + assertTrue(bearerHeader.isExpired()); + } + + static class TestMinter extends OIDCClientCredentialsMinter { + public TestMinter() { + } + + @Override + public BearerHeader authenticateAndRetrieveAuthorizationHeader(Config.OIDCClientCredentialsConfig clientConfig) throws OIDCClientCredentialsMinterException { + return null; + } + } +} From 1365b0d2ea694a18203d9c675d6143630f393b60 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Fri, 2 Aug 2024 18:17:54 +0100 Subject: [PATCH 25/28] Add CallCredentialsFactoryTest. --- .../RelationsGrpcClientsManagerTest.java | 4 +- .../authn/CallCredentialsFactoryTest.java | 53 +++++++++++++++++++ ...NimbusOIDCClientCredentialsMinterTest.java | 4 +- 3 files changed, 57 insertions(+), 4 deletions(-) create mode 100644 src/test/java/org/project_kessel/relations/client/authn/CallCredentialsFactoryTest.java diff --git a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java index a210d04..1c72739 100644 --- a/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java +++ b/src/test/java/org/project_kessel/relations/client/RelationsGrpcClientsManagerTest.java @@ -136,7 +136,7 @@ void testThreadingChaos() { @Test void testManagersHoldIntendedCredentialsInChannel() throws Exception { - Config.AuthenticationConfig authnConfig = dummyNonDisabledAuthenticationConfig(); + Config.AuthenticationConfig authnConfig = dummyAuthConfigWithGoodOIDCClientCredentials(); var manager = RelationsGrpcClientsManager.forInsecureClients("localhost:7000"); var manager2 = RelationsGrpcClientsManager.forInsecureClients("localhost:7001", authnConfig); var manager3 = RelationsGrpcClientsManager.forSecureClients("localhost:7002"); @@ -245,7 +245,7 @@ void testCreateAndShutdownPatternsInternal() throws Exception { assertEquals(0, insecureManagersSize); } - public static Config.AuthenticationConfig dummyNonDisabledAuthenticationConfig() { + public static Config.AuthenticationConfig dummyAuthConfigWithGoodOIDCClientCredentials() { return new Config.AuthenticationConfig() { @Override public Config.AuthMode mode() { diff --git a/src/test/java/org/project_kessel/relations/client/authn/CallCredentialsFactoryTest.java b/src/test/java/org/project_kessel/relations/client/authn/CallCredentialsFactoryTest.java new file mode 100644 index 0000000..426f24a --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/authn/CallCredentialsFactoryTest.java @@ -0,0 +1,53 @@ +package org.project_kessel.relations.client.authn; + +import org.junit.jupiter.api.Test; +import org.project_kessel.relations.client.Config; + +import java.util.Optional; + +import static org.junit.jupiter.api.Assertions.fail; +import static org.project_kessel.relations.client.RelationsGrpcClientsManagerTest.dummyAuthConfigWithGoodOIDCClientCredentials; + +class CallCredentialsFactoryTest { + + @Test + void testCreateOIDCClientCallCredentials() { + Config.AuthenticationConfig authnConfig = dummyAuthConfigWithGoodOIDCClientCredentials(); + try { + CallCredentialsFactory.create(authnConfig); + } catch (CallCredentialsFactory.CallCredentialsCreationException e) { + fail("CallCredentialsFactory creation for OIDC client should not throw an exception when OIDC client config is good."); + } + } + + @Test + void testFailToCreateCallCredentialsWhenAuthnConfigEmpty() { + Config.AuthenticationConfig authnConfig = null; + try { + CallCredentialsFactory.create(authnConfig); + fail("CallCredentialsFactory creation for OIDC client should throw an exception when OIDC client config is empty."); + } catch (CallCredentialsFactory.CallCredentialsCreationException e) { + } + } + + @Test + void testFailToCreateCallCredentialsForOIDCWhenConfigEmpty() { + Config.AuthenticationConfig authnConfig = new Config.AuthenticationConfig() { + @Override + public Config.AuthMode mode() { + return Config.AuthMode.OIDC_CLIENT_CREDENTIALS; + } + + @Override + public Optional clientCredentialsConfig() { + return Optional.empty(); + } + }; + try { + CallCredentialsFactory.create(authnConfig); + fail("CallCredentialsFactory creation for OIDC client should throw an exception when OIDC client config is empty."); + } catch (CallCredentialsFactory.CallCredentialsCreationException e) { + } + } + +} diff --git a/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java index c60932d..bc83e26 100644 --- a/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java +++ b/src/test/java/org/project_kessel/relations/client/authn/oidc/client/nimbus/NimbusOIDCClientCredentialsMinterTest.java @@ -12,7 +12,7 @@ public class NimbusOIDCClientCredentialsMinterTest { @Test void shouldReturnBearerHeaderWhenIdPAuthenticates() { var minter = new NimbusOIDCClientCredentialsMinter(); - var config = RelationsGrpcClientsManagerTest.dummyNonDisabledAuthenticationConfig().clientCredentialsConfig(); + var config = RelationsGrpcClientsManagerTest.dummyAuthConfigWithGoodOIDCClientCredentials().clientCredentialsConfig(); OIDCClientCredentialsMinter.BearerHeader bearerHeader = null; try { FakeIdp fakeIdp = new FakeIdp(8090); @@ -30,7 +30,7 @@ void shouldReturnBearerHeaderWhenIdPAuthenticates() { @Test void shouldThrowExceptionWhenIdPAuthenticationFails() { var minter = new NimbusOIDCClientCredentialsMinter(); - var config = RelationsGrpcClientsManagerTest.dummyNonDisabledAuthenticationConfig().clientCredentialsConfig(); + var config = RelationsGrpcClientsManagerTest.dummyAuthConfigWithGoodOIDCClientCredentials().clientCredentialsConfig(); FakeIdp fakeIdp = new FakeIdp(8090, false); try { fakeIdp.start(); From fe82e97b9ab7c695dbe5ec0c29327030a361e9ea Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Tue, 6 Aug 2024 11:31:37 +0100 Subject: [PATCH 26/28] Move CDIManagedClients tests to separate class. --- ...gedClientsTest.java => CDIManagedClientsContainerTests.java} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename src/test/java/org/project_kessel/relations/client/{CDIManagedClientsTest.java => CDIManagedClientsContainerTests.java} (99%) diff --git a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsContainerTests.java similarity index 99% rename from src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java rename to src/test/java/org/project_kessel/relations/client/CDIManagedClientsContainerTests.java index 47f07f6..fd5ca01 100644 --- a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java +++ b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsContainerTests.java @@ -23,7 +23,7 @@ * Use Weld as a test container to check CDI functionality. */ @EnableWeld -class CDIManagedClientsTest { +class CDIManagedClientsContainerTests { @WeldSetup public WeldInitiator weld = WeldInitiator.from(new Weld().setBeanDiscoveryMode(BeanDiscoveryMode.ALL).addBeanClass(TestConfig.class)).build(); From 93117cf324366677bd6fbf310cf9c7ecf7f8a35c Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Tue, 6 Aug 2024 12:11:59 +0100 Subject: [PATCH 27/28] Added CDIManagedClientsTest. --- pom.xml | 6 + .../client/CDIManagedClientsTest.java | 182 ++++++++++++++++++ 2 files changed, 188 insertions(+) create mode 100644 src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java diff --git a/pom.xml b/pom.xml index f53e8fa..cf86100 100644 --- a/pom.xml +++ b/pom.xml @@ -113,6 +113,12 @@ 5.10.2 test + + org.mockito + mockito-inline + 5.2.0 + test + org.jboss.weld diff --git a/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java new file mode 100644 index 0000000..2258d60 --- /dev/null +++ b/src/test/java/org/project_kessel/relations/client/CDIManagedClientsTest.java @@ -0,0 +1,182 @@ +package org.project_kessel.relations.client; + +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +import java.util.Optional; + +import static org.mockito.Mockito.*; + +class CDIManagedClientsTest { + @Test + void testInsecureNoAuthnMakesCorrectManagerCall() { + Config config = makeDummyConfig(false, makeDummyAuthenticationConfig(false)); + CDIManagedClients cdiManagedClients = new CDIManagedClients(); + + try (MockedStatic dummyManager = Mockito.mockStatic(RelationsGrpcClientsManager.class)) { + cdiManagedClients.getManager(config); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString()), + times(1) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString(), any()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString(), any()), + times(0) + ); + } + } + + @Test + void testInsecureWithAuthnMakesCorrectManagerCall() { + Config config = makeDummyConfig(false, makeDummyAuthenticationConfig(true)); + CDIManagedClients cdiManagedClients = new CDIManagedClients(); + + try (MockedStatic dummyManager = Mockito.mockStatic(RelationsGrpcClientsManager.class)) { + cdiManagedClients.getManager(config); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString(), any()), + times(1) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString(), any()), + times(0) + ); + } + } + + @Test + void testSecureNoAuthnMakesCorrectManagerCall() { + Config config = makeDummyConfig(true, makeDummyAuthenticationConfig(false)); + CDIManagedClients cdiManagedClients = new CDIManagedClients(); + + try (MockedStatic dummyManager = Mockito.mockStatic(RelationsGrpcClientsManager.class)) { + cdiManagedClients.getManager(config); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString(), any()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString()), + times(1) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString(), any()), + times(0) + ); + } + } + + @Test + void testSecureWithAuthnMakesCorrectManagerCall() { + Config config = makeDummyConfig(true, makeDummyAuthenticationConfig(true)); + CDIManagedClients cdiManagedClients = new CDIManagedClients(); + + try (MockedStatic dummyManager = Mockito.mockStatic(RelationsGrpcClientsManager.class)) { + cdiManagedClients.getManager(config); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forInsecureClients(anyString(), any()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString()), + times(0) + ); + dummyManager.verify( + () -> RelationsGrpcClientsManager.forSecureClients(anyString(), any()), + times(1) + ); + } + } + + static Config makeDummyConfig(boolean secure, Config.AuthenticationConfig authnConfig) { + return new Config() { + @Override + public boolean isSecureClients() { + return secure; + } + + @Override + public String targetUrl() { + return "localhost"; + } + + @Override + public Optional authenticationConfig() { + return Optional.of(authnConfig); + } + }; + } + + static Config.AuthenticationConfig makeDummyAuthenticationConfig(boolean authnEnabled) { + return new Config.AuthenticationConfig() { + @Override + public Config.AuthMode mode() { + if(!authnEnabled) { + return Config.AuthMode.DISABLED; + } + // pick some arbitrary non disabled mode + return Config.AuthMode.OIDC_CLIENT_CREDENTIALS; + } + + @Override + public Optional clientCredentialsConfig() { + if(!authnEnabled) { + return Optional.empty(); + } + + // provide dummy config matching mode, above. + return Optional.of(new Config.OIDCClientCredentialsConfig() { + @Override + public String issuer() { + return ""; + } + + @Override + public String clientId() { + return ""; + } + + @Override + public String clientSecret() { + return ""; + } + + @Override + public Optional scope() { + return Optional.empty(); + } + + @Override + public Optional oidcClientCredentialsMinterImplementation() { + return Optional.empty(); + } + }); + } + }; + } +} From 94d2d334b5e14889e267c33dee613ce4a20d93c5 Mon Sep 17 00:00:00 2001 From: Mark McLaughlin Date: Wed, 7 Aug 2024 13:05:49 +0100 Subject: [PATCH 28/28] Rewrote CertUtil to ensure resources closed properly. --- .../relations/client/util/CertUtil.java | 65 +++++++++++-------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/src/test/java/org/project_kessel/relations/client/util/CertUtil.java b/src/test/java/org/project_kessel/relations/client/util/CertUtil.java index 73aec1d..e3c9bd4 100644 --- a/src/test/java/org/project_kessel/relations/client/util/CertUtil.java +++ b/src/test/java/org/project_kessel/relations/client/util/CertUtil.java @@ -12,50 +12,59 @@ public class CertUtil { private static final String certFileName = "certs/test.crt"; public static void addTestCACertToTrustStore() { - final char sep = File.separatorChar; - File dir = new File(System.getProperty("java.home") + sep + "lib" + sep + "security"); - File file = new File(dir, "cacerts"); try { - InputStream localCertIn = new FileInputStream(file); - KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); - keystore.load(localCertIn, passphrase); - localCertIn.close(); + var keystore = loadKeystoreFromJdk(); if (keystore.containsAlias(selfSignedAlias)) { return; } - InputStream certIn = Thread.currentThread().getContextClassLoader().getResourceAsStream(certFileName); - BufferedInputStream bis = new BufferedInputStream(certIn); - CertificateFactory cf = CertificateFactory.getInstance("X.509"); - while (bis.available() > 0) { - Certificate cert = cf.generateCertificate(bis); - keystore.setCertificateEntry(selfSignedAlias, cert); - } - certIn.close(); + try(InputStream certIn = Thread.currentThread().getContextClassLoader().getResourceAsStream(certFileName); + BufferedInputStream bis = new BufferedInputStream(certIn)) { - OutputStream out = new FileOutputStream(file); - keystore.store(out, passphrase); - out.close(); - } catch (NoSuchAlgorithmException | CertificateException | KeyStoreException | IOException | - NullPointerException e) { + CertificateFactory cf = CertificateFactory.getInstance("X.509"); + while (bis.available() > 0) { + Certificate cert = cf.generateCertificate(bis); + keystore.setCertificateEntry(selfSignedAlias, cert); + } + + saveKeystoreToJdk(keystore); + } + } catch (CertificateException | KeyStoreException | IOException | NullPointerException e) { throw new RuntimeException(e); } } public static void removeTestCACertFromKeystore() { - final char sep = File.separatorChar; - File dir = new File(System.getProperty("java.home") + sep + "lib" + sep + "security"); - File file = new File(dir, "cacerts"); + var keystore = loadKeystoreFromJdk(); try { - InputStream localCertIn = new FileInputStream(file); + keystore.deleteEntry(selfSignedAlias); + saveKeystoreToJdk(keystore); + } catch (KeyStoreException e) { + throw new RuntimeException(e); + } + } + + public static KeyStore loadKeystoreFromJdk() { + try (InputStream localCertIn = new FileInputStream(getCertFile())) { KeyStore keystore = KeyStore.getInstance(KeyStore.getDefaultType()); keystore.load(localCertIn, passphrase); - keystore.deleteEntry(selfSignedAlias); - OutputStream out = new FileOutputStream(file); + return keystore; + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + public static void saveKeystoreToJdk(KeyStore keystore) { + try (OutputStream out = new FileOutputStream(getCertFile())) { keystore.store(out, passphrase); - out.close(); - } catch (KeyStoreException | CertificateException | IOException | NoSuchAlgorithmException e) { + } catch (IOException | CertificateException | KeyStoreException | NoSuchAlgorithmException e) { throw new RuntimeException(e); } } + + private static File getCertFile() { + final char sep = File.separatorChar; + File dir = new File(System.getProperty("java.home") + sep + "lib" + sep + "security"); + return new File(dir, "cacerts"); + } }