Skip to content
Draft
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
2 changes: 2 additions & 0 deletions sdk/spring/spring-cloud-azure-autoconfigure/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bugs Fixed

- Fixed `azure.scopes` using wrong default 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))

Comment on lines +11 to +12
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The issue link in this new changelog entry points to #44945, but the PR description indicates this change is intended to fix issue #47096. Please update the changelog link/issue number so it references the correct issue.

Copilot uses AI. Check for mistakes.
### Other Changes

## 7.1.0 (2026-03-11)
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 @@ -39,11 +39,14 @@ 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";
private static final String SCOPES_CHINA = "azure.scopes=https://ossrdbms-aad.database.chinacloudapi.cn/.default";
private static final String SCOPES_US_GOVERNMENT = "azure.scopes=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 +156,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,13 @@

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.core.implementation.util.AzurePasswordlessPropertiesUtils;
import com.azure.spring.cloud.core.provider.AzureProfileOptionsProvider;
import org.junit.jupiter.api.Test;

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

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

@Test
void testJdbcPropertiesGetCorrectScopeFromGlobalCloudType() {
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The method name testJdbcPropertiesGetCorrectScopeFromGlobalCloudType is misleading: it sets the cloud type to AZURE_CHINA and asserts the China-specific scope. Renaming the test to reflect the scenario (e.g., China/global properties cloud type) would make the intent clearer.

Suggested change
void testJdbcPropertiesGetCorrectScopeFromGlobalCloudType() {
void testJdbcPropertiesGetCorrectScopeFromChinaCloudTypeInGlobalProperties() {

Copilot uses AI. Check for mistakes.
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("azure.scopes"));
assertEquals(AzureProfileOptionsProvider.CloudType.AZURE_CHINA, result.getProfile().getCloudType());
Comment on lines +136 to +140
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test uses the hard-coded property key string "azure.scopes". To reduce brittleness if the underlying key changes, prefer using the canonical constant (e.g., AuthProperty.SCOPES.getPropertyKey()) when reading from the Properties produced by toPasswordlessProperties().

Copilot uses AI. Check for mistakes.
}

@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("azure.scopes"));
}
}
2 changes: 2 additions & 0 deletions sdk/spring/spring-cloud-azure-core/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@

### Bugs Fixed

- Fixed `copyAzureCommonPropertiesIgnoreNull` to respect "ignore null" semantics for `scopes` property, preventing incorrect scope overwriting during property merging.

### Other Changes

## 7.1.0 (2026-03-11)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,9 @@ public static <T extends PasswordlessProperties> void copyAzureCommonPropertiesI
copyPropertiesIgnoreNull(source.getProfile().getEnvironment(), target.getProfile().getEnvironment());
copyPropertiesIgnoreNull(source.getCredential(), target.getCredential());

target.setScopes(source.getScopes());
if (source.getScopes() != null) {
target.setScopes(source.getScopes());
Comment on lines +54 to +55
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

copyAzureCommonPropertiesIgnoreNull calls source.getScopes() twice (once in the null-check and again when setting). If getScopes() is computed (as it is for some implementations), this can be redundant and can even produce inconsistent results if the computation is not purely deterministic. Consider caching the value in a local variable and reusing it for the null-check and assignment.

Suggested change
if (source.getScopes() != null) {
target.setScopes(source.getScopes());
String[] scopes = source.getScopes();
if (scopes != null) {
target.setScopes(scopes);

Copilot uses AI. Check for mistakes.
}
target.setPasswordlessEnabled(source.isPasswordlessEnabled());
Comment on lines +54 to 57
Copy link

Copilot AI Apr 3, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new null-guard relies on source.getScopes() returning null when scopes were not explicitly configured. Some PasswordlessProperties implementations compute a non-null default in getScopes() when the backing field is null (e.g., AzureRedisPasswordlessProperties#getScopes), so merging can still overwrite the target’s cloud-type-aware default with an Azure-global scope when the per-service profile.cloudType isn’t set. Consider using a way to distinguish “explicitly set scopes” vs “computed default” (or aligning other implementations with the raw/effective scopes split used for JDBC).

Copilot uses AI. Check for mistakes.
}

Expand Down
Loading