Skip to content

Commit 8897cc2

Browse files
committed
Add SSL support to auto-configuration for Rabbit Streams
Closes gh-43932 Signed-off-by: Jay Choi <jayyoungchoi22@gmail.com>
1 parent 5cc0e4b commit 8897cc2

File tree

4 files changed

+191
-13
lines changed

4 files changed

+191
-13
lines changed

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

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
* @author Scott Frederick
5353
* @author Lasse Wulff
5454
* @author Yanming Zhou
55+
* @author Jay Choi
5556
* @since 4.0.0
5657
*/
5758
@ConfigurationProperties("spring.rabbitmq")
@@ -1311,6 +1312,8 @@ public static final class Stream {
13111312
*/
13121313
private @Nullable String name;
13131314

1315+
private final StreamSsl ssl = new StreamSsl();
1316+
13141317
public String getHost() {
13151318
return this.host;
13161319
}
@@ -1359,6 +1362,41 @@ public void setName(@Nullable String name) {
13591362
this.name = name;
13601363
}
13611364

1365+
public StreamSsl getSsl() {
1366+
return this.ssl;
1367+
}
1368+
1369+
public static class StreamSsl {
1370+
1371+
/**
1372+
* Whether to enable SSL support. Enabled automatically if "bundle" is
1373+
* provided.
1374+
*/
1375+
private @Nullable Boolean enabled;
1376+
1377+
/**
1378+
* SSL bundle name.
1379+
*/
1380+
private @Nullable String bundle;
1381+
1382+
public boolean isEnabled() {
1383+
return (this.enabled != null) ? this.enabled : this.bundle != null;
1384+
}
1385+
1386+
public void setEnabled(@Nullable Boolean enabled) {
1387+
this.enabled = enabled;
1388+
}
1389+
1390+
public @Nullable String getBundle() {
1391+
return this.bundle;
1392+
}
1393+
1394+
public void setBundle(@Nullable String bundle) {
1395+
this.bundle = bundle;
1396+
}
1397+
1398+
}
1399+
13621400
}
13631401

13641402
}

module/spring-boot-amqp/src/main/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfiguration.java

Lines changed: 42 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,13 @@
1616

1717
package org.springframework.boot.amqp.autoconfigure;
1818

19+
import javax.net.ssl.SSLException;
20+
1921
import com.rabbitmq.stream.Environment;
2022
import com.rabbitmq.stream.EnvironmentBuilder;
23+
import io.netty.handler.ssl.SslContext;
24+
import io.netty.handler.ssl.SslContextBuilder;
25+
import org.jspecify.annotations.Nullable;
2126

2227
import org.springframework.amqp.rabbit.config.ContainerCustomizer;
2328
import org.springframework.amqp.support.converter.MessageConverter;
@@ -27,6 +32,10 @@
2732
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
2833
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
2934
import org.springframework.boot.context.properties.PropertyMapper;
35+
import org.springframework.boot.ssl.SslBundle;
36+
import org.springframework.boot.ssl.SslBundles;
37+
import org.springframework.boot.ssl.SslManagerBundle;
38+
import org.springframework.boot.ssl.SslOptions;
3039
import org.springframework.context.annotation.Bean;
3140
import org.springframework.context.annotation.Configuration;
3241
import org.springframework.rabbit.stream.config.StreamRabbitListenerContainerFactory;
@@ -39,12 +48,14 @@
3948
import org.springframework.rabbit.stream.producer.RabbitStreamTemplate;
4049
import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
4150
import org.springframework.util.Assert;
51+
import org.springframework.util.StringUtils;
4252

4353
/**
4454
* Configuration for Spring RabbitMQ Stream plugin support.
4555
*
4656
* @author Gary Russell
4757
* @author Eddú Meléndez
58+
* @author Jay Choi
4859
*/
4960
@Configuration(proxyBeanMethods = false)
5061
@ConditionalOnClass(StreamRabbitListenerContainerFactory.class)
@@ -71,8 +82,8 @@ StreamRabbitListenerContainerFactory streamRabbitListenerContainerFactory(Enviro
7182
@Bean(name = "rabbitStreamEnvironment")
7283
@ConditionalOnMissingBean(name = "rabbitStreamEnvironment")
7384
Environment rabbitStreamEnvironment(RabbitProperties properties, RabbitConnectionDetails connectionDetails,
74-
ObjectProvider<EnvironmentBuilderCustomizer> customizers) {
75-
EnvironmentBuilder builder = configure(Environment.builder(), properties, connectionDetails);
85+
ObjectProvider<EnvironmentBuilderCustomizer> customizers, @Nullable SslBundles sslBundles) {
86+
EnvironmentBuilder builder = configure(Environment.builder(), properties, connectionDetails, sslBundles);
7687
customizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
7788
return builder.build();
7889
}
@@ -105,20 +116,46 @@ RabbitStreamTemplate rabbitStreamTemplate(Environment rabbitStreamEnvironment, R
105116
}
106117

107118
static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties properties,
108-
RabbitConnectionDetails connectionDetails) {
109-
return configure(builder, properties.getStream(), connectionDetails);
119+
RabbitConnectionDetails connectionDetails, @Nullable SslBundles sslBundles) {
120+
return configure(builder, properties.getStream(), connectionDetails, sslBundles);
110121
}
111122

