Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions sdk/spring/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
# Release History
## 7.3.0-beta.1 (Unreleased)

### Features Added
### Spring Cloud Azure Autoconfigure

### Breaking Changes
This section includes changes in `spring-cloud-azure-autoconfigure` module.

### Bugs Fixed
#### Bugs Fixed

Comment thread
rujche marked this conversation as resolved.
### Other Changes
- Fixed JDBC/Azure Database and Redis passwordless connection scope defaulting using the wrong `azure.scopes` value for Azure China and Azure US Government when `spring.cloud.azure.profile.cloud-type` is set to `azure_china` or `azure_us_government`. The scopes are now correctly derived from the merged cloud type. ([#47096](https://github.com/Azure/azure-sdk-for-java/issues/47096))

## 7.2.0 (2026-04-17)
- This release is compatible with Spring Boot 4.0.0-4.0.5. (Note: 4.0.x (x>5) should be supported, but they aren't tested with this release.)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,14 @@

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

import com.azure.spring.cloud.core.implementation.properties.AzurePasswordlessPropertiesMapping;
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;
import java.util.Properties;

/**
* Configuration properties for passwordless connections with Azure Database.
Expand Down Expand Up @@ -43,11 +45,22 @@ public class AzureJdbcPasswordlessProperties implements PasswordlessProperties {

/**
* Get the scopes required for the access token.
* Returns null if scopes have not been explicitly set, so that the default
* scopes can be computed from the merged cloud type after property merging.
*
* @return scopes required for the access token
* @return scopes required for the access token, or null if not explicitly set
*/
@Override
public String getScopes() {
return this.scopes;
}

/**
* Get the effective scopes, returning default cloud-specific scopes when not explicitly set.
*
* @return scopes required for the access token
*/
public String getEffectiveScopes() {
return this.scopes == null ? getDefaultScopes() : this.scopes;
}

Expand Down Expand Up @@ -120,4 +133,25 @@ public TokenCredentialProperties getCredential() {
public void setCredential(TokenCredentialProperties credential) {
this.credential = credential;
}

/**
* Convert {@link AzureJdbcPasswordlessProperties} to {@link Properties}.
* Uses the effective scopes (cloud-type-aware) rather than the raw scopes value,
* ensuring the correct default scope is used when scopes have not been explicitly set.
*
* @return converted {@link Properties} instance
*/
@Override
public Properties toPasswordlessProperties() {
Properties properties = new Properties();
for (AzurePasswordlessPropertiesMapping m : AzurePasswordlessPropertiesMapping.values()) {
String value = m == AzurePasswordlessPropertiesMapping.SCOPES
? getEffectiveScopes()
: m.getGetter().apply(this);
if (value != null) {
m.getSetter().accept(properties, value);
}
}
return properties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,13 +3,15 @@

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

import com.azure.spring.cloud.core.implementation.properties.AzurePasswordlessPropertiesMapping;
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;
import java.util.Properties;

/**
* Configuration properties for passwordless connections with Azure Redis.
Expand Down Expand Up @@ -43,11 +45,22 @@ public class AzureRedisPasswordlessProperties implements PasswordlessProperties

/**
* Get the scopes required for the access token.
* Returns null if scopes have not been explicitly set, so that the default
* scopes can be computed from the merged cloud type after property merging.
*
* @return scopes required for the access token
* @return scopes required for the access token, or null if not explicitly set
*/
@Override
public String getScopes() {
return this.scopes;
}

/**
* Get the effective scopes, returning default cloud-specific scopes when not explicitly set.
*
* @return scopes required for the access token
*/
public String getEffectiveScopes() {
return this.scopes == null ? getDefaultScopes() : this.scopes;
}

Comment thread
rujche marked this conversation as resolved.
Expand Down Expand Up @@ -121,4 +134,25 @@ public TokenCredentialProperties getCredential() {
public void setCredential(TokenCredentialProperties credential) {
this.credential = credential;
}

/**
* Convert {@link AzureRedisPasswordlessProperties} to {@link Properties}.
* Uses the effective scopes (cloud-type-aware) rather than the raw scopes value,
* ensuring the correct default scope is used when scopes have not been explicitly set.
*
* @return converted {@link Properties} instance
*/
@Override
public Properties toPasswordlessProperties() {
Properties properties = new Properties();
for (AzurePasswordlessPropertiesMapping m : AzurePasswordlessPropertiesMapping.values()) {
String value = m == AzurePasswordlessPropertiesMapping.SCOPES
? getEffectiveScopes()
: m.getGetter().apply(this);
if (value != null) {
m.getSetter().accept(properties, value);
}
}
return properties;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,17 @@ 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/";
private static final String CHINA_AUTHORITY_HOST_STRING = AuthProperty.AUTHORITY_HOST.getPropertyKey() + "=" + "https://login.chinacloudapi.cn/";
public static final String PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING = AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.getPropertyKey() + "=";
private static final String POSTGRESQL_ASSUME_MIN_SERVER_VERSION = POSTGRESQL_PROPERTY_NAME_ASSUME_MIN_SERVER_VERSION + "="
+ POSTGRESQL_PROPERTY_VALUE_ASSUME_MIN_SERVER_VERSION;
protected static final String MANAGED_IDENTITY_ENABLED_DEFAULT = "azure.managedIdentityEnabled=false";
protected static final String SCOPES_DEFAULT = "azure.scopes=https://ossrdbms-aad.database.windows.net/.default";
protected static final String SCOPES_DEFAULT = AuthProperty.SCOPES.getPropertyKey() + "="
+ "https://ossrdbms-aad.database.windows.net/.default";
private static final String SCOPES_CHINA = AuthProperty.SCOPES.getPropertyKey() + "="
+ "https://ossrdbms-aad.database.chinacloudapi.cn/.default";
private static final String SCOPES_US_GOVERNMENT = AuthProperty.SCOPES.getPropertyKey() + "="
+ "https://ossrdbms-aad.database.usgovcloudapi.net/.default";
private static final String DEFAULT_PASSWORDLESS_PROPERTIES_SUFFIX = ".spring.datasource.azure";
private MockEnvironment mockEnvironment;

Expand Down Expand Up @@ -153,14 +159,39 @@ void shouldGetCloudTypeFromAzureUsGov() {
DatabaseType.MYSQL,
MYSQL_CONNECTION_STRING,
MANAGED_IDENTITY_ENABLED_DEFAULT,
SCOPES_DEFAULT,
SCOPES_US_GOVERNMENT,
MYSQL_USER_AGENT,
US_AUTHORITY_HOST_STRING
);

assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl());
}

@Test
void shouldGetCorrectScopeFromAzureChina() {
AzureProfileConfigurationProperties azureProfileConfigurationProperties = new AzureProfileConfigurationProperties();
azureProfileConfigurationProperties.setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);
when(this.azureGlobalProperties.getProfile()).thenReturn(azureProfileConfigurationProperties);

DataSourceProperties dataSourceProperties = new DataSourceProperties();
dataSourceProperties.setUrl(POSTGRESQL_CONNECTION_STRING);

this.mockEnvironment.setProperty("spring.datasource.azure.passwordless-enabled", "true");
this.jdbcPropertiesBeanPostProcessor.postProcessBeforeInitialization(dataSourceProperties, "dataSourceProperties");

String expectedJdbcUrl = enhanceJdbcUrl(
DatabaseType.POSTGRESQL,
POSTGRESQL_CONNECTION_STRING,
MANAGED_IDENTITY_ENABLED_DEFAULT,
SCOPES_CHINA,
APPLICATION_NAME.getName() + "=" + AzureSpringIdentifier.AZURE_SPRING_POSTGRESQL_OAUTH,
POSTGRESQL_ASSUME_MIN_SERVER_VERSION,
CHINA_AUTHORITY_HOST_STRING
);

assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl());
}

@Test
void mySqlUserAgentShouldConfigureIfConnectionAttributesIsEmpty() {
DataSourceProperties dataSourceProperties = new DataSourceProperties();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,15 @@

import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties;
import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties;
import com.azure.spring.cloud.autoconfigure.implementation.passwordless.properties.AzureJdbcPasswordlessProperties;
import com.azure.spring.cloud.autoconfigure.implementation.passwordless.properties.AzureRedisPasswordlessProperties;
import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils;
import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider;
import com.azure.identity.extensions.implementation.enums.AuthProperty;
import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertTrue;

class MergeAzureCommonPropertiesTest {
Expand Down Expand Up @@ -116,4 +120,103 @@ void testGetPropertiesFromGlobalAndPasswordlessProperties() {
assertEquals("sub", result.getProfile().getSubscriptionId());
assertEquals("global-tenant-id", result.getProfile().getTenantId());
}

@Test
void testJdbcPropertiesGetCorrectScopeFromChinaCloudTypeInGlobalProperties() {
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);

AzureJdbcPasswordlessProperties jdbcProperties = new AzureJdbcPasswordlessProperties();
// User has not explicitly set scopes

AzureJdbcPasswordlessProperties result = new AzureJdbcPasswordlessProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, jdbcProperties, result);

// scopes field should be null (not explicitly set)
assertNull(result.getScopes());
// effective scopes should use the merged cloud type (AZURE_CHINA)
assertEquals("https://ossrdbms-aad.database.chinacloudapi.cn/.default", result.getEffectiveScopes());
// toPasswordlessProperties should include the correct cloud-type-aware scope
assertEquals("https://ossrdbms-aad.database.chinacloudapi.cn/.default",
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType());
Comment thread
rujche marked this conversation as resolved.
}

@Test
void testJdbcPropertiesExplicitScopesOverridesDefault() {
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);

AzureJdbcPasswordlessProperties jdbcProperties = new AzureJdbcPasswordlessProperties();
jdbcProperties.setScopes("https://custom-scope/.default");

AzureJdbcPasswordlessProperties result = new AzureJdbcPasswordlessProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, jdbcProperties, result);

// Explicit scopes should be preserved
assertEquals("https://custom-scope/.default", result.getScopes());
assertEquals("https://custom-scope/.default", result.getEffectiveScopes());
assertEquals("https://custom-scope/.default",
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
}

@Test
void testRedisPropertiesGetCorrectScopeFromChinaCloudTypeInGlobalProperties() {
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);

AzureRedisPasswordlessProperties redisProperties = new AzureRedisPasswordlessProperties();
// User has not explicitly set scopes

AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, redisProperties, result);

// scopes field should be null (not explicitly set)
assertNull(result.getScopes());
// effective scopes should use the merged cloud type (AZURE_CHINA)
assertEquals("https://*.cacheinfra.windows.net.china:10225/appid/.default", result.getEffectiveScopes());
// toPasswordlessProperties should include the correct cloud-type-aware scope
assertEquals("https://*.cacheinfra.windows.net.china:10225/appid/.default",
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType());
}

