Skip to content

Commit

Permalink
Support passwordless connections for JMS ServiceBus in Spring (Azure#…
Browse files Browse the repository at this point in the history
…33489)

Support passwordless connections for JMS ServiceBus in Spring.
  • Loading branch information
backwind1233 authored Mar 3, 2023
1 parent 31b4310 commit 0aa7f47
Show file tree
Hide file tree
Showing 37 changed files with 1,615 additions and 135 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,12 @@
"old": "method java\\.lang\\.String com\\.azure\\.spring\\.cloud\\.autoconfigure\\.jms\\.properties\\.AzureServiceBusJmsProperties::(getPassword|getRemoteUrl|getUsername)\\(\\)",
"justification": "Remove some meaningless jms properties"
},
{
"code" : "java.annotation.attributeValueChanged",
"old" : "class com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration",
"new" : "class com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration",
"justification": "Import ServiceBusJmsPasswordlessConfiguration.class"
},
{
"regex": true,
"code": "java\\.method\\.removed",
Expand Down
2 changes: 1 addition & 1 deletion sdk/boms/spring-cloud-azure-dependencies/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-sdk-bom</artifactId>
<version>1.2.9</version> <!-- NOTE: This should be updated manually. -->
<version>1.2.10</version> <!-- NOTE: This should be updated manually. -->
<type>pom</type>
<scope>import</scope>
</dependency>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.autoconfigure.implementation.passwordless;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor;
import org.springframework.boot.env.EnvironmentPostProcessor;
import org.springframework.core.Ordered;
import org.springframework.core.env.ConfigurableEnvironment;
import org.springframework.core.env.PropertiesPropertySource;

import java.util.ArrayList;
import java.util.List;
import java.util.Properties;

/**
* Add properties to 'spring.cloud.function.ineligible-definitions' to filter ineligible functions that used by passwordless autoconfigurations.
*
* @since 4.7.0
*/
public class AzurePasswordlessEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered {

/**
* The order value of the {@link AzurePasswordlessEnvironmentPostProcessor}.
*/
public static final int ORDER = ConfigDataEnvironmentPostProcessor.ORDER + 2;

@Override
public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) {
Properties properties = new Properties();
List<String> passwordlessCredentialSupplier = new ArrayList<>();
passwordlessCredentialSupplier.add("azureRedisCredentialSupplier");
passwordlessCredentialSupplier.add("azureServiceBusJmsCredentialSupplier");
properties.setProperty("spring.cloud.function.ineligible-definitions", String.join(",", passwordlessCredentialSupplier));
environment.getPropertySources().addLast(new PropertiesPropertySource("passwordless", properties));
}

@Override
public int getOrder() {
return ORDER;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,15 +3,17 @@
package com.azure.spring.cloud.autoconfigure.jdbc;

import com.azure.core.credential.TokenCredential;
import com.azure.identity.extensions.implementation.credential.TokenCredentialProviderOptions;
import com.azure.identity.extensions.implementation.credential.provider.TokenCredentialProvider;
import com.azure.identity.extensions.implementation.enums.AuthProperty;
import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties;
import com.azure.spring.cloud.autoconfigure.implementation.jdbc.DatabaseType;
import com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcConnectionString;
import com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcConnectionStringEnhancer;
import com.azure.spring.cloud.core.implementation.credential.resolver.AzureTokenCredentialResolver;
import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils;
import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier;
import com.azure.spring.cloud.service.implementation.identity.credential.provider.SpringTokenCredentialProvider;
import com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties;
import com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
Expand All @@ -35,7 +37,6 @@
import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_NAME_APPLICATION_NAME;
import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_NAME_ASSUME_MIN_SERVER_VERSION;
import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_VALUE_ASSUME_MIN_SERVER_VERSION;
import static com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils.copyPropertiesIgnoreTargetNonNull;
import static com.azure.spring.cloud.service.implementation.identity.credential.provider.SpringTokenCredentialProvider.PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME;


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

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

private Map<String, String> buildEnhancedProperties(DatabaseType databaseType, AzurePasswordlessProperties properties) {
private Map<String, String> buildEnhancedProperties(DatabaseType databaseType, AzureJdbcPasswordlessProperties properties) {
Map<String, String> result = new HashMap<>();
AzureTokenCredentialResolver resolver = applicationContext.getBean(AzureTokenCredentialResolver.class);
TokenCredential tokenCredential = resolver.resolve(properties);
TokenCredentialProvider tokenCredentialProvider = TokenCredentialProvider.createDefault(new TokenCredentialProviderOptions(properties.toPasswordlessProperties()));
TokenCredential tokenCredential = tokenCredentialProvider.get();

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

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

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

private AzurePasswordlessProperties buildAzureProperties() {
private AzureJdbcPasswordlessProperties buildAzureProperties() {
AzureGlobalProperties azureGlobalProperties = applicationContext.getBean(AzureGlobalProperties.class);
AzurePasswordlessProperties azurePasswordlessProperties = Binder.get(environment)
.bindOrCreate(SPRING_CLOUD_AZURE_DATASOURCE_PREFIX, AzurePasswordlessProperties.class);
copyPropertiesIgnoreTargetNonNull(azureGlobalProperties.getProfile(), azurePasswordlessProperties.getProfile());
copyPropertiesIgnoreTargetNonNull(azureGlobalProperties.getCredential(), azurePasswordlessProperties.getCredential());
return azurePasswordlessProperties;
AzureJdbcPasswordlessProperties azurePasswordlessProperties = Binder.get(environment)
.bindOrCreate(SPRING_CLOUD_AZURE_DATASOURCE_PREFIX, AzureJdbcPasswordlessProperties.class);

AzureJdbcPasswordlessProperties mergedProperties = new AzureJdbcPasswordlessProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties);
return mergedProperties;

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.autoconfigure.jms;

import com.azure.identity.extensions.implementation.template.AzureAuthenticationTemplate;

import java.util.Properties;
import java.util.function.Supplier;

/**
* AzureServiceBusJmsCredentialSupplier that provides a String as the password to connect Azure ServiceBus.
*
* @since 4.7.0
*/
public class AzureServiceBusJmsCredentialSupplier implements Supplier<String> {

private final AzureAuthenticationTemplate azureAuthenticationTemplate;

/**
* Create {@link AzureServiceBusJmsCredentialSupplier} instance.
* @param properties properties to initialize AzureServiceBusJmsCredentialSupplier.
*/
public AzureServiceBusJmsCredentialSupplier(Properties properties) {
azureAuthenticationTemplate = new AzureAuthenticationTemplate();
azureAuthenticationTemplate.init(properties);
}

@Override
public String get() {
return azureAuthenticationTemplate.getTokenAsPassword();
}

AzureServiceBusJmsCredentialSupplier(AzureAuthenticationTemplate azureAuthenticationTemplate) {
this.azureAuthenticationTemplate = azureAuthenticationTemplate;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
package com.azure.spring.cloud.autoconfigure.jms;

import com.azure.spring.cloud.autoconfigure.condition.ConditionalOnMissingProperty;
import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties;
import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties;
import com.azure.spring.cloud.autoconfigure.resourcemanager.AzureServiceBusResourceManagerAutoConfiguration;
import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils;
import com.azure.spring.cloud.core.provider.connectionstring.ServiceConnectionStringProvider;
import com.azure.spring.cloud.core.service.AzureServiceType;
import org.apache.qpid.jms.JmsConnectionExtensions;
Expand All @@ -31,6 +33,7 @@
import java.util.HashMap;
import java.util.Map;

import static com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier.AZURE_SPRING_PASSWORDLESS_SERVICE_BUS;
import static com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier.AZURE_SPRING_SERVICE_BUS;

/**
Expand All @@ -45,17 +48,27 @@
AzureServiceBusResourceManagerAutoConfiguration.class })
@ConditionalOnProperty(value = "spring.jms.servicebus.enabled", matchIfMissing = true)
@ConditionalOnClass({ ConnectionFactory.class, JmsConnectionFactory.class, JmsTemplate.class })
@EnableConfigurationProperties({ AzureServiceBusJmsProperties.class, JmsProperties.class })
@Import({ ServiceBusJmsConnectionFactoryConfiguration.class, ServiceBusJmsContainerConfiguration.class })
@EnableConfigurationProperties({ JmsProperties.class })
@Import({ ServiceBusJmsPasswordlessConfiguration.class, ServiceBusJmsConnectionFactoryConfiguration.class, ServiceBusJmsContainerConfiguration.class })
public class ServiceBusJmsAutoConfiguration {

@Bean
AzureServiceBusJmsProperties serviceBusJmsProperties(AzureGlobalProperties azureGlobalProperties) {
AzureServiceBusJmsProperties properties = new AzureServiceBusJmsProperties();
return mergeAzureProperties(azureGlobalProperties, properties);
}

@Bean
@ConditionalOnExpression("'premium'.equalsIgnoreCase('${spring.jms.servicebus.pricing-tier}')")
ServiceBusJmsConnectionFactoryCustomizer amqpOpenPropertiesCustomizer() {
ServiceBusJmsConnectionFactoryCustomizer amqpOpenPropertiesCustomizer(ObjectProvider<AzureServiceBusJmsCredentialSupplier> azureServiceBusJmsCredentialSupplier) {
return factory -> {
final Map<String, Object> properties = new HashMap<>();
properties.put("com.microsoft:is-client-provider", true);
properties.put("user-agent", AZURE_SPRING_SERVICE_BUS);
if (azureServiceBusJmsCredentialSupplier.getIfAvailable() != null) {
properties.put("user-agent", AZURE_SPRING_PASSWORDLESS_SERVICE_BUS);
} else {
properties.put("user-agent", AZURE_SPRING_SERVICE_BUS);
}
//set user agent
factory.setExtension(JmsConnectionExtensions.AMQP_OPEN_PROPERTIES.toString(),
(connection, uri) -> properties);
Expand All @@ -75,4 +88,10 @@ static AzureServiceBusJmsPropertiesBeanPostProcessor azureServiceBusJmsPropertie
ObjectProvider<ServiceConnectionStringProvider<AzureServiceType.ServiceBus>> connectionStringProviders) {
return new AzureServiceBusJmsPropertiesBeanPostProcessor(connectionStringProviders);
}

private AzureServiceBusJmsProperties mergeAzureProperties(AzureGlobalProperties azureGlobalProperties, AzureServiceBusJmsProperties azurePasswordlessProperties) {
AzureServiceBusJmsProperties mergedProperties = new AzureServiceBusJmsProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties);
return mergedProperties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -57,20 +57,26 @@ private <T extends ServiceBusJmsConnectionFactory> void setPrefetchPolicy(T fact
private <T extends ServiceBusJmsConnectionFactory> T createConnectionFactoryInstance(Class<T> factoryClass) {
try {
T factory;
ServiceBusConnectionString serviceBusConnectionString = new ServiceBusConnectionString(properties.getConnectionString());
String host = serviceBusConnectionString.getEndpointUri().getHost();
if (properties.isPasswordlessEnabled()) {
String remoteUrl = String.format(AMQP_URI_FORMAT,
properties.getNamespace() + "." + properties.getProfile().getEnvironment().getServiceBusDomainName(),
properties.getIdleTimeout().toMillis());
factory = factoryClass.getConstructor(String.class).newInstance(remoteUrl);
} else {
ServiceBusConnectionString serviceBusConnectionString = new ServiceBusConnectionString(properties.getConnectionString());
String host = serviceBusConnectionString.getEndpointUri().getHost();

String remoteUrl = String.format(AMQP_URI_FORMAT, host, properties.getIdleTimeout().toMillis());
String username = serviceBusConnectionString.getSharedAccessKeyName();
String password = serviceBusConnectionString.getSharedAccessKey();
String remoteUrl = String.format(AMQP_URI_FORMAT, host, properties.getIdleTimeout().toMillis());
String username = serviceBusConnectionString.getSharedAccessKeyName();
String password = serviceBusConnectionString.getSharedAccessKey();

if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
factory = factoryClass.getConstructor(String.class, String.class, String.class)
.newInstance(username, password, remoteUrl);
} else {
factory = factoryClass.getConstructor(String.class).newInstance(remoteUrl);
if (StringUtils.hasLength(username) && StringUtils.hasLength(password)) {
factory = factoryClass.getConstructor(String.class, String.class, String.class)
.newInstance(username, password, remoteUrl);
} else {
factory = factoryClass.getConstructor(String.class).newInstance(remoteUrl);
}
}

return factory;
} catch (NoSuchMethodException | SecurityException | InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException ex) {
throw new IllegalStateException("Unable to create JmsConnectionFactory", ex);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

package com.azure.spring.cloud.autoconfigure.jms;

import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties;
import org.apache.qpid.jms.JmsConnectionExtensions;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/**
* {@link EnableAutoConfiguration Auto-configuration} for Azure Service Bus JMS passwordless support.
*
* @since 4.7.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(value = "spring.jms.servicebus.passwordless-enabled", havingValue = "true")
class ServiceBusJmsPasswordlessConfiguration {

@Bean
@ConditionalOnMissingBean
AzureServiceBusJmsCredentialSupplier azureServiceBusJmsCredentialSupplier(AzureServiceBusJmsProperties azureServiceBusJmsProperties) {
return new AzureServiceBusJmsCredentialSupplier(azureServiceBusJmsProperties.toPasswordlessProperties());
}

@Bean
ServiceBusJmsConnectionFactoryCustomizer jmsAADAuthenticationCustomizer(AzureServiceBusJmsCredentialSupplier credentialSupplier) {
return factory -> {
factory.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> "$jwt");
factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), (connection, uri) ->
credentialSupplier.get()
);
};
}


}
Loading

0 comments on commit 0aa7f47

Please sign in to comment.