diff --git a/eng/code-quality-reports/src/main/resources/revapi/revapi.json b/eng/code-quality-reports/src/main/resources/revapi/revapi.json index 7dba51f8db79..68b6bceed7b1 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -138,12 +138,6 @@ "old": "method void com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties::setUsername(java.lang.String)", "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.numberOfParametersChanged", diff --git a/sdk/spring/CHANGELOG.md b/sdk/spring/CHANGELOG.md index a201fc6f2c1a..044b3cd47454 100644 --- a/sdk/spring/CHANGELOG.md +++ b/sdk/spring/CHANGELOG.md @@ -6,6 +6,7 @@ This section includes changes in `spring-cloud-azure-autoconfigure` module. #### Features Added +- Support passwordless connections for JMS ServiceBus in Spring Boot 3. [#35608](https://github.com/Azure/azure-sdk-for-java/pull/35608). - Added autoconfiguration for the Event Grid client [#35613](https://github.com/Azure/azure-sdk-for-java/pull/35613). ## 5.2.0 (2023-06-02) diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessor.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessor.java index 0c3f494fef96..a9443e2dbc64 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessor.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessor.java @@ -3,12 +3,14 @@ package com.azure.spring.cloud.autoconfigure.implementation.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.implementation.context.properties.AzureGlobalProperties; -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; @@ -32,7 +34,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; @@ -52,7 +53,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."); @@ -125,17 +126,15 @@ private void enhanceUserAgent(DatabaseType databaseType, JdbcConnectionStringEnh } } - private Map buildEnhancedProperties(DatabaseType databaseType, AzurePasswordlessProperties properties) { + private Map buildEnhancedProperties(DatabaseType databaseType, AzureJdbcPasswordlessProperties properties) { Map 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()); @@ -154,12 +153,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; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/AzureServiceBusJmsCredentialSupplier.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/AzureServiceBusJmsCredentialSupplier.java new file mode 100644 index 000000000000..3d5782d7b28f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/AzureServiceBusJmsCredentialSupplier.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.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 { + + 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; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java index 56471491f4c1..3268ca074020 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfiguration.java @@ -4,11 +4,13 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms; import com.azure.spring.cloud.autoconfigure.implementation.condition.ConditionalOnMissingProperty; +import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.autoconfigure.implementation.resourcemanager.AzureServiceBusResourceManagerAutoConfiguration; -import com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsConnectionFactoryCustomizer; +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 com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsConnectionFactoryCustomizer; import jakarta.jms.ConnectionFactory; import org.apache.qpid.jms.JmsConnectionExtensions; import org.apache.qpid.jms.JmsConnectionFactory; @@ -32,6 +34,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; /** @@ -46,17 +49,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) { return factory -> { final Map 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); @@ -70,4 +83,10 @@ static AzureServiceBusJmsPropertiesBeanPostProcessor azureServiceBusJmsPropertie ObjectProvider> connectionStringProviders) { return new AzureServiceBusJmsPropertiesBeanPostProcessor(connectionStringProviders); } + + private AzureServiceBusJmsProperties mergeAzureProperties(AzureGlobalProperties azureGlobalProperties, AzureServiceBusJmsProperties azurePasswordlessProperties) { + AzureServiceBusJmsProperties mergedProperties = new AzureServiceBusJmsProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties); + return mergedProperties; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java index 7fd5cff6e404..b7b018947d83 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsConnectionFactoryFactory.java @@ -5,8 +5,8 @@ import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.jms.ServiceBusJmsConnectionFactory; -import com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsConnectionFactoryCustomizer; import com.azure.spring.cloud.core.implementation.connectionstring.ServiceBusConnectionString; +import com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsConnectionFactoryCustomizer; import org.apache.qpid.jms.policy.JmsDefaultPrefetchPolicy; import org.springframework.util.Assert; import org.springframework.util.StringUtils; @@ -56,20 +56,26 @@ private void setPrefetchPolicy(T fact private T createConnectionFactoryInstance(Class 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); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsPasswordlessConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsPasswordlessConfiguration.java new file mode 100644 index 000000000000..68a0a551ec4e --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsPasswordlessConfiguration.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.jms; + +import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; +import com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsConnectionFactoryCustomizer; +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() + ); + }; + } + + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsProperties.java index ac96f6248d61..e732a0fc627f 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsProperties.java @@ -3,6 +3,9 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms.properties; +import com.azure.spring.cloud.autoconfigure.implementation.properties.core.authentication.TokenCredentialConfigurationProperties; +import com.azure.spring.cloud.autoconfigure.implementation.properties.core.profile.AzureProfileConfigurationProperties; +import com.azure.spring.cloud.core.properties.PasswordlessProperties; import org.springframework.beans.factory.InitializingBean; import org.springframework.boot.autoconfigure.jms.JmsPoolConnectionFactoryProperties; import org.springframework.boot.context.properties.ConfigurationProperties; @@ -11,20 +14,58 @@ import org.springframework.util.StringUtils; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; @ConfigurationProperties(prefix = AzureServiceBusJmsProperties.PREFIX) -public class AzureServiceBusJmsProperties implements InitializingBean { +public class AzureServiceBusJmsProperties implements InitializingBean, PasswordlessProperties { /** * Service Bus JMS properties prefix. */ public static final String PREFIX = "spring.jms.servicebus"; + private static final String SERVICE_BUS_SCOPE_AZURE = "https://servicebus.azure.net/.default"; + private static final String SERVICE_BUS_SCOPE_AZURE_CHINA = SERVICE_BUS_SCOPE_AZURE; + private static final String SERVICE_BUS_SCOPE_AZURE_GERMANY = SERVICE_BUS_SCOPE_AZURE; + private static final String SERVICE_BUS_SCOPE_AZURE_US_GOVERNMENT = SERVICE_BUS_SCOPE_AZURE; + + private static final Map SERVICEBUS_SCOPE_MAP = new HashMap() { + { + put(CloudType.AZURE, SERVICE_BUS_SCOPE_AZURE); + put(CloudType.AZURE_CHINA, SERVICE_BUS_SCOPE_AZURE_CHINA); + put(CloudType.AZURE_GERMANY, SERVICE_BUS_SCOPE_AZURE_GERMANY); + put(CloudType.AZURE_US_GOVERNMENT, SERVICE_BUS_SCOPE_AZURE_US_GOVERNMENT); + } + }; + + private AzureProfileConfigurationProperties profile = new AzureProfileConfigurationProperties(); + + /** + * The scopes required for the access token. + */ + private String scopes; + + private TokenCredentialConfigurationProperties credential = new TokenCredentialConfigurationProperties(); + + /** + * Whether to enable supporting azure identity token credentials. + * + * If the value is true, then 'spring.jms.servicebus.namespace' must be set. + * If the passwordlessEnabled is true, it will try to authenticate connections with Azure AD. + */ + private boolean passwordlessEnabled = false; + /** * Whether to enable Service Bus JMS autoconfiguration. */ private boolean enabled = true; + /** + * The Service Bus namespace. + */ + private String namespace; + /** * Connection string to connect to a Service Bus namespace. */ @@ -103,10 +144,60 @@ public PrefetchPolicy getPrefetchPolicy() { return prefetchPolicy; } + public String getNamespace() { + return namespace; + } + + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + @Override + public String getScopes() { + return this.scopes == null ? getDefaultScopes() : this.scopes; + } + + public void setScopes(String scopes) { + this.scopes = scopes; + } + + @Override + public boolean isPasswordlessEnabled() { + return passwordlessEnabled; + } + + public void setPasswordlessEnabled(boolean passwordlessEnabled) { + this.passwordlessEnabled = passwordlessEnabled; + } + + @Override + public AzureProfileConfigurationProperties getProfile() { + return profile; + } + + public void setProfile(AzureProfileConfigurationProperties profile) { + this.profile = profile; + } + + @Override + public TokenCredentialConfigurationProperties getCredential() { + return credential; + } + + public void setCredential(TokenCredentialConfigurationProperties credential) { + this.credential = credential; + } + @Override public void afterPropertiesSet() throws Exception { - if (!StringUtils.hasText(connectionString)) { - throw new IllegalArgumentException("'spring.jms.servicebus.connection-string' should be provided"); + if (isPasswordlessEnabled()) { + if (!StringUtils.hasText(namespace)) { + throw new IllegalArgumentException("Passwordless connections enabled, 'spring.jms.servicebus.namespace' should be provided."); + } + } else { + if (!StringUtils.hasText(connectionString)) { + throw new IllegalArgumentException("'spring.jms.servicebus.connection-string' should be provided."); + } } if (null == pricingTier || !pricingTier.matches("(?i)premium|standard|basic")) { @@ -245,4 +336,8 @@ public void setPhase(Integer phase) { this.phase = phase; } } + + private String getDefaultScopes() { + return SERVICEBUS_SCOPE_MAP.getOrDefault(getProfile().getCloudType(), SERVICE_BUS_SCOPE_AZURE); + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/AzurePasswordlessEnvironmentPostProcessor.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/AzurePasswordlessEnvironmentPostProcessor.java new file mode 100644 index 000000000000..6ffcfb07a20a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/AzurePasswordlessEnvironmentPostProcessor.java @@ -0,0 +1,42 @@ +// 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 passwordlessCredentialSupplier = new ArrayList<>(); + 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; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 0ca3aed5c6d2..2f3aa7bc275a 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -532,68 +532,68 @@ "name": "spring.datasource.azure.credential.client-id", "type": "java.lang.String", "description": "Client ID to use when performing service principal authentication with Azure.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.credential.client-secret", "type": "java.lang.String", "description": "Client secret to use when performing service principal authentication with Azure.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.credential.client-certificate-password", "type": "java.lang.String", "description": "Password of the certificate file.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.credential.client-certificate-path", "type": "java.lang.String", "description": "Path of a PEM certificate file to use when performing service principal authentication with Azure.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.credential.username", "type": "java.lang.String", "description": "Username to use when performing username\/password authentication with Azure.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.credential.password", "type": "java.lang.String", "description": "Password to use when performing username\/password authentication with Azure.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.credential.managed-identity-enabled", "type": "java.lang.Boolean", "description": "Whether to enable managed identity to authenticate with Azure. If true and the client-id is set, will use the client ID as user assigned managed identity client ID.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties", + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties", "defaultValue": false }, { "name": "spring.datasource.azure.profile.environment.active-directory-endpoint", "type": "java.lang.String", "description": "The Azure Active Directory endpoint to connect to.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.profile.tenant-id", "type": "java.lang.String", "description": "Tenant ID for Azure resources.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.profile.cloud-type", "type": "java.lang.String", "description": "Name of the Azure cloud to connect to. Supported types are: AZURE, AZURE_CHINA, AZURE_US_GOVERNMENT, OTHER. The default value is `AZURE`.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties" + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties" }, { "name": "spring.datasource.azure.passwordless-enabled", "type": "java.lang.Boolean", "description": "Whether to enable passwordless connections to Azure databases by using OAuth2 Azure Active Directory token credentials.", - "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties", + "sourceType": "com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties", "defaultValue": false }, { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring.factories b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring.factories index 397e250c05b6..3951cbadacc6 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/resources/META-INF/spring.factories @@ -1,6 +1,7 @@ org.springframework.boot.env.EnvironmentPostProcessor=\ com.azure.spring.cloud.autoconfigure.implementation.context.AzureGlobalConfigurationEnvironmentPostProcessor,\ -com.azure.spring.cloud.autoconfigure.implementation.keyvault.environment.KeyVaultEnvironmentPostProcessor +com.azure.spring.cloud.autoconfigure.implementation.keyvault.environment.KeyVaultEnvironmentPostProcessor,\ +com.azure.spring.cloud.autoconfigure.implementation.passwordless.AzurePasswordlessEnvironmentPostProcessor org.springframework.boot.diagnostics.FailureAnalyzer=\ com.azure.spring.cloud.autoconfigure.implementation.compatibility.AzureCompatibilityNotMetFailureAnalyzer diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/AbstractAzureJdbcAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/AbstractAzureJdbcAutoConfigurationTest.java index 4483ecfc091b..761fd6e19289 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/AbstractAzureJdbcAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/AbstractAzureJdbcAutoConfigurationTest.java @@ -20,7 +20,7 @@ abstract class AbstractAzureJdbcAutoConfigurationTest { public static final String PUBLIC_AUTHORITY_HOST_STRING = AuthProperty.AUTHORITY_HOST.getPropertyKey() + "=" + "https://login.microsoftonline.com/"; - + public static final String PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING = AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.getPropertyKey() + "=" + "passwordlessTokenCredential"; abstract void pluginNotOnClassPath(); abstract void wrongJdbcUrl(); abstract void enhanceUrlWithDefaultCredential(); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorTest.java index ad05da8f6785..72f36bb8853c 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorTest.java @@ -34,6 +34,7 @@ class JdbcPropertiesBeanPostProcessorTest { private static final String POSTGRESQL_CONNECTION_STRING = "jdbc:postgresql://host/database?enableSwitch1&property1=value1"; private static final String PASSWORD = "password"; private static final String US_AUTHORITY_HOST_STRING = AuthProperty.AUTHORITY_HOST.getPropertyKey() + "=" + "https://login.microsoftonline.us/"; + public static final String PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING = AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.getPropertyKey() + "=" + "passwordlessTokenCredential"; private static final String POSTGRESQL_ASSUME_MIN_SERVER_VERSION = POSTGRESQL_PROPERTY_NAME_ASSUME_MIN_SERVER_VERSION + "=" + POSTGRESQL_PROPERTY_VALUE_ASSUME_MIN_SERVER_VERSION; @@ -97,6 +98,7 @@ void shouldPostprocessWhenSwitchOn() { String expectedJdbcUrl = enhanceJdbcUrl( DatabaseType.MYSQL, MYSQL_CONNECTION_STRING, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, MYSQL_USER_AGENT, AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName() ); @@ -121,7 +123,8 @@ void shouldGetCloudTypeFromAzureUsGov() { MYSQL_CONNECTION_STRING, MYSQL_USER_AGENT, AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName(), - US_AUTHORITY_HOST_STRING + US_AUTHORITY_HOST_STRING, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING ); assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl()); @@ -138,6 +141,7 @@ void mySqlUserAgentShouldConfigureIfConnectionAttributesIsEmpty() { String expectedJdbcUrl = enhanceJdbcUrl( DatabaseType.MYSQL, MYSQL_CONNECTION_STRING, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, MYSQL_USER_AGENT, AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName() ); @@ -159,7 +163,8 @@ void mySqlUserAgentShouldConfigureIfConnectionAttributesIsNotEmpty() { String expectedJdbcUrl = enhanceJdbcUrl( DatabaseType.MYSQL, baseUrl + ",_extension_version:" + AzureSpringIdentifier.AZURE_SPRING_MYSQL_OAUTH, - AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName() + AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName(), + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING ); assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl()); @@ -179,7 +184,8 @@ void mySqlUserAgentShouldConfigureIfConnectionAttributes() { String expectedJdbcUrl = enhanceJdbcUrl( DatabaseType.MYSQL, baseUrl + ",_extension_version:" + AzureSpringIdentifier.AZURE_SPRING_MYSQL_OAUTH, - AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName() + AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName(), + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING ); assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl()); } @@ -198,6 +204,7 @@ void postgreSqlUserAgentShouldConfigureIfNonApplicationNameProvided() { baseUrl, AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName(), APPLICATION_NAME.getName() + "=" + AzureSpringIdentifier.AZURE_SPRING_POSTGRESQL_OAUTH, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, POSTGRESQL_ASSUME_MIN_SERVER_VERSION ); @@ -219,6 +226,7 @@ void postgreSqlUserAgentShouldNotConfigureIfApplicationNameExists() { DatabaseType.POSTGRESQL, baseUrl, AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName(), + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, POSTGRESQL_ASSUME_MIN_SERVER_VERSION ); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java index 598c977be605..3a85569689b7 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java @@ -9,7 +9,7 @@ import com.azure.spring.cloud.autoconfigure.implementation.context.AzureTokenCredentialAutoConfiguration; import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; 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.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.jdbc.DataSourceProperties; @@ -28,12 +28,12 @@ class JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest { private static final String MYSQL_CONNECTION_STRING = "jdbc:mysql://host/database?enableSwitch1&property1=value1"; private static final String PUBLIC_AUTHORITY_HOST_STRING = AuthProperty.AUTHORITY_HOST.getPropertyKey() + "=" + "https://login.microsoftonline.com/"; - + public static final String PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING = AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.getPropertyKey() + "=" + "passwordlessTokenCredential"; private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration(AutoConfigurations.of(AzureJdbcAutoConfiguration.class, DataSourceProperties.class, - AzurePasswordlessProperties.class, + AzureJdbcPasswordlessProperties.class, AzureGlobalPropertiesAutoConfiguration.class, AzureTokenCredentialAutoConfiguration.class)); @@ -73,6 +73,7 @@ void mySqlAuthPluginOnClassPath() { String expectedJdbcUrl = enhanceJdbcUrl( DatabaseType.MYSQL, MYSQL_CONNECTION_STRING, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, PUBLIC_AUTHORITY_HOST_STRING, MYSQL_USER_AGENT, AuthProperty.TOKEN_CREDENTIAL_PROVIDER_CLASS_NAME.getPropertyKey() + "=" + SpringTokenCredentialProvider.class.getName() @@ -112,7 +113,7 @@ void testBindSpringBootProperties() { assertThat(context).hasSingleBean(SpringTokenCredentialProviderContextProvider.class); ConfigurableEnvironment environment = context.getEnvironment(); - AzurePasswordlessProperties properties = Binder.get(environment).bindOrCreate("spring.datasource.azure", AzurePasswordlessProperties.class); + AzureJdbcPasswordlessProperties properties = Binder.get(environment).bindOrCreate("spring.datasource.azure", AzureJdbcPasswordlessProperties.class); assertNotEquals("azure-client-id", properties.getCredential().getClientId()); assertEquals("fake-jdbc-client-id", properties.getCredential().getClientId()); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/MySqlAzureJdbcAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/MySqlAzureJdbcAutoConfigurationTest.java index 14cc80e4b5bb..a743dd418be7 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/MySqlAzureJdbcAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/MySqlAzureJdbcAutoConfigurationTest.java @@ -82,6 +82,7 @@ void enhanceUrlWithDefaultCredential() { DatabaseType.MYSQL, false, connectionString, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, PUBLIC_AUTHORITY_HOST_STRING, AUTHPROPERTY_TOKENCREDENTIALPROVIDERCLASSNAME_PROPERTY, MYSQL_USER_AGENT diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java index 0ed33c4ec949..301faa2d8902 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java @@ -70,6 +70,7 @@ void enhanceUrlWithDefaultCredential() { DatabaseType.POSTGRESQL, false, connectionString, + PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING, PUBLIC_AUTHORITY_HOST_STRING, POSTGRESQL_USER_AGENT, AUTHPROPERTY_TOKENCREDENTIALPROVIDERCLASSNAME_PROPERTY, diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfigurationTests.java index fabae0271c32..5a7860c16b58 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsAutoConfigurationTests.java @@ -3,10 +3,11 @@ package com.azure.spring.cloud.autoconfigure.implementation.jms; +import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; -import com.azure.spring.jms.ServiceBusJmsConnectionFactory; import com.azure.spring.cloud.core.provider.connectionstring.StaticConnectionStringProvider; import com.azure.spring.cloud.core.service.AzureServiceType; +import com.azure.spring.jms.ServiceBusJmsConnectionFactory; import jakarta.jms.ConnectionFactory; import jakarta.jms.Session; import org.apache.qpid.jms.JmsConnectionFactory; @@ -43,6 +44,7 @@ class ServiceBusJmsAutoConfigurationTests { + "SharedAccessKey=sasKey"; private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withBean(AzureGlobalProperties.class, () -> new AzureGlobalProperties()) .withConfiguration(AutoConfigurations.of(JmsAutoConfiguration.class, ServiceBusJmsAutoConfiguration.class)); private void testQueueJmsListenerContainerFactoryWithCustomSettings(AssertableApplicationContext loaded) { diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsPasswordlessConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsPasswordlessConfigurationTest.java new file mode 100644 index 000000000000..73b40323cfbf --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/ServiceBusJmsPasswordlessConfigurationTest.java @@ -0,0 +1,148 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.jms; + +import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; +import com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsConnectionFactoryCustomizer; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.mockito.MockedConstruction; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Import; + +import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.AZURE; +import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.AZURE_CHINA; +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mockConstruction; +import static org.mockito.Mockito.when; + +class ServiceBusJmsPasswordlessConfigurationTest { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(ServiceBusJmsPasswordlessTestConfig.class); + + @Test + void testPropertyEnabledIsFalse() { + AzureGlobalProperties azureProperties = new AzureGlobalProperties(); + azureProperties.getProfile().setCloudType(AZURE); + azureProperties.getProfile().getEnvironment().setActiveDirectoryEndpoint("abc"); + azureProperties.getProfile().getEnvironment().setActiveDirectoryGraphApiVersion("v2"); + + this.contextRunner + .withPropertyValues("spring.jms.servicebus.passwordless-enabled=false") + .withPropertyValues("spring.jms.servicebus.connection-string=fake-connection-string") + .withPropertyValues("spring.jms.servicebus.pricing-tier=standard") + .withBean(AzureGlobalProperties.class, () -> azureProperties) + .run(context -> { + assertThat(context).doesNotHaveBean(AzureServiceBusJmsCredentialSupplier.class); + assertThat(context).doesNotHaveBean(ServiceBusJmsConnectionFactoryCustomizer.class); + }); + } + + @Test + void testPropertyEnabledIsTrue() { + AzureGlobalProperties azureProperties = new AzureGlobalProperties(); + azureProperties.getProfile().setCloudType(AZURE); + azureProperties.getProfile().getEnvironment().setActiveDirectoryEndpoint("abc"); + azureProperties.getProfile().getEnvironment().setActiveDirectoryGraphApiVersion("v2"); + + this.contextRunner + .withPropertyValues("spring.jms.servicebus.passwordless-enabled=true") + .withPropertyValues("spring.jms.servicebus.namespace=testnamespace") + .withPropertyValues("spring.jms.servicebus.pricing-tier=standard") + .withBean(AzureGlobalProperties.class, () -> azureProperties) + .run(context -> { + assertThat(context).hasSingleBean(AzureServiceBusJmsCredentialSupplier.class); + assertThat(context).hasSingleBean(ServiceBusJmsConnectionFactoryCustomizer.class); + }); + } + + @Test + void testServiceBusPasswordlessProperties() { + AzureGlobalProperties azureProperties = new AzureGlobalProperties(); + azureProperties.getProfile().setCloudType(AZURE); + azureProperties.getProfile().getEnvironment().setActiveDirectoryEndpoint("abc"); + azureProperties.getProfile().getEnvironment().setActiveDirectoryGraphApiVersion("v2"); + this.contextRunner + .withPropertyValues("spring.jms.servicebus.passwordless-enabled=true") + .withPropertyValues("spring.jms.servicebus.namespace=testnamespace") + .withPropertyValues("spring.jms.servicebus.pricing-tier=standard") + .withPropertyValues("spring.jms.servicebus.scopes=scopes", + "spring.jms.servicebus.profile.tenant-id=tenant-id", + "spring.jms.servicebus.profile.subscription-id=subscription-id", + "spring.jms.servicebus.profile.CloudType=AZURE_CHINA", + "spring.jms.servicebus.credential.client-id=client-id", + "spring.jms.servicebus.credential.client-secret=secret", + "spring.jms.servicebus.credential.client-certificatePath=client-certificatePath", + "spring.jms.servicebus.credential.client-certificatePassword=client-certificatePassword", + "spring.jms.servicebus.credential.username=username", + "spring.jms.servicebus.credential.password=password", + "spring.jms.servicebus.credential.managed-identity-enabled=true", + "spring.jms.servicebus.client.application-id=application-id", + "spring.jms.servicebus.proxy.hostname=hostname", + "spring.jms.servicebus.proxy.username=username", + "spring.jms.servicebus.proxy.port=1111", + "spring.jms.servicebus.proxy.password=password", + "spring.jms.servicebus.proxy.type=type") + .withBean(AzureGlobalProperties.class, () -> azureProperties) + .run(context -> { + assertThat(context).hasSingleBean(AzureServiceBusJmsCredentialSupplier.class); + assertThat(context).hasSingleBean(ServiceBusJmsConnectionFactoryCustomizer.class); + AzureServiceBusJmsProperties properties = context.getBean(AzureServiceBusJmsProperties.class); + assertThat(properties.getScopes()).isEqualTo("scopes"); + assertThat(properties.getProfile().getTenantId()).isEqualTo("tenant-id"); + assertThat(properties.getProfile().getSubscriptionId()).isEqualTo("subscription-id"); + assertThat(properties.getProfile().getCloudType()).isEqualTo(AZURE_CHINA); + assertThat(properties.getCredential().getClientId()).isEqualTo("client-id"); + assertThat(properties.getCredential().getClientSecret()).isEqualTo("secret"); + assertThat(properties.getCredential().getClientCertificatePath()).isEqualTo("client-certificatePath"); + assertThat(properties.getCredential().getClientCertificatePassword()).isEqualTo("client-certificatePassword"); + assertThat(properties.getCredential().getUsername()).isEqualTo("username"); + assertThat(properties.getCredential().getPassword()).isEqualTo("password"); + assertThat(properties.getCredential().isManagedIdentityEnabled()).isTrue(); + + }); + } + + @Test + void testAzureServiceBusJmsCredentialSupplier() { + AzureGlobalProperties azureProperties = new AzureGlobalProperties(); + azureProperties.getProfile().setCloudType(AZURE); + azureProperties.getProfile().getEnvironment().setActiveDirectoryEndpoint("abc"); + azureProperties.getProfile().getEnvironment().setActiveDirectoryGraphApiVersion("v2"); + + this.contextRunner + .withPropertyValues("spring.jms.servicebus.passwordless-enabled=true") + .withBean(AzureGlobalProperties.class, () -> azureProperties); + + try (MockedConstruction supplierMockedConstruction = mockConstruction(AzureServiceBusJmsCredentialSupplier.class, + (azureServiceBusJmsCredentialSupplierMocker, mockerContext) -> { + when(azureServiceBusJmsCredentialSupplierMocker.get()).thenReturn("fake-token"); + + contextRunner.run(runnerContext -> { + assertThat(runnerContext).hasSingleBean(AzureServiceBusJmsCredentialSupplier.class); + assertThat(runnerContext).hasSingleBean(ServiceBusJmsConnectionFactoryCustomizer.class); + runnerContext.getBean(AzureServiceBusJmsCredentialSupplier.class).get().equals("fake-token"); + }); + + })) { + Assertions.assertNotNull(supplierMockedConstruction); + } + + } + + @EnableConfigurationProperties + @Import({ServiceBusJmsPasswordlessConfiguration.class}) + static class ServiceBusJmsPasswordlessTestConfig { + + @Bean + @ConfigurationProperties(AzureServiceBusJmsProperties.PREFIX) + AzureServiceBusJmsProperties jmsProperties() { + return new AzureServiceBusJmsProperties(); + } + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsPropertiesTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsPropertiesTests.java index 7c04bfb944c8..7364ad7642d9 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsPropertiesTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jms/properties/AzureServiceBusJmsPropertiesTests.java @@ -7,6 +7,10 @@ import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.NullAndEmptySource; import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.context.annotation.Bean; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -15,13 +19,16 @@ class AzureServiceBusJmsPropertiesTests { static final String CONNECTION_STRING = "Endpoint=sb://host/;SharedAccessKeyName=sasKeyName;" + "SharedAccessKey=sasKey"; + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withUserConfiguration(AzureServiceBusJmsPropertiesTestConfig.class); + @Test void connectionStringNotValid() { AzureServiceBusJmsProperties prop = new AzureServiceBusJmsProperties(); Exception ex = assertThrows(IllegalArgumentException.class, prop::afterPropertiesSet); - String expectedMessage = "'spring.jms.servicebus.connection-string' should be provided"; + String expectedMessage = "'spring.jms.servicebus.connection-string' should be provided."; String actualMessage = ex.getMessage(); System.out.println("message:" + actualMessage); assertTrue(actualMessage.contains(expectedMessage)); @@ -29,7 +36,7 @@ void connectionStringNotValid() { @ParameterizedTest @NullAndEmptySource - @ValueSource(strings = { "xx" }) + @ValueSource(strings = {"xx"}) void pricingTierNotValid(String pricingTier) { AzureServiceBusJmsProperties prop = new AzureServiceBusJmsProperties(); prop.setConnectionString(CONNECTION_STRING); @@ -43,4 +50,26 @@ void pricingTierNotValid(String pricingTier) { assertTrue(actualMessage.contains(expectedMessage)); } + @Test + void testPasswordlessEnabled() { + contextRunner + .withPropertyValues("spring.jms.servicebus.passwordless-enabled=true") + .run(context -> { + Exception ex = assertThrows(IllegalStateException.class, () -> context.getBean(AzureServiceBusJmsProperties.class)); + String actualMessage = ex.getCause().getMessage(); + String expectedMessage = "Passwordless connections enabled, 'spring.jms.servicebus.namespace' should be provided."; + assertTrue(actualMessage.contains(expectedMessage)); + }); + } + + @EnableConfigurationProperties + static class AzureServiceBusJmsPropertiesTestConfig { + + @Bean + @ConfigurationProperties(AzureServiceBusJmsProperties.PREFIX) + AzureServiceBusJmsProperties jmsProperties() { + return new AzureServiceBusJmsProperties(); + } + } + } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/MergeAzureCommonPropertiesTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/MergeAzureCommonPropertiesTest.java new file mode 100644 index 000000000000..9b4db99190af --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/MergeAzureCommonPropertiesTest.java @@ -0,0 +1,113 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.passwordless; + +import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties; +import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; +import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider; +import org.junit.jupiter.api.Test; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +class MergeAzureCommonPropertiesTest { + + @Test + void testGetPropertiesFromGlobalProperties() { + AzureGlobalProperties globalProperties = new AzureGlobalProperties(); + globalProperties.getCredential().setClientId("global-client-id"); + globalProperties.getCredential().setClientSecret("global-client-secret"); + globalProperties.getCredential().setPassword("global-password"); + globalProperties.getCredential().setUsername("global-user-name"); + globalProperties.getCredential().setManagedIdentityEnabled(true); + globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA); + globalProperties.getProfile().setSubscriptionId("global-sub"); + globalProperties.getProfile().setTenantId("global-tenant-id"); + + AzureServiceBusJmsProperties passwordlessProperties = new AzureServiceBusJmsProperties(); + + AzureServiceBusJmsProperties result = new AzureServiceBusJmsProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, passwordlessProperties, result); + + assertEquals("https://servicebus.azure.net/.default", result.getScopes()); + assertEquals("global-client-id", result.getCredential().getClientId()); + assertEquals("global-client-secret", result.getCredential().getClientSecret()); + assertEquals("global-password", result.getCredential().getPassword()); + assertEquals("global-user-name", result.getCredential().getUsername()); + assertEquals(false, result.getCredential().isManagedIdentityEnabled()); + assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType()); + assertEquals("global-sub", result.getProfile().getSubscriptionId()); + assertEquals("global-tenant-id", result.getProfile().getTenantId()); + } + + @Test + void testGetPropertiesFromAzurePasswordlessProperties() { + AzureGlobalProperties globalProperties = new AzureGlobalProperties(); + + AzureServiceBusJmsProperties passwordlessProperties = new AzureServiceBusJmsProperties(); + passwordlessProperties.setScopes("scopes-us-gov"); + passwordlessProperties.getCredential().setClientId("client-id"); + passwordlessProperties.getCredential().setClientSecret("client-secret"); + passwordlessProperties.getCredential().setPassword("password"); + passwordlessProperties.getCredential().setUsername("user-name"); + passwordlessProperties.getCredential().setManagedIdentityEnabled(true); + passwordlessProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT); + passwordlessProperties.getProfile().setSubscriptionId("sub"); + passwordlessProperties.getProfile().setTenantId("tenant-id"); + + AzureServiceBusJmsProperties result = new AzureServiceBusJmsProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, passwordlessProperties, result); + + assertEquals("scopes-us-gov", result.getScopes()); + assertEquals("client-id", result.getCredential().getClientId()); + assertEquals("client-secret", result.getCredential().getClientSecret()); + assertEquals("password", result.getCredential().getPassword()); + assertEquals("user-name", result.getCredential().getUsername()); + assertEquals(true, result.getCredential().isManagedIdentityEnabled()); + assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, result.getProfile().getCloudType()); + assertEquals("sub", result.getProfile().getSubscriptionId()); + assertEquals("tenant-id", result.getProfile().getTenantId()); + + } + + @Test + void testGetPropertiesFromGlobalAndPasswordlessProperties() { + + AzureGlobalProperties globalProperties = new AzureGlobalProperties(); + globalProperties.getCredential().setClientId("global-client-id"); + globalProperties.getCredential().setClientSecret("global-client-secret"); + globalProperties.getCredential().setPassword("global-password"); + globalProperties.getCredential().setUsername("global-user-name"); + globalProperties.getCredential().setManagedIdentityEnabled(true); + globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA); + globalProperties.getProfile().setSubscriptionId("global-sub"); + globalProperties.getProfile().setTenantId("global-tenant-id"); + globalProperties.getClient().setApplicationId("global-application-id"); + globalProperties.getProxy().setUsername("global-proxy-username"); + globalProperties.getProxy().setPassword("global-proxy-password"); + globalProperties.getProxy().setHostname("global-proxy-hostname"); + globalProperties.getProxy().setPort(1111); + + AzureServiceBusJmsProperties passwordlessProperties = new AzureServiceBusJmsProperties(); + passwordlessProperties.setScopes("scope"); + passwordlessProperties.getCredential().setClientSecret("client-secret"); + passwordlessProperties.getCredential().setPassword("password"); + passwordlessProperties.getCredential().setManagedIdentityEnabled(true); + passwordlessProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT); + passwordlessProperties.getProfile().setSubscriptionId("sub"); + + AzureServiceBusJmsProperties result = new AzureServiceBusJmsProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, passwordlessProperties, result); + + assertEquals("scope", result.getScopes()); + assertEquals("global-client-id", result.getCredential().getClientId()); + assertEquals("client-secret", result.getCredential().getClientSecret()); + assertEquals("password", result.getCredential().getPassword()); + assertEquals("global-user-name", result.getCredential().getUsername()); + assertEquals(true, result.getCredential().isManagedIdentityEnabled()); + assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, result.getProfile().getCloudType()); + assertEquals("sub", result.getProfile().getSubscriptionId()); + assertEquals("global-tenant-id", result.getProfile().getTenantId()); + } +} diff --git a/sdk/spring/spring-cloud-azure-core/pom.xml b/sdk/spring/spring-cloud-azure-core/pom.xml index 6b4d2ca24c1b..5906b4e047d7 100644 --- a/sdk/spring/spring-cloud-azure-core/pom.xml +++ b/sdk/spring/spring-cloud-azure-core/pom.xml @@ -62,6 +62,13 @@ 1.11.2 + + com.azure + azure-identity-extensions + 1.1.5 + true + + com.azure azure-storage-blob diff --git a/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtils.java b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtils.java new file mode 100644 index 000000000000..54df0ceecbe7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtils.java @@ -0,0 +1,107 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.core.implementation.util; + +import com.azure.spring.cloud.core.properties.AzureProperties; +import com.azure.spring.cloud.core.properties.PasswordlessProperties; +import org.springframework.beans.BeanUtils; +import org.springframework.beans.BeanWrapper; +import org.springframework.beans.BeanWrapperImpl; + +import java.beans.PropertyDescriptor; +import java.util.HashSet; +import java.util.Objects; +import java.util.Set; +import java.util.function.Predicate; + +/** + * Util class for AzurePasswordlessProperties. + */ +public final class AzurePasswordlessPropertiesUtils { + + private AzurePasswordlessPropertiesUtils() { + + } + + /** + * Copy common properties from source {@link AzureProperties} object to target {@link T extends PasswordlessProperties} object. + * If a field x.y.z exists in both source and target object, the source value will override the target value. + * + * @param source The source {@link AzureProperties} object. + * @param target The target object. + * @param The type of the target that extends PasswordlessProperties. + */ + public static void copyAzureCommonProperties(AzureProperties source, T target) { + // call explicitly for these fields could be defined as final + + BeanUtils.copyProperties(source.getProfile(), target.getProfile()); + BeanUtils.copyProperties(source.getProfile().getEnvironment(), target.getProfile().getEnvironment()); + BeanUtils.copyProperties(source.getCredential(), target.getCredential()); + + } + + /** + * Copy common properties from source {@link PasswordlessProperties} object to target {@link T extends PasswordlessProperties} object. Ignore the source + * value if it is null. + * + * @param source The source {@link PasswordlessProperties} object. + * @param target The target object. + * @param The type of the target that extends PasswordlessProperties. + */ + public static void copyAzureCommonPropertiesIgnoreNull(PasswordlessProperties source, T target) { + + copyPropertiesIgnoreNull(source.getProfile(), target.getProfile()); + copyPropertiesIgnoreNull(source.getProfile().getEnvironment(), target.getProfile().getEnvironment()); + copyPropertiesIgnoreNull(source.getCredential(), target.getCredential()); + + target.setScopes(source.getScopes()); + target.setPasswordlessEnabled(source.isPasswordlessEnabled()); + } + + /** + * Merge properties from a {@link AzureProperties} object and a {@link PasswordlessProperties} object. If a same property appears in both two objects, the + * value from the latter will take precedence. + * @param defaultProperties The default properties, the merge result will take value from this property as default. + * @param properties The overridden properties, the merge result will take value from this if a same property + * appears in two property objects. + * @param target The merge result. + * @param The type of the merge result. + */ + public static void mergeAzureCommonProperties(AzureProperties defaultProperties, + PasswordlessProperties properties, + T target) { + copyAzureCommonProperties(defaultProperties, target); + copyAzureCommonPropertiesIgnoreNull(properties, target); + } + + /** + * Copy common properties from source object to target object. Ignore the source value if it is null. + * + * @param source The source object. + * @param target The target object. + */ + public static void copyPropertiesIgnoreNull(Object source, Object target) { + BeanUtils.copyProperties(source, target, findNullPropertyNames(source)); + } + + private static String[] findPropertyNames(Object source, Predicate predicate) { + final Set emptyNames = new HashSet<>(); + + final BeanWrapper beanWrapper = new BeanWrapperImpl(source); + PropertyDescriptor[] pds = beanWrapper.getPropertyDescriptors(); + + for (PropertyDescriptor pd : pds) { + Object srcValue = beanWrapper.getPropertyValue(pd.getName()); + if (predicate.test(srcValue)) { + emptyNames.add(pd.getName()); + } + } + return emptyNames.toArray(new String[0]); + } + + private static String[] findNullPropertyNames(Object source) { + return findPropertyNames(source, Objects::isNull); + } + +} diff --git a/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzureSpringIdentifier.java b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzureSpringIdentifier.java index 933b6c014496..036e42ec8f9d 100644 --- a/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzureSpringIdentifier.java +++ b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzureSpringIdentifier.java @@ -57,6 +57,7 @@ private AzureSpringIdentifier() { * Azure Spring ServiceBus */ public static final String AZURE_SPRING_SERVICE_BUS = "az-sp-bus/" + VERSION; + public static final String AZURE_SPRING_PASSWORDLESS_SERVICE_BUS = "az-sp-pl-sb/" + VERSION; /** * Azure Spring Storage Blob diff --git a/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/properties/PasswordlessProperties.java b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/properties/PasswordlessProperties.java new file mode 100644 index 000000000000..2389c228a364 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/properties/PasswordlessProperties.java @@ -0,0 +1,137 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.core.properties; + +import com.azure.identity.extensions.implementation.enums.AuthProperty; +import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider; +import com.azure.spring.cloud.core.provider.authentication.TokenCredentialOptionsProvider; + +import java.util.Properties; +import java.util.function.BiConsumer; +import java.util.function.Function; + +/** + * Unified properties for Azure passwordless clients. + */ +public interface PasswordlessProperties extends TokenCredentialOptionsProvider, AzureProfileOptionsProvider { + + /** + * Get the scopes required for the access token. + * + * @return scopes required for the access token + */ + String getScopes(); + + /** + * Set the scopes to get the access token. + * + * @param scopes the scopes required for the access token + */ + void setScopes(String scopes); + + /** + * Whether to enable connections authenticating with Azure AD, default is false. + * + * @return Whether to enable connections authenticating with Azure AD. + */ + boolean isPasswordlessEnabled(); + + /** + * Set the passwordlessEnabled value. + * + * @param passwordlessEnabled the passwordlessEnabled + */ + void setPasswordlessEnabled(boolean passwordlessEnabled); + + /** + * Convert {@link PasswordlessProperties} to {@link Properties}. + * @return converted {@link Properties} instance + */ + default Properties toPasswordlessProperties() { + Properties target = new Properties(); + for (AzurePasswordlessPropertiesMapping m : AzurePasswordlessPropertiesMapping.values()) { + if (m.getter.apply(this) != null) { + m.setter.accept(target, m.getter.apply(this)); + } + } + return target; + } + + /** + * A mapping util used to convert a {@link PasswordlessProperties} instance to a {@link Properties} instance. + */ + enum AzurePasswordlessPropertiesMapping { + + /** + * Getter function and setter biConsumer for scopes. + */ + scopes(p -> p.getScopes(), + (p, s) -> p.setProperty(AuthProperty.SCOPES.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for clientCertificatePassword. + */ + clientCertificatePassword(p -> p.getCredential().getClientCertificatePassword(), + (p, s) -> p.setProperty(AuthProperty.CLIENT_CERTIFICATE_PASSWORD.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for clientCertificatePath. + */ + clientCertificatePath(p -> p.getCredential().getClientCertificatePath(), + (p, s) -> p.setProperty(AuthProperty.CLIENT_CERTIFICATE_PATH.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for clientId. + */ + clientId(p -> p.getCredential().getClientId(), + (p, s) -> p.setProperty(AuthProperty.CLIENT_ID.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for clientSecret. + */ + clientSecret(p -> p.getCredential().getClientSecret(), + (p, s) -> p.setProperty(AuthProperty.CLIENT_SECRET.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for managedIdentityEnabled. + */ + managedIdentityEnabled(p -> String.valueOf(p.getCredential().isManagedIdentityEnabled()), + (p, s) -> p.setProperty(AuthProperty.MANAGED_IDENTITY_ENABLED.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for password. + */ + password(p -> p.getCredential().getPassword(), + (p, s) -> p.setProperty(AuthProperty.PASSWORD.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for username. + */ + username(p -> p.getCredential().getUsername(), + (p, s) -> p.setProperty(AuthProperty.USERNAME.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for tenantId. + */ + tenantId(p -> p.getProfile().getTenantId(), + (p, s) -> p.setProperty(AuthProperty.TENANT_ID.getPropertyKey(), s)), + + /** + * Getter function and setter biConsumer for authorityHost. + */ + authorityHost(p -> p.getProfile().getEnvironment().getActiveDirectoryEndpoint(), + (p, s) -> p.setProperty(AuthProperty.AUTHORITY_HOST.getPropertyKey(), s)); + + private Function getter; + private BiConsumer setter; + + AzurePasswordlessPropertiesMapping(Function getter, BiConsumer setter) { + this.getter = getter; + this.setter = setter; + } + + } +} + diff --git a/sdk/spring/spring-cloud-azure-core/src/test/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtilsTest.java b/sdk/spring/spring-cloud-azure-core/src/test/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtilsTest.java new file mode 100644 index 000000000000..dc552df767f7 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-core/src/test/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtilsTest.java @@ -0,0 +1,385 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.core.implementation.util; + +import com.azure.core.management.AzureEnvironment; +import com.azure.spring.cloud.core.properties.AzureProperties; +import com.azure.spring.cloud.core.properties.PasswordlessProperties; +import com.azure.spring.cloud.core.properties.authentication.TokenCredentialProperties; +import com.azure.spring.cloud.core.properties.client.ClientProperties; +import com.azure.spring.cloud.core.properties.profile.AzureProfileProperties; +import com.azure.spring.cloud.core.properties.proxy.ProxyProperties; +import com.azure.spring.cloud.core.properties.retry.RetryProperties; +import com.azure.spring.cloud.core.provider.RetryOptionsProvider; +import org.junit.jupiter.api.Test; + +import java.time.Duration; + +import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.AZURE; +import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.AZURE_CHINA; +import static com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider.CloudType.OTHER; +import static com.azure.spring.cloud.core.provider.RetryOptionsProvider.RetryMode.FIXED; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +class AzurePasswordlessPropertiesUtilsTest { + + @Test + void testCopyPropertiesToNewObjectShouldEqual() { + AzurePropertiesA source = new AzurePropertiesA(); + source.client.setApplicationId("client-application-id-A"); + source.profile.setCloudType(AZURE_CHINA); + source.profile.setTenantId("profile-tenant-id-A"); + source.profile.setSubscriptionId("profile-sub-id-A"); + source.profile.getEnvironment().setActiveDirectoryEndpoint("aad-endpoint-A"); + source.proxy.setType("proxy-type-A"); + source.proxy.setHostname("proxy-hostname-A"); + source.proxy.setPort(1234); + source.proxy.setUsername("proxy-username-A"); + source.proxy.setPassword("proxy-password-A"); + source.retry.setMode(FIXED); + source.retry.getExponential().setMaxRetries(3); + source.retry.getExponential().setBaseDelay(Duration.ofSeconds(4)); + source.retry.getExponential().setMaxDelay(Duration.ofSeconds(5)); + source.retry.getFixed().setDelay(Duration.ofSeconds(6)); + source.retry.getFixed().setMaxRetries(7); + source.credential.setClientId("credential-client-id-A"); + source.credential.setClientSecret("credential-client-secret-A"); + source.credential.setClientCertificatePath("credential-client-cert-path-A"); + source.credential.setClientCertificatePassword("credential-client-cert-password-A"); + source.credential.setUsername("credential-username-A"); + source.credential.setPassword("credential-password-A"); + source.credential.setManagedIdentityEnabled(true); + + final PasswordlessPropertiesB target = new PasswordlessPropertiesB(); + AzurePasswordlessPropertiesUtils.copyAzureCommonProperties(source, target); + + assertEquals(AZURE_CHINA, target.profile.getCloudType()); + assertEquals("profile-tenant-id-A", target.profile.getTenantId()); + assertEquals("profile-sub-id-A", target.profile.getSubscriptionId()); + assertEquals("aad-endpoint-A", target.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE_CHINA.getActiveDirectoryGraphApiVersion(), target.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-A", target.credential.getClientId()); + assertEquals("credential-client-secret-A", target.credential.getClientSecret()); + assertEquals("credential-client-cert-path-A", target.credential.getClientCertificatePath()); + assertEquals("credential-client-cert-password-A", target.credential.getClientCertificatePassword()); + assertEquals("credential-username-A", target.credential.getUsername()); + assertEquals("credential-password-A", target.credential.getPassword()); + + } + + @Test + void testCopyPropertiesToObjectWithSameFieldsSetShouldOverrideWithNull() { + PasswordlessPropertiesB target = new PasswordlessPropertiesB(); + target.profile.setCloudType(AZURE); + target.profile.setTenantId("profile-tenant-id-B"); + target.profile.setSubscriptionId("profile-sub-id-B"); + target.profile.getEnvironment().setActiveDirectoryEndpoint("aad-endpoint-B"); + target.credential.setClientId("credential-client-id-B"); + target.credential.setClientSecret("credential-client-secret-B"); + target.credential.setClientCertificatePath("credential-client-cert-path-B"); + target.credential.setClientCertificatePassword("credential-client-cert-password-B"); + target.credential.setUsername("credential-username-B"); + target.credential.setPassword("credential-password-B"); + target.credential.setManagedIdentityEnabled(true); + + // assert properties have been set correctly to target + assertEquals(AZURE, target.profile.getCloudType()); + assertEquals("profile-tenant-id-B", target.profile.getTenantId()); + assertEquals("profile-sub-id-B", target.profile.getSubscriptionId()); + assertEquals("aad-endpoint-B", target.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryGraphApiVersion(), target.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-B", target.credential.getClientId()); + assertEquals("credential-client-secret-B", target.credential.getClientSecret()); + assertEquals("credential-client-cert-path-B", target.credential.getClientCertificatePath()); + assertEquals("credential-client-cert-password-B", target.credential.getClientCertificatePassword()); + assertEquals("credential-username-B", target.credential.getUsername()); + assertEquals("credential-password-B", target.credential.getPassword()); + + AzurePropertiesA source = new AzurePropertiesA(); + source.client.setApplicationId("client-application-id-A"); + source.profile.setCloudType(AZURE_CHINA); + source.profile.setTenantId("profile-tenant-id-A"); + source.proxy.setHostname("proxy-hostname-A"); + source.retry.getExponential().setMaxRetries(13); + source.retry.getExponential().setMaxDelay(Duration.ofSeconds(14)); + source.credential.setClientId("credential-client-id-A"); + + AzurePasswordlessPropertiesUtils.copyAzureCommonProperties(source, target); + + // assert properties have been set correctly to target after copy + assertEquals(AZURE_CHINA, target.profile.getCloudType()); + assertEquals("profile-tenant-id-A", target.profile.getTenantId()); + assertNull(target.profile.getSubscriptionId()); + assertEquals(AzureEnvironment.AZURE_CHINA.getActiveDirectoryEndpoint(), target.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE_CHINA.getActiveDirectoryGraphApiVersion(), target.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-A", target.credential.getClientId()); + assertNull(target.credential.getClientSecret()); + assertNull(target.credential.getClientCertificatePath()); + assertNull(target.credential.getClientCertificatePassword()); + assertNull(target.credential.getUsername()); + assertNull(target.credential.getPassword()); + } + + @Test + void testCopyPropertiesIgnoresNullToObjectWithSameFieldsSetShouldOverrideWithoutNull() { + PasswordlessPropertiesB target = new PasswordlessPropertiesB(); + target.profile.setCloudType(AZURE); + target.profile.setTenantId("profile-tenant-id-B"); + target.profile.setSubscriptionId("profile-sub-id-B"); + target.profile.getEnvironment().setActiveDirectoryEndpoint("aad-endpoint-B"); + target.credential.setClientId("credential-client-id-B"); + target.credential.setClientSecret("credential-client-secret-B"); + target.credential.setClientCertificatePath("credential-client-cert-path-B"); + target.credential.setClientCertificatePassword("credential-client-cert-password-B"); + target.credential.setUsername("credential-username-B"); + target.credential.setPassword("credential-password-B"); + target.credential.setManagedIdentityEnabled(true); + + // assert properties have been set correctly to target + assertEquals(AZURE, target.profile.getCloudType()); + assertEquals("profile-tenant-id-B", target.profile.getTenantId()); + assertEquals("profile-sub-id-B", target.profile.getSubscriptionId()); + assertEquals("aad-endpoint-B", target.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryGraphApiVersion(), target.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-B", target.credential.getClientId()); + assertEquals("credential-client-secret-B", target.credential.getClientSecret()); + assertEquals("credential-client-cert-path-B", target.credential.getClientCertificatePath()); + assertEquals("credential-client-cert-password-B", target.credential.getClientCertificatePassword()); + assertEquals("credential-username-B", target.credential.getUsername()); + assertEquals("credential-password-B", target.credential.getPassword()); + + PasswordlessPropertiesB source = new PasswordlessPropertiesB(); + source.profile.setCloudType(AZURE_CHINA); + source.profile.setTenantId("profile-tenant-id-A"); + source.profile.getEnvironment().setActiveDirectoryEndpoint("aad-endpoint-A"); + source.credential.setClientId("credential-client-id-A"); + source.setPasswordlessEnabled(true); + source.setScopes("fake-scopes"); + + AzurePasswordlessPropertiesUtils.copyAzureCommonPropertiesIgnoreNull(source, target); + + // assert properties have been set correctly to target after copy + assertEquals(AZURE_CHINA, target.profile.getCloudType()); + assertEquals("profile-tenant-id-A", target.profile.getTenantId()); + assertEquals("profile-sub-id-B", target.profile.getSubscriptionId()); + assertEquals("aad-endpoint-A", target.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE_CHINA.getActiveDirectoryGraphApiVersion(), target.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-A", target.credential.getClientId()); + assertEquals("credential-client-secret-B", target.credential.getClientSecret()); + assertEquals("credential-client-cert-path-B", target.credential.getClientCertificatePath()); + assertEquals("credential-client-cert-password-B", target.credential.getClientCertificatePassword()); + assertEquals("credential-username-B", target.credential.getUsername()); + assertEquals("credential-password-B", target.credential.getPassword()); + assertTrue(target.isPasswordlessEnabled()); + assertEquals("fake-scopes", target.getScopes()); + + } + + @Test + void testMergePropertiesObjectWithSameFieldsSetShouldTakeLater() { + AzurePropertiesA defaultProperties = new AzurePropertiesA(); + defaultProperties.client.setApplicationId("client-application-id-B"); + defaultProperties.profile.setCloudType(AZURE); + defaultProperties.profile.setTenantId("profile-tenant-id-B"); + defaultProperties.profile.setSubscriptionId("profile-sub-id-B"); + defaultProperties.profile.getEnvironment().setActiveDirectoryEndpoint("aad-endpoint-B"); + defaultProperties.proxy.setType("proxy-type-B"); + defaultProperties.proxy.setHostname("proxy-hostname-B"); + defaultProperties.proxy.setPort(1234); + defaultProperties.proxy.setUsername("proxy-username-B"); + defaultProperties.proxy.setPassword("proxy-password-B"); + defaultProperties.retry.setMode(FIXED); + defaultProperties.retry.getExponential().setMaxRetries(3); + defaultProperties.retry.getExponential().setBaseDelay(Duration.ofSeconds(4)); + defaultProperties.retry.getExponential().setMaxDelay(Duration.ofSeconds(5)); + defaultProperties.retry.getFixed().setDelay(Duration.ofSeconds(6)); + defaultProperties.retry.getFixed().setMaxRetries(7); + defaultProperties.credential.setClientId("credential-client-id-B"); + defaultProperties.credential.setClientSecret("credential-client-secret-B"); + defaultProperties.credential.setClientCertificatePath("credential-client-cert-path-B"); + defaultProperties.credential.setClientCertificatePassword("credential-client-cert-password-B"); + defaultProperties.credential.setUsername("credential-username-B"); + defaultProperties.credential.setPassword("credential-password-B"); + defaultProperties.credential.setManagedIdentityEnabled(true); + + // assert properties have been set correctly to defaultProperties + assertEquals(AZURE, defaultProperties.profile.getCloudType()); + assertEquals("profile-tenant-id-B", defaultProperties.profile.getTenantId()); + assertEquals("profile-sub-id-B", defaultProperties.profile.getSubscriptionId()); + assertEquals("aad-endpoint-B", defaultProperties.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryGraphApiVersion(), defaultProperties.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-B", defaultProperties.credential.getClientId()); + assertEquals("credential-client-secret-B", defaultProperties.credential.getClientSecret()); + assertEquals("credential-client-cert-path-B", defaultProperties.credential.getClientCertificatePath()); + assertEquals("credential-client-cert-password-B", defaultProperties.credential.getClientCertificatePassword()); + assertEquals("credential-username-B", defaultProperties.credential.getUsername()); + assertEquals("credential-password-B", defaultProperties.credential.getPassword()); + + PasswordlessPropertiesB propertiesToOverride = new PasswordlessPropertiesB(); + propertiesToOverride.profile.setCloudType(AZURE_CHINA); + propertiesToOverride.profile.setTenantId("profile-tenant-id-A"); + propertiesToOverride.profile.getEnvironment().setActiveDirectoryEndpoint("aad-endpoint-A"); + propertiesToOverride.credential.setClientId("credential-client-id-A"); + propertiesToOverride.setScopes("fake-scopes-1"); + + PasswordlessPropertiesB result = new PasswordlessPropertiesB(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(defaultProperties, propertiesToOverride, result); + + // assert properties have been set correctly to result after copy + assertEquals(AZURE_CHINA, result.profile.getCloudType()); + assertEquals("profile-tenant-id-A", result.profile.getTenantId()); + assertEquals("profile-sub-id-B", result.profile.getSubscriptionId()); + assertEquals("aad-endpoint-A", result.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals(AzureEnvironment.AZURE_CHINA.getActiveDirectoryGraphApiVersion(), result.profile.getEnvironment().getActiveDirectoryGraphApiVersion()); + assertEquals("credential-client-id-A", result.credential.getClientId()); + assertEquals("credential-client-secret-B", result.credential.getClientSecret()); + assertEquals("credential-client-cert-path-B", result.credential.getClientCertificatePath()); + assertEquals("credential-client-cert-password-B", result.credential.getClientCertificatePassword()); + assertEquals("credential-username-B", result.credential.getUsername()); + assertEquals("credential-password-B", result.credential.getPassword()); + assertEquals("fake-scopes-1", result.getScopes()); + assertFalse(result.isPasswordlessEnabled()); + } + + @Test + void testCopyPropertiesIgnoreNullToObjectWithDifferentFieldsSetShouldMerge() { + PasswordlessPropertiesB source = new PasswordlessPropertiesB(); + source.credential.setClientId("client-id-A"); + source.getProfile().setCloudType(AZURE); + source.setScopes("fake-scopes"); + source.setPasswordlessEnabled(true); + + PasswordlessPropertiesB target = new PasswordlessPropertiesB(); + target.credential.setClientSecret("client-secret-B"); + target.profile.setCloudType(OTHER); + target.profile.getEnvironment().setActiveDirectoryEndpoint("abc"); + + assertTrue(source.isPasswordlessEnabled()); + assertFalse(target.isPasswordlessEnabled()); + assertEquals("fake-scopes", source.getScopes()); + assertNull(target.getScopes()); + + assertEquals(AZURE, source.getProfile().getCloudType()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryEndpoint(), + source.profile.getEnvironment().getActiveDirectoryEndpoint()); + assertEquals("client-secret-B", target.credential.getClientSecret()); + + AzurePasswordlessPropertiesUtils.copyAzureCommonPropertiesIgnoreNull(source, target); + + // target properties should be merged properties from source + target + assertEquals("client-id-A", target.credential.getClientId()); + assertEquals("client-secret-B", target.credential.getClientSecret()); + assertEquals(AZURE, source.getProfile().getCloudType()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryEndpoint(), + target.profile.getEnvironment().getActiveDirectoryEndpoint()); + + + // source properties should not be updated + assertNull(source.credential.getClientSecret()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryEndpoint(), + source.profile.getEnvironment().getActiveDirectoryEndpoint()); + + assertTrue(source.isPasswordlessEnabled()); + assertTrue(target.isPasswordlessEnabled()); + assertEquals("fake-scopes", source.getScopes()); + assertEquals("fake-scopes", target.getScopes()); + } + + @Test + void testCopyPropertiesSourceNotChanged() { + AzurePropertiesA source = new AzurePropertiesA(); + source.credential.setClientId("client-id-A"); + source.getProfile().setCloudType(AZURE); + + PasswordlessPropertiesB target = new PasswordlessPropertiesB(); + + AzurePasswordlessPropertiesUtils.copyAzureCommonProperties(source, target); + + assertEquals("client-id-A", target.credential.getClientId()); + + // Update target will not affect source + target.profile.setCloudType(OTHER); + target.profile.getEnvironment().setActiveDirectoryEndpoint("abc"); + + assertNull(source.retry.getExponential().getBaseDelay()); + assertEquals(AzureEnvironment.AZURE.getActiveDirectoryEndpoint(), + source.profile.getEnvironment().getActiveDirectoryEndpoint()); + } + + + static class AzurePropertiesA implements AzureProperties, RetryOptionsProvider { + + private final ClientProperties client = new ClientProperties(); + private final ProxyProperties proxy = new ProxyProperties(); + private final RetryProperties retry = new RetryProperties(); + private final TokenCredentialProperties credential = new TokenCredentialProperties(); + private final AzureProfileProperties profile = new AzureProfileProperties(); + + @Override + public ClientProperties getClient() { + return client; + } + + @Override + public ProxyProperties getProxy() { + return proxy; + } + + @Override + public RetryProperties getRetry() { + return retry; + } + + @Override + public TokenCredentialProperties getCredential() { + return credential; + } + + @Override + public AzureProfileProperties getProfile() { + return profile; + } + } + + static class PasswordlessPropertiesB implements PasswordlessProperties { + + private final TokenCredentialProperties credential = new TokenCredentialProperties(); + private final AzureProfileProperties profile = new AzureProfileProperties(); + private String scopes; + private boolean passwordlessEnabled = false; + + @Override + public TokenCredentialProperties getCredential() { + return credential; + } + + @Override + public AzureProfileProperties getProfile() { + return profile; + } + + @Override + public String getScopes() { + return scopes; + } + + @Override + public void setScopes(String scopes) { + this.scopes = scopes; + } + + @Override + public boolean isPasswordlessEnabled() { + return passwordlessEnabled; + } + + @Override + public void setPasswordlessEnabled(boolean passwordlessEnabled) { + this.passwordlessEnabled = passwordlessEnabled; + } + } +} diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/servicebus/jms/ServiceBusJmsPasswordlessIT.java b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/servicebus/jms/ServiceBusJmsPasswordlessIT.java new file mode 100644 index 000000000000..76a5ffdd4651 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/java/com/azure/spring/cloud/integration/tests/servicebus/jms/ServiceBusJmsPasswordlessIT.java @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.spring.cloud.integration.tests.servicebus.jms; + +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.Timeout; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jms.annotation.JmsListener; +import org.springframework.jms.core.JmsTemplate; +import org.springframework.test.context.ActiveProfiles; + +import java.util.concurrent.Exchanger; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) +@ActiveProfiles("servicebus-jms-passwordless") +public class ServiceBusJmsPasswordlessIT { + private static final Logger LOGGER = LoggerFactory.getLogger(ServiceBusJmsPasswordlessIT.class); + private static final String DATA = "service bus jms passwordless test"; + private static final String QUEUE_NAME = "passwordless_que001"; + private final Exchanger EXCHANGER = new Exchanger<>(); + + @Autowired + private JmsTemplate jmsTemplate; + + @Test + @Timeout(70) + void testServiceBusJmsOperation() throws InterruptedException { + LOGGER.info("ServiceBusJmsPasswordlessIT begin."); + jmsTemplate.convertAndSend(QUEUE_NAME, DATA); + LOGGER.info("Send message: {}", DATA); + String msg = EXCHANGER.exchange(null); + Assertions.assertEquals(DATA, msg); + LOGGER.info("ServiceBusJmsPasswordlessIT end."); + } + + @JmsListener(destination = QUEUE_NAME, containerFactory = "jmsListenerContainerFactory") + void receiveQueueMessage(String message) throws InterruptedException { + LOGGER.info("Received message from queue: {}", message); + EXCHANGER.exchange(message); + } +} diff --git a/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-servicebus-jms-passwordless.yml b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-servicebus-jms-passwordless.yml new file mode 100644 index 000000000000..14a77cc58923 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-integration-tests/src/test/resources/application-servicebus-jms-passwordless.yml @@ -0,0 +1,8 @@ +spring: + jms: + servicebus: + enabled: true + pricing-tier: standard + passwordless-enabled: true + namespace: ${AZURE_SERVICE_BUS_NAMESPACE} + diff --git a/sdk/spring/spring-cloud-azure-integration-tests/test-resources/servicebus/test-resources.json b/sdk/spring/spring-cloud-azure-integration-tests/test-resources/servicebus/test-resources.json index 636ef04e1bbf..fce64f635082 100644 --- a/sdk/spring/spring-cloud-azure-integration-tests/test-resources/servicebus/test-resources.json +++ b/sdk/spring/spring-cloud-azure-integration-tests/test-resources/servicebus/test-resources.json @@ -84,6 +84,31 @@ "enableExpress": false } }, + { + "type": "Microsoft.ServiceBus/namespaces/queues", + "apiVersion": "2022-01-01-preview", + "name": "[concat(variables('azureServiceBusNamespaceName'), '/passwordless_que001')]", + "location": "[variables('location')]", + "dependsOn": [ + "[resourceId('Microsoft.ServiceBus/namespaces', variables('azureServiceBusNamespaceName'))]" + ], + "properties": { + "maxMessageSizeInKilobytes": 256, + "lockDuration": "PT30S", + "maxSizeInMegabytes": 1024, + "requiresDuplicateDetection": false, + "requiresSession": false, + "defaultMessageTimeToLive": "P14D", + "deadLetteringOnMessageExpiration": false, + "enableBatchedOperations": true, + "duplicateDetectionHistoryTimeWindow": "PT10M", + "maxDeliveryCount": 10, + "status": "Active", + "autoDeleteOnIdle": "P10675199DT2H48M5.4775807S", + "enablePartitioning": false, + "enableExpress": false + } + }, { "type": "Microsoft.ServiceBus/namespaces/queues", "apiVersion": "2022-01-01-preview", diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureJdbcPasswordlessProperties.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureJdbcPasswordlessProperties.java new file mode 100644 index 000000000000..689b13a25c0a --- /dev/null +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureJdbcPasswordlessProperties.java @@ -0,0 +1,125 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.service.implementation.passwordless; + +import com.azure.spring.cloud.core.properties.PasswordlessProperties; +import com.azure.spring.cloud.core.properties.authentication.TokenCredentialProperties; +import com.azure.spring.cloud.core.properties.profile.AzureProfileProperties; + +import java.util.HashMap; +import java.util.Map; + +/** + * Configuration properties for passwordless connections with Azure Database. + */ +public class AzureJdbcPasswordlessProperties implements PasswordlessProperties { + + private static final String JDBC_SCOPE_AZURE = "https://ossrdbms-aad.database.windows.net/.default"; + private static final String JDBC_SCOPE_AZURE_CHINA = "https://ossrdbms-aad.database.chinacloudapi.cn/.default"; + private static final String JDBC_SCOPE_AZURE_GERMANY = "https://ossrdbms-aad.database.cloudapi.de/.default"; + private static final String JDBC_SCOPE_AZURE_US_GOVERNMENT = "https://ossrdbms-aad.database.usgovcloudapi.net/.default"; + + private static final Map JDBC_SCOPE_MAP = new HashMap() { + { + put(CloudType.AZURE, JDBC_SCOPE_AZURE); + put(CloudType.AZURE_CHINA, JDBC_SCOPE_AZURE_CHINA); + put(CloudType.AZURE_GERMANY, JDBC_SCOPE_AZURE_GERMANY); + put(CloudType.AZURE_US_GOVERNMENT, JDBC_SCOPE_AZURE_US_GOVERNMENT); + } + }; + + /** + * Whether to enable supporting azure identity token credentials, by default is false. + * + * If the passwordlessEnabled is true, but the 'spring.datasource.password' property is not empty, it will still use username/password to authenticate connections. + * To use passwordless connections, you need to remove 'spring.datasource.password' property. + */ + private boolean passwordlessEnabled = false; + + private AzureProfileProperties profile = new AzureProfileProperties(); + + private String scopes; + + private TokenCredentialProperties credential = new TokenCredentialProperties(); + + /** + * Get the scopes required for the access token. + * + * @return scopes required for the access token + */ + @Override + public String getScopes() { + return this.scopes == null ? getDefaultScopes() : this.scopes; + } + + /** + * Set the scopes required for the access token. + * + * @param scopes the scopes required for the access token + */ + public void setScopes(String scopes) { + this.scopes = scopes; + } + + /** + * Whether to enable connections authenticating with Azure AD, default is false. + * + * @return enable connections authenticating with Azure AD if true, otherwise false. + */ + @Override + public boolean isPasswordlessEnabled() { + return this.passwordlessEnabled; + } + + /** + * Set the value to enable/disable connections authenticating with Azure AD. + * If not set, by default the value is false. + * + * @param passwordlessEnabled the passwordlessEnabled + */ + @Override + public void setPasswordlessEnabled(boolean passwordlessEnabled) { + this.passwordlessEnabled = passwordlessEnabled; + } + + private String getDefaultScopes() { + return JDBC_SCOPE_MAP.getOrDefault(getProfile().getCloudType(), JDBC_SCOPE_AZURE); + } + + /** + * Get the profile + * @return the profile + */ + @Override + public AzureProfileProperties getProfile() { + return profile; + } + + /** + * Set the profile + * @param profile the profile properties related to an Azure subscription + */ + public void setProfile(AzureProfileProperties profile) { + this.profile = profile; + } + + /** + * Get the credential properties. + * + * @return the credential properties. + */ + @Override + public TokenCredentialProperties getCredential() { + return credential; + } + + /** + * Set the credential properties. + * + * @param credential the credential properties + */ + public void setCredential(TokenCredentialProperties credential) { + this.credential = credential; + } +} diff --git a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureKafkaPasswordlessProperties.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureKafkaPasswordlessProperties.java new file mode 100644 index 000000000000..9b99c5f0795f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureKafkaPasswordlessProperties.java @@ -0,0 +1,98 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.service.implementation.passwordless; + +import com.azure.spring.cloud.core.properties.PasswordlessProperties; +import com.azure.spring.cloud.core.properties.authentication.TokenCredentialProperties; +import com.azure.spring.cloud.core.properties.profile.AzureProfileProperties; + +/** + * Configuration properties for passwordless connections with Azure ServiceBus. + */ +public class AzureKafkaPasswordlessProperties implements PasswordlessProperties { + + private boolean passwordlessEnabled = false; + + private AzureProfileProperties profile = new AzureProfileProperties(); + + private TokenCredentialProperties credential = new TokenCredentialProperties(); + + /** + * Get the scopes required for the access token. + * This method is not available in AzureKafkaPasswordlessProperties, will always return null. + * + * @return null + */ + @Override + public String getScopes() { + return null; + } + + /** + * Set the scopes required for the access token. + * + * This method is not available in AzureKafkaPasswordlessProperties + */ + @Override + public void setScopes(String scopes) { + throw new RuntimeException("This method is not available in AzureKafkaPasswordlessProperties"); + } + + /** + * Whether to enable connections authenticating with Azure AD, default is false. + * + * @return enable connections authenticating with Azure AD if true, otherwise false. + */ + @Override + public boolean isPasswordlessEnabled() { + return this.passwordlessEnabled; + } + + /** + * Set the value to enable/disable connections authenticating with Azure AD. + * If not set, by default the value is false. + * + * @param passwordlessEnabled the passwordlessEnabled + */ + @Override + public void setPasswordlessEnabled(boolean passwordlessEnabled) { + this.passwordlessEnabled = passwordlessEnabled; + } + + /** + * Get the profile + * @return the profile + */ + @Override + public AzureProfileProperties getProfile() { + return profile; + } + + /** + * Set the profile + * @param profile the profile properties related to an Azure subscription + */ + public void setProfile(AzureProfileProperties profile) { + this.profile = profile; + } + + /** + * Get the credential properties. + * + * @return the credential properties. + */ + @Override + public TokenCredentialProperties getCredential() { + return credential; + } + + /** + * Set the credential properties. + * + * @param credential the credential properties + */ + public void setCredential(TokenCredentialProperties credential) { + this.credential = credential; + } +} diff --git a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java index 03f3b5cfc03e..403828fa7bdb 100644 --- a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java +++ b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/kafka/KafkaOAuth2AuthenticateCallbackHandlerTest.java @@ -11,8 +11,8 @@ import com.azure.spring.cloud.service.implementation.jaas.Jaas; import com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties; import org.apache.kafka.common.config.types.Password; -import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; import org.apache.kafka.common.security.oauthbearer.OAuthBearerLoginModule; +import org.apache.kafka.common.security.oauthbearer.OAuthBearerTokenCallback; import org.junit.jupiter.api.Test; import org.mockito.Mockito; import org.springframework.test.util.ReflectionTestUtils; diff --git a/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml b/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml index 0b318ae54255..45fded1050c6 100644 --- a/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml @@ -141,6 +141,11 @@ netty-codec-http 4.1.90.Final + + com.azure + azure-identity-extensions + 1.1.5 +