Skip to content

Commit 0aa7f47

Browse files
authored
Support passwordless connections for JMS ServiceBus in Spring (Azure#33489)
Support passwordless connections for JMS ServiceBus in Spring.
1 parent 31b4310 commit 0aa7f47

File tree

37 files changed

+1615
-135
lines changed

37 files changed

+1615
-135
lines changed

eng/code-quality-reports/src/main/resources/revapi/revapi.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,12 @@
136136
"old": "method java\\.lang\\.String com\\.azure\\.spring\\.cloud\\.autoconfigure\\.jms\\.properties\\.AzureServiceBusJmsProperties::(getPassword|getRemoteUrl|getUsername)\\(\\)",
137137
"justification": "Remove some meaningless jms properties"
138138
},
139+
{
140+
"code" : "java.annotation.attributeValueChanged",
141+
"old" : "class com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration",
142+
"new" : "class com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration",
143+
"justification": "Import ServiceBusJmsPasswordlessConfiguration.class"
144+
},
139145
{
140146
"regex": true,
141147
"code": "java\\.method\\.removed",

sdk/boms/spring-cloud-azure-dependencies/pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,7 @@
5151
<dependency>
5252
<groupId>com.azure</groupId>
5353
<artifactId>azure-sdk-bom</artifactId>
54-
<version>1.2.9</version> <!-- NOTE: This should be updated manually. -->
54+
<version>1.2.10</version> <!-- NOTE: This should be updated manually. -->
5555
<type>pom</type>
5656
<scope>import</scope>
5757
</dependency>
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.cloud.autoconfigure.implementation.passwordless;
5+
6+
import org.springframework.boot.SpringApplication;
7+
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
8+
import org.springframework.boot.env.EnvironmentPostProcessor;
9+
import org.springframework.core.Ordered;
10+
import org.springframework.core.env.ConfigurableEnvironment;
11+
import org.springframework.core.env.PropertiesPropertySource;
12+
13+
import java.util.ArrayList;
14+
import java.util.List;
15+
import java.util.Properties;
16+
17+
/**
18+
* Add properties to 'spring.cloud.function.ineligible-definitions' to filter ineligible functions that used by passwordless autoconfigurations.
19+
*
20+
* @since 4.7.0
21+
*/
22+
public class AzurePasswordlessEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {
23+
24+
/**
25+
* The order value of the {@link AzurePasswordlessEnvironmentPostProcessor}.
26+
*/
27+
public static final int ORDER = ConfigDataEnvironmentPostProcessor.ORDER + 2;
28+
29+
@Override
30+
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
31+
Properties properties = new Properties();
32+
List<String> passwordlessCredentialSupplier = new ArrayList<>();
33+
passwordlessCredentialSupplier.add("azureRedisCredentialSupplier");
34+
passwordlessCredentialSupplier.add("azureServiceBusJmsCredentialSupplier");
35+
properties.setProperty("spring.cloud.function.ineligible-definitions", String.join(",", passwordlessCredentialSupplier));
36+
environment.getPropertySources().addLast(new PropertiesPropertySource("passwordless", properties));
37+
}
38+
39+
@Override
40+
public int getOrder() {
41+
return ORDER;
42+
}
43+
}

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessor.java

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,15 +3,17 @@
33
package com.azure.spring.cloud.autoconfigure.jdbc;
44

55
import com.azure.core.credential.TokenCredential;
6+
import com.azure.identity.extensions.implementation.credential.TokenCredentialProviderOptions;
7+
import com.azure.identity.extensions.implementation.credential.provider.TokenCredentialProvider;
68
import com.azure.identity.extensions.implementation.enums.AuthProperty;
79
import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties;
810
import com.azure.spring.cloud.autoconfigure.implementation.jdbc.DatabaseType;
911
import com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcConnectionString;
1012
import com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcConnectionStringEnhancer;
11-
import com.azure.spring.cloud.core.implementation.credential.resolver.AzureTokenCredentialResolver;
13+
import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils;
1214
import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
1315
import com.azure.spring.cloud.service.implementation.identity.credential.provider.SpringTokenCredentialProvider;
14-
import com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties;
16+
import com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties;
1517
import org.slf4j.Logger;
1618
import org.slf4j.LoggerFactory;
1719
import org.springframework.beans.BeansException;
@@ -35,7 +37,6 @@
3537
import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_NAME_APPLICATION_NAME;
3638
import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_NAME_ASSUME_MIN_SERVER_VERSION;
3739
import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_VALUE_ASSUME_MIN_SERVER_VERSION;
38-
import static com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils.copyPropertiesIgnoreTargetNonNull;
3940
import static com.azure.spring.cloud.service.implementation.identity.credential.provider.SpringTokenCredentialProvider.PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME;
4041

