Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

SmallRye GraphQL 2.11 #43850

Merged
merged 8 commits into from
Oct 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 @@
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.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

Check warning on line 161 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'. Raw Output: {"message": "[Quarkus.TermsSuggestions] Depending on the context, consider using 'by using' or 'that uses' rather than 'using'.", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 161, "column": 5}}}, "severity": "INFO"}

Check warning on line 161 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Typesafe'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Typesafe'?", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 161, "column": 16}}}, "severity": "WARNING"}
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

Check warning on line 164 in docs/src/main/asciidoc/tls-registry-reference.adoc

View workflow job for this annotation

GitHub Actions / Linting with Vale

[vale] reported by reviewdog 🐶 [Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Typesafe'? Raw Output: {"message": "[Quarkus.Spelling] Use correct American English spelling. Did you really mean 'Typesafe'?", "location": {"path": "docs/src/main/asciidoc/tls-registry-reference.adoc", "range": {"start": {"line": 164, "column": 60}}}, "severity": "WARNING"}
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
Loading