Add SSL bundle support to Rabbit auto-configuration

This commit is contained in:
Scott Frederick
2023-10-11 15:39:51 -05:00
parent bdaf7a7603
commit 5556739c8c
15 changed files with 460 additions and 15 deletions

View File

@@ -38,6 +38,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
@@ -69,6 +70,7 @@ import org.springframework.core.io.ResourceLoader;
* @author Chris Bono
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Scott Frederick
* @since 1.0.0
*/
@AutoConfiguration
@@ -97,9 +99,10 @@ public class RabbitAutoConfiguration {
@ConditionalOnMissingBean
RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader,
RabbitConnectionDetails connectionDetails, ObjectProvider<CredentialsProvider> credentialsProvider,
ObjectProvider<CredentialsRefreshService> credentialsRefreshService) {
ObjectProvider<CredentialsRefreshService> credentialsRefreshService,
ObjectProvider<SslBundles> sslBundles) {
RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader,
this.properties, connectionDetails);
this.properties, connectionDetails, sslBundles.getIfAvailable());
configurer.setCredentialsProvider(credentialsProvider.getIfUnique());
configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique());
return configurer;
@@ -122,7 +125,7 @@ public class RabbitAutoConfiguration {
CachingConnectionFactoryConfigurer rabbitCachingConnectionFactoryConfigurer,
ObjectProvider<ConnectionFactoryCustomizer> connectionFactoryCustomizers) throws Exception {
RabbitConnectionFactoryBean connectionFactoryBean = new RabbitConnectionFactoryBean();
RabbitConnectionFactoryBean connectionFactoryBean = new SslBundleRabbitConnectionFactoryBean();
rabbitConnectionFactoryBeanConfigurer.configure(connectionFactoryBean);
connectionFactoryBean.afterPropertiesSet();
com.rabbitmq.client.ConnectionFactory connectionFactory = connectionFactoryBean.getObject();

View File

@@ -24,6 +24,8 @@ import com.rabbitmq.client.impl.CredentialsRefreshService;
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address;
import org.springframework.boot.context.properties.PropertyMapper;
import org.springframework.boot.ssl.SslBundle;
import org.springframework.boot.ssl.SslBundles;
import org.springframework.core.io.ResourceLoader;
import org.springframework.util.Assert;
import org.springframework.util.unit.DataSize;
@@ -35,6 +37,7 @@ import org.springframework.util.unit.DataSize;
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
* @since 2.6.0
*/
public class RabbitConnectionFactoryBeanConfigurer {
@@ -45,6 +48,8 @@ public class RabbitConnectionFactoryBeanConfigurer {
private final RabbitConnectionDetails connectionDetails;
private final SslBundles sslBundles;
private CredentialsProvider credentialsProvider;
private CredentialsRefreshService credentialsRefreshService;
@@ -65,17 +70,33 @@ public class RabbitConnectionFactoryBeanConfigurer {
* priority over the properties.
* @param resourceLoader the resource loader
* @param properties the properties
* @param connectionDetails the connection details.
* @param connectionDetails the connection details
* @since 3.1.0
*/
public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties,
RabbitConnectionDetails connectionDetails) {
this(resourceLoader, properties, connectionDetails, null);
}
/**
* Creates a new configurer that will use the given {@code resourceLoader},
* {@code properties}, {@code connectionDetails}, and {@code sslBundles}. The
* connection details have priority over the properties.
* @param resourceLoader the resource loader
* @param properties the properties
* @param connectionDetails the connection details
* @param sslBundles the SSL bundles
* @since 3.2.0
*/
public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties,
RabbitConnectionDetails connectionDetails, SslBundles sslBundles) {
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
Assert.notNull(properties, "Properties must not be null");
Assert.notNull(connectionDetails, "ConnectionDetails must not be null");
this.resourceLoader = resourceLoader;
this.rabbitProperties = properties;
this.connectionDetails = connectionDetails;
this.sslBundles = sslBundles;
}
public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
@@ -111,15 +132,23 @@ public class RabbitConnectionFactoryBeanConfigurer {
RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl();
if (ssl.determineEnabled()) {
factory.setUseSSL(true);
map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
map.from(ssl::getKeyStore).to(factory::setKeyStore);
map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm);
map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
map.from(ssl::getTrustStore).to(factory::setTrustStore);
map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm);
if (ssl.getBundle() != null) {
SslBundle bundle = this.sslBundles.getBundle(ssl.getBundle());
if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) {
sslFactory.setSslBundle(bundle);
}
}
else {
map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
map.from(ssl::getKeyStore).to(factory::setKeyStore);
map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm);
map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
map.from(ssl::getTrustStore).to(factory::setTrustStore);
map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm);
}
map.from(ssl::isValidateServerCertificate)
.to((validate) -> factory.setSkipServerCertificateValidation(!validate));
map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification);