@Test
void testRedisPropertiesGetCorrectScopeFromUsGovCloudTypeInGlobalProperties() {
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT);

AzureRedisPasswordlessProperties redisProperties = new AzureRedisPasswordlessProperties();
// User has not explicitly set scopes

AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, redisProperties, result);

// scopes field should be null (not explicitly set)
assertNull(result.getScopes());
// effective scopes should use the merged cloud type (AZURE_US_GOVERNMENT)
assertEquals("https://*.cacheinfra.windows.us.government.net:10225/appid/.default", result.getEffectiveScopes());
// toPasswordlessProperties should include the correct cloud-type-aware scope
assertEquals("https://*.cacheinfra.windows.us.government.net:10225/appid/.default",
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, result.getProfile().getCloudType());
}

@Test
void testRedisPropertiesExplicitScopesOverridesDefault() {
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);

AzureRedisPasswordlessProperties redisProperties = new AzureRedisPasswordlessProperties();
redisProperties.setScopes("https://custom-redis-scope/.default");

AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties();
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, redisProperties, result);

// Explicit scopes should be preserved
assertEquals("https://custom-redis-scope/.default", result.getScopes());
assertEquals("https://custom-redis-scope/.default", result.getEffectiveScopes());
assertEquals("https://custom-redis-scope/.default",
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,10 @@ public static <T extends PasswordlessProperties> void copyAzureCommonPropertiesI
copyPropertiesIgnoreNull(source.getProfile().getEnvironment(), target.getProfile().getEnvironment());
copyPropertiesIgnoreNull(source.getCredential(), target.getCredential());

target.setScopes(source.getScopes());
String scopes = source.getScopes();
if (scopes != null) {
target.setScopes(scopes);
}
target.setPasswordlessEnabled(source.isPasswordlessEnabled());
Comment thread
rujche marked this conversation as resolved.
}

Expand Down
Loading