Add SSL bundle support to Cassandra auto-configuration

Update Cassandra auto-configuration so that an SSL can be configured
via an SSL bundle.

Closes gh-25602
pull/35107/head
Scott Frederick 2 years ago committed by Phillip Webb
parent 909c09c8ab
commit 682457377a

@ -32,6 +32,7 @@ import com.datastax.oss.driver.api.core.config.DefaultDriverOption;
import com.datastax.oss.driver.api.core.config.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.DriverOption;
import com.datastax.oss.driver.api.core.config.ProgrammaticDriverConfigLoaderBuilder;
import com.datastax.oss.driver.api.core.ssl.ProgrammaticSslEngineFactory;
import com.datastax.oss.driver.internal.core.config.typesafe.DefaultDriverConfigLoader;
import com.datastax.oss.driver.internal.core.config.typesafe.DefaultProgrammaticDriverConfigLoaderBuilder;
import com.typesafe.config.Config;
@ -43,16 +44,22 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Connection;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Controlconnection;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Request;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Ssl;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Throttler;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.ThrottlerType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.boot.ssl.SslOptions;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope;
import org.springframework.core.io.Resource;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
* {@link EnableAutoConfiguration Auto-configuration} for Cassandra.
@ -66,6 +73,7 @@ import org.springframework.core.io.Resource;
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @since 1.3.0
*/
@AutoConfiguration
@ -106,10 +114,10 @@ public class CassandraAutoConfiguration {
@Scope("prototype")
public CqlSessionBuilder cassandraSessionBuilder(DriverConfigLoader driverConfigLoader,
CassandraConnectionDetails connectionDetails,
ObjectProvider<CqlSessionBuilderCustomizer> builderCustomizers) {
ObjectProvider<CqlSessionBuilderCustomizer> builderCustomizers, ObjectProvider<SslBundles> sslBundles) {
CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader);
configureAuthentication(builder, connectionDetails);
configureSsl(builder, connectionDetails);
configureSsl(builder, connectionDetails, sslBundles.getIfAvailable());
builder.withKeyspace(this.properties.getKeyspaceName());
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder;
@ -122,15 +130,38 @@ public class CassandraAutoConfiguration {
}
}
private void configureSsl(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails) {
if (connectionDetails instanceof PropertiesCassandraConnectionDetails && this.properties.isSsl()) {
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, CassandraConnectionDetails connectionDetails,
SslBundles sslBundles) {
if (!(connectionDetails instanceof PropertiesCassandraConnectionDetails)) {
return;
}
Ssl properties = this.properties.getSsl();
if (properties == null || !properties.isEnabled()) {
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();
String[] ciphers = (!CollectionUtils.isEmpty(options.getCiphers()) ? null
: options.getCiphers().toArray(String[]::new));
builder.withSslEngineFactory(new ProgrammaticSslEngineFactory(sslBundle.createSslContext(), ciphers));
}
@Bean(destroyMethod = "")

@ -1,5 +1,5 @@
/*
* Copyright 2012-2022 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.
@ -31,6 +31,7 @@ import org.springframework.core.io.Resource;
* @author Phillip Webb
* @author Mark Paluch
* @author Stephane Nicoll
* @author Scott Frederick
* @since 1.3.0
*/
@ConfigurationProperties(prefix = "spring.cassandra")
@ -89,9 +90,9 @@ public class CassandraProperties {
private String schemaAction = "none";
/**
* Enable SSL support.
* SSL configuration.
*/
private boolean ssl = false;
private Ssl ssl = new Ssl();
/**
* Connection configuration.
@ -185,11 +186,11 @@ public class CassandraProperties {
this.compression = compression;
}
public boolean isSsl() {
public Ssl getSsl() {
return this.ssl;
}
public void setSsl(boolean ssl) {
public void setSsl(Ssl ssl) {
this.ssl = ssl;
}
@ -217,6 +218,36 @@ public class CassandraProperties {
return this.controlconnection;
}
public static class Ssl {
/**
* Whether to enable SSL support.
*/
private Boolean enabled;
/**
* SSL bundle name.
*/
private String bundle;
public boolean isEnabled() {
return (this.enabled != null) ? this.enabled : this.bundle != null;
}
public void setEnabled(boolean enabled) {
this.enabled = enabled;
}
public String getBundle() {
return this.bundle;
}
public void setBundle(String bundle) {
this.bundle = bundle;
}
}
public static class Connection {
/**

@ -34,11 +34,14 @@ import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration.PropertiesCassandraConnectionDetails;
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;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
/**
@ -50,11 +53,12 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
class CassandraAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void cqlSessionBuildHasScopePrototype() {
@ -67,6 +71,53 @@ class CassandraAutoConfigurationTests {
});
}
@Test
void cqlSessionBuilderWithNoSslConfiguration() {
this.contextRunner.run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", false);
});
}
@Test
void cqlSessionBuilderWithSslEnabled() {
this.contextRunner.withPropertyValues("spring.cassandra.ssl.enabled=true").run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", true);
});
}
@Test
void cqlSessionBuilderWithSslBundle() {
this.contextRunner
.withPropertyValues("spring.cassandra.ssl.bundle=test-bundle",
"spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks",
"spring.ssl.bundle.jks.test-bundle.keystore.password=secret",
"spring.ssl.bundle.jks.test-bundle.key.password=password")
.run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", true);
});
}
@Test
void cqlSessionBuilderWithSslBundleAndSslDisabled() {
this.contextRunner
.withPropertyValues("spring.cassandra.ssl.enabled=false", "spring.cassandra.ssl.bundle=test-bundle")
.run((context) -> {
CqlSessionBuilder builder = context.getBean(CqlSessionBuilder.class);
assertThat(builder).hasFieldOrPropertyWithValue("programmaticSslFactory", false);
});
}
@Test
void cqlSessionBuilderWithInvalidSslBundle() {
this.contextRunner.withPropertyValues("spring.cassandra.ssl.bundle=test-bundle")
.run((context) -> assertThatException().isThrownBy(() -> context.getBean(CqlSessionBuilder.class))
.withRootCauseInstanceOf(NoSuchSslBundleException.class)
.withMessageContaining("test-bundle"));
}
@Test
void driverConfigLoaderWithDefaultConfiguration() {
this.contextRunner.run((context) -> {

@ -374,6 +374,33 @@ If the port is the same for all your contact points you can use a shortcut and o
TIP: Those two examples are identical as the port default to `9042`.
If you need to configure the port, use `spring.cassandra.port`.
The auto-configured `CqlSession` can be configured to use SSL for communication with the server by setting the properties as shown in this example:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
----
spring:
cassandra:
keyspace-name: "mykeyspace"
contact-points: "cassandrahost1,cassandrahost2"
local-datacenter: "datacenter1"
ssl:
enabled: true
----
Custom SSL trust material can be configured in an <<features#features.ssl,SSL bundle>> and applied to the `CqlSession` as shown in this example:
[source,yaml,indent=0,subs="verbatim",configprops,configblocks]
----
spring:
cassandra:
keyspace-name: "mykeyspace"
contact-points: "cassandrahost1,cassandrahost2"
local-datacenter: "datacenter1"
ssl:
bundle: "example"
----
[NOTE]
====
The Cassandra driver has its own configuration infrastructure that loads an `application.conf` at the root of the classpath.

Loading…
Cancel
Save