4142

@@ -55,7 +56,7 @@ class JdbcPropertiesBeanPostProcessor implements BeanPostProcessor, EnvironmentA
5556
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
5657
if (bean instanceof DataSourceProperties) {
5758
DataSourceProperties dataSourceProperties = (DataSourceProperties) bean;
58-
AzurePasswordlessProperties properties = buildAzureProperties();
59+
AzureJdbcPasswordlessProperties properties = buildAzureProperties();
5960

6061
if (!properties.isPasswordlessEnabled()) {
6162
LOGGER.debug("Feature passwordless authentication is not enabled, skip enhancing jdbc url.");
@@ -128,17 +129,15 @@ private void enhanceUserAgent(DatabaseType databaseType, JdbcConnectionStringEnh
128129
}
129130
}
130131

131-
private Map<String, String> buildEnhancedProperties(DatabaseType databaseType, AzurePasswordlessProperties properties) {
132+
private Map<String, String> buildEnhancedProperties(DatabaseType databaseType, AzureJdbcPasswordlessProperties properties) {
132133
Map<String, String> result = new HashMap<>();
133-
AzureTokenCredentialResolver resolver = applicationContext.getBean(AzureTokenCredentialResolver.class);
134-
TokenCredential tokenCredential = resolver.resolve(properties);
134+
TokenCredentialProvider tokenCredentialProvider = TokenCredentialProvider.createDefault(new TokenCredentialProviderOptions(properties.toPasswordlessProperties()));
135+
TokenCredential tokenCredential = tokenCredentialProvider.get();
135136

136-
if (tokenCredential != null) {
137-
LOGGER.debug("Add SpringTokenCredentialProvider as the default token credential provider.");
138-
AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.setProperty(result, PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME);
139-
applicationContext.registerBean(PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME, TokenCredential.class, () -> tokenCredential);
140-
}
137+
AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.setProperty(result, PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME);
138+
applicationContext.registerBean(PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME, TokenCredential.class, () -> tokenCredential);
141139

140+
LOGGER.debug("Add SpringTokenCredentialProvider as the default token credential provider.");
142141
AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.setProperty(result, SPRING_TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME);
143142
AuthProperty.AUTHORITY_HOST.setProperty(result, properties.getProfile().getEnvironment().getActiveDirectoryEndpoint());
144143

@@ -157,12 +156,14 @@ public void setApplicationContext(ApplicationContext applicationContext) throws
157156
this.applicationContext = (GenericApplicationContext) applicationContext;
158157
}
159158

160-
private AzurePasswordlessProperties buildAzureProperties() {
159+
private AzureJdbcPasswordlessProperties buildAzureProperties() {
161160
AzureGlobalProperties azureGlobalProperties = applicationContext.getBean(AzureGlobalProperties.class);
162-
AzurePasswordlessProperties azurePasswordlessProperties = Binder.get(environment)
163-
.bindOrCreate(SPRING_CLOUD_AZURE_DATASOURCE_PREFIX, AzurePasswordlessProperties.class);
164-
copyPropertiesIgnoreTargetNonNull(azureGlobalProperties.getProfile(), azurePasswordlessProperties.getProfile());
165-
copyPropertiesIgnoreTargetNonNull(azureGlobalProperties.getCredential(), azurePasswordlessProperties.getCredential());
166-
return azurePasswordlessProperties;
161+
AzureJdbcPasswordlessProperties azurePasswordlessProperties = Binder.get(environment)
162+
.bindOrCreate(SPRING_CLOUD_AZURE_DATASOURCE_PREFIX, AzureJdbcPasswordlessProperties.class);
163+
164+
AzureJdbcPasswordlessProperties mergedProperties = new AzureJdbcPasswordlessProperties();
165+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties);
166+
return mergedProperties;
167+
167168
}
168169
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.cloud.autoconfigure.jms;
5+
6+
import com.azure.identity.extensions.implementation.template.AzureAuthenticationTemplate;
7+
8+
import java.util.Properties;
9+
import java.util.function.Supplier;
10+
11+
/**
12+
* AzureServiceBusJmsCredentialSupplier that provides a String as the password to connect Azure ServiceBus.
13+
*
14+
* @since 4.7.0
15+
*/
16+
public class AzureServiceBusJmsCredentialSupplier implements Supplier<String> {
17+
18+
private final AzureAuthenticationTemplate azureAuthenticationTemplate;
19+
20+
/**
21+
* Create {@link AzureServiceBusJmsCredentialSupplier} instance.
22+
* @param properties properties to initialize AzureServiceBusJmsCredentialSupplier.
23+
*/
24+
public AzureServiceBusJmsCredentialSupplier(Properties properties) {
25+
azureAuthenticationTemplate = new AzureAuthenticationTemplate();
26+
azureAuthenticationTemplate.init(properties);
27+
}
28+
29+
@Override
30+
public String get() {
31+
return azureAuthenticationTemplate.getTokenAsPassword();
32+
}
33+
34+
AzureServiceBusJmsCredentialSupplier(AzureAuthenticationTemplate azureAuthenticationTemplate) {
35+
this.azureAuthenticationTemplate = azureAuthenticationTemplate;
36+
}
37+
}

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfiguration.java

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
package com.azure.spring.cloud.autoconfigure.jms;
55

66
import com.azure.spring.cloud.autoconfigure.condition.ConditionalOnMissingProperty;
7+
import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties;
78
import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties;
89
import com.azure.spring.cloud.autoconfigure.resourcemanager.AzureServiceBusResourceManagerAutoConfiguration;
10+
import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils;
911
import com.azure.spring.cloud.core.provider.connectionstring.ServiceConnectionStringProvider;
1012
import com.azure.spring.cloud.core.service.AzureServiceType;
1113
import org.apache.qpid.jms.JmsConnectionExtensions;
@@ -31,6 +33,7 @@
3133
import java.util.HashMap;
3234
import java.util.Map;
3335

36+
import static com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier.AZURE_SPRING_PASSWORDLESS_SERVICE_BUS;
3437
import static com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier.AZURE_SPRING_SERVICE_BUS;
3538

3639
/**
@@ -45,17 +48,27 @@
4548
AzureServiceBusResourceManagerAutoConfiguration.class })
4649
@ConditionalOnProperty(value = "spring.jms.servicebus.enabled", matchIfMissing = true)
4750
@ConditionalOnClass({ ConnectionFactory.class, JmsConnectionFactory.class, JmsTemplate.class })
48-
@EnableConfigurationProperties({ AzureServiceBusJmsProperties.class, JmsProperties.class })
49-
@Import({ ServiceBusJmsConnectionFactoryConfiguration.class, ServiceBusJmsContainerConfiguration.class })
51+
@EnableConfigurationProperties({ JmsProperties.class })
52+
@Import({ ServiceBusJmsPasswordlessConfiguration.class, ServiceBusJmsConnectionFactoryConfiguration.class, ServiceBusJmsContainerConfiguration.class })
5053
public class ServiceBusJmsAutoConfiguration {
5154

55+
@Bean
56+
AzureServiceBusJmsProperties serviceBusJmsProperties(AzureGlobalProperties azureGlobalProperties) {
57+
AzureServiceBusJmsProperties properties = new AzureServiceBusJmsProperties();
58+
return mergeAzureProperties(azureGlobalProperties, properties);
59+
}
60+
5261
@Bean
5362
@ConditionalOnExpression("'premium'.equalsIgnoreCase('${spring.jms.servicebus.pricing-tier}')")
54-
ServiceBusJmsConnectionFactoryCustomizer amqpOpenPropertiesCustomizer() {
63+
ServiceBusJmsConnectionFactoryCustomizer amqpOpenPropertiesCustomizer(ObjectProvider<AzureServiceBusJmsCredentialSupplier> azureServiceBusJmsCredentialSupplier) {
5564
return factory -> {
5665
final Map<String, Object> properties = new HashMap<>();
5766
properties.put("com.microsoft:is-client-provider", true);
58-
properties.put("user-agent", AZURE_SPRING_SERVICE_BUS);
67+
if (azureServiceBusJmsCredentialSupplier.getIfAvailable() != null) {
68+
properties.put("user-agent", AZURE_SPRING_PASSWORDLESS_SERVICE_BUS);
69+
} else {
70+
properties.put("user-agent", AZURE_SPRING_SERVICE_BUS);
71+
}
5972
//set user agent
6073
factory.setExtension(JmsConnectionExtensions.AMQP_OPEN_PROPERTIES.toString(),
6174
(connection, uri) -> properties);
@@ -75,4 +88,10 @@ static AzureServiceBusJmsPropertiesBeanPostProcessor azureServiceBusJmsPropertie
7588
ObjectProvider<ServiceConnectionStringProvider<AzureServiceType.ServiceBus>> connectionStringProviders) {
7689
return new AzureServiceBusJmsPropertiesBeanPostProcessor(connectionStringProviders);
7790
}
91+
92+
private AzureServiceBusJmsProperties mergeAzureProperties(AzureGlobalProperties azureGlobalProperties, AzureServiceBusJmsProperties azurePasswordlessProperties) {
93+
AzureServiceBusJmsProperties mergedProperties = new AzureServiceBusJmsProperties();
94+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties);
95+
return mergedProperties;
96+
}
7897
}

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsConnectionFactoryFactory.java

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,20 +57,26 @@ private <T extends ServiceBusJmsConnectionFactory> void setPrefetchPolicy(T fact
5757
private <T extends ServiceBusJmsConnectionFactory> T createConnectionFactoryInstance(Class<T> factoryClass) {
5858
try {
5959
T factory;
60-
ServiceBusConnectionString serviceBusConnectionString = new ServiceBusConnectionString(properties.getConnectionString());
61-
String host = serviceBusConnectionString.getEndpointUri().getHost();
60+
if (properties.isPasswordlessEnabled()) {
61+
String remoteUrl = String.format(AMQP_URI_FORMAT,
62+
properties.getNamespace() + "." + properties.getProfile().getEnvironment().getServiceBusDomainName(),
63+
properties.getIdleTimeout().toMillis());
64+
factory = factoryClass.getConstructor(String.class).newInstance(remoteUrl);
65+
} else {
66+
ServiceBusConnectionString serviceBusConnectionString = new ServiceBusConnectionString(properties.getConnectionString());
67+
String host = serviceBusConnectionString.getEndpointUri().getHost();
6268

63-
String remoteUrl = String.format(AMQP_URI_FORMAT, host, properties.getIdleTimeout().toMillis());
64-
String username = serviceBusConnectionString.getSharedAccessKeyName();
65-
String password = serviceBusConnectionString.getSharedAccessKey();
69+
String remoteUrl = String.format(AMQP_URI_FORMAT, host, properties.getIdleTimeout().toMillis());
70+
String username = serviceBusConnectionString.getSharedAccessKeyName();
71+
String password = serviceBusConnectionString.getSharedAccessKey();
6672

67-
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
68-
factory = factoryClass.getConstructor(String.class, String.class, String.class)
69-
.newInstance(username, password, remoteUrl);
70-
} else {
71-
factory = factoryClass.getConstructor(String.class).newInstance(remoteUrl);
73+
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
74+
factory = factoryClass.getConstructor(String.class, String.class, String.class)
75+
.newInstance(username, password, remoteUrl);
76+
} else {
77+
factory = factoryClass.getConstructor(String.class).newInstance(remoteUrl);
78+
}
7279
}
73-
7480
return factory;
7581
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
7682
throw new IllegalStateException("Unable to create JmsConnectionFactory", ex);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License.
3+
4+
package com.azure.spring.cloud.autoconfigure.jms;
5+
6+
import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties;
7+
import org.apache.qpid.jms.JmsConnectionExtensions;
8+
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
9+
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
10+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
11+
import org.springframework.context.annotation.Bean;
12+
import org.springframework.context.annotation.Configuration;
13+
14+
/**
15+
* {@link EnableAutoConfiguration Auto-configuration} for Azure Service Bus JMS passwordless support.
16+
*
17+
* @since 4.7.0
18+
*/
19+
@Configuration(proxyBeanMethods = false)
20+
@ConditionalOnProperty(value = "spring.jms.servicebus.passwordless-enabled", havingValue = "true")
21+
class ServiceBusJmsPasswordlessConfiguration {
22+
23+
@Bean
24+
@ConditionalOnMissingBean
25+
AzureServiceBusJmsCredentialSupplier azureServiceBusJmsCredentialSupplier(AzureServiceBusJmsProperties azureServiceBusJmsProperties) {
26+
return new AzureServiceBusJmsCredentialSupplier(azureServiceBusJmsProperties.toPasswordlessProperties());
27+
}
28+
29+
@Bean
30+
ServiceBusJmsConnectionFactoryCustomizer jmsAADAuthenticationCustomizer(AzureServiceBusJmsCredentialSupplier credentialSupplier) {
31+
return factory -> {
32+
factory.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> "$jwt");
33+
factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), (connection, uri) ->
34+
credentialSupplier.get()
35+
);
36+
};
37+
}
38+
39+
40+
}

0 commit comments

Comments
 (0)