112123
private static EnvironmentBuilder configure(EnvironmentBuilder builder, RabbitProperties.Stream stream,
113-
RabbitConnectionDetails connectionDetails) {
124+
RabbitConnectionDetails connectionDetails, @Nullable SslBundles sslBundles) {
114125
builder.lazyInitialization(true);
115126
PropertyMapper map = PropertyMapper.get();
116127
map.from(stream.getHost()).to(builder::host);
117128
map.from(stream.getPort()).to(builder::port);
118129
map.from(stream.getVirtualHost()).orFrom(connectionDetails::getVirtualHost).to(builder::virtualHost);
119130
map.from(stream.getUsername()).orFrom(connectionDetails::getUsername).to(builder::username);
120131
map.from(stream.getPassword()).orFrom(connectionDetails::getPassword).to(builder::password);
132+
RabbitProperties.Stream.StreamSsl ssl = stream.getSsl();
133+
if (ssl.isEnabled()) {
134+
if (StringUtils.hasLength(ssl.getBundle())) {
135+
Assert.notNull(sslBundles, "SSL bundle name has been set but no SSL bundles found in context");
136+
builder.tls().sslContext(createSslContext(sslBundles.getBundle(ssl.getBundle())));
137+
}
138+
else {
139+
builder.tls();
140+
}
141+
}
121142
return builder;
122143
}
123144

145+
private static SslContext createSslContext(SslBundle sslBundle) {
146+
SslOptions options = sslBundle.getOptions();
147+
SslManagerBundle managers = sslBundle.getManagers();
148+
try {
149+
return SslContextBuilder.forClient()
150+
.keyManager(managers.getKeyManagerFactory())
151+
.trustManager(managers.getTrustManagerFactory())
152+
.ciphers(SslOptions.asSet(options.getCiphers()))
153+
.protocols(options.getEnabledProtocols())
154+
.build();
155+
}
156+
catch (SSLException ex) {
157+
throw new IllegalStateException("Failed to create SSL context for RabbitMQ Stream", ex);
158+
}
159+
}
160+
124161
}

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

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
* @author Stephane Nicoll
3939
* @author Rafael Carvalho
4040
* @author Scott Frederick
41+
* @author Jay Choi
4142
*/
4243
class RabbitPropertiesTests {
4344

@@ -381,4 +382,34 @@ void hostPropertyMustBeSingleHost() {
381382
.withMessageContaining("spring.rabbitmq.host");
382383
}
383384

385+
@Test
386+
void streamSslIsDisabledByDefault() {
387+
assertThat(this.properties.getStream().getSsl().isEnabled()).isFalse();
388+
}
389+
390+
@Test
391+
void streamSslIsEnabledWhenEnabledIsTrue() {
392+
this.properties.getStream().getSsl().setEnabled(true);
393+
assertThat(this.properties.getStream().getSsl().isEnabled()).isTrue();
394+
}
395+
396+
@Test
397+
void streamSslIsDisabledWhenEnabledIsFalse() {
398+
this.properties.getStream().getSsl().setEnabled(false);
399+
assertThat(this.properties.getStream().getSsl().isEnabled()).isFalse();
400+
}
401+
402+
@Test
403+
void streamSslIsEnabledWhenBundleIsSet() {
404+
this.properties.getStream().getSsl().setBundle("test-bundle");
405+
assertThat(this.properties.getStream().getSsl().isEnabled()).isTrue();
406+
}
407+
408+
@Test
409+
void streamSslIsDisabledWhenBundleIsSetButEnabledIsFalse() {
410+
this.properties.getStream().getSsl().setBundle("test-bundle");
411+
this.properties.getStream().getSsl().setEnabled(false);
412+
assertThat(this.properties.getStream().getSsl().isEnabled()).isFalse();
413+
}
414+
384415
}

