From 682457377ae314dfe172b0166550d511df4e98dd Mon Sep 17 00:00:00 2001 From: Scott Frederick Date: Fri, 24 Mar 2023 14:55:22 -0500 Subject: [PATCH] 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 --- .../cassandra/CassandraAutoConfiguration.java | 51 ++++++++++++++---- .../cassandra/CassandraProperties.java | 41 ++++++++++++-- .../CassandraAutoConfigurationTests.java | 53 ++++++++++++++++++- .../src/docs/asciidoc/data/nosql.adoc | 27 ++++++++++ 4 files changed, 156 insertions(+), 16 deletions(-) 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 5d7e039050..112a877d90 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 @@ -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 builderCustomizers) { + ObjectProvider builderCustomizers, ObjectProvider 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 = "") diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java index d4f6c67dde..bd25ed980f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cassandra/CassandraProperties.java @@ -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 { /** diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java index 31ef493afb..7942acf74c 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cassandra/CassandraAutoConfigurationTests.java @@ -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) -> { diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc index 3f619e2c2b..c83616b5c4 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/data/nosql.adoc @@ -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 <> 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.