Skip to content

Commit fe164e0

Browse files
authored
Merge pull request #43850 from jmartisk/smallrye-graphql-2.11
SmallRye GraphQL 2.11
2 parents 92b0659 + 3375bb2 commit fe164e0

File tree

21 files changed

+1134
-46
lines changed

21 files changed

+1134
-46
lines changed

bom/application/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@
5757
<smallrye-health.version>4.1.0</smallrye-health.version>
5858
<smallrye-metrics.version>4.0.0</smallrye-metrics.version>
5959
<smallrye-open-api.version>3.13.0</smallrye-open-api.version>
60-
<smallrye-graphql.version>2.10.0</smallrye-graphql.version>
60+
<smallrye-graphql.version>2.11.0</smallrye-graphql.version>
6161
<smallrye-fault-tolerance.version>6.5.0</smallrye-fault-tolerance.version>
6262
<smallrye-jwt.version>4.6.0</smallrye-jwt.version>
6363
<smallrye-context-propagation.version>2.1.2</smallrye-context-propagation.version>

docs/src/main/asciidoc/tls-registry-reference.adoc

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,11 +21,12 @@ The TLS Registry consolidates settings and supports multiple named configuration
2121
Therefore, you can tailor TLS settings for different application parts.
2222
This flexibility is particularly useful when different components require distinct security configurations.
2323

24-
The TLS Registry extension is automatically included in your project when you use compatible extensions, such as Quarkus REST, gRPC
24+
The TLS Registry extension is automatically included in your project when you use compatible extensions, such as Quarkus REST, gRPC, SmallRye GraphQL Client
2525
ifndef::no-reactive-routes[]
2626
, or Reactive Routes
2727
endif::no-reactive-routes[]
2828
.
29+
2930
As a result, applications that use the TLS Registry can be ready to handle secure communications out of the box.
3031
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.
3132

@@ -149,6 +150,24 @@ quarkus.http.tls-configuration-name=MY_TLS_CONFIGURATION
149150
quarkus.grpc.clients.hello.tls-configuration-name=MY_TLS_CONFIGURATION
150151
----
151152

153+
.Example configuration for a SmallRye GraphQL client:
154+
[source,properties]
155+
----
156+
quarkus.smallrye-graphql-client.my-client.tls-configuration-name=MY_TLS_CONFIGURATION
157+
----
158+
159+
[NOTE]
160+
====
161+
When using the Typesafe GraphQL client with a certificate
162+
reloading mechanism (see <<reloading-certificates>>), it is essential to
163+
override the bean's scope to `RequestScoped` (or another similar scope
164+
shorter than application). This is because by default, the Typesafe client is an
165+
application-scoped bean, so shortening the scope guarantees that new instances of the bean
166+
created after a certificate reload will be configured with the latest
167+
certificate. Dynamic clients are `@Dependent` scoped, so you should
168+
inject them into components with an appropriate scope.
169+
====
170+
152171
== Configuring TLS
153172

154173
TLS configuration primarily involves managing keystores and truststores.

extensions/smallrye-graphql-client/deployment/pom.xml

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,10 @@
4141
<groupId>io.smallrye</groupId>
4242
<artifactId>smallrye-graphql-client-model-builder</artifactId>
4343
</dependency>
44+
<dependency>
45+
<groupId>io.quarkus</groupId>
46+
<artifactId>quarkus-tls-registry-deployment</artifactId>
47+
</dependency>
4448
<dependency>
4549
<groupId>io.smallrye</groupId>
4650
<artifactId>smallrye-graphql-client-model</artifactId>
@@ -81,6 +85,16 @@
8185
<artifactId>quarkus-elytron-security-properties-file-deployment</artifactId>
8286
<scope>test</scope>
8387
</dependency>
88+
<dependency>
89+
<groupId>org.assertj</groupId>
90+
<artifactId>assertj-core</artifactId>
91+
<scope>test</scope>
92+
</dependency>
93+
<dependency>
94+
<groupId>io.smallrye.certs</groupId>
95+
<artifactId>smallrye-certificate-generator-junit5</artifactId>
96+
<scope>test</scope>
97+
</dependency>
8498
</dependencies>
8599

86100
<build>

extensions/smallrye-graphql-client/deployment/src/main/java/io/quarkus/smallrye/graphql/client/deployment/SmallRyeGraphQLClientProcessor.java

Lines changed: 42 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,6 @@
99
import java.util.List;
1010
import java.util.Map;
1111

