Skip to content

Commit

Permalink
Added implementation of TLS-registry for GraphQL Client
Browse files Browse the repository at this point in the history
  • Loading branch information
mskacelik committed Sep 30, 2024
1 parent c018767 commit 1b41820
Show file tree
Hide file tree
Showing 14 changed files with 877 additions and 11 deletions.
14 changes: 14 additions & 0 deletions extensions/smallrye-graphql-client/deployment/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@
<groupId>io.smallrye</groupId>
<artifactId>smallrye-graphql-client-model-builder</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-tls-registry-deployment</artifactId>
</dependency>
<dependency>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-graphql-client-model</artifactId>
Expand Down Expand Up @@ -81,6 +85,16 @@
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.smallrye.certs</groupId>
<artifactId>smallrye-certificate-generator-junit5</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import java.util.List;
import java.util.Map;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.inject.Singleton;

import org.eclipse.microprofile.graphql.Input;
Expand All @@ -26,9 +25,12 @@
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
import io.quarkus.arc.processor.BuiltinScope;
import io.quarkus.deployment.Feature;
import io.quarkus.deployment.annotations.BuildProducer;
import io.quarkus.deployment.annotations.BuildStep;
import io.quarkus.deployment.annotations.Consume;
import io.quarkus.deployment.annotations.Record;
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
Expand All @@ -40,6 +42,7 @@
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
import io.quarkus.runtime.RuntimeValue;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientBuildConfig;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientCertificateUpdateEventListener;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientSupport;
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientsConfig;
import io.quarkus.smallrye.graphql.client.runtime.SmallRyeGraphQLClientRecorder;
Expand All @@ -52,6 +55,7 @@ public class SmallRyeGraphQLClientProcessor {
private static final DotName GRAPHQL_CLIENT_API = DotName
.createSimple("io.smallrye.graphql.client.typesafe.api.GraphQLClientApi");
private static final DotName GRAPHQL_CLIENT = DotName.createSimple("io.smallrye.graphql.client.GraphQLClient");
private static final String CERTIFICATE_UPDATE_EVENT_LISTENER = GraphQLClientCertificateUpdateEventListener.class.getName();
private static final String NAMED_DYNAMIC_CLIENTS = "io.smallrye.graphql.client.impl.dynamic.cdi.NamedDynamicClients";

@BuildStep
Expand Down Expand Up @@ -124,10 +128,11 @@ void initializeTypesafeClient(BeanArchiveIndexBuildItem index,
}
}

