Skip to content

Commit

Permalink
Merge pull request quarkusio#36503 from cescoffier/mongo-client-side-…
Browse files Browse the repository at this point in the history
…encryption-jvm

Allows Mongo client settings customization
  • Loading branch information
cescoffier authored Oct 19, 2023
2 parents 89ed09e + 9dc91bf commit 7dcbe67
Show file tree
Hide file tree
Showing 10 changed files with 399 additions and 9 deletions.
49 changes: 49 additions & 0 deletions docs/src/main/asciidoc/mongodb.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -746,6 +746,55 @@ quarkus.mongodb.native.dns.use-vertx-dns-resolver=true
quarkus.mongodb.native.dns.lookup-timeout=10s # the default is 5s
----

== Customize the Mongo client configuration programmatically

If you need to customize the Mongo client configuration programmatically, you need to implement the `io.quarkus.mongodb.runtime.MongoClientCustomizer` interface and expose it as a CDI _application scoped_ bean:

[source, java]
----
@ApplicationScoped
public class MyCustomizer implements MongoClientCustomizer {
@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-app");
}
}
----

The bean can customize a specific client using the `@MongoClientName` qualifier to indicate the client name.
When there is no qualifier, it customizes the default client.
At most one customizer can be used per client.
If multiple customizers targeting the same client are detected, an exception is thrown at build time.

This feature can be used to configure client-side field level encryption (CSFLE).
Follows the instructions from https://www.mongodb.com/docs/manual/core/csfle/[the Mongo web site] to configure CSFLE:

[source, java]
----
@ApplicationScoped
public class MyCustomizer implements MongoClientCustomizer {
@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
Map<String, Map<String, Object>> kmsProviders = getKmsProviders();
String dek = getDataEncryptionKey();
Map<String, BsonDocument> schema = getSchema(dek);
Map<String, Object> extraOptions = new HashMap<>();
extraOptions.put("cryptSharedLibPath", "<path to crypt shared library>");
return builder.autoEncryptionSettings(AutoEncryptionSettings.builder()
.keyVaultNamespace(KEY_VAULT_NAMESPACE)
.kmsProviders(kmsProviders)
.schemaMap(schemaMap)
.extraOptions(extraOptions)
.build());
}
}
----

IMPORTANT: Client-side field level encryption, and feature relying on https://github.com/mongodb/libmongocrypt[Mongo Crypt] in general, are not supported in native mode.

== Configuration Reference

include::{generated-dir}/config/quarkus-mongodb.adoc[opts=optional, leveloffset=+1]
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@

import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.Supplier;
Expand Down Expand Up @@ -34,8 +36,11 @@

