Skip to content

Commit 5556739

Browse files
Add SSL bundle support to Rabbit auto-configuration
1 parent bdaf7a7 commit 5556739

File tree

15 files changed

+460
-15
lines changed

15 files changed

+460
-15
lines changed

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfiguration.java

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
3939
import org.springframework.boot.autoconfigure.condition.ConditionalOnSingleCandidate;
4040
import org.springframework.boot.context.properties.EnableConfigurationProperties;
41+
import org.springframework.boot.ssl.SslBundles;
4142
import org.springframework.context.annotation.Bean;
4243
import org.springframework.context.annotation.Configuration;
4344
import org.springframework.context.annotation.Import;
@@ -69,6 +70,7 @@
6970
* @author Chris Bono
7071
* @author Moritz Halbritter
7172
* @author Andy Wilkinson
73+
* @author Scott Frederick
7274
* @since 1.0.0
7375
*/
7476
@AutoConfiguration
@@ -97,9 +99,10 @@ RabbitConnectionDetails rabbitConnectionDetails() {
9799
@ConditionalOnMissingBean
98100
RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader,
99101
RabbitConnectionDetails connectionDetails, ObjectProvider<CredentialsProvider> credentialsProvider,
100-
ObjectProvider<CredentialsRefreshService> credentialsRefreshService) {
102+
ObjectProvider<CredentialsRefreshService> credentialsRefreshService,
103+
ObjectProvider<SslBundles> sslBundles) {
101104
RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader,
102-
this.properties, connectionDetails);
105+
this.properties, connectionDetails, sslBundles.getIfAvailable());
103106
configurer.setCredentialsProvider(credentialsProvider.getIfUnique());
104107
configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique());
105108
return configurer;
@@ -122,7 +125,7 @@ CachingConnectionFactory rabbitConnectionFactory(
122125
CachingConnectionFactoryConfigurer rabbitCachingConnectionFactoryConfigurer,
123126
ObjectProvider<ConnectionFactoryCustomizer> connectionFactoryCustomizers) throws Exception {
124127

125-
RabbitConnectionFactoryBean connectionFactoryBean = new RabbitConnectionFactoryBean();
128+
RabbitConnectionFactoryBean connectionFactoryBean = new SslBundleRabbitConnectionFactoryBean();
126129
rabbitConnectionFactoryBeanConfigurer.configure(connectionFactoryBean);
127130
connectionFactoryBean.afterPropertiesSet();
128131
com.rabbitmq.client.ConnectionFactory connectionFactory = connectionFactoryBean.getObject();

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java

Lines changed: 39 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
2525
import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address;
2626
import org.springframework.boot.context.properties.PropertyMapper;
27+
import org.springframework.boot.ssl.SslBundle;
28+
import org.springframework.boot.ssl.SslBundles;
2729
import org.springframework.core.io.ResourceLoader;
2830
import org.springframework.util.Assert;
2931
import org.springframework.util.unit.DataSize;
@@ -35,6 +37,7 @@
3537
* @author Moritz Halbritter
3638
* @author Andy Wilkinson
3739
* @author Phillip Webb
40+
* @author Scott Frederick
3841
* @since 2.6.0
3942
*/
4043
public class RabbitConnectionFactoryBeanConfigurer {
@@ -45,6 +48,8 @@ public class RabbitConnectionFactoryBeanConfigurer {
4548

4649
private final RabbitConnectionDetails connectionDetails;
4750

51+
private final SslBundles sslBundles;
52+
4853
private CredentialsProvider credentialsProvider;
4954

5055
private CredentialsRefreshService credentialsRefreshService;
@@ -65,17 +70,33 @@ public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, Rabb
6570
* priority over the properties.
6671
* @param resourceLoader the resource loader
6772
* @param properties the properties
68-
* @param connectionDetails the connection details.
73+
* @param connectionDetails the connection details
6974
* @since 3.1.0
7075
*/
7176
public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties,
7277
RabbitConnectionDetails connectionDetails) {
78+
this(resourceLoader, properties, connectionDetails, null);
79+
}
80+
81+
/**
82+
* Creates a new configurer that will use the given {@code resourceLoader},
83+
* {@code properties}, {@code connectionDetails}, and {@code sslBundles}. The
84+
* connection details have priority over the properties.
85+
* @param resourceLoader the resource loader
86+
* @param properties the properties
87+
* @param connectionDetails the connection details
88+
* @param sslBundles the SSL bundles
89+
* @since 3.2.0
90+
*/
91+
public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties,
92+
RabbitConnectionDetails connectionDetails, SslBundles sslBundles) {
7393
Assert.notNull(resourceLoader, "ResourceLoader must not be null");
7494
Assert.notNull(properties, "Properties must not be null");
7595
Assert.notNull(connectionDetails, "ConnectionDetails must not be null");
7696
this.resourceLoader = resourceLoader;
7797
this.rabbitProperties = properties;
7898
this.connectionDetails = connectionDetails;
99+
this.sslBundles = sslBundles;
79100
}
80101

81102
public void setCredentialsProvider(CredentialsProvider credentialsProvider) {
@@ -111,15 +132,23 @@ public void configure(RabbitConnectionFactoryBean factory) {
111132
RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl();
112133
if (ssl.determineEnabled()) {
113134
factory.setUseSSL(true);
114-
map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
115-
map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
116-
map.from(ssl::getKeyStore).to(factory::setKeyStore);
117-
map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
118-
map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm);
119-
map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
120-
map.from(ssl::getTrustStore).to(factory::setTrustStore);
121-
map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
122-
map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm);
135+
if (ssl.getBundle() != null) {
136+
SslBundle bundle = this.sslBundles.getBundle(ssl.getBundle());
137+
if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) {
138+
sslFactory.setSslBundle(bundle);
139+
}
140+
}
141+
else {
142+
map.from(ssl::getAlgorithm).whenNonNull().to(factory::setSslAlgorithm);
143+
map.from(ssl::getKeyStoreType).to(factory::setKeyStoreType);
144+
map.from(ssl::getKeyStore).to(factory::setKeyStore);
145+
map.from(ssl::getKeyStorePassword).to(factory::setKeyStorePassphrase);
146+
map.from(ssl::getKeyStoreAlgorithm).whenNonNull().to(factory::setKeyStoreAlgorithm);
147+
map.from(ssl::getTrustStoreType).to(factory::setTrustStoreType);
148+
map.from(ssl::getTrustStore).to(factory::setTrustStore);
149+
map.from(ssl::getTrustStorePassword).to(factory::setTrustStorePassphrase);
150+
map.from(ssl::getTrustStoreAlgorithm).whenNonNull().to(factory::setTrustStoreAlgorithm);
151+
}
123152
map.from(ssl::isValidateServerCertificate)
124153
.to((validate) -> factory.setSkipServerCertificateValidation(!validate));
125154
map.from(ssl::getVerifyHostname).to(factory::setEnableHostnameVerification);

spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitProperties.java

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,7 @@
4646
* @author Franjo Zilic
4747
* @author Eddú Meléndez
4848
* @author Rafael Carvalho
49+
* @author Scott Frederick
4950
* @since 1.0.0
5051
*/
5152
@ConfigurationProperties(prefix = "spring.rabbitmq")
@@ -400,6 +401,11 @@ public class Ssl {
400401
*/
401402
private Boolean enabled;
402403

404+
/**
405+
* SSL bundle name.
406+
*/
407+
private String bundle;
408+
403409
/**
404410
* Path to the key store that holds the SSL certificate.
405411
*/
@@ -467,7 +473,7 @@ public Boolean getEnabled() {
467473
* @see #getEnabled() ()
468474
*/
469475
public boolean determineEnabled() {
470-
boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false);
476+
boolean defaultEnabled = Optional.ofNullable(getEnabled()).orElse(false) || this.bundle != null;
471477
if (CollectionUtils.isEmpty(RabbitProperties.this.parsedAddresses)) {
472478
return defaultEnabled;
473479
}
@@ -479,6 +485,14 @@ public void setEnabled(Boolean enabled) {
479485
this.enabled = enabled;
480486
}
481487

488+
public String getBundle() {
489+
return this.bundle;
490+
}
491+
492+
public void setBundle(String bundle) {
493+
this.bundle = bundle;
494+
}
495+
482496
public String getKeyStore() {
483497
return this.keyStore;
484498
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.autoconfigure.amqp;
18+
19+
import org.springframework.amqp.rabbit.connection.RabbitConnectionFactoryBean;
20+
import org.springframework.boot.ssl.SslBundle;
21+
22+
/**
23+
* A {@link RabbitConnectionFactoryBean} that can be configured with custom SSL trust
24+
* material from an {@link SslBundle}.
25+
*
26+
* @author Scott Frederick
27+
*/
28+
class SslBundleRabbitConnectionFactoryBean extends RabbitConnectionFactoryBean {
29+
30+
private SslBundle sslBundle;
31+
32+
private boolean enableHostnameVerification;
33+
34+
@Override
35+
protected void setUpSSL() {
36+
if (this.sslBundle != null) {
37+
this.connectionFactory.useSslProtocol(this.sslBundle.createSslContext());
38+
if (this.enableHostnameVerification) {
39+
this.connectionFactory.enableHostnameVerification();
40+
}
41+
}
42+
else {
43+
super.setUpSSL();
44+
}
45+
}
46+
47+
void setSslBundle(SslBundle sslBundle) {
48+
this.sslBundle = sslBundle;
49+
}
50+
51+
@Override
52+
public void setEnableHostnameVerification(boolean enable) {
53+
this.enableHostnameVerification = enable;
54+
super.setEnableHostnameVerification(enable);
55+
}
56+
57+
}

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitAutoConfigurationTests.java

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@
6060
import org.springframework.amqp.support.converter.MessageConverter;
6161
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
6262
import org.springframework.boot.autoconfigure.AutoConfigurations;
63+
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
6364
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
6465
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
6566
import org.springframework.boot.test.system.CapturedOutput;
@@ -102,12 +103,13 @@
102103
* @author Moritz Halbritter
103104
* @author Andy Wilkinson
104105
* @author Phillip Webb
106+
* @author Scott Frederick
105107
*/
106108
@ExtendWith(OutputCaptureExtension.class)
107109
class RabbitAutoConfigurationTests {
108110

109111
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
110-
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class));
112+
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class, SslAutoConfiguration.class));
111113

112114
@Test
113115
void testDefaultRabbitConfiguration() {
@@ -777,6 +779,16 @@ void enableSsl() {
777779
});
778780
}
779781

