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.DriverConfigLoader;
import com.datastax.oss.driver.api.core.config.DriverOption; 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.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.DefaultDriverConfigLoader;
import com.datastax.oss.driver.internal.core.config.typesafe.DefaultProgrammaticDriverConfigLoaderBuilder; import com.datastax.oss.driver.internal.core.config.typesafe.DefaultProgrammaticDriverConfigLoaderBuilder;
import com.typesafe.config.Config; 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.Connection;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Controlconnection; import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Controlconnection;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.Request; 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.Throttler;
import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.ThrottlerType; import org.springframework.boot.autoconfigure.cassandra.CassandraProperties.ThrottlerType;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.context.properties.PropertyMapper; 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.Bean;
import org.springframework.context.annotation.Lazy; import org.springframework.context.annotation.Lazy;
import org.springframework.context.annotation.Scope; import org.springframework.context.annotation.Scope;
import org.springframework.core.io.Resource; import org.springframework.core.io.Resource;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/** /**
* {@link EnableAutoConfiguration Auto-configuration} for Cassandra. * {@link EnableAutoConfiguration Auto-configuration} for Cassandra.
@ -66,6 +73,7 @@ import org.springframework.core.io.Resource;
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
* @since 1.3.0 * @since 1.3.0
*/ */
@AutoConfiguration @AutoConfiguration
@ -106,10 +114,10 @@ public class CassandraAutoConfiguration {
@Scope("prototype") @Scope("prototype")
public CqlSessionBuilder cassandraSessionBuilder(DriverConfigLoader driverConfigLoader, public CqlSessionBuilder cassandraSessionBuilder(DriverConfigLoader driverConfigLoader,
CassandraConnectionDetails connectionDetails, CassandraConnectionDetails connectionDetails,
ObjectProvider<CqlSessionBuilderCustomizer> builderCustomizers) { ObjectProvider<CqlSessionBuilderCustomizer> builderCustomizers, ObjectProvider<SslBundles> sslBundles) {
CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader); CqlSessionBuilder builder = CqlSession.builder().withConfigLoader(driverConfigLoader);
configureAuthentication(builder, connectionDetails); configureAuthentication(builder, connectionDetails);
configureSsl(builder, connectionDetails); configureSsl(builder, connectionDetails, sslBundles.getIfAvailable());
builder.withKeyspace(this.properties.getKeyspaceName()); builder.withKeyspace(this.properties.getKeyspaceName());
builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder)); builderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
return builder; return builder;
@ -122,15 +130,38 @@ public class CassandraAutoConfiguration {
} }
} }
private void configureSsl(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails) { private void configureSsl(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails,
if (connectionDetails instanceof PropertiesCassandraConnectionDetails && this.properties.isSsl()) { SslBundles sslBundles) {
try { if (!(connectionDetails instanceof PropertiesCassandraConnectionDetails)) {
builder.withSslContext(SSLContext.getDefault()); return;
} }
catch (NoSuchAlgorithmException ex) { Ssl properties = this.properties.getSsl();
throw new IllegalStateException("Could not setup SSL default context for Cassandra", ex); 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 = "") @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"); * 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.
@ -31,6 +31,7 @@ import org.springframework.core.io.Resource;
* @author Phillip Webb * @author Phillip Webb
* @author Mark Paluch * @author Mark Paluch
* @author Stephane Nicoll * @author Stephane Nicoll
* @author Scott Frederick
* @since 1.3.0 * @since 1.3.0
*/ */
@ConfigurationProperties(prefix = "spring.cassandra") @ConfigurationProperties(prefix = "spring.cassandra")
@ -89,9 +90,9 @@ public class CassandraProperties {
private String schemaAction = "none"; private String schemaAction = "none";
/** /**
* Enable SSL support. * SSL configuration.
*/ */
private boolean ssl = false; private Ssl ssl = new Ssl();
/** /**
* Connection configuration. * Connection configuration.
@ -185,11 +186,11 @@ public class CassandraProperties {
this.compression = compression; this.compression = compression;
} }
public boolean isSsl() { public Ssl getSsl() {
return this.ssl; return this.ssl;
} }
public void setSsl(boolean ssl) { public void setSsl(Ssl ssl) {
this.ssl = ssl; this.ssl = ssl;
} }
@ -217,6 +218,36 @@ public class CassandraProperties {
return this.controlconnection; 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 { 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.AutoConfigurations;
import org.springframework.boot.autoconfigure.cassandra.CassandraAutoConfiguration.PropertiesCassandraConnectionDetails; 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.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatException;
import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.assertj.core.api.Assertions.assertThatThrownBy;
/** /**
@ -50,11 +53,12 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* @author Moritz Halbritter * @author Moritz Halbritter
* @author Andy Wilkinson * @author Andy Wilkinson
* @author Phillip Webb * @author Phillip Webb
* @author Scott Frederick
*/ */
class CassandraAutoConfigurationTests { class CassandraAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class)); .withConfiguration(AutoConfigurations.of(CassandraAutoConfiguration.class, SslAutoConfiguration.class));
@Test @Test
void cqlSessionBuildHasScopePrototype() { 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 @Test
void driverConfigLoaderWithDefaultConfiguration() { void driverConfigLoaderWithDefaultConfiguration() {
this.contextRunner.run((context) -> { 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`. TIP: Those two examples are identical as the port default to `9042`.
If you need to configure the port, use `spring.cassandra.port`. 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] [NOTE]
==== ====
The Cassandra driver has its own configuration infrastructure that loads an `application.conf` at the root of the classpath. The Cassandra driver has its own configuration infrastructure that loads an `application.conf` at the root of the classpath.

Loading…
Cancel
Save