BuiltinScope scope = BuiltinScope.from(index.getIndex().getClassByName(apiClass));
// an equivalent of io.smallrye.graphql.client.typesafe.impl.cdi.GraphQlClientBean that produces typesafe client instances
SyntheticBeanBuildItem bean = SyntheticBeanBuildItem.configure(apiClassInfo.name())
.addType(apiClassInfo.name())
.scope(ApplicationScoped.class)
.scope(scope == null ? BuiltinScope.APPLICATION.getInfo() : scope.getInfo())
.addInjectionPoint(ClassType.create(DotName.createSimple(ClientModels.class)))
.createWith(recorder.typesafeClientSupplier(apiClass))
.unremovable()
Expand Down Expand Up @@ -165,13 +170,12 @@ void setTypesafeApiClasses(BeanArchiveIndexBuildItem index,
*/
@BuildStep
@Record(RUNTIME_INIT)
GraphQLClientConfigInitializedBuildItem mergeClientConfigurations(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
SmallRyeGraphQLClientRecorder recorder,
@Consume(SyntheticBeansRuntimeInitBuildItem.class)
GraphQLClientConfigInitializedBuildItem mergeClientConfigurations(SmallRyeGraphQLClientRecorder recorder,
GraphQLClientsConfig quarkusConfig,
BeanArchiveIndexBuildItem index) {
// to store config keys of all clients found in the application code
List<String> knownConfigKeys = new ArrayList<>();

Map<String, String> shortNamesToQualifiedNames = new HashMap<>();
for (AnnotationInstance annotation : index.getIndex().getAnnotations(GRAPHQL_CLIENT_API)) {
ClassInfo clazz = annotation.target().asClass();
Expand Down Expand Up @@ -241,4 +245,9 @@ void setAdditionalClassesToIndex(BuildProducer<AdditionalIndexedClassesBuildItem
}
}

@BuildStep
void registerCertificateUpdateEventListener(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
additionalBeans.produce(new AdditionalBeanBuildItem(CERTIFICATE_UPDATE_EVENT_LISTENER));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package io.quarkus.smallrye.graphql.client.deployment.ssl;

import java.security.KeyStore;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

import io.smallrye.graphql.client.vertx.ssl.SSLTools;
import io.vertx.core.Vertx;
import io.vertx.core.http.ClientAuth;
import io.vertx.core.http.HttpServer;
import io.vertx.core.http.HttpServerOptions;
import io.vertx.core.net.JksOptions;

public class SSLTestingTools {
static Vertx vertx = Vertx.vertx();

public HttpServer runServer(String keystorePath, String keystorePassword,
String truststorePath, String truststorePassword)
throws InterruptedException, ExecutionException, TimeoutException {
HttpServerOptions options = new HttpServerOptions();
options.setSsl(true);
options.setHost("localhost");

if (keystorePath != null) {
JksOptions keystoreOptions = new JksOptions();
KeyStore keyStore = SSLTools.createKeyStore(keystorePath, "PKCS12", keystorePassword);
keystoreOptions.setValue(SSLTools.asBuffer(keyStore, keystorePassword.toCharArray()));
keystoreOptions.setPassword(keystorePassword);
options.setKeyStoreOptions(keystoreOptions);
}
if (truststorePath != null) {
options.setClientAuth(ClientAuth.REQUIRED);
JksOptions truststoreOptions = new JksOptions();
KeyStore trustStore = SSLTools.createKeyStore(truststorePath, "PKCS12", truststorePassword);
truststoreOptions.setValue(SSLTools.asBuffer(trustStore, truststorePassword.toCharArray()));
truststoreOptions.setPassword(truststorePassword);
options.setTrustStoreOptions(truststoreOptions);
}

HttpServer server = vertx.createHttpServer(options);
server.requestHandler(request -> {
request.response().send("{\n" +
" \"data\": {\n" +
" \"result\": \"HelloWorld\"\n" +
" }\n" +
"}");
});

return server.listen(63805).toCompletionStage().toCompletableFuture().get(10, TimeUnit.SECONDS);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package io.quarkus.smallrye.graphql.client.deployment.ssl;

import jakarta.inject.Inject;

import org.eclipse.microprofile.graphql.Query;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;
import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi;
import io.vertx.core.http.HttpServer;

@Certificates(baseDir = "target/certs", certificates = {
@Certificate(name = "graphql", password = "password", formats = { Format.PKCS12 }, client = true),
@Certificate(name = "wrong-graphql", password = "wrong-password", formats = { Format.PKCS12 }, client = true)
})
public class TypesafeGraphQLClientClientAuthenticationBadKeystoreTest {

private static final int PORT = 63805;
private static final SSLTestingTools TOOLS = new SSLTestingTools();
private static HttpServer server;

private static final String CONFIGURATION = """
quarkus.smallrye-graphql-client.my-client.tls-bucket-name=my-tls-client
quarkus.tls.my-tls-client.key-store.p12.path=target/certs/wrong-graphql-client-keystore.p12
quarkus.tls.my-tls-client.key-store.p12.password=wrong-password
quarkus.smallrye-graphql-client.my-client.url=https://127.0.0.1:%d/
quarkus.tls.my-tls-client.trust-all=true
""".formatted(PORT);

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(MyApi.class, SSLTestingTools.class)
.addAsResource(new StringAsset(CONFIGURATION),
"application.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));

@GraphQLClientApi(configKey = "my-client")
private interface MyApi {
@Query
String getResult();
}

@Inject
MyApi myApi;

@BeforeAll
static void setupServer() throws Exception {
server = TOOLS.runServer("target/certs/graphql-keystore.p12",
"password", "target/certs/graphql-server-truststore.p12", "password");
}

@Test
void clientAuthentication_badKeystore() {
try {
myApi.getResult();
Assertions.fail("Should not be able to connect");
} catch (Exception e) {
// verify that the server rejected the client's certificate
assertHasCauseContainingMessage(e, "Received fatal alert: certificate_unknown");
}
}

@AfterAll
static void closeServer() {
server.close();
}

private void assertHasCauseContainingMessage(Throwable t, String message) {
Throwable throwable = t;
while (throwable.getCause() != null) {
throwable = throwable.getCause();
if (throwable.getMessage().contains(message)) {
t.printStackTrace();
return;
}
}
throw new RuntimeException("Unexpected exception", t);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
package io.quarkus.smallrye.graphql.client.deployment.ssl;

import static org.assertj.core.api.AssertionsForClassTypes.assertThat;

import jakarta.inject.Inject;

import org.eclipse.microprofile.graphql.Query;
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
import org.jboss.shrinkwrap.api.asset.StringAsset;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import io.quarkus.test.QuarkusUnitTest;
import io.smallrye.certs.Format;
import io.smallrye.certs.junit5.Certificate;
import io.smallrye.certs.junit5.Certificates;
import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi;
import io.vertx.core.http.HttpServer;

@Certificates(baseDir = "target/certs", certificates = @Certificate(name = "graphql", password = "password", formats = {
Format.PKCS12 }, client = true))
public class TypesafeGraphQLClientClientAuthenticationCorrectKeystoreTest {

private static final int PORT = 63805;
private static final SSLTestingTools TOOLS = new SSLTestingTools();
private static final String EXPECTED_RESPONSE = "HelloWorld";
private static HttpServer server;

private static final String CONFIGURATION = """
quarkus.smallrye-graphql-client.my-client.tls-bucket-name=my-tls-client
quarkus.tls.my-tls-client.key-store.p12.path=target/certs/graphql-client-keystore.p12
quarkus.tls.my-tls-client.key-store.p12.password=password
quarkus.smallrye-graphql-client.my-client.url=https://127.0.0.1:%d/
quarkus.tls.my-tls-client.trust-all=true
""".formatted(PORT);

@RegisterExtension
static QuarkusUnitTest test = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar
.addClasses(MyApi.class, SSLTestingTools.class)
.addAsResource(new StringAsset(CONFIGURATION),
"application.properties")
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));

@GraphQLClientApi(configKey = "my-client")
private interface MyApi {
@Query
String getResult();
}

@Inject
MyApi myApi;

@BeforeAll
static void setupServer() throws Exception {
server = TOOLS.runServer("target/certs/graphql-keystore.p12",
"password", "target/certs/graphql-server-truststore.p12", "password");
}

@Test
void clientAuthentication_correctKeystore() {
assertThat(myApi.getResult()).isEqualTo(EXPECTED_RESPONSE);
}

@AfterAll
static void closeServer() {
server.close();
}
}
Loading

0 comments on commit 1b41820

Please sign in to comment.