Add SSL service connection support for Couchbase

See gh-41137
This commit is contained in:
Moritz Halbritter 2025-02-11 10:19:57 +01:00
parent 9c520d6af7
commit 0ccf1b81d8
6 changed files with 62 additions and 34 deletions

View File

@ -93,15 +93,16 @@ public class CouchbaseAutoConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(CouchbaseConnectionDetails.class) @ConditionalOnMissingBean(CouchbaseConnectionDetails.class)
PropertiesCouchbaseConnectionDetails couchbaseConnectionDetails() { PropertiesCouchbaseConnectionDetails couchbaseConnectionDetails(ObjectProvider<SslBundles> sslBundles) {
return new PropertiesCouchbaseConnectionDetails(this.properties); return new PropertiesCouchbaseConnectionDetails(this.properties, sslBundles.getIfAvailable());
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public ClusterEnvironment couchbaseClusterEnvironment( public ClusterEnvironment couchbaseClusterEnvironment(
ObjectProvider<ClusterEnvironmentBuilderCustomizer> customizers, ObjectProvider<SslBundles> sslBundles) { ObjectProvider<ClusterEnvironmentBuilderCustomizer> customizers,
Builder builder = initializeEnvironmentBuilder(sslBundles.getIfAvailable()); CouchbaseConnectionDetails connectionDetails) {
Builder builder = initializeEnvironmentBuilder(connectionDetails);
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder.build(); return builder.build();
} }
@ -143,7 +144,7 @@ public class CouchbaseAutoConfiguration {
return Cluster.connect(connectionDetails.getConnectionString(), options); return Cluster.connect(connectionDetails.getConnectionString(), options);
} }
private ClusterEnvironment.Builder initializeEnvironmentBuilder(SslBundles sslBundles) { private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails) {
ClusterEnvironment.Builder builder = ClusterEnvironment.builder(); ClusterEnvironment.Builder builder = ClusterEnvironment.builder();
Timeouts timeouts = this.properties.getEnv().getTimeouts(); Timeouts timeouts = this.properties.getEnv().getTimeouts();
builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue()) builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue())
@ -159,31 +160,24 @@ public class CouchbaseAutoConfiguration {
builder.ioConfig((config) -> config.maxHttpConnections(io.getMaxEndpoints()) builder.ioConfig((config) -> config.maxHttpConnections(io.getMaxEndpoints())
.numKvConnections(io.getMinEndpoints()) .numKvConnections(io.getMinEndpoints())
.idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout())); .idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout()));
if (this.properties.getEnv().getSsl().getEnabled()) { SslBundle sslBundle = connectionDetails.getSslBundle();
configureSsl(builder, sslBundles); if (sslBundle != null) {
configureSsl(builder, sslBundle);
} }
return builder; return builder;
} }
private void configureSsl(Builder builder, SslBundles sslBundles) { private void configureSsl(Builder builder, SslBundle sslBundle) {
Ssl sslProperties = this.properties.getEnv().getSsl(); Assert.state(!sslBundle.getOptions().isSpecified(), "SSL Options cannot be specified with Couchbase");
SslBundle sslBundle = (StringUtils.hasText(sslProperties.getBundle()))
? sslBundles.getBundle(sslProperties.getBundle()) : null;
Assert.state(sslBundle == null || !sslBundle.getOptions().isSpecified(),
"SSL Options cannot be specified with Couchbase");
builder.securityConfig((config) -> { builder.securityConfig((config) -> {
config.enableTls(true); config.enableTls(true);
TrustManagerFactory trustManagerFactory = getTrustManagerFactory(sslBundle); TrustManagerFactory trustManagerFactory = sslBundle.getManagers().getTrustManagerFactory();
if (trustManagerFactory != null) { if (trustManagerFactory != null) {
config.trustManagerFactory(trustManagerFactory); config.trustManagerFactory(trustManagerFactory);
} }
}); });
} }
private TrustManagerFactory getTrustManagerFactory(SslBundle sslBundle) {
return (sslBundle != null) ? sslBundle.getManagers().getTrustManagerFactory() : null;
}
@Configuration(proxyBeanMethods = false) @Configuration(proxyBeanMethods = false)
@ConditionalOnClass(ObjectMapper.class) @ConditionalOnClass(ObjectMapper.class)
static class JacksonConfiguration { static class JacksonConfiguration {
@ -247,8 +241,11 @@ public class CouchbaseAutoConfiguration {
private final CouchbaseProperties properties; private final CouchbaseProperties properties;
PropertiesCouchbaseConnectionDetails(CouchbaseProperties properties) { private final SslBundles sslBundles;
PropertiesCouchbaseConnectionDetails(CouchbaseProperties properties, SslBundles sslBundles) {
this.properties = properties; this.properties = properties;
this.sslBundles = sslBundles;
} }
@Override @Override
@ -266,6 +263,19 @@ public class CouchbaseAutoConfiguration {
return this.properties.getPassword(); return this.properties.getPassword();
} }
@Override
public SslBundle getSslBundle() {
Ssl ssl = this.properties.getEnv().getSsl();
if (!ssl.getEnabled()) {
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();
}
} }
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -17,6 +17,7 @@
package org.springframework.boot.autoconfigure.couchbase; package org.springframework.boot.autoconfigure.couchbase;
import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
/** /**
* Details required to establish a connection to a Couchbase service. * Details required to establish a connection to a Couchbase service.
@ -46,4 +47,13 @@ public interface CouchbaseConnectionDetails extends ConnectionDetails {
*/ */
String getPassword(); String getPassword();
/**
* SSL bundle to use.
* @return the SSL bundle to use
* @since 3.5.0
*/
default SslBundle getSslBundle() {
return 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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -196,7 +196,7 @@ class CouchbaseAutoConfigurationTests {
testClusterEnvironment((env) -> { testClusterEnvironment((env) -> {
SecurityConfig securityConfig = env.securityConfig(); SecurityConfig securityConfig = env.securityConfig();
assertThat(securityConfig.tlsEnabled()).isTrue(); assertThat(securityConfig.tlsEnabled()).isTrue();
assertThat(securityConfig.trustManagerFactory()).isNull(); assertThat(securityConfig.trustManagerFactory()).isNotNull();
}, "spring.couchbase.env.ssl.enabled=true"); }, "spring.couchbase.env.ssl.enabled=true");
} }

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -19,6 +19,7 @@ package org.springframework.boot.testcontainers.service.connection.couchbase;
import org.testcontainers.couchbase.CouchbaseContainer; import org.testcontainers.couchbase.CouchbaseContainer;
import org.springframework.boot.autoconfigure.couchbase.CouchbaseConnectionDetails; import org.springframework.boot.autoconfigure.couchbase.CouchbaseConnectionDetails;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory;
import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
@ -66,6 +67,11 @@ class CouchbaseContainerConnectionDetailsFactory
return getContainer().getConnectionString(); return getContainer().getConnectionString();
} }
@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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -26,6 +26,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; 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.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage; import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate; import org.springframework.data.couchbase.core.ReactiveCouchbaseTemplate;
@ -38,16 +40,15 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Scott Frederick * @author Scott Frederick
*/ */
@Testcontainers(disabledWithoutDocker = true) @Testcontainers(disabledWithoutDocker = true)
@SpringBootTest(properties = { "spring.couchbase.env.ssl.bundle=client", "spring.data.couchbase.bucket-name=cbbucket", @SpringBootTest(properties = { "spring.data.couchbase.bucket-name=cbbucket" })
"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" })
class SampleCouchbaseApplicationReactiveSslTests { class SampleCouchbaseApplicationReactiveSslTests {
private static final String BUCKET_NAME = "cbbucket"; private static final String BUCKET_NAME = "cbbucket";
@Container @Container
@ServiceConnection @ServiceConnection
@PemKeyStore(certificate = "classpath:ssl/test-client.crt", privateKey = "classpath:ssl/test-client.key")
@PemTrustStore(certificate = "classpath:ssl/test-ca.crt")
static final CouchbaseContainer couchbase = TestImage.container(SecureCouchbaseContainer.class) static final CouchbaseContainer couchbase = TestImage.container(SecureCouchbaseContainer.class)
.withBucket(new BucketDefinition(BUCKET_NAME)); .withBucket(new BucketDefinition(BUCKET_NAME));

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"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
@ -24,6 +24,8 @@ import org.testcontainers.junit.jupiter.Testcontainers;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.data.couchbase.DataCouchbaseTest; import org.springframework.boot.test.autoconfigure.data.couchbase.DataCouchbaseTest;
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.testcontainers.service.connection.ServiceConnection;
import org.springframework.boot.testsupport.container.TestImage; import org.springframework.boot.testsupport.container.TestImage;
import org.springframework.data.couchbase.core.CouchbaseTemplate; import org.springframework.data.couchbase.core.CouchbaseTemplate;
@ -36,17 +38,16 @@ import static org.assertj.core.api.Assertions.assertThat;
* @author Scott Frederick * @author Scott Frederick
*/ */
@Testcontainers(disabledWithoutDocker = true) @Testcontainers(disabledWithoutDocker = true)
@DataCouchbaseTest(properties = { "spring.couchbase.env.ssl.bundle=client", "spring.couchbase.env.timeouts.connect=2m", @DataCouchbaseTest(
"spring.data.couchbase.bucket-name=cbbucket", properties = { "spring.couchbase.env.timeouts.connect=2m", "spring.data.couchbase.bucket-name=cbbucket" })
"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" })
class SampleCouchbaseApplicationSslTests { class SampleCouchbaseApplicationSslTests {
private static final String BUCKET_NAME = "cbbucket"; private static final String BUCKET_NAME = "cbbucket";
@Container @Container
@ServiceConnection @ServiceConnection
@PemKeyStore(certificate = "classpath:ssl/test-client.crt", privateKey = "classpath:ssl/test-client.key")
@PemTrustStore(certificate = "classpath:ssl/test-ca.crt")
static final CouchbaseContainer couchbase = TestImage.container(SecureCouchbaseContainer.class) static final CouchbaseContainer couchbase = TestImage.container(SecureCouchbaseContainer.class)
.withBucket(new BucketDefinition(BUCKET_NAME)); .withBucket(new BucketDefinition(BUCKET_NAME));