View File

@@ -46,6 +46,7 @@ import org.springframework.util.unit.DataSize;
* @author Franjo Zilic
* @author Eddú Meléndez
* @author Rafael Carvalho
* @author Scott Frederick
* @since 1.0.0
*/
@ConfigurationProperties(prefix = "spring.rabbitmq")
@@ -400,6 +401,11 @@ public class RabbitProperties {
*/
private Boolean enabled;
/**
* SSL bundle name.
*/
private String bundle;
/**
* Path to the key store that holds the SSL certificate.
*/
@@ -467,7 +473,7 @@ public class RabbitProperties {
* @see #getEnabled() ()
*/
public boolean determineEnabled() {
boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false);
boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false) || this.bundle != null;
if (CollectionUtils.isEmpty(RabbitProperties.this.parsedAddresses)) {
return defaultEnabled;
}
@@ -479,6 +485,14 @@ public class RabbitProperties {
this.enabled = enabled;
}
public String getBundle() {
return this.bundle;
}
public void setBundle(String bundle) {
this.bundle = bundle;
}
public String getKeyStore() {
return this.keyStore;
}

View File

@@ -0,0 +1,57 @@
/*
* 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.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.autoconfigure.amqp;
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
import org.springframework.boot.ssl.SslBundle;
/**
* A {@link RabbitConnectionFactoryBean} that can be configured with custom SSL trust
* material from an {@link SslBundle}.
*
* @author Scott Frederick
*/
class SslBundleRabbitConnectionFactoryBean extends RabbitConnectionFactoryBean {
private SslBundle sslBundle;
private boolean enableHostnameVerification;
@Override
protected void setUpSSL() {
if (this.sslBundle != null) {
this.connectionFactory.useSslProtocol(this.sslBundle.createSslContext());
if (this.enableHostnameVerification) {
this.connectionFactory.enableHostnameVerification();
}
}
else {
super.setUpSSL();
}
}
void setSslBundle(SslBundle sslBundle) {
this.sslBundle = sslBundle;
}
@Override
public void setEnableHostnameVerification(boolean enable) {
this.enableHostnameVerification = enable;
super.setEnableHostnameVerification(enable);
}
}

View File

@@ -60,6 +60,7 @@ import org.springframework.amqp.rabbit.retry.MessageRecoverer;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.boot.test.system.CapturedOutput;
@@ -102,12 +103,13 @@ import static org.mockito.Mockito.mock;
* @author Moritz Halbritter
* @author Andy Wilkinson
* @author Phillip Webb
* @author Scott Frederick
*/
@ExtendWith(OutputCaptureExtension.class)
class RabbitAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class, SslAutoConfiguration.class));
@Test
void testDefaultRabbitConfiguration() {
@@ -777,6 +779,16 @@ class RabbitAutoConfigurationTests {
});
}
@Test
void enableSslWithInvalidSslBundleFails() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)
.withPropertyValues("spring.rabbitmq.ssl.bundle=invalid")
.run((context) -> {
assertThat(context).hasFailed();
assertThat(context).getFailure().hasMessageContaining("SSL bundle name 'invalid' cannot be found");
});
}
@Test
// Make sure that we at least attempt to load the store
void enableSslWithNonExistingKeystoreShouldFail() {
@@ -827,6 +839,19 @@ class RabbitAutoConfigurationTests {
});
}
@Test
void enableSslWithBundle() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)
.withPropertyValues("spring.rabbitmq.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) -> {
com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context);
assertThat(rabbitConnectionFactory.isSSL()).isTrue();
});
}
@Test
void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() {
this.contextRunner.withUserConfiguration(TestConfiguration.class)

View File

@@ -35,6 +35,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
* @author Andy Wilkinson
* @author Stephane Nicoll
* @author Rafael Carvalho
* @author Scott Frederick
*/
class RabbitPropertiesTests {
@@ -321,6 +322,12 @@ class RabbitPropertiesTests {
assertThat(this.properties.getSsl().determineEnabled()).isTrue();
}
@Test
void determineSslEnabledIsTrueWhenBundleIsSetAndNoAddresses() {
this.properties.getSsl().setBundle("test");
assertThat(this.properties.getSsl().determineEnabled()).isTrue();
}
@Test
void propertiesUseConsistentDefaultValues() {
ConnectionFactory connectionFactory = new ConnectionFactory();