12-
import jakarta.enterprise.context.ApplicationScoped;
1312
import jakarta.inject.Singleton;
1413

1514
import org.eclipse.microprofile.graphql.Input;
@@ -26,9 +25,12 @@
2625
import io.quarkus.arc.deployment.BeanArchiveIndexBuildItem;
2726
import io.quarkus.arc.deployment.BeanContainerBuildItem;
2827
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
28+
import io.quarkus.arc.deployment.SyntheticBeansRuntimeInitBuildItem;
29+
import io.quarkus.arc.processor.BuiltinScope;
2930
import io.quarkus.deployment.Feature;
3031
import io.quarkus.deployment.annotations.BuildProducer;
3132
import io.quarkus.deployment.annotations.BuildStep;
33+
import io.quarkus.deployment.annotations.Consume;
3234
import io.quarkus.deployment.annotations.Record;
3335
import io.quarkus.deployment.builditem.AdditionalIndexedClassesBuildItem;
3436
import io.quarkus.deployment.builditem.CombinedIndexBuildItem;
@@ -40,6 +42,7 @@
4042
import io.quarkus.deployment.builditem.nativeimage.ServiceProviderBuildItem;
4143
import io.quarkus.runtime.RuntimeValue;
4244
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientBuildConfig;
45+
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientCertificateUpdateEventListener;
4346
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientSupport;
4447
import io.quarkus.smallrye.graphql.client.runtime.GraphQLClientsConfig;
4548
import io.quarkus.smallrye.graphql.client.runtime.SmallRyeGraphQLClientRecorder;
@@ -52,6 +55,7 @@ public class SmallRyeGraphQLClientProcessor {
5255
private static final DotName GRAPHQL_CLIENT_API = DotName
5356
.createSimple("io.smallrye.graphql.client.typesafe.api.GraphQLClientApi");
5457
private static final DotName GRAPHQL_CLIENT = DotName.createSimple("io.smallrye.graphql.client.GraphQLClient");
58+
private static final String CERTIFICATE_UPDATE_EVENT_LISTENER = GraphQLClientCertificateUpdateEventListener.class.getName();
5559
private static final String NAMED_DYNAMIC_CLIENTS = "io.smallrye.graphql.client.impl.dynamic.cdi.NamedDynamicClients";
5660

5761
@BuildStep
@@ -70,23 +74,37 @@ void setupServiceProviders(BuildProducer<ServiceProviderBuildItem> services) {
7074
.allProvidersFromClassPath("io.smallrye.graphql.client.typesafe.api.TypesafeGraphQLClientBuilder"));
7175
services.produce(ServiceProviderBuildItem
7276
.allProvidersFromClassPath("io.smallrye.graphql.client.dynamic.api.DynamicGraphQLClientBuilder"));
73-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Argument"));
74-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Directive"));
7577
services.produce(
76-
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.DirectiveArgument"));
77-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Document"));
78-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Enum"));
79-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Field"));
80-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Fragment"));
78+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.ArgumentFactory"));
8179
services.produce(
82-
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.FragmentReference"));
83-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.InlineFragment"));
84-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.InputObject"));
80+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.DirectiveFactory"));
8581
services.produce(
86-
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.InputObjectField"));
87-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Operation"));
88-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.Variable"));
89-
services.produce(ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.VariableType"));
82+
ServiceProviderBuildItem
83+
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.DirectiveArgumentFactory"));
84+
services.produce(
85+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.DocumentFactory"));
86+
services.produce(
87+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.EnumFactory"));
88+
services.produce(
89+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.FieldFactory"));
90+
services.produce(
91+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.FragmentFactory"));
92+
services.produce(
93+
ServiceProviderBuildItem
94+
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.FragmentReferenceFactory"));
95+
services.produce(ServiceProviderBuildItem
96+
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.InlineFragmentFactory"));
97+
services.produce(ServiceProviderBuildItem
98+
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.InputObjectFactory"));
99+
services.produce(
100+
ServiceProviderBuildItem
101+
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.InputObjectFieldFactory"));
102+
services.produce(
103+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.OperationFactory"));
104+
services.produce(
105+
ServiceProviderBuildItem.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.VariableFactory"));
106+
services.produce(ServiceProviderBuildItem
107+
.allProvidersFromClassPath("io.smallrye.graphql.client.core.factory.VariableTypeFactory"));
90108
}
91109

92110
@BuildStep
@@ -124,10 +142,11 @@ void initializeTypesafeClient(BeanArchiveIndexBuildItem index,
124142
}
125143
}
126144

145+
BuiltinScope scope = BuiltinScope.from(index.getIndex().getClassByName(apiClass));
127146
// an equivalent of io.smallrye.graphql.client.typesafe.impl.cdi.GraphQlClientBean that produces typesafe client instances
128147
SyntheticBeanBuildItem bean = SyntheticBeanBuildItem.configure(apiClassInfo.name())
129148
.addType(apiClassInfo.name())
130-
.scope(ApplicationScoped.class)
149+
.scope(scope == null ? BuiltinScope.APPLICATION.getInfo() : scope.getInfo())
131150
.addInjectionPoint(ClassType.create(DotName.createSimple(ClientModels.class)))
132151
.createWith(recorder.typesafeClientSupplier(apiClass))
133152
.unremovable()
@@ -165,13 +184,12 @@ void setTypesafeApiClasses(BeanArchiveIndexBuildItem index,
165184
*/
166185
@BuildStep
167186
@Record(RUNTIME_INIT)
168-
GraphQLClientConfigInitializedBuildItem mergeClientConfigurations(BuildProducer<SyntheticBeanBuildItem> syntheticBeans,
169-
SmallRyeGraphQLClientRecorder recorder,
187+
@Consume(SyntheticBeansRuntimeInitBuildItem.class)
188+
GraphQLClientConfigInitializedBuildItem mergeClientConfigurations(SmallRyeGraphQLClientRecorder recorder,
170189
GraphQLClientsConfig quarkusConfig,
171190
BeanArchiveIndexBuildItem index) {
172191
// to store config keys of all clients found in the application code
173192
List<String> knownConfigKeys = new ArrayList<>();
174-
175193
Map<String, String> shortNamesToQualifiedNames = new HashMap<>();
176194
for (AnnotationInstance annotation : index.getIndex().getAnnotations(GRAPHQL_CLIENT_API)) {
177195
ClassInfo clazz = annotation.target().asClass();
@@ -241,4 +259,9 @@ void setAdditionalClassesToIndex(BuildProducer<AdditionalIndexedClassesBuildItem
241259
}
242260
}
243261

262+
@BuildStep
263+
void registerCertificateUpdateEventListener(BuildProducer<AdditionalBeanBuildItem> additionalBeans) {
264+
additionalBeans.produce(new AdditionalBeanBuildItem(CERTIFICATE_UPDATE_EVENT_LISTENER));
265+
}
266+
244267
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
package io.quarkus.smallrye.graphql.client.deployment.ssl;
2+
3+
import java.security.KeyStore;
4+
import java.util.concurrent.ExecutionException;
5+
import java.util.concurrent.TimeUnit;
6+
import java.util.concurrent.TimeoutException;
7+
8+
import io.smallrye.graphql.client.vertx.ssl.SSLTools;
9+
import io.vertx.core.Vertx;
10+
import io.vertx.core.http.ClientAuth;
11+
import io.vertx.core.http.HttpServer;
12+
import io.vertx.core.http.HttpServerOptions;
13+
import io.vertx.core.net.PfxOptions;
14+
15+
public class SSLTestingTools {
16+
private Vertx vertx;
17+
18+
public HttpServer runServer(String keystorePath, String keystorePassword,
19+
String truststorePath, String truststorePassword)
20+
throws InterruptedException, ExecutionException, TimeoutException {
21+
vertx = Vertx.vertx();
22+
HttpServerOptions options = new HttpServerOptions();
23+
options.setSsl(true);
24+
options.setHost("localhost");
25+
26+
if (keystorePath != null) {
27+
PfxOptions keystoreOptions = new PfxOptions();
28+
KeyStore keyStore = SSLTools.createKeyStore(keystorePath, "PKCS12", keystorePassword);
29+
keystoreOptions.setValue(SSLTools.asBuffer(keyStore, keystorePassword.toCharArray()));
30+
keystoreOptions.setPassword(keystorePassword);
31+
options.setKeyCertOptions(keystoreOptions);
32+
}
33+
34+
if (truststorePath != null) {
35+
options.setClientAuth(ClientAuth.REQUIRED);
36+
PfxOptions truststoreOptions = new PfxOptions();
37+
KeyStore trustStore = SSLTools.createKeyStore(truststorePath, "PKCS12", truststorePassword);
38+
truststoreOptions.setValue(SSLTools.asBuffer(trustStore, truststorePassword.toCharArray()));
39+
truststoreOptions.setPassword(truststorePassword);
40+
options.setTrustOptions(truststoreOptions);
41+
}
42+
43+
HttpServer server = vertx.createHttpServer(options);
44+
server.requestHandler(request -> {
45+
request.response().send("{\n" +
46+
" \"data\": {\n" +
47+
" \"result\": \"HelloWorld\"\n" +
48+
" }\n" +
49+
"}");
50+
});
51+
52+
return server.listen(63805).toCompletionStage().toCompletableFuture().get(10, TimeUnit.SECONDS);
53+
}
54+
55+
public void close() {
56+
vertx.close().toCompletionStage().toCompletableFuture().join();
57+
}
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
package io.quarkus.smallrye.graphql.client.deployment.ssl;
2+
3+
import jakarta.inject.Inject;
4+
5+
import org.eclipse.microprofile.graphql.Query;
6+
import org.jboss.shrinkwrap.api.asset.EmptyAsset;
7+
import org.jboss.shrinkwrap.api.asset.StringAsset;
8+
import org.junit.jupiter.api.AfterAll;
9+
import org.junit.jupiter.api.Assertions;
10+
import org.junit.jupiter.api.BeforeAll;
11+
import org.junit.jupiter.api.Test;
12+
import org.junit.jupiter.api.extension.RegisterExtension;
13+
14+
import io.quarkus.test.QuarkusUnitTest;
15+
import io.smallrye.certs.Format;
16+
import io.smallrye.certs.junit5.Certificate;
17+
import io.smallrye.certs.junit5.Certificates;
18+
import io.smallrye.graphql.client.typesafe.api.GraphQLClientApi;
19+
import io.vertx.core.http.HttpServer;
20+
21+
@Certificates(baseDir = "target/certs", certificates = {
22+
@Certificate(name = "graphql", password = "password", formats = { Format.PKCS12 }, client = true),
23+
@Certificate(name = "wrong-graphql", password = "wrong-password", formats = { Format.PKCS12 }, client = true)
24+
})
25+
public class TypesafeGraphQLClientClientAuthenticationBadKeystoreTest {
26+
27+
private static final int PORT = 63805;
28+
private static final SSLTestingTools TOOLS = new SSLTestingTools();
29+
private static HttpServer server;
30+
31+
private static final String CONFIGURATION = """
32+
quarkus.smallrye-graphql-client.my-client.tls-configuration-name=my-tls-client
33+
quarkus.tls.my-tls-client.key-store.p12.path=target/certs/wrong-graphql-client-keystore.p12
34+
quarkus.tls.my-tls-client.key-store.p12.password=wrong-password
35+
quarkus.smallrye-graphql-client.my-client.url=https://127.0.0.1:%d/
36+
quarkus.tls.my-tls-client.trust-all=true
37+
""".formatted(PORT);
38+
39+
@RegisterExtension
40+
static QuarkusUnitTest test = new QuarkusUnitTest()
41+
.withApplicationRoot((jar) -> jar
42+
.addClasses(MyApi.class, SSLTestingTools.class)
43+
.addAsResource(new StringAsset(CONFIGURATION),
44+
"application.properties")
45+
.addAsManifestResource(EmptyAsset.INSTANCE, "beans.xml"));
46+
47+
@GraphQLClientApi(configKey = "my-client")
48+
private interface MyApi {
49+
@Query
50+
String getResult();
51+
}
52+
53+
@Inject
54+
MyApi myApi;
55+
56+
@BeforeAll
57+
static void setupServer() throws Exception {
58+
server = TOOLS.runServer("target/certs/graphql-keystore.p12",
59+
"password", "target/certs/graphql-server-truststore.p12", "password");
60+
}
61+
62+
@Test
63+
void clientAuthentication_badKeystore() {
64+
try {
65+
myApi.getResult();
66+
Assertions.fail("Should not be able to connect");
67+
} catch (Exception e) {
68+
// verify that the server rejected the client's certificate
69+
assertHasCauseContainingMessage(e, "Received fatal alert: certificate_unknown");
70+
}
71+
}
72+
73+
@AfterAll
74+
static void closeServer() {
75+
server.close();
76+
TOOLS.close();
77+
}
78+
79+
private void assertHasCauseContainingMessage(Throwable t, String message) {
80+
Throwable throwable = t;
81+
while (throwable.getCause() != null) {
82+
throwable = throwable.getCause();
83+
if (throwable.getMessage().contains(message)) {
84+
t.printStackTrace();
85+
return;
86+
}
87+
}
88+
throw new RuntimeException("Unexpected exception", t);
89+
}
90+
}

0 commit comments

Comments
 (0)