Add SSL service connection support for Cassandra

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

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.
@ -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> 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<CqlSessionBuilderCustomizer> builderCustomizers, ObjectProvider<SslBundles> sslBundles) {
ObjectProvider<CqlSessionBuilderCustomizer> 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) {

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.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.
*

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.
@ -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();
}
}
}

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.
@ -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();
}
}
}

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.
@ -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

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.
@ -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