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 bdd7e064b9846..8561cad9d7b5b 100644 --- a/eng/code-quality-reports/src/main/resources/revapi/revapi.json +++ b/eng/code-quality-reports/src/main/resources/revapi/revapi.json @@ -136,6 +136,12 @@ "old": "method java\\.lang\\.String com\\.azure\\.spring\\.cloud\\.autoconfigure\\.jms\\.properties\\.AzureServiceBusJmsProperties::(getPassword|getRemoteUrl|getUsername)\\(\\)", "justification": "Remove some meaningless jms properties" }, + { + "code" : "java.annotation.attributeValueChanged", + "old" : "class com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration", + "new" : "class com.azure.spring.cloud.autoconfigure.jms.ServiceBusJmsAutoConfiguration", + "justification": "Import ServiceBusJmsPasswordlessConfiguration.class" + }, { "regex": true, "code": "java\\.method\\.removed", diff --git a/sdk/boms/spring-cloud-azure-dependencies/pom.xml b/sdk/boms/spring-cloud-azure-dependencies/pom.xml index 1b0e9bf7d7d7e..a619c9cdc8c48 100644 --- a/sdk/boms/spring-cloud-azure-dependencies/pom.xml +++ b/sdk/boms/spring-cloud-azure-dependencies/pom.xml @@ -51,7 +51,7 @@ com.azure azure-sdk-bom - 1.2.9 + 1.2.10 pom import 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 0000000000000..fa46fef2af5fb --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/AzurePasswordlessEnvironmentPostProcessor.java @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.implementation.passwordless; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.context.config.ConfigDataEnvironmentPostProcessor; +import org.springframework.boot.env.EnvironmentPostProcessor; +import org.springframework.core.Ordered; +import org.springframework.core.env.ConfigurableEnvironment; +import org.springframework.core.env.PropertiesPropertySource; + +import java.util.ArrayList; +import java.util.List; +import java.util.Properties; + +/** + * Add properties to 'spring.cloud.function.ineligible-definitions' to filter ineligible functions that used by passwordless autoconfigurations. + * + * @since 4.7.0 + */ +public class AzurePasswordlessEnvironmentPostProcessor implements EnvironmentPostProcessor, Ordered { + + /** + * The order value of the {@link AzurePasswordlessEnvironmentPostProcessor}. + */ + public static final int ORDER = ConfigDataEnvironmentPostProcessor.ORDER + 2; + + @Override + public void postProcessEnvironment(ConfigurableEnvironment environment, SpringApplication application) { + Properties properties = new Properties(); + List passwordlessCredentialSupplier = new ArrayList<>(); + passwordlessCredentialSupplier.add("azureRedisCredentialSupplier"); + passwordlessCredentialSupplier.add("azureServiceBusJmsCredentialSupplier"); + properties.setProperty("spring.cloud.function.ineligible-definitions", String.join(",", passwordlessCredentialSupplier)); + environment.getPropertySources().addLast(new PropertiesPropertySource("passwordless", properties)); + } + + @Override + public int getOrder() { + return ORDER; + } +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessor.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessor.java index aa1aa765cc996..7e16de6c04045 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessor.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessor.java @@ -3,15 +3,17 @@ package com.azure.spring.cloud.autoconfigure.jdbc; import com.azure.core.credential.TokenCredential; +import com.azure.identity.extensions.implementation.credential.TokenCredentialProviderOptions; +import com.azure.identity.extensions.implementation.credential.provider.TokenCredentialProvider; import com.azure.identity.extensions.implementation.enums.AuthProperty; import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.jdbc.DatabaseType; import com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcConnectionString; import com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcConnectionStringEnhancer; -import com.azure.spring.cloud.core.implementation.credential.resolver.AzureTokenCredentialResolver; +import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; import com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier; import com.azure.spring.cloud.service.implementation.identity.credential.provider.SpringTokenCredentialProvider; -import com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties; +import com.azure.spring.cloud.service.implementation.passwordless.AzureJdbcPasswordlessProperties; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.BeansException; @@ -35,7 +37,6 @@ import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_NAME_APPLICATION_NAME; import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_NAME_ASSUME_MIN_SERVER_VERSION; import static com.azure.spring.cloud.autoconfigure.implementation.jdbc.JdbcPropertyConstants.POSTGRESQL_PROPERTY_VALUE_ASSUME_MIN_SERVER_VERSION; -import static com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils.copyPropertiesIgnoreTargetNonNull; import static com.azure.spring.cloud.service.implementation.identity.credential.provider.SpringTokenCredentialProvider.PASSWORDLESS_TOKEN_CREDENTIAL_BEAN_NAME; @@ -55,7 +56,7 @@ class JdbcPropertiesBeanPostProcessor implements BeanPostProcessor, EnvironmentA public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException { if (bean instanceof DataSourceProperties) { DataSourceProperties dataSourceProperties = (DataSourceProperties) bean; - AzurePasswordlessProperties properties = buildAzureProperties(); + AzureJdbcPasswordlessProperties properties = buildAzureProperties(); if (!properties.isPasswordlessEnabled()) { LOGGER.debug("Feature passwordless authentication is not enabled, skip enhancing jdbc url."); @@ -128,17 +129,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()); @@ -157,12 +156,14 @@ public void setApplicationContext(ApplicationContext applicationContext) throws this.applicationContext = (GenericApplicationContext) applicationContext; } - private AzurePasswordlessProperties buildAzureProperties() { + private AzureJdbcPasswordlessProperties buildAzureProperties() { AzureGlobalProperties azureGlobalProperties = applicationContext.getBean(AzureGlobalProperties.class); - AzurePasswordlessProperties azurePasswordlessProperties = Binder.get(environment) - .bindOrCreate(SPRING_CLOUD_AZURE_DATASOURCE_PREFIX, AzurePasswordlessProperties.class); - copyPropertiesIgnoreTargetNonNull(azureGlobalProperties.getProfile(), azurePasswordlessProperties.getProfile()); - copyPropertiesIgnoreTargetNonNull(azureGlobalProperties.getCredential(), azurePasswordlessProperties.getCredential()); - return azurePasswordlessProperties; + AzureJdbcPasswordlessProperties azurePasswordlessProperties = Binder.get(environment) + .bindOrCreate(SPRING_CLOUD_AZURE_DATASOURCE_PREFIX, AzureJdbcPasswordlessProperties.class); + + AzureJdbcPasswordlessProperties mergedProperties = new AzureJdbcPasswordlessProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties); + return mergedProperties; + } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsCredentialSupplier.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsCredentialSupplier.java new file mode 100644 index 0000000000000..a54f916913b56 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/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.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/jms/ServiceBusJmsAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfiguration.java index 412ebcc113fe7..d003479c08028 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfiguration.java @@ -4,8 +4,10 @@ package com.azure.spring.cloud.autoconfigure.jms; import com.azure.spring.cloud.autoconfigure.condition.ConditionalOnMissingProperty; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.autoconfigure.resourcemanager.AzureServiceBusResourceManagerAutoConfiguration; +import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; import com.azure.spring.cloud.core.provider.connectionstring.ServiceConnectionStringProvider; import com.azure.spring.cloud.core.service.AzureServiceType; import org.apache.qpid.jms.JmsConnectionExtensions; @@ -31,6 +33,7 @@ import java.util.HashMap; import java.util.Map; +import static com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier.AZURE_SPRING_PASSWORDLESS_SERVICE_BUS; import static com.azure.spring.cloud.core.implementation.util.AzureSpringIdentifier.AZURE_SPRING_SERVICE_BUS; /** @@ -45,17 +48,27 @@ AzureServiceBusResourceManagerAutoConfiguration.class }) @ConditionalOnProperty(value = "spring.jms.servicebus.enabled", matchIfMissing = true) @ConditionalOnClass({ ConnectionFactory.class, JmsConnectionFactory.class, JmsTemplate.class }) -@EnableConfigurationProperties({ AzureServiceBusJmsProperties.class, JmsProperties.class }) -@Import({ ServiceBusJmsConnectionFactoryConfiguration.class, ServiceBusJmsContainerConfiguration.class }) +@EnableConfigurationProperties({ JmsProperties.class }) +@Import({ ServiceBusJmsPasswordlessConfiguration.class, ServiceBusJmsConnectionFactoryConfiguration.class, ServiceBusJmsContainerConfiguration.class }) public class ServiceBusJmsAutoConfiguration { + @Bean + AzureServiceBusJmsProperties serviceBusJmsProperties(AzureGlobalProperties azureGlobalProperties) { + AzureServiceBusJmsProperties properties = new AzureServiceBusJmsProperties(); + return mergeAzureProperties(azureGlobalProperties, properties); + } + @Bean @ConditionalOnExpression("'premium'.equalsIgnoreCase('${spring.jms.servicebus.pricing-tier}')") - ServiceBusJmsConnectionFactoryCustomizer amqpOpenPropertiesCustomizer() { + ServiceBusJmsConnectionFactoryCustomizer amqpOpenPropertiesCustomizer(ObjectProvider azureServiceBusJmsCredentialSupplier) { 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); @@ -75,4 +88,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/jms/ServiceBusJmsConnectionFactoryFactory.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsConnectionFactoryFactory.java index fd0f5905af41c..f5965851f5efa 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsConnectionFactoryFactory.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsConnectionFactoryFactory.java @@ -57,20 +57,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/jms/ServiceBusJmsPasswordlessConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsPasswordlessConfiguration.java new file mode 100644 index 0000000000000..537eba723c89f --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsPasswordlessConfiguration.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.jms; + +import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties; +import org.apache.qpid.jms.JmsConnectionExtensions; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for Azure Service Bus JMS passwordless support. + * + * @since 4.7.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnProperty(value = "spring.jms.servicebus.passwordless-enabled", havingValue = "true") +class ServiceBusJmsPasswordlessConfiguration { + + @Bean + @ConditionalOnMissingBean + AzureServiceBusJmsCredentialSupplier azureServiceBusJmsCredentialSupplier(AzureServiceBusJmsProperties azureServiceBusJmsProperties) { + return new AzureServiceBusJmsCredentialSupplier(azureServiceBusJmsProperties.toPasswordlessProperties()); + } + + @Bean + ServiceBusJmsConnectionFactoryCustomizer jmsAADAuthenticationCustomizer(AzureServiceBusJmsCredentialSupplier credentialSupplier) { + return factory -> { + factory.setExtension(JmsConnectionExtensions.USERNAME_OVERRIDE.toString(), (connection, uri) -> "$jwt"); + factory.setExtension(JmsConnectionExtensions.PASSWORD_OVERRIDE.toString(), (connection, uri) -> + credentialSupplier.get() + ); + }; + } + + +} diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/properties/AzureServiceBusJmsProperties.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/properties/AzureServiceBusJmsProperties.java index a2abbd9f23de4..881b309b4c19b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/properties/AzureServiceBusJmsProperties.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/jms/properties/AzureServiceBusJmsProperties.java @@ -3,6 +3,9 @@ package com.azure.spring.cloud.autoconfigure.jms.properties; +import com.azure.spring.cloud.autoconfigure.properties.core.authentication.TokenCredentialConfigurationProperties; +import com.azure.spring.cloud.autoconfigure.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,23 +14,61 @@ import org.springframework.util.StringUtils; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; /** * {@link ConfigurationProperties} for configuring Azure Service Bus JMS. */ @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 Servive Bus JMS autoconfiguration. */ private boolean enabled = true; + /** + * The Service Bus namespace. + */ + private String namespace; + /** * Connection string to connect to a Service Bus namespace. */ @@ -79,16 +120,16 @@ public JmsPoolConnectionFactoryProperties getPool() { } /** - * Get the connection string to connect to a Service Bus namesapce. - * @return the connection string to connect to a Service Bus namesapce. + * Get the connection string to connect to a Service Bus namespace. + * @return the connection string to connect to a Service Bus namespace. */ public String getConnectionString() { return connectionString; } /** - * Set the connection string to connect to a Service Bus namesapce. - * @param connectionString the connection string to connect to a Service Bus namesapce. + * Set the connection string to connect to a Service Bus namespace. + * @param connectionString the connection string to connect to a Service Bus namespace. */ public void setConnectionString(String connectionString) { this.connectionString = connectionString; @@ -158,6 +199,97 @@ public PrefetchPolicy getPrefetchPolicy() { return prefetchPolicy; } + /** + * Get the Service Bus namespace. + * @return the Service Bus namespace. + */ + public String getNamespace() { + return namespace; + } + + /** + * Set the Service Bus namespace. + * @param namespace the Service Bus namespace. + */ + public void setNamespace(String namespace) { + this.namespace = namespace; + } + + /** + * 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 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 + */ + public void setPasswordlessEnabled(boolean passwordlessEnabled) { + this.passwordlessEnabled = passwordlessEnabled; + } + + /** + * Get the profile + * @return the profile + */ + @Override + public AzureProfileConfigurationProperties getProfile() { + return profile; + } + + /** + * Set the profile + * @param profile the profile properties related to an Azure subscription + */ + public void setProfile(AzureProfileConfigurationProperties profile) { + this.profile = profile; + } + + /** + * Get the credential properties. + * + * @return the credential properties. + */ + @Override + public TokenCredentialConfigurationProperties getCredential() { + return credential; + } + + /** + * Set the credential properties. + * + * @param credential the credential properties + */ + public void setCredential(TokenCredentialConfigurationProperties credential) { + this.credential = credential; + } + /** * Validate spring.jms.servicebus related properties. * @@ -165,8 +297,14 @@ public PrefetchPolicy getPrefetchPolicy() { */ @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")) { @@ -413,4 +551,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/redis/AzureJedisPasswordlessAutoConfiguration.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfiguration.java index c3d1f2f6c36e4..20138c1c79856 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfiguration.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfiguration.java @@ -6,6 +6,7 @@ import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.implementation.redis.passwordless.data.jedis.AzureJedisConnectionFactory; import com.azure.spring.cloud.autoconfigure.implementation.redis.passwordless.data.jedis.AzureRedisCredentialSupplier; +import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; import com.azure.spring.cloud.service.implementation.passwordless.AzureRedisPasswordlessProperties; import org.apache.commons.pool2.impl.GenericObjectPool; import org.springframework.boot.autoconfigure.AutoConfigureBefore; @@ -24,11 +25,8 @@ import org.springframework.data.redis.connection.jedis.JedisConnection; import redis.clients.jedis.Jedis; -import java.util.Properties; - import static com.azure.spring.cloud.autoconfigure.redis.AzureJedisPasswordlessUtil.getJedisClientConfiguration; import static com.azure.spring.cloud.autoconfigure.redis.AzureJedisPasswordlessUtil.getStandaloneConfig; -import static com.azure.spring.cloud.autoconfigure.redis.AzureJedisPasswordlessUtil.mergeAzureProperties; /** * Azure Redis passwordless connection configuration using Jedis. @@ -51,9 +49,8 @@ AzureRedisPasswordlessProperties redisPasswordlessProperties() { @Bean @ConditionalOnMissingBean - AzureRedisCredentialSupplier azureRedisCredentialSupplier(AzureGlobalProperties azureGlobalProperties, AzureRedisPasswordlessProperties azureRedisPasswordlessProperties) { - Properties properties = mergeAzureProperties(azureGlobalProperties, azureRedisPasswordlessProperties).toProperties(); - return new AzureRedisCredentialSupplier(properties); + AzureRedisCredentialSupplier azureRedisCredentialSupplier(AzureRedisPasswordlessProperties azureRedisPasswordlessProperties, AzureGlobalProperties azureGlobalProperties) { + return new AzureRedisCredentialSupplier(mergeAzureProperties(azureGlobalProperties, azureRedisPasswordlessProperties).toPasswordlessProperties()); } @Bean @@ -61,8 +58,13 @@ AzureRedisCredentialSupplier azureRedisCredentialSupplier(AzureGlobalProperties AzureJedisConnectionFactory azureRedisConnectionFactory(RedisProperties redisProperties, AzureRedisCredentialSupplier azureRedisCredentialSupplier) { RedisStandaloneConfiguration standaloneConfig = getStandaloneConfig(redisProperties); JedisClientConfiguration clientConfiguration = getJedisClientConfiguration(redisProperties); - return new AzureJedisConnectionFactory(standaloneConfig, clientConfiguration, azureRedisCredentialSupplier); } + private AzureRedisPasswordlessProperties mergeAzureProperties(AzureGlobalProperties azureGlobalProperties, AzureRedisPasswordlessProperties azurePasswordlessProperties) { + AzureRedisPasswordlessProperties mergedProperties = new AzureRedisPasswordlessProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, azurePasswordlessProperties, mergedProperties); + return mergedProperties; + } + } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessUtil.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessUtil.java index e9164c0510797..fc101cc2aa50b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessUtil.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessUtil.java @@ -3,9 +3,6 @@ package com.azure.spring.cloud.autoconfigure.redis; -import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; -import com.azure.spring.cloud.core.implementation.util.AzurePropertiesUtils; -import com.azure.spring.cloud.service.implementation.passwordless.AzureRedisPasswordlessProperties; import org.springframework.boot.autoconfigure.data.redis.RedisProperties; import org.springframework.boot.context.properties.PropertyMapper; import org.springframework.data.redis.connection.RedisPassword; @@ -30,13 +27,6 @@ final class AzureJedisPasswordlessUtil { private AzureJedisPasswordlessUtil() { } - static AzureRedisPasswordlessProperties mergeAzureProperties(AzureGlobalProperties azureGlobalProperties, AzureRedisPasswordlessProperties redisPasswordlessProperties) { - AzureRedisPasswordlessProperties mergedProperties = new AzureRedisPasswordlessProperties(); - AzurePropertiesUtils.mergeAzureCommonProperties(azureGlobalProperties, redisPasswordlessProperties, mergedProperties); - mergedProperties.setScopes(redisPasswordlessProperties.getScopes()); - return mergedProperties; - } - static JedisClientConfiguration getJedisClientConfiguration(RedisProperties redisProperties) { JedisClientConfiguration.JedisClientConfigurationBuilder builder = applyProperties(redisProperties, JedisClientConfiguration.builder()); 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 ffb8615299244..8147e9658a53d 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 @@ -1536,136 +1536,136 @@ "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_GERMANY, 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 }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties", "defaultValue": false }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.azure.profile.cloud-type", "type": "java.lang.String", "description": "Name of the Azure cloud to connect to. Supported types are: AZURE, AZURE_CHINA, AZURE_GERMANY, 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.AzureRedisPasswordlessProperties" }, { "name": "spring.redis.azure.passwordless-enabled", "type": "java.lang.Boolean", "description": "Whether to enable passwordless connections to Azure Redis Cache 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.AzureRedisPasswordlessProperties", "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 cd77a65a88225..1e9972501de1f 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,7 +1,8 @@ org.springframework.boot.env.EnvironmentPostProcessor=com.azure.spring.cloud.autoconfigure.cloudfoundry.environment.VcapProcessor,\ com.azure.spring.cloud.autoconfigure.keyvault.environment.KeyVaultEnvironmentPostProcessor,\ com.azure.spring.cloud.autoconfigure.cloudfoundry.AzureCloudFoundryEnvironmentPostProcessor,\ -com.azure.spring.cloud.autoconfigure.context.AzureGlobalConfigurationEnvironmentPostProcessor +com.azure.spring.cloud.autoconfigure.context.AzureGlobalConfigurationEnvironmentPostProcessor,\ +com.azure.spring.cloud.autoconfigure.implementation.passwordless.AzurePasswordlessEnvironmentPostProcessor org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ com.azure.spring.cloud.autoconfigure.aad.AadAutoConfiguration,\ diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/AbstractAzureJdbcAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/AbstractAzureJdbcAutoConfigurationTest.java index 7af077e0d265c..3326ddd16ec86 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/AbstractAzureJdbcAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/AbstractAzureJdbcAutoConfigurationTest.java @@ -21,7 +21,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/jdbc/JdbcPropertiesBeanPostProcessorTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessorTest.java index b88603d293f1f..d4e1078c038e3 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessorTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessorTest.java @@ -35,6 +35,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; @@ -98,6 +99,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() ); @@ -122,7 +124,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()); @@ -139,6 +142,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() ); @@ -160,7 +164,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()); @@ -180,7 +185,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()); } @@ -199,6 +205,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 ); @@ -220,6 +227,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/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java index 0a112b792a9b3..3065375e4f677 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/JdbcPropertiesBeanPostProcessorWithApplicationContextRunnerTest.java @@ -11,7 +11,7 @@ import com.azure.spring.cloud.autoconfigure.implementation.jdbc.DatabaseType; import com.azure.spring.cloud.autoconfigure.implementation.jdbc.SpringTokenCredentialProviderContextProvider; 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; @@ -30,12 +30,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)); @@ -75,6 +75,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() @@ -114,7 +115,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/jdbc/MySqlAzureJdbcAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/MySqlAzureJdbcAutoConfigurationTest.java index 8040af547a8db..d4470b51112c8 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/MySqlAzureJdbcAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/MySqlAzureJdbcAutoConfigurationTest.java @@ -84,6 +84,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/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java index 2395ee9d501f4..722ebc7f6997b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jdbc/PostgreSqlAzureJdbcAutoConfigurationTest.java @@ -72,6 +72,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/jms/AzureServiceBusJmsPropertiesTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsPropertiesTests.java index 2001b29260590..e198123d55a96 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsPropertiesTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/AzureServiceBusJmsPropertiesTests.java @@ -8,6 +8,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; @@ -16,13 +20,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)); @@ -30,7 +37,7 @@ void connectionStringNotValid() { @ParameterizedTest @NullAndEmptySource - @ValueSource(strings = { "xx" }) + @ValueSource(strings = {"xx"}) void pricingTierNotValid(String pricingTier) { AzureServiceBusJmsProperties prop = new AzureServiceBusJmsProperties(); prop.setConnectionString(CONNECTION_STRING); @@ -44,4 +51,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/jms/ServiceBusJmsAutoConfigurationTests.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfigurationTests.java index 53857f90d8510..7115e8e0773c8 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfigurationTests.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsAutoConfigurationTests.java @@ -3,6 +3,7 @@ package com.azure.spring.cloud.autoconfigure.jms; +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties; import com.azure.spring.cloud.core.provider.connectionstring.StaticConnectionStringProvider; import com.azure.spring.cloud.core.service.AzureServiceType; @@ -42,6 +43,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) { @@ -127,7 +129,6 @@ void autoconfigurationEnabledAndContextSuccessWithNonpremiumTier(String pricingT assertThat(context).hasSingleBean(ConnectionFactory.class); assertThat(context).hasSingleBean(JmsTemplate.class); assertThat(context).hasSingleBean(DefaultJmsListenerContainerFactoryConfigurer.class); - assertThat(context).hasBean("jmsListenerContainerFactory"); assertThat(context).hasBean("topicJmsListenerContainerFactory"); assertThat(context).doesNotHaveBean("amqpOpenPropertiesCustomizer"); }); diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsPasswordlessConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsPasswordlessConfigurationTest.java new file mode 100644 index 0000000000000..bcbba7d264be1 --- /dev/null +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/jms/ServiceBusJmsPasswordlessConfigurationTest.java @@ -0,0 +1,147 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.spring.cloud.autoconfigure.jms; + +import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; +import com.azure.spring.cloud.autoconfigure.jms.properties.AzureServiceBusJmsProperties; +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/redis/AzureJedisPasswordlessUtilTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/passwordless/MergeAzureCommonPropertiesTest.java similarity index 66% rename from sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessUtilTest.java rename to sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/passwordless/MergeAzureCommonPropertiesTest.java index f90da9f7beb0e..dcfacec34df46 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessUtilTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/passwordless/MergeAzureCommonPropertiesTest.java @@ -1,19 +1,17 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -package com.azure.spring.cloud.autoconfigure.redis; +package com.azure.spring.cloud.autoconfigure.passwordless; import com.azure.spring.cloud.autoconfigure.context.AzureGlobalProperties; -import com.azure.spring.cloud.core.properties.client.ClientProperties; -import com.azure.spring.cloud.core.properties.proxy.ProxyProperties; +import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils; import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider; -import com.azure.spring.cloud.service.implementation.passwordless.AzurePasswordlessProperties; import com.azure.spring.cloud.service.implementation.passwordless.AzureRedisPasswordlessProperties; import org.junit.jupiter.api.Test; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertEquals; -class AzureJedisPasswordlessUtilTest { +class MergeAzureCommonPropertiesTest { @Test void testGetPropertiesFromGlobalProperties() { @@ -26,15 +24,11 @@ void testGetPropertiesFromGlobalProperties() { 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); AzureRedisPasswordlessProperties passwordlessProperties = new AzureRedisPasswordlessProperties(); - AzurePasswordlessProperties result = AzureJedisPasswordlessUtil.mergeAzureProperties(globalProperties, passwordlessProperties); + AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, passwordlessProperties, result); assertEquals("https://*.cacheinfra.windows.net:10225/appid/.default", result.getScopes()); assertEquals("global-client-id", result.getCredential().getClientId()); @@ -45,11 +39,6 @@ void testGetPropertiesFromGlobalProperties() { assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType()); assertEquals("global-sub", result.getProfile().getSubscriptionId()); assertEquals("global-tenant-id", result.getProfile().getTenantId()); - assertEquals("global-application-id", result.getClient().getApplicationId()); - assertEquals("global-proxy-username", result.getProxy().getUsername()); - assertEquals("global-proxy-password", result.getProxy().getPassword()); - assertEquals("global-proxy-hostname", result.getProxy().getHostname()); - assertEquals(1111, result.getProxy().getPort()); } @Test @@ -66,13 +55,9 @@ void testGetPropertiesFromAzurePasswordlessProperties() { passwordlessProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT); passwordlessProperties.getProfile().setSubscriptionId("sub"); passwordlessProperties.getProfile().setTenantId("tenant-id"); - ((ClientProperties) passwordlessProperties.getClient()).setApplicationId("passwordless-application-id"); - ((ProxyProperties) passwordlessProperties.getProxy()).setUsername("proxy-username"); - ((ProxyProperties) passwordlessProperties.getProxy()).setPassword("proxy-password"); - ((ProxyProperties) passwordlessProperties.getProxy()).setHostname("proxy-hostname"); - ((ProxyProperties) passwordlessProperties.getProxy()).setPort(2222); - AzurePasswordlessProperties result = AzureJedisPasswordlessUtil.mergeAzureProperties(globalProperties, passwordlessProperties); + AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, passwordlessProperties, result); assertEquals("scopes-us-gov", result.getScopes()); assertEquals("client-id", result.getCredential().getClientId()); @@ -83,11 +68,6 @@ void testGetPropertiesFromAzurePasswordlessProperties() { assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, result.getProfile().getCloudType()); assertEquals("sub", result.getProfile().getSubscriptionId()); assertEquals("tenant-id", result.getProfile().getTenantId()); - assertEquals("passwordless-application-id", result.getClient().getApplicationId()); - assertEquals("proxy-username", result.getProxy().getUsername()); - assertEquals("proxy-password", result.getProxy().getPassword()); - assertEquals("proxy-hostname", result.getProxy().getHostname()); - assertEquals(2222, result.getProxy().getPort()); } @@ -116,12 +96,9 @@ void testGetPropertiesFromGlobalAndPasswordlessProperties() { passwordlessProperties.getCredential().setManagedIdentityEnabled(true); passwordlessProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT); passwordlessProperties.getProfile().setSubscriptionId("sub"); - ((ClientProperties) passwordlessProperties.getClient()).setApplicationId("passwordless-application-id"); - ((ProxyProperties) passwordlessProperties.getProxy()).setUsername("proxy-username"); - ((ProxyProperties) passwordlessProperties.getProxy()).setHostname("proxy-hostname"); - ((ProxyProperties) passwordlessProperties.getProxy()).setPort(2222); - AzurePasswordlessProperties result = AzureJedisPasswordlessUtil.mergeAzureProperties(globalProperties, passwordlessProperties); + AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties(); + AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, passwordlessProperties, result); assertEquals("scope", result.getScopes()); assertEquals("global-client-id", result.getCredential().getClientId()); @@ -132,10 +109,5 @@ void testGetPropertiesFromGlobalAndPasswordlessProperties() { assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, result.getProfile().getCloudType()); assertEquals("sub", result.getProfile().getSubscriptionId()); assertEquals("global-tenant-id", result.getProfile().getTenantId()); - assertEquals("passwordless-application-id", result.getClient().getApplicationId()); - assertEquals("proxy-username", result.getProxy().getUsername()); - assertEquals("global-proxy-password", result.getProxy().getPassword()); - assertEquals("proxy-hostname", result.getProxy().getHostname()); - assertEquals(2222, result.getProxy().getPort()); } } diff --git a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfigurationTest.java b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfigurationTest.java index 355056b7544f7..8d19c2b7d1f0b 100644 --- a/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfigurationTest.java +++ b/sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/redis/AzureJedisPasswordlessAutoConfigurationTest.java @@ -11,6 +11,7 @@ import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.boot.autoconfigure.AutoConfigurations; +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.Configuration; @@ -236,6 +237,7 @@ void testRedisConfigurationWithClientName() { } @Configuration(proxyBeanMethods = false) + @EnableConfigurationProperties static class CustomConfiguration { @Bean diff --git a/sdk/spring/spring-cloud-azure-core/pom.xml b/sdk/spring/spring-cloud-azure-core/pom.xml index db344875526dc..cf405d9f0460f 100644 --- a/sdk/spring/spring-cloud-azure-core/pom.xml +++ b/sdk/spring/spring-cloud-azure-core/pom.xml @@ -59,6 +59,13 @@ 1.10.1 + + com.azure + azure-identity-extensions + 1.1.1 + 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 0000000000000..54df0ceecbe78 --- /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 a4a8333f10c8b..42e2c1712101a 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 @@ -55,6 +55,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 0000000000000..2389c228a3645 --- /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 0000000000000..dc552df767f73 --- /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 0000000000000..76a5ffdd46514 --- /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 0000000000000..14a77cc58923c --- /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 636ef04e1bbf2..fce64f6350827 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 0000000000000..689b13a25c0af --- /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 0000000000000..9b99c5f0795f0 --- /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/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessProperties.java b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessProperties.java index 2658052ff1f1e..93bfe4680e0ba 100644 --- a/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessProperties.java +++ b/sdk/spring/spring-cloud-azure-service/src/main/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessProperties.java @@ -3,14 +3,18 @@ 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 com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider; + import java.util.HashMap; import java.util.Map; /** * Configuration properties for passwordless connections with Azure Redis. */ -public class AzureRedisPasswordlessProperties extends AzurePasswordlessProperties { +public class AzureRedisPasswordlessProperties implements PasswordlessProperties { private static final String REDIS_SCOPE_AZURE = "https://*.cacheinfra.windows.net:10225/appid/.default"; private static final String REDIS_SCOPE_AZURE_CHINA = "https://*.cacheinfra.windows.net.china:10225/appid/.default"; @@ -26,13 +30,97 @@ public class AzureRedisPasswordlessProperties extends AzurePasswordlessPropertie } }; + private AzureProfileProperties profile = new AzureProfileProperties(); + + private String scopes; + + /** + * Whether to enable supporting azure identity token credentials, by default is false. + * + * If the passwordlessEnabled is true, but the redis password properties is not null, it will still use username/password to authenticate connections. + */ + private boolean passwordlessEnabled = false; + + 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 super.getScopes() == null ? getRedisScopes() : super.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 + */ + @Override + 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; } - private String getRedisScopes() { + /** + * 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 REDIS_SCOPE_MAP.getOrDefault(getProfile().getCloudType(), REDIS_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/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 03f3b5cfc03e5..403828fa7bdb5 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-service/src/test/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessPropertiesTest.java b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessPropertiesTest.java index 30c5f85c36350..82bb22b7e9d43 100644 --- a/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessPropertiesTest.java +++ b/sdk/spring/spring-cloud-azure-service/src/test/java/com/azure/spring/cloud/service/implementation/passwordless/AzureRedisPasswordlessPropertiesTest.java @@ -71,7 +71,7 @@ void testToProperties() { azureRedisPasswordlessProperties.setCredential(credential); azureRedisPasswordlessProperties.setProfile(profile); - Properties properties = azureRedisPasswordlessProperties.toProperties(); + Properties properties = azureRedisPasswordlessProperties.toPasswordlessProperties(); Assertions.assertEquals("fake-client-id", properties.getProperty(AuthProperty.CLIENT_ID.getPropertyKey())); 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 5a07b009ebe4f..79b95cf7365a0 100644 --- a/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml +++ b/sdk/spring/spring-cloud-azure-starter-servicebus-jms/pom.xml @@ -143,6 +143,11 @@ netty-codec-http 4.1.89.Final + + com.azure + azure-identity-extensions + 1.1.1 +