Skip to content

Commit ab2626d

Browse files
authored
Fix passwordless JDBC and Redis azure.scopes defaulting to Azure Global when cloud-type=azure_china/azure_us_government (#48683)
1 parent 4d09a52 commit ab2626d

6 files changed

Lines changed: 214 additions & 9 deletions

File tree

sdk/spring/CHANGELOG.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
# Release History
22
## 7.3.0-beta.1 (Unreleased)
33

4-
### Features Added
4+
### Spring Cloud Azure Autoconfigure
55

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

8-
### Bugs Fixed
8+
#### Bugs Fixed
99

10-
### Other Changes
10+
- 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))
1111

1212
## 7.2.0 (2026-04-17)
1313
- 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.)

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/properties/AzureJdbcPasswordlessProperties.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,14 @@
33

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

6+
import com.azure.spring.cloud.core.implementation.properties.AzurePasswordlessPropertiesMapping;
67
import com.azure.spring.cloud.core.properties.PasswordlessProperties;
78
import com.azure.spring.cloud.core.properties.authentication.TokenCredentialProperties;
89
import com.azure.spring.cloud.core.properties.profile.AzureProfileProperties;
910

1011
import java.util.HashMap;
1112
import java.util.Map;
13+
import java.util.Properties;
1214

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

4446
/**
4547
* Get the scopes required for the access token.
48+
* Returns null if scopes have not been explicitly set, so that the default
49+
* scopes can be computed from the merged cloud type after property merging.
4650
*
47-
* @return scopes required for the access token
51+
* @return scopes required for the access token, or null if not explicitly set
4852
*/
4953
@Override
5054
public String getScopes() {
55+
return this.scopes;
56+
}
57+
58+
/**
59+
* Get the effective scopes, returning default cloud-specific scopes when not explicitly set.
60+
*
61+
* @return scopes required for the access token
62+
*/
63+
public String getEffectiveScopes() {
5164
return this.scopes == null ? getDefaultScopes() : this.scopes;
5265
}
5366

@@ -120,4 +133,25 @@ public TokenCredentialProperties getCredential() {
120133
public void setCredential(TokenCredentialProperties credential) {
121134
this.credential = credential;
122135
}
136+
137+
/**
138+
* Convert {@link AzureJdbcPasswordlessProperties} to {@link Properties}.
139+
* Uses the effective scopes (cloud-type-aware) rather than the raw scopes value,
140+
* ensuring the correct default scope is used when scopes have not been explicitly set.
141+
*
142+
* @return converted {@link Properties} instance
143+
*/
144+
@Override
145+
public Properties toPasswordlessProperties() {
146+
Properties properties = new Properties();
147+
for (AzurePasswordlessPropertiesMapping m : AzurePasswordlessPropertiesMapping.values()) {
148+
String value = m == AzurePasswordlessPropertiesMapping.SCOPES
149+
? getEffectiveScopes()
150+
: m.getGetter().apply(this);
151+
if (value != null) {
152+
m.getSetter().accept(properties, value);
153+
}
154+
}
155+
return properties;
156+
}
123157
}

sdk/spring/spring-cloud-azure-autoconfigure/src/main/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/properties/AzureRedisPasswordlessProperties.java

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,15 @@
33

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

6+
import com.azure.spring.cloud.core.implementation.properties.AzurePasswordlessPropertiesMapping;
67
import com.azure.spring.cloud.core.properties.PasswordlessProperties;
78
import com.azure.spring.cloud.core.properties.authentication.TokenCredentialProperties;
89
import com.azure.spring.cloud.core.properties.profile.AzureProfileProperties;
910
import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider;
1011

1112
import java.util.HashMap;
1213
import java.util.Map;
14+
import java.util.Properties;
1315

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

4446
/**
4547
* Get the scopes required for the access token.
48+
* Returns null if scopes have not been explicitly set, so that the default
49+
* scopes can be computed from the merged cloud type after property merging.
4650
*
47-
* @return scopes required for the access token
51+
* @return scopes required for the access token, or null if not explicitly set
4852
*/
4953
@Override
5054
public String getScopes() {
55+
return this.scopes;
56+
}
57+
58+
/**
59+
* Get the effective scopes, returning default cloud-specific scopes when not explicitly set.
60+
*
61+
* @return scopes required for the access token
62+
*/
63+
public String getEffectiveScopes() {
5164
return this.scopes == null ? getDefaultScopes() : this.scopes;
5265
}
5366

