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,8 +130,25 @@ public class CassandraAutoConfiguration { | |||
| 		} | ||||
| 	} | ||||
| 
 | ||||
| 	private void configureSsl(CqlSessionBuilder builder, CassandraConnectionDetails connectionDetails) { | ||||
| 		if (connectionDetails instanceof PropertiesCassandraConnectionDetails && this.properties.isSsl()) { | ||||
| 	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()); | ||||
| 		} | ||||
|  | @ -131,6 +156,12 @@ public class CassandraAutoConfiguration { | |||
| 			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