diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java index a609e07460..28d1a2e31f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfiguration.java @@ -44,6 +44,8 @@ import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfigurati import org.springframework.boot.autoconfigure.couchbase.CouchbaseProperties.Timeouts; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.ssl.SslBundle; +import org.springframework.boot.ssl.SslBundles; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; @@ -69,8 +71,7 @@ public class CouchbaseAutoConfiguration { private final CouchbaseProperties properties; - CouchbaseAutoConfiguration(CouchbaseProperties properties, - ObjectProvider connectionDetails) { + CouchbaseAutoConfiguration(CouchbaseProperties properties) { this.properties = properties; } @@ -83,8 +84,8 @@ public class CouchbaseAutoConfiguration { @Bean @ConditionalOnMissingBean public ClusterEnvironment couchbaseClusterEnvironment(CouchbaseConnectionDetails connectionDetails, - ObjectProvider customizers) { - Builder builder = initializeEnvironmentBuilder(connectionDetails); + ObjectProvider customizers, ObjectProvider sslBundles) { + Builder builder = initializeEnvironmentBuilder(connectionDetails, sslBundles.getIfAvailable()); customizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder.build(); } @@ -99,7 +100,8 @@ public class CouchbaseAutoConfiguration { return Cluster.connect(connectionDetails.getConnectionString(), options); } - private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails) { + private ClusterEnvironment.Builder initializeEnvironmentBuilder(CouchbaseConnectionDetails connectionDetails, + SslBundles sslBundles) { ClusterEnvironment.Builder builder = ClusterEnvironment.builder(); Timeouts timeouts = this.properties.getEnv().getTimeouts(); builder.timeoutConfig((config) -> config.kvTimeout(timeouts.getKeyValue()) @@ -117,13 +119,28 @@ public class CouchbaseAutoConfiguration { .idleHttpConnectionTimeout(io.getIdleHttpConnectionTimeout())); if ((connectionDetails instanceof PropertiesCouchbaseConnectionDetails) && this.properties.getEnv().getSsl().getEnabled()) { - builder.securityConfig((config) -> config.enableTls(true) - .trustManagerFactory(getTrustManagerFactory(this.properties.getEnv().getSsl()))); + configureSsl(builder, sslBundles); } return builder; } - private TrustManagerFactory getTrustManagerFactory(CouchbaseProperties.Ssl ssl) { + private void configureSsl(Builder builder, SslBundles sslBundles) { + builder.securityConfig((config) -> config.enableTls(true) + .trustManagerFactory(getTrustManagerFactory(this.properties.getEnv().getSsl(), sslBundles))); + } + + private TrustManagerFactory getTrustManagerFactory(CouchbaseProperties.Ssl ssl, SslBundles sslBundles) { + if (ssl.getKeyStore() != null) { + return loadTrustManagerFactory(ssl); + } + if (ssl.getBundle() != null) { + SslBundle bundle = sslBundles.getBundle(ssl.getBundle()); + return bundle.getManagers().getTrustManagerFactory(); + } + throw new IllegalStateException("A key store or bundle must be configured when SSL is enabled"); + } + + private TrustManagerFactory loadTrustManagerFactory(CouchbaseProperties.Ssl ssl) { String resource = ssl.getKeyStore(); try { TrustManagerFactory trustManagerFactory = TrustManagerFactory diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java index 4dc0e252ff..c8d9b3ae36 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2020 the original author or authors. + * Copyright 2012-2023 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. @@ -148,8 +148,8 @@ public class CouchbaseProperties { public static class Ssl { /** - * Whether to enable SSL support. Enabled automatically if a "keyStore" is - * provided unless specified otherwise. + * Whether to enable SSL support. Enabled automatically if a "keyStore" or + * "bundle" is provided unless specified otherwise. */ private Boolean enabled; @@ -163,8 +163,14 @@ public class CouchbaseProperties { */ private String keyStorePassword; + /** + * SSL bundle name. + */ + private String bundle; + public Boolean getEnabled() { - return (this.enabled != null) ? this.enabled : StringUtils.hasText(this.keyStore); + return (this.enabled != null) ? this.enabled + : StringUtils.hasText(this.keyStore) || StringUtils.hasText(this.bundle); } public void setEnabled(Boolean enabled) { @@ -187,6 +193,14 @@ public class CouchbaseProperties { this.keyStorePassword = keyStorePassword; } + public String getBundle() { + return this.bundle; + } + + public void setBundle(String bundle) { + this.bundle = bundle; + } + } public static class Timeouts { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java index 2381016fc7..c6db5efff8 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/couchbase/CouchbaseAutoConfigurationTests.java @@ -36,6 +36,8 @@ import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.couchbase.CouchbaseAutoConfiguration.PropertiesCouchbaseConnectionDetails; import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration; +import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration; +import org.springframework.boot.ssl.NoSuchSslBundleException; import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -56,7 +58,7 @@ import static org.mockito.Mockito.mock; class CouchbaseAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() - .withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class)); + .withConfiguration(AutoConfigurations.of(CouchbaseAutoConfiguration.class, SslAutoConfiguration.class)); @Test void connectionStringIsRequired() { @@ -179,7 +181,7 @@ class CouchbaseAutoConfigurationTests { } @Test - void enableSslNoEnabledFlag() { + void enableSslWithKeyStore() { testClusterEnvironment((env) -> { SecurityConfig securityConfig = env.securityConfig(); assertThat(securityConfig.tlsEnabled()).isTrue(); @@ -187,6 +189,30 @@ class CouchbaseAutoConfigurationTests { }, "spring.couchbase.env.ssl.keyStore=classpath:test.jks", "spring.couchbase.env.ssl.keyStorePassword=secret"); } + @Test + void enableSslWithBundle() { + testClusterEnvironment((env) -> { + SecurityConfig securityConfig = env.securityConfig(); + assertThat(securityConfig.tlsEnabled()).isTrue(); + assertThat(securityConfig.trustManagerFactory()).isNotNull(); + }, "spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks", + "spring.ssl.bundle.jks.test-bundle.keystore.password=secret", + "spring.couchbase.env.ssl.bundle=test-bundle"); + } + + @Test + void enableSslWithInvalidBundle() { + this.contextRunner + .withPropertyValues("spring.couchbase.connection-string=localhost", + "spring.couchbase.env.ssl.bundle=test-bundle") + .run((context) -> { + assertThat(context).hasFailed(); + assertThat(context.getStartupFailure()).rootCause() + .isInstanceOf(NoSuchSslBundleException.class) + .hasMessageContaining("test-bundle"); + }); + } + @Test void disableSslEvenWithKeyStore() { testClusterEnvironment((env) -> { @@ -197,6 +223,15 @@ class CouchbaseAutoConfigurationTests { "spring.couchbase.env.ssl.keyStorePassword=secret"); } + @Test + void disableSslEvenWithBundle() { + testClusterEnvironment((env) -> { + SecurityConfig securityConfig = env.securityConfig(); + assertThat(securityConfig.tlsEnabled()).isFalse(); + assertThat(securityConfig.trustManagerFactory()).isNull(); + }, "spring.couchbase.env.ssl.enabled=false", "spring.couchbase.env.ssl.bundle=test-bundle"); + } + private void testClusterEnvironment(Consumer environmentConsumer, String... environment) { this.contextRunner.withUserConfiguration(CouchbaseTestConfiguration.class) .withPropertyValues("spring.couchbase.connection-string=localhost") diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index c83616b5c4..668248fd42 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -455,7 +455,7 @@ Generally, you provide the https://github.com/couchbaselabs/sdk-rfcs/blob/master ---- It is also possible to customize some of the `ClusterEnvironment` settings. -For instance, the following configuration changes the timeout to open a new `Bucket` and enables SSL support: +For instance, the following configuration changes the timeout to open a new `Bucket` and enables SSL support with a reference to a configured <>: [source,yaml,indent=0,subs="verbatim",configprops,configblocks] ---- @@ -465,8 +465,7 @@ For instance, the following configuration changes the timeout to open a new `Buc timeouts: connect: "3s" ssl: - key-store: "/location/of/keystore.jks" - key-store-password: "secret" + bundle: "example" ---- TIP: Check the `spring.couchbase.env.*` properties for more details.