module/spring-boot-amqp/src/test/java/org/springframework/boot/amqp/autoconfigure/RabbitStreamConfigurationTests.java

Lines changed: 80 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import org.springframework.amqp.rabbit.listener.RabbitListenerEndpointRegistry;
3434
import org.springframework.amqp.support.converter.MessageConverter;
3535
import org.springframework.boot.autoconfigure.AutoConfigurations;
36+
import org.springframework.boot.autoconfigure.ssl.SslAutoConfiguration;
37+
import org.springframework.boot.ssl.NoSuchSslBundleException;
38+
import org.springframework.boot.ssl.SslBundles;
3639
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
3740
import org.springframework.context.annotation.Bean;
3841
import org.springframework.context.annotation.Configuration;
@@ -50,8 +53,11 @@
5053
import org.springframework.rabbit.stream.support.converter.StreamMessageConverter;
5154

5255
import static org.assertj.core.api.Assertions.assertThat;
56+
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
57+
import static org.mockito.BDDMockito.given;
5358
import static org.mockito.BDDMockito.then;
5459
import static org.mockito.Mockito.mock;
60+
import static org.mockito.Mockito.never;
5561

5662
/**
5763
* Tests for {@link RabbitStreamConfiguration}.
@@ -60,11 +66,12 @@
6066
* @author Andy Wilkinson
6167
* @author Eddú Meléndez
6268
* @author Moritz Halbritter
69+
* @author Jay Choi
6370
*/
6471
class RabbitStreamConfigurationTests {
6572

6673
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
67-
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class));
74+
.withConfiguration(AutoConfigurations.of(RabbitAutoConfiguration.class, SslAutoConfiguration.class));
6875

6976
@Test
7077
@SuppressWarnings("unchecked")
@@ -146,7 +153,7 @@ void environmentUsesConnectionDetailsByDefault() {
146153
EnvironmentBuilder builder = mock(EnvironmentBuilder.class);
147154
RabbitProperties properties = new RabbitProperties();
148155
RabbitStreamConfiguration.configure(builder, properties,
149-
new TestRabbitConnectionDetails("guest", "guest", "vhost"));
156+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
150157
then(builder).should().port(5552);
151158
then(builder).should().host("localhost");
152159
then(builder).should().virtualHost("vhost");
@@ -162,7 +169,7 @@ void whenStreamPortIsSetThenEnvironmentUsesCustomPort() {
162169
RabbitProperties properties = new RabbitProperties();
163170
properties.getStream().setPort(5553);
164171
RabbitStreamConfiguration.configure(builder, properties,
165-
new TestRabbitConnectionDetails("guest", "guest", "vhost"));
172+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
166173
then(builder).should().port(5553);
167174
}
168175

@@ -172,7 +179,7 @@ void whenStreamHostIsSetThenEnvironmentUsesCustomHost() {
172179
RabbitProperties properties = new RabbitProperties();
173180
properties.getStream().setHost("stream.rabbit.example.com");
174181
RabbitStreamConfiguration.configure(builder, properties,
175-
new TestRabbitConnectionDetails("guest", "guest", "vhost"));
182+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
176183
then(builder).should().host("stream.rabbit.example.com");
177184
}
178185

@@ -182,7 +189,7 @@ void whenStreamVirtualHostIsSetThenEnvironmentUsesCustomVirtualHost() {
182189
RabbitProperties properties = new RabbitProperties();
183190
properties.getStream().setVirtualHost("stream-virtual-host");
184191
RabbitStreamConfiguration.configure(builder, properties,
185-
new TestRabbitConnectionDetails("guest", "guest", "vhost"));
192+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
186193
then(builder).should().virtualHost("stream-virtual-host");
187194
}
188195

@@ -192,7 +199,7 @@ void whenStreamVirtualHostIsNotSetButDefaultVirtualHostIsSetThenEnvironmentUsesD
192199
RabbitProperties properties = new RabbitProperties();
193200
properties.setVirtualHost("properties-virtual-host");
194201
RabbitStreamConfiguration.configure(builder, properties,
195-
new TestRabbitConnectionDetails("guest", "guest", "default-virtual-host"));
202+
new TestRabbitConnectionDetails("guest", "guest", "default-virtual-host"), null);
196203
then(builder).should().virtualHost("default-virtual-host");
197204
}
198205