@@ -121,4 +134,25 @@ public TokenCredentialProperties getCredential() {
121134
public void setCredential(TokenCredentialProperties credential) {
122135
this.credential = credential;
123136
}
137+
138+
/**
139+
* Convert {@link AzureRedisPasswordlessProperties} to {@link Properties}.
140+
* Uses the effective scopes (cloud-type-aware) rather than the raw scopes value,
141+
* ensuring the correct default scope is used when scopes have not been explicitly set.
142+
*
143+
* @return converted {@link Properties} instance
144+
*/
145+
@Override
146+
public Properties toPasswordlessProperties() {
147+
Properties properties = new Properties();
148+
for (AzurePasswordlessPropertiesMapping m : AzurePasswordlessPropertiesMapping.values()) {
149+
String value = m == AzurePasswordlessPropertiesMapping.SCOPES
150+
? getEffectiveScopes()
151+
: m.getGetter().apply(this);
152+
if (value != null) {
153+
m.getSetter().accept(properties, value);
154+
}
155+
}
156+
return properties;
157+
}
124158
}

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/jdbc/JdbcPropertiesBeanPostProcessorTest.java

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,17 @@ class JdbcPropertiesBeanPostProcessorTest {
3939
private static final String POSTGRESQL_CONNECTION_STRING = "jdbc:postgresql://host/database?enableSwitch1&property1=value1";
4040
private static final String PASSWORD = "password";
4141
private static final String US_AUTHORITY_HOST_STRING = AuthProperty.AUTHORITY_HOST.getPropertyKey() + "=" + "https://login.microsoftonline.us/";
42+
private static final String CHINA_AUTHORITY_HOST_STRING = AuthProperty.AUTHORITY_HOST.getPropertyKey() + "=" + "https://login.chinacloudapi.cn/";
4243
public static final String PUBLIC_TOKEN_CREDENTIAL_BEAN_NAME_STRING = AuthProperty.TOKEN_CREDENTIAL_BEAN_NAME.getPropertyKey() + "=";
4344
private static final String POSTGRESQL_ASSUME_MIN_SERVER_VERSION = POSTGRESQL_PROPERTY_NAME_ASSUME_MIN_SERVER_VERSION + "="
4445
+ POSTGRESQL_PROPERTY_VALUE_ASSUME_MIN_SERVER_VERSION;
4546
protected static final String MANAGED_IDENTITY_ENABLED_DEFAULT = "azure.managedIdentityEnabled=false";
46-
protected static final String SCOPES_DEFAULT = "azure.scopes=https://ossrdbms-aad.database.windows.net/.default";
47+
protected static final String SCOPES_DEFAULT = AuthProperty.SCOPES.getPropertyKey() + "="
48+
+ "https://ossrdbms-aad.database.windows.net/.default";
49+
private static final String SCOPES_CHINA = AuthProperty.SCOPES.getPropertyKey() + "="
50+
+ "https://ossrdbms-aad.database.chinacloudapi.cn/.default";
51+
private static final String SCOPES_US_GOVERNMENT = AuthProperty.SCOPES.getPropertyKey() + "="
52+
+ "https://ossrdbms-aad.database.usgovcloudapi.net/.default";
4753
private static final String DEFAULT_PASSWORDLESS_PROPERTIES_SUFFIX = ".spring.datasource.azure";
4854
private MockEnvironment mockEnvironment;
4955

@@ -153,14 +159,39 @@ void shouldGetCloudTypeFromAzureUsGov() {
153159
DatabaseType.MYSQL,
154160
MYSQL_CONNECTION_STRING,
155161
MANAGED_IDENTITY_ENABLED_DEFAULT,
156-
SCOPES_DEFAULT,
162+
SCOPES_US_GOVERNMENT,
157163
MYSQL_USER_AGENT,
158164
US_AUTHORITY_HOST_STRING
159165
);
160166

161167
assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl());
162168
}
163169