import io.quarkus.arc.deployment.AdditionalBeanBuildItem;
import io.quarkus.arc.deployment.BeanContainerBuildItem;
import io.quarkus.arc.deployment.BeanDiscoveryFinishedBuildItem;
import io.quarkus.arc.deployment.BeanRegistrationPhaseBuildItem;
import io.quarkus.arc.deployment.SyntheticBeanBuildItem;
import io.quarkus.arc.deployment.ValidationPhaseBuildItem;
import io.quarkus.arc.processor.BeanInfo;
import io.quarkus.arc.processor.BuildExtension;
import io.quarkus.arc.processor.DotNames;
import io.quarkus.arc.processor.InjectionPointInfo;
Expand All @@ -60,6 +65,7 @@
import io.quarkus.mongodb.MongoClientName;
import io.quarkus.mongodb.reactive.ReactiveMongoClient;
import io.quarkus.mongodb.runtime.MongoClientBeanUtil;
import io.quarkus.mongodb.runtime.MongoClientCustomizer;
import io.quarkus.mongodb.runtime.MongoClientRecorder;
import io.quarkus.mongodb.runtime.MongoClientSupport;
import io.quarkus.mongodb.runtime.MongoClients;
Expand All @@ -79,6 +85,8 @@ public class MongoClientProcessor {
private static final DotName MONGO_CLIENT = DotName.createSimple(MongoClient.class.getName());
private static final DotName REACTIVE_MONGO_CLIENT = DotName.createSimple(ReactiveMongoClient.class.getName());

private static final DotName MONGO_CLIENT_CUSTOMIZER = DotName.createSimple(MongoClientCustomizer.class.getName());

private static final String SERVICE_BINDING_INTERFACE_NAME = "io.quarkus.kubernetes.service.binding.runtime.ServiceBindingConverter";

@BuildStep
Expand Down Expand Up @@ -433,4 +441,37 @@ void runtimeInitializedClasses(BuildProducer<RuntimeInitializedClassBuildItem> r
runtimeInitializedClasses.produce(new RuntimeInitializedClassBuildItem(ObjectId.class.getName()));
runtimeInitializedClasses.produce(new RuntimeInitializedClassBuildItem("com.mongodb.internal.dns.DefaultDnsResolver"));
}

/**
* Ensure we have at most one customizer per Mongo client.
*
* @param beans the beans
* @param validation the producer used to report issues
*/
@BuildStep
void validateMongoConfigCustomizers(BeanDiscoveryFinishedBuildItem beans,
BuildProducer<ValidationPhaseBuildItem.ValidationErrorBuildItem> validation) {
HashMap<String, List<String>> customizers = new HashMap<>();

for (BeanInfo bean : beans.getBeans()) {
if (bean.hasType(MONGO_CLIENT_CUSTOMIZER)) {
var name = bean.getQualifier(MONGO_CLIENT_ANNOTATION);
if (name.isPresent()) {
String clientName = name.get().value().asString();
customizers.computeIfAbsent(clientName, k -> new ArrayList<>()).add(bean.getBeanClass().toString());
} else {
customizers.computeIfAbsent(MongoClientBeanUtil.DEFAULT_MONGOCLIENT_NAME, k -> new ArrayList<>())
.add(bean.getBeanClass().toString());
}
}
}

for (Map.Entry<String, List<String>> entry : customizers.entrySet()) {
if (entry.getValue().size() > 1) {
validation.produce(new ValidationPhaseBuildItem.ValidationErrorBuildItem(
new IllegalStateException("Multiple Mongo client customizers found for client " + entry.getKey() + ": "
+ String.join(", ", entry.getValue()))));
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package io.quarkus.mongodb.customization;

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

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.internal.MongoClientImpl;

import io.quarkus.arc.ClientProxy;
import io.quarkus.mongodb.MongoTestBase;
import io.quarkus.mongodb.runtime.MongoClientCustomizer;
import io.quarkus.test.QuarkusUnitTest;

public class DefaultCustomizerTest extends MongoTestBase {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(MongoTestBase.class, MyCustomizer.class))
.withConfigurationResource("default-mongoclient.properties");

@Inject
MongoClient client;

@Test
void testCustomizationOnDefaultConnection() {
MongoClientImpl clientImpl = (MongoClientImpl) ClientProxy.unwrap(client);
Assertions.assertThat(clientImpl.getSettings().getApplicationName()).isEqualTo("my-app");
}

@ApplicationScoped
public static class MyCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-app");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.quarkus.mongodb.customization;

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

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;
import com.mongodb.client.internal.MongoClientImpl;

import io.quarkus.arc.ClientProxy;
import io.quarkus.mongodb.MongoClientName;
import io.quarkus.mongodb.MongoTestBase;
import io.quarkus.mongodb.runtime.MongoClientCustomizer;
import io.quarkus.test.QuarkusUnitTest;

public class NamedCustomizerTest extends MongoTestBase {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(MongoTestBase.class, MyCustomizer.class))
.withConfigurationResource("named-mongoclient.properties");

@Inject
MongoClient client;

@Inject
@MongoClientName("second")
MongoClient secondClient;

@Test
void testCustomizationOnTwoConnections() {
MongoClientImpl clientImpl = (MongoClientImpl) ClientProxy.unwrap(client);
MongoClientImpl secondClientImpl = (MongoClientImpl) ClientProxy.unwrap(secondClient);
Assertions.assertThat(clientImpl.getSettings().getApplicationName()).isEqualTo("my-app");
Assertions.assertThat(secondClientImpl.getSettings().getApplicationName()).isEqualTo("my-second-app");
}

@ApplicationScoped
public static class MyCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-app");
}
}

@ApplicationScoped
@MongoClientName("second")
public static class MySecondCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-second-app");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
package io.quarkus.mongodb.customization;

import static org.junit.jupiter.api.Assertions.fail;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.inject.Inject;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;

import io.quarkus.mongodb.MongoTestBase;
import io.quarkus.mongodb.runtime.MongoClientCustomizer;
import io.quarkus.test.QuarkusUnitTest;

public class TooManyDefaultCustomizersTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(MongoTestBase.class, MyCustomizer.class, MySecondCustomizer.class))
.withConfigurationResource("default-mongoclient.properties")
.assertException(t -> Assertions.assertThat(t).isInstanceOf(DeploymentException.class)
.hasMessageContaining("Multiple Mongo client customizers found for client <default>: "));

@Inject
MongoClient client;

@Test
void test() {
fail("Should not be run");
}

@ApplicationScoped
public static class MyCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-app");
}
}

@ApplicationScoped
public static class MySecondCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-second-app");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
package io.quarkus.mongodb.customization;

import static org.junit.jupiter.api.Assertions.fail;

import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.spi.DeploymentException;
import jakarta.inject.Inject;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.RegisterExtension;

import com.mongodb.MongoClientSettings;
import com.mongodb.client.MongoClient;

import io.quarkus.mongodb.MongoClientName;
import io.quarkus.mongodb.MongoTestBase;
import io.quarkus.mongodb.runtime.MongoClientCustomizer;
import io.quarkus.test.QuarkusUnitTest;

public class TooManyNamedCustomizersTest {

@RegisterExtension
static final QuarkusUnitTest config = new QuarkusUnitTest()
.withApplicationRoot((jar) -> jar.addClasses(MongoTestBase.class, MyCustomizer.class, MySecondCustomizer.class))
.withConfigurationResource("named-mongoclient.properties")
.assertException(t -> Assertions.assertThat(t).isInstanceOf(DeploymentException.class)
.hasMessageContaining("Multiple Mongo client customizers found for client second: "));

@Inject
@MongoClientName("second")
MongoClient client;

@Test
void test() {
fail("Should not be run");
}

@ApplicationScoped
@MongoClientName("second")
public static class MyCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-app");
}
}

@ApplicationScoped
@MongoClientName("second")
public static class MySecondCustomizer implements MongoClientCustomizer {

@Override
public MongoClientSettings.Builder customize(MongoClientSettings.Builder builder) {
return builder.applicationName("my-second-app");
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import java.lang.annotation.Retention;
import java.lang.annotation.Target;

import jakarta.enterprise.util.AnnotationLiteral;
import jakarta.inject.Qualifier;

/**
Expand Down Expand Up @@ -38,4 +39,22 @@
* @return the value
*/
String value() default "";

class Literal extends AnnotationLiteral<MongoClientName> implements MongoClientName {

public static Literal of(String value) {
return new Literal(value);
}

private final String value;

public Literal(String value) {
this.value = value;
}

@Override
public String value() {
return value;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -205,4 +205,5 @@ public class MongoClientConfig {
*/
@ConfigItem
public Optional<UuidRepresentation> uuidRepresentation;

}
Loading

0 comments on commit 7dcbe67

Please sign in to comment.