@@ -203,7 +210,7 @@ void whenStreamCredentialsAreNotSetThenEnvironmentUsesConnectionDetailsCredentia
203210
properties.setUsername("alice");
204211
properties.setPassword("secret");
205212
RabbitStreamConfiguration.configure(builder, properties,
206-
new TestRabbitConnectionDetails("bob", "password", "vhost"));
213+
new TestRabbitConnectionDetails("bob", "password", "vhost"), null);
207214
then(builder).should().username("bob");
208215
then(builder).should().password("password");
209216
}
@@ -217,7 +224,7 @@ void whenStreamCredentialsAreSetThenEnvironmentUsesStreamCredentials() {
217224
properties.getStream().setUsername("bob");
218225
properties.getStream().setPassword("confidential");
219226
RabbitStreamConfiguration.configure(builder, properties,
220-
new TestRabbitConnectionDetails("charlotte", "hidden", "vhost"));
227+
new TestRabbitConnectionDetails("charlotte", "hidden", "vhost"), null);
221228
then(builder).should().username("bob");
222229
then(builder).should().password("confidential");
223230
}
@@ -297,6 +304,71 @@ void environmentCreatedByBuilderCanBeCustomized() {
297304
});
298305
}
299306

307+
@Test
308+
void whenStreamSslIsNotConfiguredThenTlsIsNotUsed() {
309+
EnvironmentBuilder builder = mock(EnvironmentBuilder.class);
310+
RabbitProperties properties = new RabbitProperties();
311+
RabbitStreamConfiguration.configure(builder, properties,
312+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
313+
then(builder).should(never()).tls();
314+
}
315+
316+
@Test
317+
void whenStreamSslIsEnabledThenTlsIsUsed() {
318+
EnvironmentBuilder builder = mock(EnvironmentBuilder.class);
319+
RabbitProperties properties = new RabbitProperties();
320+
properties.getStream().getSsl().setEnabled(true);
321+
RabbitStreamConfiguration.configure(builder, properties,
322+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
323+
then(builder).should().tls();
324+
}
325+
326+
@Test
327+
void whenStreamSslBundleIsConfiguredThenTlsIsUsed() {
328+
this.contextRunner.withPropertyValues("spring.rabbitmq.stream.ssl.bundle=test-bundle",
329+
"spring.ssl.bundle.jks.test-bundle.keystore.location=classpath:org/springframework/boot/amqp/autoconfigure/test.jks",
330+
"spring.ssl.bundle.jks.test-bundle.keystore.password=secret")
331+
.run((context) -> {
332+
assertThat(context).hasNotFailed();
333+
assertThat(context).hasSingleBean(Environment.class);
334+
});
335+
}
336+
337+
@Test
338+
void whenStreamSslIsDisabledThenTlsIsNotUsed() {
339+
EnvironmentBuilder builder = mock(EnvironmentBuilder.class);
340+
RabbitProperties properties = new RabbitProperties();
341+
properties.getStream().getSsl().setEnabled(false);
342+
RabbitStreamConfiguration.configure(builder, properties,
343+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
344+
then(builder).should(never()).tls();
345+
}
346+
347+
@Test
348+
void whenStreamSslIsDisabledWithBundleThenTlsIsNotUsed() {
349+
EnvironmentBuilder builder = mock(EnvironmentBuilder.class);
350+
RabbitProperties properties = new RabbitProperties();
351+
properties.getStream().getSsl().setEnabled(false);
352+
properties.getStream().getSsl().setBundle("some-bundle");
353+
RabbitStreamConfiguration.configure(builder, properties,
354+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), null);
355+
then(builder).should(never()).tls();
356+
}
357+
358+
@Test
359+
void whenStreamSslBundleIsInvalidThenFails() {
360+
EnvironmentBuilder builder = mock(EnvironmentBuilder.class);
361+
SslBundles sslBundles = mock(SslBundles.class);
362+
given(sslBundles.getBundle("invalid-bundle")).willThrow(
363+
new NoSuchSslBundleException("invalid-bundle", "SSL bundle name 'invalid-bundle' cannot be found"));
364+
RabbitProperties properties = new RabbitProperties();
365+
properties.getStream().getSsl().setBundle("invalid-bundle");
366+
assertThatExceptionOfType(NoSuchSslBundleException.class)
367+
.isThrownBy(() -> RabbitStreamConfiguration.configure(builder, properties,
368+
new TestRabbitConnectionDetails("guest", "guest", "vhost"), sslBundles))
369+
.withMessageContaining("invalid-bundle");
370+
}
371+
300372
@Configuration(proxyBeanMethods = false)
301373
static class TestConfiguration {
302374

0 commit comments

Comments
 (0)