170+
@Test
171+
void shouldGetCorrectScopeFromAzureChina() {
172+
AzureProfileConfigurationProperties azureProfileConfigurationProperties = new AzureProfileConfigurationProperties();
173+
azureProfileConfigurationProperties.setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);
174+
when(this.azureGlobalProperties.getProfile()).thenReturn(azureProfileConfigurationProperties);
175+
176+
DataSourceProperties dataSourceProperties = new DataSourceProperties();
177+
dataSourceProperties.setUrl(POSTGRESQL_CONNECTION_STRING);
178+
179+
this.mockEnvironment.setProperty("spring.datasource.azure.passwordless-enabled", "true");
180+
this.jdbcPropertiesBeanPostProcessor.postProcessBeforeInitialization(dataSourceProperties, "dataSourceProperties");
181+
182+
String expectedJdbcUrl = enhanceJdbcUrl(
183+
DatabaseType.POSTGRESQL,
184+
POSTGRESQL_CONNECTION_STRING,
185+
MANAGED_IDENTITY_ENABLED_DEFAULT,
186+
SCOPES_CHINA,
187+
APPLICATION_NAME.getName() + "=" + AzureSpringIdentifier.AZURE_SPRING_POSTGRESQL_OAUTH,
188+
POSTGRESQL_ASSUME_MIN_SERVER_VERSION,
189+
CHINA_AUTHORITY_HOST_STRING
190+
);
191+
192+
assertEquals(expectedJdbcUrl, dataSourceProperties.getUrl());
193+
}
194+
164195
@Test
165196
void mySqlUserAgentShouldConfigureIfConnectionAttributesIsEmpty() {
166197
DataSourceProperties dataSourceProperties = new DataSourceProperties();

sdk/spring/spring-cloud-azure-autoconfigure/src/test/java/com/azure/spring/cloud/autoconfigure/implementation/passwordless/MergeAzureCommonPropertiesTest.java

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@
55

66
import com.azure.spring.cloud.autoconfigure.implementation.context.properties.AzureGlobalProperties;
77
import com.azure.spring.cloud.autoconfigure.implementation.jms.properties.AzureServiceBusJmsProperties;
8+
import com.azure.spring.cloud.autoconfigure.implementation.passwordless.properties.AzureJdbcPasswordlessProperties;
9+
import com.azure.spring.cloud.autoconfigure.implementation.passwordless.properties.AzureRedisPasswordlessProperties;
810
import com.azure.spring.cloud.core.implementation.util.AzurePasswordlessPropertiesUtils;
911
import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider;
12+
import com.azure.identity.extensions.implementation.enums.AuthProperty;
1013
import org.junit.jupiter.api.Test;
1114

1215
import static org.junit.jupiter.api.Assertions.assertEquals;
16+
import static org.junit.jupiter.api.Assertions.assertNull;
1317
import static org.junit.jupiter.api.Assertions.assertTrue;
1418

1519
class MergeAzureCommonPropertiesTest {
@@ -116,4 +120,103 @@ void testGetPropertiesFromGlobalAndPasswordlessProperties() {
116120
assertEquals("sub", result.getProfile().getSubscriptionId());
117121
assertEquals("global-tenant-id", result.getProfile().getTenantId());
118122
}
123+
124+
@Test
125+
void testJdbcPropertiesGetCorrectScopeFromChinaCloudTypeInGlobalProperties() {
126+
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
127+
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);
128+
129+
AzureJdbcPasswordlessProperties jdbcProperties = new AzureJdbcPasswordlessProperties();
130+
// User has not explicitly set scopes
131+
132+
AzureJdbcPasswordlessProperties result = new AzureJdbcPasswordlessProperties();
133+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, jdbcProperties, result);
134+
135+
// scopes field should be null (not explicitly set)
136+
assertNull(result.getScopes());
137+
// effective scopes should use the merged cloud type (AZURE_CHINA)
138+
assertEquals("https://ossrdbms-aad.database.chinacloudapi.cn/.default", result.getEffectiveScopes());
139+
// toPasswordlessProperties should include the correct cloud-type-aware scope
140+
assertEquals("https://ossrdbms-aad.database.chinacloudapi.cn/.default",
141+
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
142+
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType());
143+
}
144+
145+
@Test
146+
void testJdbcPropertiesExplicitScopesOverridesDefault() {
147+
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
148+
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);
149+
150+
AzureJdbcPasswordlessProperties jdbcProperties = new AzureJdbcPasswordlessProperties();
151+
jdbcProperties.setScopes("https://custom-scope/.default");
152+
153+
AzureJdbcPasswordlessProperties result = new AzureJdbcPasswordlessProperties();
154+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, jdbcProperties, result);
155+
156+
// Explicit scopes should be preserved
157+
assertEquals("https://custom-scope/.default", result.getScopes());
158+
assertEquals("https://custom-scope/.default", result.getEffectiveScopes());
159+
assertEquals("https://custom-scope/.default",
160+
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
161+
}
162+
163+
@Test
164+
void testRedisPropertiesGetCorrectScopeFromChinaCloudTypeInGlobalProperties() {
165+
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
166+
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);
167+
168+
AzureRedisPasswordlessProperties redisProperties = new AzureRedisPasswordlessProperties();
169+
// User has not explicitly set scopes
170+
171+
AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties();
172+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, redisProperties, result);
173+
174+
// scopes field should be null (not explicitly set)
175+
assertNull(result.getScopes());
176+
// effective scopes should use the merged cloud type (AZURE_CHINA)
177+
assertEquals("https://*.cacheinfra.windows.net.china:10225/appid/.default", result.getEffectiveScopes());
178+
// toPasswordlessProperties should include the correct cloud-type-aware scope
179+
assertEquals("https://*.cacheinfra.windows.net.china:10225/appid/.default",
180+
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
181+
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType());
182+
}
183+
184+
@Test
185+
void testRedisPropertiesGetCorrectScopeFromUsGovCloudTypeInGlobalProperties() {
186+
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
187+
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT);
188+
189+
AzureRedisPasswordlessProperties redisProperties = new AzureRedisPasswordlessProperties();
190+
// User has not explicitly set scopes
191+
192+
AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties();
193+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, redisProperties, result);
194+
195+
// scopes field should be null (not explicitly set)
196+
assertNull(result.getScopes());
197+
// effective scopes should use the merged cloud type (AZURE_US_GOVERNMENT)
198+
assertEquals("https://*.cacheinfra.windows.us.government.net:10225/appid/.default", result.getEffectiveScopes());
199+
// toPasswordlessProperties should include the correct cloud-type-aware scope
200+
assertEquals("https://*.cacheinfra.windows.us.government.net:10225/appid/.default",
201+
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
202+
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_US_GOVERNMENT, result.getProfile().getCloudType());
203+
}
204+
205+
@Test
206+
void testRedisPropertiesExplicitScopesOverridesDefault() {
207+
AzureGlobalProperties globalProperties = new AzureGlobalProperties();
208+
globalProperties.getProfile().setCloudType(AzureProfileOptionsProvider.CloudType.AZURE_CHINA);
209+
210+
AzureRedisPasswordlessProperties redisProperties = new AzureRedisPasswordlessProperties();
211+
redisProperties.setScopes("https://custom-redis-scope/.default");
212+
213+
AzureRedisPasswordlessProperties result = new AzureRedisPasswordlessProperties();
214+
AzurePasswordlessPropertiesUtils.mergeAzureCommonProperties(globalProperties, redisProperties, result);
215+
216+
// Explicit scopes should be preserved
217+
assertEquals("https://custom-redis-scope/.default", result.getScopes());
218+
assertEquals("https://custom-redis-scope/.default", result.getEffectiveScopes());
219+
assertEquals("https://custom-redis-scope/.default",
220+
result.toPasswordlessProperties().getProperty(AuthProperty.SCOPES.getPropertyKey()));
221+
}
119222
}

sdk/spring/spring-cloud-azure-core/src/main/java/com/azure/spring/cloud/core/implementation/util/AzurePasswordlessPropertiesUtils.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,10 @@ public static <T extends PasswordlessProperties> void copyAzureCommonPropertiesI
5151
copyPropertiesIgnoreNull(source.getProfile().getEnvironment(), target.getProfile().getEnvironment());
5252
copyPropertiesIgnoreNull(source.getCredential(), target.getCredential());
5353

54-
target.setScopes(source.getScopes());
54+
String scopes = source.getScopes();
55+
if (scopes != null) {
56+
target.setScopes(scopes);
57+
}
5558
target.setPasswordlessEnabled(source.isPasswordlessEnabled());
5659
}
5760

0 commit comments

Comments
 (0)