Add SSL service connection support for MongoDB

See gh-41137
This commit is contained in:
Moritz Halbritter 2025-02-11 10:20:32 +01:00
parent 0ccf1b81d8
commit 109fd6f97d
12 changed files with 167 additions and 55 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -18,6 +18,7 @@ package org.springframework.boot.autoconfigure.data.mongo;
import com.mongodb.client.MongoClient;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
@ -27,6 +28,7 @@ import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails;
import org.springframework.boot.autoconfigure.mongo.MongoProperties;
import org.springframework.boot.autoconfigure.mongo.PropertiesMongoConnectionDetails;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.data.mongodb.core.MongoTemplate;
@ -59,8 +61,9 @@ public class MongoDataAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MongoConnectionDetails.class)
PropertiesMongoConnectionDetails mongoConnectionDetails(MongoProperties properties) {
return new PropertiesMongoConnectionDetails(properties);
PropertiesMongoConnectionDetails mongoConnectionDetails(MongoProperties properties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesMongoConnectionDetails(properties, sslBundles.getIfAvailable());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -48,8 +48,9 @@ public class MongoAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MongoConnectionDetails.class)
PropertiesMongoConnectionDetails mongoConnectionDetails(MongoProperties properties) {
return new PropertiesMongoConnectionDetails(properties);
PropertiesMongoConnectionDetails mongoConnectionDetails(MongoProperties properties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesMongoConnectionDetails(properties, sslBundles.getIfAvailable());
}
@Bean
@ -70,9 +71,9 @@ public class MongoAutoConfiguration {
@Bean
StandardMongoClientSettingsBuilderCustomizer standardMongoSettingsCustomizer(MongoProperties properties,
MongoConnectionDetails connectionDetails, ObjectProvider<SslBundles> sslBundles) {
return new StandardMongoClientSettingsBuilderCustomizer(connectionDetails.getConnectionString(),
properties.getUuidRepresentation(), properties.getSsl(), sslBundles.getIfAvailable());
MongoConnectionDetails connectionDetails) {
return new StandardMongoClientSettingsBuilderCustomizer(connectionDetails,
properties.getUuidRepresentation());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.mongo;
import com.mongodb.ConnectionString;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
/**
* Details required to establish a connection to a MongoDB service.
@ -36,6 +37,15 @@ public interface MongoConnectionDetails extends ConnectionDetails {
*/
ConnectionString getConnectionString();
/**
* SSL bundle to use.
* @return the SSL bundle to use
* @since 3.5.0
*/
default SslBundle getSslBundle() {
return null;
}
/**
* GridFS configuration.
* @return the GridFS configuration or {@code null}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -53,8 +53,9 @@ public class MongoReactiveAutoConfiguration {
@Bean
@ConditionalOnMissingBean(MongoConnectionDetails.class)
PropertiesMongoConnectionDetails mongoConnectionDetails(MongoProperties properties) {
return new PropertiesMongoConnectionDetails(properties);
PropertiesMongoConnectionDetails mongoConnectionDetails(MongoProperties properties,
ObjectProvider<SslBundles> sslBundles) {
return new PropertiesMongoConnectionDetails(properties, sslBundles.getIfAvailable());
}
@Bean
@ -77,9 +78,9 @@ public class MongoReactiveAutoConfiguration {
@Bean
StandardMongoClientSettingsBuilderCustomizer standardMongoSettingsCustomizer(MongoProperties properties,
MongoConnectionDetails connectionDetails, ObjectProvider<SslBundles> sslBundles) {
return new StandardMongoClientSettingsBuilderCustomizer(connectionDetails.getConnectionString(),
properties.getUuidRepresentation(), properties.getSsl(), sslBundles.getIfAvailable());
MongoConnectionDetails connectionDetails) {
return new StandardMongoClientSettingsBuilderCustomizer(connectionDetails,
properties.getUuidRepresentation());
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,6 +23,10 @@ import java.util.List;
import com.mongodb.ConnectionString;
import org.springframework.boot.autoconfigure.mongo.MongoProperties.Ssl;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/**
@ -38,8 +42,11 @@ public class PropertiesMongoConnectionDetails implements MongoConnectionDetails
private final MongoProperties properties;
public PropertiesMongoConnectionDetails(MongoProperties properties) {
private final SslBundles sslBundles;
public PropertiesMongoConnectionDetails(MongoProperties properties, SslBundles sslBundles) {
this.properties = properties;
this.sslBundles = sslBundles;
}
@Override
@ -90,6 +97,19 @@ public class PropertiesMongoConnectionDetails implements MongoConnectionDetails
PropertiesMongoConnectionDetails.this.properties.getGridfs().getBucket());
}
@Override
public SslBundle getSslBundle() {
Ssl ssl = this.properties.getSsl();
if (!ssl.isEnabled()) {
return null;
}
if (StringUtils.hasLength(ssl.getBundle())) {
Assert.notNull(this.sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
return this.sslBundles.getBundle(ssl.getBundle());
}
return SslBundle.systemDefault();
}
private List<String> getOptions() {
List<String> options = new ArrayList<>();
if (StringUtils.hasText(this.properties.getReplicaSetName())) {

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -41,28 +41,57 @@ public class StandardMongoClientSettingsBuilderCustomizer implements MongoClient
private final UuidRepresentation uuidRepresentation;
private final MongoConnectionDetails connectionDetails;
private final MongoProperties.Ssl ssl;
private final SslBundles sslBundles;
private int order = 0;
/**
* Create a new instance.
* @param connectionString the connection string
* @param uuidRepresentation the uuid representation
* @param ssl the ssl properties
* @param sslBundles the ssl bundles
* @deprecated since 3.5.0 for removal in 3.7.0 in favor of
* {@link #StandardMongoClientSettingsBuilderCustomizer(MongoConnectionDetails, UuidRepresentation)}
*/
@Deprecated(forRemoval = true, since = "3.5.0")
public StandardMongoClientSettingsBuilderCustomizer(ConnectionString connectionString,
UuidRepresentation uuidRepresentation, MongoProperties.Ssl ssl, SslBundles sslBundles) {
this.connectionDetails = null;
this.connectionString = connectionString;
this.uuidRepresentation = uuidRepresentation;
this.ssl = ssl;
this.sslBundles = sslBundles;
}
public StandardMongoClientSettingsBuilderCustomizer(MongoConnectionDetails connectionDetails,
UuidRepresentation uuidRepresentation) {
this.connectionString = null;
this.ssl = null;
this.sslBundles = null;
this.connectionDetails = connectionDetails;
this.uuidRepresentation = uuidRepresentation;
}
@Override
public void customize(MongoClientSettings.Builder settingsBuilder) {
settingsBuilder.uuidRepresentation(this.uuidRepresentation);
if (this.connectionDetails != null) {
settingsBuilder.applyConnectionString(this.connectionDetails.getConnectionString());
settingsBuilder.applyToSslSettings(this::configureSslIfNeeded);
}
else {
settingsBuilder.uuidRepresentation(this.uuidRepresentation);
settingsBuilder.applyConnectionString(this.connectionString);
if (this.ssl.isEnabled()) {
settingsBuilder.applyToSslSettings(this::configureSsl);
}
}
}
private void configureSsl(SslSettings.Builder settings) {
settings.enabled(true);
@ -73,6 +102,15 @@ public class StandardMongoClientSettingsBuilderCustomizer implements MongoClient
}
}
private void configureSslIfNeeded(SslSettings.Builder settings) {
SslBundle sslBundle = this.connectionDetails.getSslBundle();
if (sslBundle != null) {
settings.enabled(true);
Assert.state(!sslBundle.getOptions().isSpecified(), "SSL options cannot be specified with MongoDB");
settings.context(sslBundle.createSslContext());
}
}
@Override
public int getOrder() {
return this.order;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -80,7 +80,7 @@ class MongoAutoConfigurationTests {
this.contextRunner.withPropertyValues("spring.data.mongodb.ssl.enabled=true").run((context) -> {
SslSettings sslSettings = getSettings(context).getSslSettings();
assertThat(sslSettings.isEnabled()).isTrue();
assertThat(sslSettings.getContext()).isNull();
assertThat(sslSettings.getContext()).isNotNull();
});
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -88,7 +88,7 @@ class MongoReactiveAutoConfigurationTests {
this.contextRunner.withPropertyValues("spring.data.mongodb.ssl.enabled=true").run((context) -> {
SslSettings sslSettings = getSettings(context).getSslSettings();
assertThat(sslSettings.isEnabled()).isTrue();
assertThat(sslSettings.getContext()).isNull();
assertThat(sslSettings.getContext()).isNotNull();
});
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -19,24 +19,41 @@ package org.springframework.boot.autoconfigure.mongo;
import java.util.List;
import com.mongodb.ConnectionString;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.ssl.DefaultSslBundleRegistry;
import org.springframework.boot.ssl.SslBundle;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link PropertiesMongoConnectionDetails}.
*
* @author Christoph Dreis
* @author Scott Frederick
* @author Moritz Halbritter
*/
class PropertiesMongoConnectionDetailsTests {
private final MongoProperties properties = new MongoProperties();
private MongoProperties properties;
private DefaultSslBundleRegistry sslBundleRegistry;
private PropertiesMongoConnectionDetails connectionDetails;
@BeforeEach
void setUp() {
this.properties = new MongoProperties();
this.sslBundleRegistry = new DefaultSslBundleRegistry();
this.connectionDetails = new PropertiesMongoConnectionDetails(this.properties, this.sslBundleRegistry);
}
@Test
void credentialsCanBeConfiguredWithUsername() {
this.properties.setUsername("user");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getUsername()).isEqualTo("user");
assertThat(connectionString.getPassword()).isEmpty();
assertThat(connectionString.getCredential().getUserName()).isEqualTo("user");
@ -48,7 +65,7 @@ class PropertiesMongoConnectionDetailsTests {
void credentialsCanBeConfiguredWithUsernameAndPassword() {
this.properties.setUsername("user");
this.properties.setPassword("secret".toCharArray());
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getUsername()).isEqualTo("user");
assertThat(connectionString.getPassword()).isEqualTo("secret".toCharArray());
assertThat(connectionString.getCredential().getUserName()).isEqualTo("user");
@ -59,13 +76,13 @@ class PropertiesMongoConnectionDetailsTests {
@Test
void databaseCanBeConfigured() {
this.properties.setDatabase("db");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getDatabase()).isEqualTo("db");
}
@Test
void databaseHasDefaultWhenNotConfigured() {
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getDatabase()).isEqualTo("test");
}
@ -74,7 +91,7 @@ class PropertiesMongoConnectionDetailsTests {
this.properties.setUsername("user");
this.properties.setDatabase("db");
this.properties.setAuthenticationDatabase("authdb");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getDatabase()).isEqualTo("db");
assertThat(connectionString.getCredential().getSource()).isEqualTo("authdb");
assertThat(connectionString.getCredential().getUserName()).isEqualTo("user");
@ -83,14 +100,14 @@ class PropertiesMongoConnectionDetailsTests {
@Test
void authenticationDatabaseIsNotConfiguredWhenUsernameIsNotConfigured() {
this.properties.setAuthenticationDatabase("authdb");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getCredential()).isNull();
}
@Test
void replicaSetCanBeConfigured() {
this.properties.setReplicaSetName("test");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getRequiredReplicaSetName()).isEqualTo("test");
}
@ -99,7 +116,7 @@ class PropertiesMongoConnectionDetailsTests {
this.properties.setUsername("user");
this.properties.setDatabase("db");
this.properties.setReplicaSetName("test");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getDatabase()).isEqualTo("db");
assertThat(connectionString.getRequiredReplicaSetName()).isEqualTo("test");
}
@ -107,14 +124,14 @@ class PropertiesMongoConnectionDetailsTests {
@Test
void replicaSetCanBeNull() {
this.properties.setReplicaSetName(null);
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getRequiredReplicaSetName()).isNull();
}
@Test
void replicaSetCanBeBlank() {
this.properties.setReplicaSetName("");
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getRequiredReplicaSetName()).isNull();
}
@ -122,18 +139,32 @@ class PropertiesMongoConnectionDetailsTests {
void whenAdditionalHostsAreConfiguredThenTheyAreIncludedInHostsOfConnectionString() {
this.properties.setHost("mongo1.example.com");
this.properties.setAdditionalHosts(List.of("mongo2.example.com", "mongo3.example.com"));
ConnectionString connectionString = getConnectionString();
ConnectionString connectionString = this.connectionDetails.getConnectionString();
assertThat(connectionString.getHosts()).containsExactly("mongo1.example.com", "mongo2.example.com",
"mongo3.example.com");
}
private PropertiesMongoConnectionDetails createConnectionDetails() {
return new PropertiesMongoConnectionDetails(this.properties);
@Test
void shouldReturnSslBundle() {
SslBundle bundle1 = mock(SslBundle.class);
this.sslBundleRegistry.registerBundle("bundle-1", bundle1);
this.properties.getSsl().setBundle("bundle-1");
SslBundle sslBundle = this.connectionDetails.getSslBundle();
assertThat(sslBundle).isSameAs(bundle1);
}
private ConnectionString getConnectionString() {
PropertiesMongoConnectionDetails connectionDetails = createConnectionDetails();
return connectionDetails.getConnectionString();
@Test
void shouldReturnSystemDefaultBundleIfSslIsEnabledButBundleNotSet() {
this.properties.getSsl().setEnabled(true);
SslBundle sslBundle = this.connectionDetails.getSslBundle();
assertThat(sslBundle).isNotNull();
}
@Test
void shouldReturnNullIfSslIsNotEnabled() {
this.properties.getSsl().setEnabled(false);
SslBundle sslBundle = this.connectionDetails.getSslBundle();
assertThat(sslBundle).isNull();
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2023 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -20,6 +20,7 @@ import com.mongodb.ConnectionString;
import org.testcontainers.containers.MongoDBContainer;
import org.springframework.boot.autoconfigure.mongo.MongoConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@ -59,6 +60,11 @@ class MongoContainerConnectionDetailsFactory
return new ConnectionString(getContainer().getReplicaSetUrl());
}
@Override
public SslBundle getSslBundle() {
return super.getSslBundle();
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -25,6 +25,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.testcontainers.service.connection.PemKeyStore;
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.data.mongodb.core.ReactiveMongoTemplate;
@ -37,14 +39,13 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Scott Frederick
*/
@Testcontainers(disabledWithoutDocker = true)
@SpringBootTest(properties = { "spring.data.mongodb.ssl.bundle=client",
"spring.ssl.bundle.pem.client.keystore.certificate=classpath:ssl/test-client.crt",
"spring.ssl.bundle.pem.client.keystore.private-key=classpath:ssl/test-client.key",
"spring.ssl.bundle.pem.client.truststore.certificate=classpath:ssl/test-ca.crt" })
@SpringBootTest
class SampleMongoApplicationReactiveSslTests {
@Container
@ServiceConnection
@PemKeyStore(certificate = "classpath:ssl/test-client.crt", privateKey = "classpath:ssl/test-client.key")
@PemTrustStore("classpath:ssl/test-ca.crt")
static final MongoDBContainer mongoDb = TestImage.container(SecureMongoContainer.class);
@Autowired

View File

@ -1,5 +1,5 @@
/*
* Copyright 2012-2024 the original author or authors.
* Copyright 2012-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -23,6 +23,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.mongo.DataMongoTest;
import org.springframework.boot.testcontainers.service.connection.PemKeyStore;
import org.springframework.boot.testcontainers.service.connection.PemTrustStore;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.data.mongodb.core.MongoTemplate;
@ -36,14 +38,13 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Eddú Meléndez
*/
@Testcontainers(disabledWithoutDocker = true)
@DataMongoTest(properties = { "spring.data.mongodb.ssl.bundle=client",
"spring.ssl.bundle.pem.client.keystore.certificate=classpath:ssl/test-client.crt",
"spring.ssl.bundle.pem.client.keystore.private-key=classpath:ssl/test-client.key",
"spring.ssl.bundle.pem.client.truststore.certificate=classpath:ssl/test-ca.crt" })
@DataMongoTest
class SampleMongoApplicationSslTests {
@Container
@ServiceConnection
@PemKeyStore(certificate = "classpath:ssl/test-client.crt", privateKey = "classpath:ssl/test-client.key")
@PemTrustStore("classpath:ssl/test-ca.crt")
static final MongoDBContainer mongoDb = TestImage.container(SecureMongoContainer.class);
@Autowired