diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java index 7d0f78be8c..a666296324 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfiguration.java @@ -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. @@ -17,15 +17,12 @@ package org.springframework.boot.autoconfigure.cassandra; import java.io.IOException; -import java.security.NoSuchAlgorithmException; import java.time.Duration; import java.util.Collections; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import javax.net.ssl.SSLContext; - import com.datastax.oss.driver.api.core.CqlSession; import com.datastax.oss.driver.api.core.CqlSessionBuilder; import com.datastax.oss.driver.api.core.config.DefaultDriverOption; @@ -99,8 +96,8 @@ public class CassandraAutoConfiguration { @Bean @ConditionalOnMissingBean(CassandraConnectionDetails.class) - PropertiesCassandraConnectionDetails cassandraConnectionDetails() { - return new PropertiesCassandraConnectionDetails(this.properties); + PropertiesCassandraConnectionDetails cassandraConnectionDetails(ObjectProvider sslBundles) { + return new PropertiesCassandraConnectionDetails(this.properties, sslBundles.getIfAvailable()); } @Bean @@ -115,10 +112,10 @@ public class CassandraAutoConfiguration { @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) public CqlSessionBuilder cassandraSessionBuilder(DriverConfigLoader driverConfigLoader, CassandraConnectionDetails connectionDetails, - ObjectProvider builderCustomizers, ObjectProvider sslBundles) { + ObjectProvider builderCustomizers) { CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader); configureAuthentication(builder, connectionDetails); - configureSsl(builder, sslBundles.getIfAvailable()); + configureSsl(builder, connectionDetails); builder.withKeyspace(this.properties.getKeyspaceName()); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); return builder; @@ -131,30 +128,11 @@ public class CassandraAutoConfiguration { } } - private void configureSsl(CqlSessionBuilder builder, SslBundles sslBundles) { - Ssl properties = this.properties.getSsl(); - if (properties == null || !properties.isEnabled()) { + private void configureSsl(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails) { + SslBundle sslBundle = connectionDetails.getSslBundle(); + if (sslBundle == null) { return; } - String bundleName = properties.getBundle(); - if (!StringUtils.hasLength(bundleName)) { - configureDefaultSslContext(builder); - } - else { - configureSsl(builder, sslBundles.getBundle(bundleName)); - } - } - - private void configureDefaultSslContext(CqlSessionBuilder builder) { - try { - builder.withSslContext(SSLContext.getDefault()); - } - catch (NoSuchAlgorithmException ex) { - throw new IllegalStateException("Could not setup SSL default context for Cassandra", ex); - } - } - - private void configureSsl(CqlSessionBuilder builder, SslBundle sslBundle) { SslOptions options = sslBundle.getOptions(); Assert.state(options.getEnabledProtocols() == null, "SSL protocol options cannot be specified with Cassandra"); builder @@ -320,8 +298,11 @@ public class CassandraAutoConfiguration { private final CassandraProperties properties; - private PropertiesCassandraConnectionDetails(CassandraProperties properties) { + private final SslBundles sslBundles; + + private PropertiesCassandraConnectionDetails(CassandraProperties properties, SslBundles sslBundles) { this.properties = properties; + this.sslBundles = sslBundles; } @Override @@ -346,6 +327,19 @@ public class CassandraAutoConfiguration { return this.properties.getLocalDatacenter(); } + @Override + public SslBundle getSslBundle() { + Ssl ssl = this.properties.getSsl(); + if (ssl == null || !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 Node asNode(String contactPoint) { int i = contactPoint.lastIndexOf(':'); if (i >= 0) { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java index b02972269a..e98961f6ef 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraConnectionDetails.java @@ -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.cassandra; import java.util.List; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; /** * Details required to establish a connection to a Cassandra service. @@ -59,6 +60,15 @@ public interface CassandraConnectionDetails extends ConnectionDetails { */ String getLocalDatacenter(); + /** + * SSL bundle to use. + * @return the SSL bundle to use + * @since 3.5.0 + */ + default SslBundle getSslBundle() { + return null; + } + /** * A Cassandra node. * diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/CassandraContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/CassandraContainerConnectionDetailsFactory.java index e8b82096ff..f23cf7e3d5 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/CassandraContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/CassandraContainerConnectionDetailsFactory.java @@ -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. @@ -22,6 +22,7 @@ import java.util.List; import org.testcontainers.cassandra.CassandraContainer; import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails; +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; @@ -75,6 +76,11 @@ class CassandraContainerConnectionDetailsFactory return getContainer().getLocalDatacenter(); } + @Override + public SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/DeprecatedCassandraContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/DeprecatedCassandraContainerConnectionDetailsFactory.java index 2ed24ac444..c9932b18ad 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/DeprecatedCassandraContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/cassandra/DeprecatedCassandraContainerConnectionDetailsFactory.java @@ -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. @@ -22,6 +22,7 @@ import java.util.List; import org.testcontainers.containers.CassandraContainer; import org.springframework.boot.autoconfigure.cassandra.CassandraConnectionDetails; +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; @@ -78,6 +79,11 @@ class DeprecatedCassandraContainerConnectionDetailsFactory return getContainer().getLocalDatacenter(); } + @Override + public SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java index da7694d780..dbc5049c8d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationReactiveSslTests.java @@ -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. @@ -28,6 +28,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.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.JksKeyStore; +import org.springframework.boot.testcontainers.service.connection.JksTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -43,15 +45,13 @@ import static org.assertj.core.api.Assertions.assertThat; @Testcontainers(disabledWithoutDocker = true) @SpringBootTest(properties = { "spring.cassandra.schema-action=create-if-not-exists", "spring.cassandra.connection.connect-timeout=60s", "spring.cassandra.connection.init-query-timeout=60s", - "spring.cassandra.request.timeout=60s", "spring.cassandra.ssl.bundle=client", - "spring.ssl.bundle.jks.client.keystore.location=classpath:ssl/test-client.p12", - "spring.ssl.bundle.jks.client.keystore.password=password", - "spring.ssl.bundle.jks.client.truststore.location=classpath:ssl/test-ca.p12", - "spring.ssl.bundle.jks.client.truststore.password=password" }) + "spring.cassandra.request.timeout=60s" }) class SampleCassandraApplicationReactiveSslTests { @Container @ServiceConnection + @JksTrustStore(location = "classpath:ssl/test-ca.p12", password = "password") + @JksKeyStore(location = "classpath:ssl/test-client.p12", password = "password") static final SecureCassandraContainer cassandra = TestImage.container(SecureCassandraContainer.class); @Autowired diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java index e930fb4623..9303c6f551 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-data-cassandra/src/dockerTest/java/smoketest/data/cassandra/SampleCassandraApplicationSslTests.java @@ -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. @@ -27,6 +27,8 @@ import org.testcontainers.junit.jupiter.Testcontainers; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.data.cassandra.DataCassandraTest; import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.boot.testcontainers.service.connection.JksKeyStore; +import org.springframework.boot.testcontainers.service.connection.JksTrustStore; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; import org.springframework.context.annotation.Bean; @@ -43,15 +45,13 @@ import static org.assertj.core.api.Assertions.assertThat; @Testcontainers(disabledWithoutDocker = true) @DataCassandraTest(properties = { "spring.cassandra.schema-action=create-if-not-exists", "spring.cassandra.connection.connect-timeout=60s", "spring.cassandra.connection.init-query-timeout=60s", - "spring.cassandra.request.timeout=60s", "spring.cassandra.ssl.bundle=client", - "spring.ssl.bundle.jks.client.keystore.location=classpath:ssl/test-client.p12", - "spring.ssl.bundle.jks.client.keystore.password=password", - "spring.ssl.bundle.jks.client.truststore.location=classpath:ssl/test-ca.p12", - "spring.ssl.bundle.jks.client.truststore.password=password" }) + "spring.cassandra.request.timeout=60s" }) class SampleCassandraApplicationSslTests { @Container @ServiceConnection + @JksTrustStore(location = "classpath:ssl/test-ca.p12", password = "password") + @JksKeyStore(location = "classpath:ssl/test-client.p12", password = "password") static final SecureCassandraContainer cassandra = TestImage.container(SecureCassandraContainer.class); @Autowired