782+
@Test
783+
void enableSslWithInvalidSslBundleFails() {
784+
this.contextRunner.withUserConfiguration(TestConfiguration.class)
785+
.withPropertyValues("spring.rabbitmq.ssl.bundle=invalid")
786+
.run((context) -> {
787+
assertThat(context).hasFailed();
788+
assertThat(context).getFailure().hasMessageContaining("SSL bundle name 'invalid' cannot be found");
789+
});
790+
}
791+
780792
@Test
781793
// Make sure that we at least attempt to load the store
782794
void enableSslWithNonExistingKeystoreShouldFail() {
@@ -827,6 +839,19 @@ void enableSslWithInvalidTrustStoreTypeShouldFail() {
827839
});
828840
}
829841

842+
@Test
843+
void enableSslWithBundle() {
844+
this.contextRunner.withUserConfiguration(TestConfiguration.class)
845+
.withPropertyValues("spring.rabbitmq.ssl.bundle=test-bundle",
846+
"spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:test.jks",
847+
"spring.ssl.bundle.jks.test-bundle.keystore.password=secret",
848+
"spring.ssl.bundle.jks.test-bundle.key.password=password")
849+
.run((context) -> {
850+
com.rabbitmq.client.ConnectionFactory rabbitConnectionFactory = getTargetConnectionFactory(context);
851+
assertThat(rabbitConnectionFactory.isSSL()).isTrue();
852+
});
853+
}
854+
830855
@Test
831856
void enableSslWithKeystoreTypeAndTrustStoreTypeShouldWork() {
832857
this.contextRunner.withUserConfiguration(TestConfiguration.class)

spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/RabbitPropertiesTests.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
* @author Andy Wilkinson
3636
* @author Stephane Nicoll
3737
* @author Rafael Carvalho
38+
* @author Scott Frederick
3839
*/
3940
class RabbitPropertiesTests {
4041

@@ -321,6 +322,12 @@ void determineSslReturnFlagPropertyWhenNoAddresses() {
321322
assertThat(this.properties.getSsl().determineEnabled()).isTrue();
322323
}
323324

325+
@Test
326+
void determineSslEnabledIsTrueWhenBundleIsSetAndNoAddresses() {
327+
this.properties.getSsl().setBundle("test");
328+
assertThat(this.properties.getSsl().determineEnabled()).isTrue();
329+
}
330+
324331
@Test
325332
void propertiesUseConsistentDefaultValues() {
326333
ConnectionFactory connectionFactory = new ConnectionFactory();
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
/*
2+
* Copyright 2012-2023 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package smoketest.amqp;
18+
19+
import java.time.Duration;
20+
21+
import org.awaitility.Awaitility;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.extension.ExtendWith;
24+
import org.testcontainers.junit.jupiter.Container;
25+
import org.testcontainers.junit.jupiter.Testcontainers;
26+
27+
import org.springframework.beans.factory.annotation.Autowired;
28+
import org.springframework.boot.test.context.SpringBootTest;
29+
import org.springframework.boot.test.system.CapturedOutput;
30+
import org.springframework.boot.test.system.OutputCaptureExtension;
31+
import org.springframework.test.context.DynamicPropertyRegistry;
32+
import org.springframework.test.context.DynamicPropertySource;
33+
34+
import static org.assertj.core.api.Assertions.assertThat;
35+
36+
/**
37+
* Smoke tests for RabbitMQ with SSL using an SSL bundle for SSL configuration.
38+
*
39+
* @author Scott Frederick
40+
*/
41+
@SpringBootTest(properties = { "spring.rabbitmq.ssl.bundle=client",
42+
"spring.ssl.bundle.pem.client.keystore.certificate=classpath:ssl/test-client.crt",
43+
"spring.ssl.bundle.pem.client.keystore.private-key=classpath:ssl/test-client.key",
44+
"spring.ssl.bundle.pem.client.truststore.certificate=classpath:ssl/test-ca.crt" })
45+
@Testcontainers(disabledWithoutDocker = true)
46+
@ExtendWith(OutputCaptureExtension.class)
47+
class SampleAmqpSimpleApplicationSslTests {
48+
49+
@Container
50+
static final SecureRabbitMqContainer rabbit = new SecureRabbitMqContainer();
51+
52+
@DynamicPropertySource
53+
static void secureRabbitMqProperties(DynamicPropertyRegistry registry) {
54+
registry.add("spring.rabbitmq.host", rabbit::getHost);
55+
registry.add("spring.rabbitmq.port", rabbit::getAmqpsPort);
56+
registry.add("spring.rabbitmq.username", rabbit::getAdminUsername);
57+
registry.add("spring.rabbitmq.password", rabbit::getAdminPassword);
58+
}
59+
60+
@Autowired
61+
private Sender sender;
62+
63+
@Test
64+
void sendSimpleMessage(CapturedOutput output) {
65+
this.sender.send("Test message");
66+
Awaitility.waitAtMost(Duration.ofMinutes(1)).untilAsserted(() -> assertThat(output).contains("Test message"));
67+
}
68+
69+
}

0 commit comments

Comments
 (0)