From e26ccbe0286dfdab05a85e21f5d6e53092d9fc46 Mon Sep 17 00:00:00 2001 From: Moritz Halbritter Date: Tue, 11 Feb 2025 10:17:59 +0100 Subject: [PATCH] Add SSL service connection support for AMQP See gh-41137 --- .../AbstractConnectionFactoryConfigurer.java | 2 +- .../CachingConnectionFactoryConfigurer.java | 4 +- .../PropertiesRabbitConnectionDetails.java | 26 +++++++++- .../amqp/RabbitAutoConfiguration.java | 9 ++-- .../amqp/RabbitConnectionDetails.java | 12 ++++- ...RabbitConnectionFactoryBeanConfigurer.java | 36 ++++++------- ...ropertiesRabbitConnectionDetailsTests.java | 50 ++++++++++++++++--- ...bbitContainerConnectionDetailsFactory.java | 10 +++- .../SampleAmqpSimpleApplicationSslTests.java | 23 +++------ 9 files changed, 121 insertions(+), 51 deletions(-) diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java index 5a3bc1cb0d1..dc346dddc3b 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/AbstractConnectionFactoryConfigurer.java @@ -48,7 +48,7 @@ public abstract class AbstractConnectionFactoryConfigurer sslBundles) { + return new PropertiesRabbitConnectionDetails(this.properties, sslBundles.getIfAvailable()); } @Bean @ConditionalOnMissingBean RabbitConnectionFactoryBeanConfigurer rabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitConnectionDetails connectionDetails, ObjectProvider credentialsProvider, - ObjectProvider credentialsRefreshService, - ObjectProvider sslBundles) { + ObjectProvider credentialsRefreshService) { RabbitConnectionFactoryBeanConfigurer configurer = new RabbitConnectionFactoryBeanConfigurer(resourceLoader, - this.properties, connectionDetails, sslBundles.getIfAvailable()); + this.properties, connectionDetails); configurer.setCredentialsProvider(credentialsProvider.getIfUnique()); configurer.setCredentialsRefreshService(credentialsRefreshService.getIfUnique()); return configurer; diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java index 4cdc51c2155..ae4d595458f 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionDetails.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -19,6 +19,7 @@ package org.springframework.boot.autoconfigure.amqp; import java.util.List; import org.springframework.boot.autoconfigure.service.connection.ConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.util.Assert; /** @@ -73,6 +74,15 @@ public interface RabbitConnectionDetails extends ConnectionDetails { return addresses.get(0); } + /** + * SSL bundle to use. + * @return the SSL bundle to use + * @since 3.5.0 + */ + default SslBundle getSslBundle() { + return null; + } + /** * A RabbitMQ address. * diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java index 20ef4b5299c..cc8a44ea052 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/amqp/RabbitConnectionFactoryBeanConfigurer.java @@ -48,8 +48,6 @@ public class RabbitConnectionFactoryBeanConfigurer { private final RabbitConnectionDetails connectionDetails; - private final SslBundles sslBundles; - private CredentialsProvider credentialsProvider; private CredentialsRefreshService credentialsRefreshService; @@ -61,7 +59,7 @@ public class RabbitConnectionFactoryBeanConfigurer { * @param properties the properties */ public RabbitConnectionFactoryBeanConfigurer(ResourceLoader resourceLoader, RabbitProperties properties) { - this(resourceLoader, properties, new PropertiesRabbitConnectionDetails(properties)); + this(resourceLoader, properties, new PropertiesRabbitConnectionDetails(properties, null)); } /** @@ -96,7 +94,6 @@ public class RabbitConnectionFactoryBeanConfigurer { this.resourceLoader = resourceLoader; this.rabbitProperties = properties; this.connectionDetails = connectionDetails; - this.sslBundles = sslBundles; } public void setCredentialsProvider(CredentialsProvider credentialsProvider) { @@ -129,16 +126,14 @@ public class RabbitConnectionFactoryBeanConfigurer { .asInt(Duration::getSeconds) .to(factory::setRequestedHeartbeat); map.from(this.rabbitProperties::getRequestedChannelMax).to(factory::setRequestedChannelMax); - RabbitProperties.Ssl ssl = this.rabbitProperties.getSsl(); - if (ssl.determineEnabled()) { - factory.setUseSSL(true); - if (ssl.getBundle() != null) { - SslBundle bundle = this.sslBundles.getBundle(ssl.getBundle()); - if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) { - sslFactory.setSslBundle(bundle); - } - } - else { + SslBundle sslBundle = this.connectionDetails.getSslBundle(); + if (sslBundle != null) { + applySslBundle(factory, sslBundle); + } + else { + 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); @@ -148,10 +143,10 @@ public class RabbitConnectionFactoryBeanConfigurer { 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::isVerifyHostname).to(factory::setEnableHostnameVerification); } - map.from(ssl::isValidateServerCertificate) - .to((validate) -> factory.setSkipServerCertificateValidation(!validate)); - map.from(ssl::isVerifyHostname).to(factory::setEnableHostnameVerification); } map.from(this.rabbitProperties::getConnectionTimeout) .whenNonNull() @@ -169,4 +164,11 @@ public class RabbitConnectionFactoryBeanConfigurer { .to(factory::setMaxInboundMessageBodySize); } + private static void applySslBundle(RabbitConnectionFactoryBean factory, SslBundle bundle) { + factory.setUseSSL(true); + if (factory instanceof SslBundleRabbitConnectionFactoryBean sslFactory) { + sslFactory.setSslBundle(bundle); + } + } + } diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetailsTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetailsTests.java index a3b0162b066..2db99c686ac 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetailsTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/amqp/PropertiesRabbitConnectionDetailsTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -18,11 +18,15 @@ package org.springframework.boot.autoconfigure.amqp; import java.util.List; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails.Address; +import org.springframework.boot.ssl.DefaultSslBundleRegistry; +import org.springframework.boot.ssl.SslBundle; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; /** * Tests for {@link PropertiesRabbitConnectionDetails}. @@ -33,13 +37,24 @@ class PropertiesRabbitConnectionDetailsTests { private static final int DEFAULT_PORT = 5672; + private DefaultSslBundleRegistry sslBundleRegistry; + + private RabbitProperties properties; + + private PropertiesRabbitConnectionDetails propertiesRabbitConnectionDetails; + + @BeforeEach + void setUp() { + this.properties = new RabbitProperties(); + this.sslBundleRegistry = new DefaultSslBundleRegistry(); + this.propertiesRabbitConnectionDetails = new PropertiesRabbitConnectionDetails(this.properties, + this.sslBundleRegistry); + } + @Test void getAddresses() { - RabbitProperties properties = new RabbitProperties(); - properties.setAddresses(List.of("localhost", "localhost:1234", "[::1]", "[::1]:32863")); - PropertiesRabbitConnectionDetails propertiesRabbitConnectionDetails = new PropertiesRabbitConnectionDetails( - properties); - List
addresses = propertiesRabbitConnectionDetails.getAddresses(); + this.properties.setAddresses(List.of("localhost", "localhost:1234", "[::1]", "[::1]:32863")); + List
addresses = this.propertiesRabbitConnectionDetails.getAddresses(); assertThat(addresses.size()).isEqualTo(4); assertThat(addresses.get(0).host()).isEqualTo("localhost"); assertThat(addresses.get(0).port()).isEqualTo(DEFAULT_PORT); @@ -51,4 +66,27 @@ class PropertiesRabbitConnectionDetailsTests { assertThat(addresses.get(3).port()).isEqualTo(32863); } + @Test + void shouldReturnSslBundle() { + SslBundle bundle1 = mock(SslBundle.class); + this.sslBundleRegistry.registerBundle("bundle-1", bundle1); + this.properties.getSsl().setBundle("bundle-1"); + SslBundle sslBundle = this.propertiesRabbitConnectionDetails.getSslBundle(); + assertThat(sslBundle).isSameAs(bundle1); + } + + @Test + void shouldReturnNullIfSslIsEnabledButBundleNotSet() { + this.properties.getSsl().setEnabled(true); + SslBundle sslBundle = this.propertiesRabbitConnectionDetails.getSslBundle(); + assertThat(sslBundle).isNull(); + } + + @Test + void shouldReturnNullIfSslIsNotEnabled() { + this.properties.getSsl().setEnabled(false); + SslBundle sslBundle = this.propertiesRabbitConnectionDetails.getSslBundle(); + assertThat(sslBundle).isNull(); + } + } diff --git a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/amqp/RabbitContainerConnectionDetailsFactory.java b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/amqp/RabbitContainerConnectionDetailsFactory.java index 6c92958428d..f665a5648f0 100644 --- a/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/amqp/RabbitContainerConnectionDetailsFactory.java +++ b/spring-boot-project/spring-boot-testcontainers/src/main/java/org/springframework/boot/testcontainers/service/connection/amqp/RabbitContainerConnectionDetailsFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2023 the original author or authors. + * Copyright 2012-2025 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. @@ -22,6 +22,7 @@ import java.util.List; import org.testcontainers.containers.RabbitMQContainer; import org.springframework.boot.autoconfigure.amqp.RabbitConnectionDetails; +import org.springframework.boot.ssl.SslBundle; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionDetailsFactory; import org.springframework.boot.testcontainers.service.connection.ContainerConnectionSource; import org.springframework.boot.testcontainers.service.connection.ServiceConnection; @@ -66,10 +67,15 @@ class RabbitContainerConnectionDetailsFactory @Override public List
getAddresses() { - URI uri = URI.create(getContainer().getAmqpUrl()); + URI uri = URI.create((getSslBundle() != null) ? getContainer().getAmqpsUrl() : getContainer().getAmqpUrl()); return List.of(new Address(uri.getHost(), uri.getPort())); } + @Override + public SslBundle getSslBundle() { + return super.getSslBundle(); + } + } } diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java index 7ef58e9e7f8..1e336c1a541 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-amqp/src/dockerTest/java/smoketest/amqp/SampleAmqpSimpleApplicationSslTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2024 the original author or authors. + * Copyright 2012-2025 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. @@ -28,9 +28,10 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.testcontainers.service.connection.PemKeyStore; +import org.springframework.boot.testcontainers.service.connection.PemTrustStore; +import org.springframework.boot.testcontainers.service.connection.ServiceConnection; import org.springframework.boot.testsupport.container.TestImage; -import org.springframework.test.context.DynamicPropertyRegistry; -import org.springframework.test.context.DynamicPropertySource; import static org.assertj.core.api.Assertions.assertThat; @@ -39,25 +40,17 @@ import static org.assertj.core.api.Assertions.assertThat; * * @author Scott Frederick */ -@SpringBootTest(properties = { "spring.rabbitmq.ssl.bundle=client", - "spring.ssl.bundle.pem.client.keystore.certificate=classpath:ssl/test-client.crt", - "spring.ssl.bundle.pem.client.keystore.private-key=classpath:ssl/test-client.key", - "spring.ssl.bundle.pem.client.truststore.certificate=classpath:ssl/test-ca.crt" }) +@SpringBootTest @Testcontainers(disabledWithoutDocker = true) @ExtendWith(OutputCaptureExtension.class) class SampleAmqpSimpleApplicationSslTests { @Container + @ServiceConnection + @PemKeyStore(certificate = "classpath:ssl/test-client.crt", privateKey = "classpath:ssl/test-client.key") + @PemTrustStore("classpath:ssl/test-ca.crt") static final SecureRabbitMqContainer rabbit = TestImage.container(SecureRabbitMqContainer.class); - @DynamicPropertySource - static void secureRabbitMqProperties(DynamicPropertyRegistry registry) { - registry.add("spring.rabbitmq.host", rabbit::getHost); - registry.add("spring.rabbitmq.port", rabbit::getAmqpsPort); - registry.add("spring.rabbitmq.username", rabbit::getAdminUsername); - registry.add("spring.rabbitmq.password", rabbit::getAdminPassword); - } - @Autowired private Sender sender;