Skip to content

Commit

Permalink
Merge pull request quarkusio#43850 from jmartisk/smallrye-graphql-2.11
Browse files Browse the repository at this point in the history
SmallRye GraphQL 2.11
  • Loading branch information
jmartisk authored Oct 15, 2024
2 parents 92b0659 + 3375bb2 commit fe164e0
Show file tree
Hide file tree
Showing 21 changed files with 1,134 additions and 46 deletions.
2 changes: 1 addition & 1 deletion bom/application/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@
<smallrye-health.version>4.1.0</smallrye-health.version>
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
<smallrye-open-api.version>3.13.0</smallrye-open-api.version>
<smallrye-graphql.version>2.10.0</smallrye-graphql.version>
<smallrye-graphql.version>2.11.0</smallrye-graphql.version>
<smallrye-fault-tolerance.version>6.5.0</smallrye-fault-tolerance.version>
<smallrye-jwt.version>4.6.0</smallrye-jwt.version>
<smallrye-context-propagation.version>2.1.2</smallrye-context-propagation.version>
Expand Down
21 changes: 20 additions & 1 deletion docs/src/main/asciidoc/tls-registry-reference.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,12 @@ The TLS Registry consolidates settings and supports multiple named configuration
Therefore, you can tailor TLS settings for different application parts.
This flexibility is particularly useful when different components require distinct security configurations.

The TLS Registry extension is automatically included in your project when you use compatible extensions, such as Quarkus REST, gRPC
The TLS Registry extension is automatically included in your project when you use compatible extensions, such as Quarkus REST, gRPC, SmallRye GraphQL Client
ifndef::no-reactive-routes[]
, or Reactive Routes
endif::no-reactive-routes[]
.

As a result, applications that use the TLS Registry can be ready to handle secure communications out of the box.
TLS Registry also provides features like automatic certificate reloading, Let's Encrypt (ACME) integration, Kubernetes Cert-Manager support, and compatibility with various keystore formats, such as PKCS12, PEM, and JKS.

Expand Down Expand Up @@ -149,6 +150,24 @@ quarkus.http.tls-configuration-name=MY_TLS_CONFIGURATION
quarkus.grpc.clients.hello.tls-configuration-name=MY_TLS_CONFIGURATION
----

.Example configuration for a SmallRye GraphQL client:
[source,properties]
----
quarkus.smallrye-graphql-client.my-client.tls-configuration-name=MY_TLS_CONFIGURATION
----

[NOTE]
====
When using the Typesafe GraphQL client with a certificate
reloading mechanism (see <<reloading-certificates>>), it is essential to
override the bean's scope to `RequestScoped` (or another similar scope
shorter than application). This is because by default, the Typesafe client is an
application-scoped bean, so shortening the scope guarantees that new instances of the bean
created after a certificate reload will be configured with the latest
certificate. Dynamic clients are `@Dependent` scoped, so you should
inject them into components with an appropriate scope.
====

== Configuring TLS

TLS configuration primarily involves managing keystores and truststores.
Expand Down
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 All @@ -70,23 +74,37 @@ void setupServiceProviders(BuildProducer<ServiceProviderBuildItem> services) {
.allProvidersFromClassPath("io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder"));
services.produce(ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Argument"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Directive"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.DirectiveArgument"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Document"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Enum"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Field"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Fragment"));
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.ArgumentFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.FragmentReference"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.InlineFragment"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.InputObject"));
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.DirectiveFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.InputObjectField"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Operation"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Variable"));
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.VariableType"));
ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.DirectiveArgumentFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.DocumentFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.EnumFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.FieldFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.FragmentFactory"));
services.produce(
ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.FragmentReferenceFactory"));
services.produce(ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.InlineFragmentFactory"));
services.produce(ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.InputObjectFactory"));
services.produce(
ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.InputObjectFieldFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.OperationFactory"));
services.produce(
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.VariableFactory"));
services.produce(ServiceProviderBuildItem
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.VariableTypeFactory"));
}

@BuildStep
Expand Down Expand Up @@ -124,10 +142,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 +184,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 +259,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,58 @@
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.PfxOptions;

public class SSLTestingTools {
private Vertx vertx;

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

if (keystorePath != null) {
PfxOptions keystoreOptions = new PfxOptions();
KeyStore keyStore = SSLTools.createKeyStore(keystorePath, "PKCS12", keystorePassword);
keystoreOptions.setValue(SSLTools.asBuffer(keyStore, keystorePassword.toCharArray()));
keystoreOptions.setPassword(keystorePassword);
options.setKeyCertOptions(keystoreOptions);
}

if (truststorePath != null) {
options.setClientAuth(ClientAuth.REQUIRED);
PfxOptions truststoreOptions = new PfxOptions();
KeyStore trustStore = SSLTools.createKeyStore(truststorePath, "PKCS12", truststorePassword);
truststoreOptions.setValue(SSLTools.asBuffer(trustStore, truststorePassword.toCharArray()));
truststoreOptions.setPassword(truststorePassword);
options.setTrustOptions(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);
}

public void close() {
vertx.close().toCompletionStage().toCompletableFuture().join();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
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-configuration-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();
TOOLS.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);
}
}
Loading

0 comments on commit fe164e0

Please sign in to comment.