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
This commit is contained in:
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…
Reference in New Issue