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.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,8 +130,25 @@ 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) { | ||||||
|  | 		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 { | 		try { | ||||||
| 			builder.withSslContext(SSLContext.getDefault()); | 			builder.withSslContext(SSLContext.getDefault()); | ||||||
| 		} | 		} | ||||||
|  | @ -131,6 +156,12 @@ public class CassandraAutoConfiguration { | ||||||
| 			throw new IllegalStateException("Could not setup SSL default context for Cassandra", 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…
	
		Reference in New Issue