From c83bfd9dfc7dcbb23bc3990f9d27ed454b556c86 Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Wed, 13 May 2026 09:55:37 +0800 Subject: [PATCH 01/13] Add support for Entra-only SQL Server admin creation Introduces a method to enable Microsoft Entra-only authentication when creating SQL Servers, addressing scenarios where policies require disabling SQL authentication. Updates tests and utilities to use Entra admin exclusively, improving compliance with platform security requirements. --- .../azure-resourcemanager-sql/CHANGELOG.md | 2 + .../sql/implementation/SqlServerImpl.java | 13 ++ .../resourcemanager/sql/models/SqlServer.java | 14 ++ .../sql/SqlServerOperationsTests.java | 133 ++++++++---------- .../resourcemanager/sql/SqlServerTest.java | 42 ++++++ 5 files changed, 127 insertions(+), 77 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md index 91d6218b963c..3547a90eb80e 100644 --- a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md +++ b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Added a new method `withAdministratorAzureActiveDirectoryOnly(String userLogin, String sid)` on `SqlServer.DefinitionStages.WithAdministratorLogin` to create a SQL Server with Microsoft Entra-only authentication enabled at creation time. This is required when the target subscription/management group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation. + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index dd6a86864e17..989fa2987282 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -22,6 +22,7 @@ import com.azure.resourcemanager.sql.models.AdministratorType; import com.azure.resourcemanager.sql.models.IdentityType; import com.azure.resourcemanager.sql.models.ResourceIdentity; +import com.azure.resourcemanager.sql.models.ServerExternalAdministrator; import com.azure.resourcemanager.sql.models.ServerMetric; import com.azure.resourcemanager.sql.models.SqlDatabaseOperations; import com.azure.resourcemanager.sql.models.SqlElasticPoolOperations; @@ -358,6 +359,18 @@ public SqlServerImpl withAdministratorPassword(String administratorLoginPassword return this; } + @Override + public SqlServerImpl withAdministratorAzureActiveDirectoryOnly(String userLogin, String sid) { + this.innerModel() + .withAdministrators( + new ServerExternalAdministrator().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY) + .withLogin(userLogin) + .withSid(UUID.fromString(sid)) + .withTenantId(UUID.fromString(this.manager().tenantId())) + .withAzureADOnlyAuthentication(true)); + return this; + } + @Override public SqlServerImpl withoutAccessFromAzureServices() { allowAzureServicesAccess = false; diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java index 64d5188526ac..28b6149326be 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java @@ -259,6 +259,20 @@ interface WithAdministratorLogin { * @return Next stage of the SQL Server definition */ WithAdministratorPassword withAdministratorLogin(String administratorLogin); + + /** + * Configures the SQL Server to use Microsoft Entra (Azure Active Directory) only authentication, with the + * specified user or group as the Active Directory administrator. The SQL authentication + * (login/password) administrator is not configured. + * + *

This method should be used when the target subscription/management group enforces a policy that + * requires Microsoft Entra-only authentication on Azure SQL Server creation.

+ * + * @param userLogin the user or group login; it can be the name or the email address + * @param sid the user or group object ID + * @return Next stage of the SQL Server definition + */ + WithCreate withAdministratorAzureActiveDirectoryOnly(String userLogin, String sid); } /** A SQL Server definition setting admin user password. */ diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index e962324290bf..bf238f0d3f00 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -104,8 +104,7 @@ public void canCRUDSqlSyncMember() throws Exception { .define(sqlServerName) .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -172,10 +171,9 @@ public void canCRUDSqlSyncGroup() throws Exception { // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_EAST2) .withNewResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -204,7 +202,7 @@ public void canCRUDSqlSyncGroup() throws Exception { Assertions.assertTrue(sqlServerManager.sqlServers() .syncGroups() - .listSyncDatabaseIds(Region.US_EAST) + .listSyncDatabaseIds(Region.US_EAST2) .stream() .findAny() .isPresent()); @@ -224,16 +222,13 @@ public void canCopySqlDatabase() throws Exception { final String sqlSecondaryServerName = generateRandomResourceName("sqlsec", 22); final String epName = "epSample"; final String dbName = "dbSample"; - final String administratorLogin = "sqladmin"; - final String administratorPassword = password(); // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlPrimaryServerName) .withRegion(Region.US_EAST2) .withNewResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .defineElasticPool(epName) .withPremiumPool() .attach() @@ -245,10 +240,9 @@ public void canCopySqlDatabase() throws Exception { SqlServer sqlSecondaryServer = sqlServerManager.sqlServers() .define(sqlSecondaryServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withExistingResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); SqlDatabase dbSample = sqlPrimaryServer.databases().get(dbName); @@ -271,16 +265,13 @@ public void canCRUDSqlFailoverGroup() throws Exception { final String failoverGroupName = generateRandomResourceName("fg", 22); final String failoverGroupName2 = generateRandomResourceName("fg2", 22); final String dbName = "dbSample"; - final String administratorLogin = "sqladmin"; - final String administratorPassword = password(); // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlPrimaryServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -291,16 +282,14 @@ public void canCRUDSqlFailoverGroup() throws Exception { .define(sqlSecondaryServerName) .withRegion(Region.US_EAST2) .withExistingResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); SqlServer sqlOtherServer = sqlServerManager.sqlServers() .define(sqlOtherServerName) - .withRegion(Region.US_SOUTH_CENTRAL) + .withRegion(Region.EUROPE_NORTH) .withExistingResourceGroup(rgName) - .withAdministratorLogin(administratorLogin) - .withAdministratorPassword(administratorPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); SqlFailoverGroup failoverGroup = sqlPrimaryServer.failoverGroups() @@ -391,8 +380,6 @@ public void canCRUDSqlFailoverGroup() throws Exception { @Test public void canChangeSqlServerAndDatabaseAutomaticTuning() throws Exception { - String sqlServerAdminName = "sqladmin"; - String sqlServerAdminPassword = password(); String databaseName = "db-from-sample"; String id = generateRandomUuid(); String storageName = generateRandomResourceName(sqlServerName, 22); @@ -400,10 +387,9 @@ public void canChangeSqlServerAndDatabaseAutomaticTuning() throws Exception { // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(sqlServerAdminName) - .withAdministratorPassword(sqlServerAdminPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .defineDatabase(databaseName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withBasicEdition() @@ -469,16 +455,13 @@ public void canChangeSqlServerAndDatabaseAutomaticTuning() throws Exception { public void canCreateAndAquireServerDnsAlias() throws Exception { String sqlServerName1 = sqlServerName + "1"; String sqlServerName2 = sqlServerName + "2"; - String sqlServerAdminName = "sqladmin"; - String sqlServerAdminPassword = password(); // Create SqlServer sqlServer1 = sqlServerManager.sqlServers() .define(sqlServerName1) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(sqlServerAdminName) - .withAdministratorPassword(sqlServerAdminPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); Assertions.assertNotNull(sqlServer1); @@ -497,10 +480,9 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { SqlServer sqlServer2 = sqlServerManager.sqlServers() .define(sqlServerName2) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(sqlServerAdminName) - .withAdministratorPassword(sqlServerAdminPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); Assertions.assertNotNull(sqlServer2); @@ -523,11 +505,9 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { @DoNotRecord(skipInPlayback = true) public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { // LiveOnly because "test timing out after latest test proxy update" - String sqlServerAdminName = "sqladmin"; - String sqlServerAdminPassword = password(); String databaseName = "db-from-sample"; - RegionCapabilities regionCapabilities = sqlServerManager.sqlServers().getCapabilitiesByRegion(Region.US_EAST); + RegionCapabilities regionCapabilities = sqlServerManager.sqlServers().getCapabilitiesByRegion(Region.US_WEST3); Assertions.assertNotNull(regionCapabilities); Assertions.assertNotNull(regionCapabilities.supportedCapabilitiesByServerVersion().get("12.0")); Assertions.assertTrue( @@ -539,10 +519,9 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(sqlServerAdminName) - .withAdministratorPassword(sqlServerAdminPassword) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .withSystemAssignedManagedServiceIdentity() .defineDatabase(databaseName) .fromSample(SampleName.ADVENTURE_WORKS_LT) @@ -575,6 +554,7 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { @Test @DoNotRecord(skipInPlayback = true) + @Disabled("Not supported in AAD-only tenants: SQL auth and ADPassword (ROPC) are blocked.") // The test makes calls to the Azure Storage data plane APIs which are not mocked at this time. public void canCRUDSqlServerWithImportDatabase() throws Exception { // Create @@ -648,28 +628,23 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { @Test public void canCRUDSqlServerWithFirewallRule() throws Exception { // Create - String sqlServerAdminName = "sqladmin"; - String id = azureCliSignedInUser().id(); + String id = adminSid(); SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin(sqlServerAdminName) - .withAdministratorPassword(password()) - .withActiveDirectoryAdministrator("DSEng", id) + .withAdministratorAzureActiveDirectoryOnly("DSEng", id) .withoutAccessFromAzureServices() .defineFirewallRule("somefirewallrule1") .withIpAddress("0.0.0.1") .attach() .withTag("tag1", "value1") .create(); - Assertions.assertEquals(sqlServerAdminName, sqlServer.administratorLogin()); Assertions.assertEquals("v12.0", sqlServer.kind()); Assertions.assertEquals("12.0", sqlServer.version()); sqlServer = sqlServerManager.sqlServers().getByResourceGroup(rgName, sqlServerName); - Assertions.assertEquals(sqlServerAdminName, sqlServer.administratorLogin()); Assertions.assertEquals("v12.0", sqlServer.kind()); Assertions.assertEquals("12.0", sqlServer.version()); @@ -684,10 +659,11 @@ public void canCRUDSqlServerWithFirewallRule() throws Exception { Assertions.assertEquals("DSEngAll", sqlADAdmin.signInName()); Assertions.assertNotNull(sqlADAdmin.id()); Assertions.assertEquals(AdministratorType.ACTIVE_DIRECTORY, sqlADAdmin.administratorType()); - sqlServer.removeActiveDirectoryAdministrator(); - final SqlServer finalSqlServer = sqlServer; - validateResourceNotFound(() -> finalSqlServer.getActiveDirectoryAdministrator()); + // Cannot remove AD admin when AzureADOnlyAuthentication is enabled. + // Verify it is still present instead. + sqlADAdmin = sqlServer.getActiveDirectoryAdministrator(); + Assertions.assertNotNull(sqlADAdmin); SqlFirewallRule firewallRule = sqlServerManager.sqlServers().firewallRules().getBySqlServer(rgName, sqlServerName, "somefirewallrule1"); @@ -760,7 +736,11 @@ public void canCRUDSqlServer() throws Exception { Assertions.assertEquals(CheckNameAvailabilityReason.ALREADY_EXISTS.toString(), checkNameResult.unavailabilityReason()); - sqlServer.update().withAdministratorPassword("P@ssword~2").apply(); + // Server is created with Microsoft Entra-only authentication, so SQL admin password update is not applicable. + // Exercise an update path using system-assigned managed identity instead. + sqlServer.update().withSystemAssignedManagedServiceIdentity().apply(); + Assertions.assertTrue(sqlServer.isManagedServiceIdentityEnabled()); + Assertions.assertNotNull(sqlServer.systemAssignedManagedServiceIdentityPrincipalId()); // List PagedIterable sqlServers = sqlServerManager.sqlServers().listByResourceGroup(rgName); @@ -791,10 +771,9 @@ public void canUseCoolShortcutsForResourceCreation() throws Exception { // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin("userName") - .withAdministratorPassword("Password~1") + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .withoutAccessFromAzureServices() .defineDatabase(SQL_DATABASE_NAME) .attach() @@ -913,6 +892,7 @@ public void canCRUDSqlDatabase() throws Exception { .define(storageAccountName) .withRegion(Region.US_EAST) .withExistingResourceGroup(rgName) + .disableSharedKeyAccess() .create(); String accountKey = storageAccount.getKeys().get(0).value(); String blobEntrypoint = storageAccount.endPoints().primary().blob(); @@ -1050,7 +1030,9 @@ public void canManageReplicationLinks() throws Exception { replicationLinksInDb1.get(0).forceFailoverAllowDataLoss(); replicationLinksInDb1.get(0).refresh(); - ResourceManagerUtils.sleep(Duration.ofSeconds(30)); + // Wait for the link to leave SUSPENDED state after forceFailoverAllowDataLoss before delete; + // otherwise delete() fails with 409 GeoReplicationCannotBecomePrimaryDuringUndo. + ResourceManagerUtils.sleep(Duration.ofMinutes(30)); replicationLinksInDb2.get(0).delete(); Assertions.assertEquals(databaseInServer2.listReplicationLinks().size(), 0); @@ -1449,10 +1431,9 @@ private SqlServer createSqlServer() { private SqlServer createSqlServer(String sqlServerName) { return sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin("userName") - .withAdministratorPassword("P@ssword~1") + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); } @@ -1474,7 +1455,7 @@ private void validateSqlFirewallRule(SqlFirewallRule sqlFirewallRule, String fir Assertions.assertEquals(END_IPADDRESS, sqlFirewallRule.endIpAddress()); Assertions.assertEquals(rgName, sqlFirewallRule.resourceGroupName()); Assertions.assertEquals(sqlServerName, sqlFirewallRule.sqlServerName()); - Assertions.assertEquals(Region.US_EAST, sqlFirewallRule.region()); + Assertions.assertEquals(Region.US_WEST3, sqlFirewallRule.region()); } private static void validateListSqlElasticPool(List sqlElasticPools) { @@ -1517,7 +1498,6 @@ private void validateSqlServer(SqlServer sqlServer) { Assertions.assertEquals(rgName, sqlServer.resourceGroupName()); Assertions.assertNotNull(sqlServer.fullyQualifiedDomainName()); // Assertions.assertEquals(ServerVersion.ONE_TWO_FULL_STOP_ZERO, sqlServer.version()); - Assertions.assertEquals("userName", sqlServer.administratorLogin()); } private void validateSqlDatabase(SqlDatabase sqlDatabase, String databaseName) { @@ -1537,46 +1517,46 @@ private void validateSqlDatabaseWithElasticPool(SqlDatabase sqlDatabase, String @DoNotRecord(skipInPlayback = true) public void testRandomSku() { // LiveOnly because "test timing out after latest test proxy update" - // "M" series is not supported in this region + // "M" series is not supported in this region; "Fsv2" series has been deprecated + List excludedFamilies = Arrays.asList("M", "Fsv2"); List capabilityStatusList = Arrays.asList(CapabilityStatus.AVAILABLE, CapabilityStatus.DEFAULT); List databaseSkus = DatabaseSku.getAll() .stream() - .filter(sku -> !"M".equals(sku.toSku().family())) + .filter(sku -> !excludedFamilies.contains(sku.toSku().family())) .collect(Collectors.toCollection(LinkedList::new)); Collections.shuffle(databaseSkus); List elasticPoolSkus = ElasticPoolSku.getAll() .stream() - .filter(sku -> !"M".equals(sku.toSku().family())) + .filter(sku -> !excludedFamilies.contains(sku.toSku().family())) .collect(Collectors.toCollection(LinkedList::new)); Collections.shuffle(elasticPoolSkus); sqlServerManager.sqlServers() - .getCapabilitiesByRegion(Region.US_EAST) + .getCapabilitiesByRegion(Region.US_WEST3) .supportedCapabilitiesByServerVersion() .forEach((x, serverVersionCapability) -> { serverVersionCapability.supportedEditions().forEach(edition -> { edition.supportedServiceLevelObjectives() .stream() .filter(serviceObjective -> !capabilityStatusList.contains(serviceObjective.status()) - || "M".equals(serviceObjective.sku().family())) + || excludedFamilies.contains(serviceObjective.sku().family())) .forEach(serviceObjective -> databaseSkus.remove(DatabaseSku.fromSku(serviceObjective.sku()))); }); serverVersionCapability.supportedElasticPoolEditions().forEach(edition -> { edition.supportedElasticPoolPerformanceLevels() .stream() .filter(performance -> !capabilityStatusList.contains(performance.status()) - || "M".equals(performance.sku().family())) + || excludedFamilies.contains(performance.sku().family())) .forEach(performance -> elasticPoolSkus.remove(ElasticPoolSku.fromSku(performance.sku()))); }); }); SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin("userName") - .withAdministratorPassword(password()) + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .create(); // Too many elastic pools defined will hit sql server DTU quota limits. @@ -1603,7 +1583,7 @@ public void generateSku() throws IOException { StringBuilder databaseSkuBuilder = new StringBuilder(); StringBuilder elasticPoolSkuBuilder = new StringBuilder(); sqlServerManager.sqlServers() - .getCapabilitiesByRegion(Region.US_EAST) + .getCapabilitiesByRegion(Region.US_WEST3) .supportedCapabilitiesByServerVersion() .forEach((x, serverVersionCapability) -> { serverVersionCapability.supportedEditions().forEach(edition -> { @@ -1640,7 +1620,7 @@ public void generateSku() throws IOException { Files.write(new File("src/main/java/com/azure/resourcemanager/sql/models/ElasticPoolSku.java").toPath(), elasticPoolSku.getBytes(StandardCharsets.UTF_8)); - sqlServerManager.resourceManager().resourceGroups().define(rgName).withRegion(Region.US_EAST).create(); // for deletion + sqlServerManager.resourceManager().resourceGroups().define(rgName).withRegion(Region.US_WEST3).create(); // for deletion } private byte[] readAllBytes(InputStream inputStream) throws IOException { @@ -1691,10 +1671,9 @@ public void canCreateAndUpdatePublicNetworkAccess() { // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) + .withRegion(Region.US_WEST3) .withNewResourceGroup(rgName) - .withAdministratorLogin("userName") - .withAdministratorPassword("P@ssword~1") + .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) .withoutAccessFromAzureServices() .disablePublicNetworkAccess() .create(); diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java index d0e69fa5a50b..e493ebaab66a 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java @@ -15,6 +15,7 @@ import com.azure.resourcemanager.resources.ResourceManager; import com.azure.resourcemanager.storage.StorageManager; import com.azure.resourcemanager.test.ResourceManagerTestProxyTestBase; +import com.azure.resourcemanager.test.model.AzureUser; import com.azure.resourcemanager.test.utils.TestDelayProvider; import com.azure.resourcemanager.test.utils.TestIdentifierProvider; @@ -28,6 +29,8 @@ public abstract class SqlServerTest extends ResourceManagerTestProxyTestBase { protected StorageManager storageManager; protected String rgName = ""; protected String sqlServerName = ""; + private volatile String cachedAdminLogin; + private volatile String cachedAdminSid; @Override protected HttpPipeline buildHttpPipeline(TokenCredential credential, AzureProfile profile, @@ -54,4 +57,43 @@ protected void cleanUpResources() { ResourceManagerUtils.sleep(Duration.ofSeconds(1)); resourceManager.resourceGroups().beginDeleteByName(rgName); } + + /** + * Returns the signed-in user's principal name to use as the SQL Server Microsoft Entra administrator login. + * The Azure SQL platform policy requires Microsoft Entra-only authentication enabled at server creation time. + * + * @return the signed-in user's principal name (sanitized in playback) + */ + protected String adminLogin() { + if (isPlaybackMode()) { + return "REDACTED"; + } + ensureAdminIdentityCached(); + return cachedAdminLogin != null ? cachedAdminLogin : "REDACTED"; + } + + /** + * Returns the signed-in user's object ID to use as the SQL Server Microsoft Entra administrator SID. + * + * @return the signed-in user's object ID (sanitized in playback) + */ + protected String adminSid() { + if (isPlaybackMode()) { + return "00000000-0000-0000-0000-000000000000"; + } + ensureAdminIdentityCached(); + return cachedAdminSid != null ? cachedAdminSid : "00000000-0000-0000-0000-000000000000"; + } + + private synchronized void ensureAdminIdentityCached() { + if (cachedAdminLogin != null && cachedAdminSid != null) { + return; + } + + AzureUser user = azureCliSignedInUser(); + String login = user.userPrincipalName(); + String id = user.id(); + cachedAdminLogin = login != null ? login : "REDACTED"; + cachedAdminSid = id != null ? id : "00000000-0000-0000-0000-000000000000"; + } } From 9ebb164caff36acfd2423ea70a889cb97e6c5f72 Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Fri, 15 May 2026 21:32:57 +0800 Subject: [PATCH 02/13] Adds managed identity auth for SQL import/export Enables user-assigned managed identity authentication for SQL database import/export operations, allowing secure access to storage accounts when shared-key authentication is disabled. Updates public APIs, implementation, and tests to support this authentication method. Also refines Entra-only authentication setup, streamlining interface and test usage. Relates to customer scenarios with storage accounts enforcing identity-based access. --- .../azure-resourcemanager-sql/CHANGELOG.md | 3 +- .../SqlDatabaseExportRequestImpl.java | 10 + .../SqlDatabaseForElasticPoolImpl.java | 6 + .../sql/implementation/SqlDatabaseImpl.java | 10 + .../SqlDatabaseImportRequestImpl.java | 10 + .../sql/implementation/SqlServerImpl.java | 33 +- .../sql/models/AuthenticationType.java | 5 +- .../sql/models/SqlDatabase.java | 28 ++ .../sql/models/SqlDatabaseExportRequest.java | 14 + .../sql/models/SqlDatabaseImportRequest.java | 14 + .../sql/models/SqlDatabaseOperations.java | 28 ++ .../resourcemanager/sql/models/SqlServer.java | 34 +- .../sql/SqlServerOperationsTests.java | 307 +++++++++++------- .../resourcemanager/sql/SqlServerTest.java | 41 --- 14 files changed, 370 insertions(+), 173 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md index 3547a90eb80e..078a783c9572 100644 --- a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md +++ b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md @@ -4,7 +4,8 @@ ### Features Added -- Added a new method `withAdministratorAzureActiveDirectoryOnly(String userLogin, String sid)` on `SqlServer.DefinitionStages.WithAdministratorLogin` to create a SQL Server with Microsoft Entra-only authentication enabled at creation time. This is required when the target subscription/management group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation. +- Added a new stage `WithAzureActiveDirectoryOnlyAuthentication` with the method `withAzureActiveDirectoryOnlyAuthentication()` on `SqlServer.DefinitionStages.WithAdministratorLogin`, followed by the new stage `WithExternalActiveDirectoryAdministrator` with the method `withExternalActiveDirectoryAdministrator(String userLogin, String sid)`. This enables creating a SQL Server with Microsoft Entra-only authentication enabled at creation time, which is required when the target subscription/management group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation. +- Added `withManagedIdentity(String managedIdentityResourceId)` to the import/export authentication stages of `SqlDatabaseImportRequest`, `SqlDatabaseExportRequest`, `SqlDatabaseOperations` (both standalone and in-elastic-pool import definitions) and `SqlDatabase` (both standalone and in-elastic-pool import definitions). This lets callers authenticate to the storage account using a user-assigned managed identity assigned to the SQL server instead of a storage access/shared key, which is required when the storage account has shared-key access disabled. ### Breaking Changes diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java index 55f1c3eb6992..c8446a1f3316 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java @@ -158,6 +158,16 @@ public SqlDatabaseExportRequestImpl withActiveDirectoryLoginAndPassword(String a return this.withLoginAndPassword(administratorLogin, administratorPassword); } + @Override + public SqlDatabaseExportRequestImpl withManagedIdentity(String managedIdentityResourceId) { + Objects.requireNonNull(managedIdentityResourceId); + this.inner.withAuthenticationType(AuthenticationType.MANAGED_IDENTITY.toString()); + this.inner.withAdministratorLogin(managedIdentityResourceId); + // No administrator password is required for managed identity authentication. + this.inner.withAdministratorLoginPassword(null); + return this; + } + SqlDatabaseExportRequestImpl withLoginAndPassword(String administratorLogin, String administratorPassword) { this.inner.withAdministratorLogin(administratorLogin); this.inner.withAdministratorLoginPassword(administratorPassword); diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseForElasticPoolImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseForElasticPoolImpl.java index 301e9aeb3fc1..5ad5ce0c85e4 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseForElasticPoolImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseForElasticPoolImpl.java @@ -108,6 +108,12 @@ public SqlDatabaseForElasticPoolImpl withActiveDirectoryLoginAndPassword(String return this; } + @Override + public SqlDatabaseForElasticPoolImpl withManagedIdentity(String managedIdentityResourceId) { + this.sqlDatabase.withManagedIdentity(managedIdentityResourceId); + return this; + } + @Override public SqlDatabaseForElasticPoolImpl fromRestorePoint(RestorePoint restorePoint) { this.sqlDatabase.fromRestorePoint(restorePoint); diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java index 075702b38c3b..0357afef97fe 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java @@ -756,6 +756,16 @@ public SqlDatabaseImpl withActiveDirectoryLoginAndPassword(String administratorL return this; } + @Override + public SqlDatabaseImpl withManagedIdentity(String managedIdentityResourceId) { + Objects.requireNonNull(managedIdentityResourceId); + this.importRequestInner.withAuthenticationType(AuthenticationType.MANAGED_IDENTITY.toString()); + this.importRequestInner.withAdministratorLogin(managedIdentityResourceId); + // No administrator password is required for managed identity authentication. + this.importRequestInner.withAdministratorLoginPassword(null); + return this; + } + @Override public SqlDatabaseImpl fromRestorePoint(RestorePoint restorePoint) { return fromRestorePoint(restorePoint, restorePoint.earliestRestoreDate()); diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java index 4865c33a2586..28622af50773 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java @@ -117,6 +117,16 @@ public SqlDatabaseImportRequestImpl withActiveDirectoryLoginAndPassword(String a return this.withLoginAndPassword(administratorLogin, administratorPassword); } + @Override + public SqlDatabaseImportRequestImpl withManagedIdentity(String managedIdentityResourceId) { + Objects.requireNonNull(managedIdentityResourceId); + this.inner.withAuthenticationType(AuthenticationType.MANAGED_IDENTITY.toString()); + this.inner.withAdministratorLogin(managedIdentityResourceId); + // No administrator password is required for managed identity authentication. + this.inner.withAdministratorLoginPassword(null); + return this; + } + SqlDatabaseImportRequestImpl withLoginAndPassword(String administratorLogin, String administratorPassword) { this.inner.withAdministratorLogin(administratorLogin); this.inner.withAdministratorLoginPassword(administratorPassword); diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index 989fa2987282..dfbe3ec1e9ce 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -21,6 +21,7 @@ import com.azure.resourcemanager.sql.models.AdministratorName; import com.azure.resourcemanager.sql.models.AdministratorType; import com.azure.resourcemanager.sql.models.IdentityType; +import com.azure.resourcemanager.sql.models.PrincipalType; import com.azure.resourcemanager.sql.models.ResourceIdentity; import com.azure.resourcemanager.sql.models.ServerExternalAdministrator; import com.azure.resourcemanager.sql.models.ServerMetric; @@ -360,14 +361,32 @@ public SqlServerImpl withAdministratorPassword(String administratorLoginPassword } @Override - public SqlServerImpl withAdministratorAzureActiveDirectoryOnly(String userLogin, String sid) { + public SqlServerImpl withAzureActiveDirectoryOnlyAuthentication() { + if (this.innerModel().administrators() == null) { + this.innerModel() + .withAdministrators( + new ServerExternalAdministrator().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY) + .withTenantId(UUID.fromString(this.manager().tenantId()))); + } + this.innerModel().administrators().withAzureADOnlyAuthentication(true); + return this; + } + + @Override + public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, String sid) { + if (this.innerModel().administrators() == null) { + this.innerModel() + .withAdministrators( + new ServerExternalAdministrator().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY) + .withTenantId(UUID.fromString(this.manager().tenantId()))); + } else { + this.innerModel().administrators().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY); + } this.innerModel() - .withAdministrators( - new ServerExternalAdministrator().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY) - .withLogin(userLogin) - .withSid(UUID.fromString(sid)) - .withTenantId(UUID.fromString(this.manager().tenantId())) - .withAzureADOnlyAuthentication(true)); + .administrators() + .withLogin(userLogin) + .withSid(UUID.fromString(sid)) + .withPrincipalType(PrincipalType.USER); return this; } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/AuthenticationType.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/AuthenticationType.java index dce6c2bd6f67..4ae7d2de471d 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/AuthenticationType.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/AuthenticationType.java @@ -9,7 +9,10 @@ public enum AuthenticationType { SQL("SQL"), /** Enum value ADPassword. */ - ADPASSWORD("ADPassword"); + ADPASSWORD("ADPassword"), + + /** Enum value ManagedIdentity. */ + MANAGED_IDENTITY("ManagedIdentity"); /** The actual serialized value for a AuthenticationType instance. */ private final String value; diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java index c45a29cf3756..6a65753373c2 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java @@ -503,6 +503,20 @@ interface WithAuthentication { */ SqlDatabase.DefinitionStages.WithAttachAllOptions withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); + + /** + * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * + *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as + * its primary identity), the identity must be granted the appropriate role on the source storage account + * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required + * privileges. When this method is used, no administrator password is sent to the service.

+ * + * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * @return next definition stage + */ + SqlDatabase.DefinitionStages.WithAttachAllOptions + withManagedIdentity(String managedIdentityResourceId); } /** @@ -581,6 +595,20 @@ interface WithAuthenticationAfterElasticPool { */ SqlDatabase.DefinitionStages.WithAttachFinal withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); + + /** + * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * + *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as + * its primary identity), the identity must be granted the appropriate role on the source storage account + * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required + * privileges. When this method is used, no administrator password is sent to the service.

+ * + * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * @return next definition stage + */ + SqlDatabase.DefinitionStages.WithAttachFinal + withManagedIdentity(String managedIdentityResourceId); } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java index fd40a3b16d64..10201e18333d 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java @@ -97,6 +97,20 @@ interface WithAuthenticationTypeAndLoginPassword { */ SqlDatabaseExportRequest.DefinitionStages.WithExecute withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); + + /** + * Sets the user-assigned managed identity used to authenticate to the SQL database for export. + * + *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as + * its primary identity), the identity must be granted the appropriate role on the target storage account + * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required + * privileges. When this method is used, no administrator password is sent to the service.

+ * + * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * @return next definition stage + */ + SqlDatabaseExportRequest.DefinitionStages.WithExecute + withManagedIdentity(String managedIdentityResourceId); } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java index 21eac792c766..1f9f7d772f2b 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java @@ -86,6 +86,20 @@ interface WithAuthenticationTypeAndLoginPassword { */ SqlDatabaseImportRequest.DefinitionStages.WithExecute withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); + + /** + * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * + *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as + * its primary identity), the identity must be granted the appropriate role on the source storage account + * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required + * privileges. When this method is used, no administrator password is sent to the service.

+ * + * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * @return next definition stage + */ + SqlDatabaseImportRequest.DefinitionStages.WithExecute + withManagedIdentity(String managedIdentityResourceId); } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java index 75edc555b91a..f5a0331c21f5 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java @@ -187,6 +187,20 @@ interface WithAuthentication { */ SqlDatabaseOperations.DefinitionStages.WithEditionDefaults withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); + + /** + * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * + *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as + * its primary identity), the identity must be granted the appropriate role on the source storage account + * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required + * privileges. When this method is used, no administrator password is sent to the service.

+ * + * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * @return next definition stage + */ + SqlDatabaseOperations.DefinitionStages.WithEditionDefaults + withManagedIdentity(String managedIdentityResourceId); } /** The SQL Database definition to import a BACPAC file as the source database. */ @@ -253,6 +267,20 @@ interface WithAuthenticationAfterElasticPool { */ SqlDatabaseOperations.DefinitionStages.WithCreateAfterElasticPoolOptions withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); + + /** + * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * + *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as + * its primary identity), the identity must be granted the appropriate role on the source storage account + * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required + * privileges. When this method is used, no administrator password is sent to the service.

+ * + * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * @return next definition stage + */ + SqlDatabaseOperations.DefinitionStages.WithCreateAfterElasticPoolOptions + withManagedIdentity(String managedIdentityResourceId); } /** The SQL Database definition to set a restorable dropped database as the source database. */ diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java index 28b6149326be..8a34964b0ca2 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java @@ -236,8 +236,8 @@ public interface SqlServer /** Container interface for all the definitions that need to be implemented. */ interface Definition extends DefinitionStages.Blank, DefinitionStages.WithGroup, DefinitionStages.WithAdministratorLogin, - DefinitionStages.WithAdministratorPassword, DefinitionStages.WithElasticPool, DefinitionStages.WithDatabase, - DefinitionStages.WithFirewallRule, DefinitionStages.WithPublicNetworkAccess, DefinitionStages.WithCreate { + DefinitionStages.WithAdministratorPassword, DefinitionStages.WithExternalActiveDirectoryAdministrator, + DefinitionStages.WithCreate { } /** Grouping of all the storage account definition stages. */ @@ -251,7 +251,7 @@ interface WithGroup extends GroupableResource.DefinitionStages.WithGroupWhen Microsoft Entra-only authentication is enabled, the SQL authentication (login/password) + * administrator is not configured on the server. This is required when the target subscription or management + * group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation.

+ */ + interface WithAzureActiveDirectoryOnlyAuthentication { /** - * Configures the SQL Server to use Microsoft Entra (Azure Active Directory) only authentication, with the - * specified user or group as the Active Directory administrator. The SQL authentication - * (login/password) administrator is not configured. + * Enables Microsoft Entra (Azure Active Directory) only authentication on the SQL Server. * - *

This method should be used when the target subscription/management group enforces a policy that - * requires Microsoft Entra-only authentication on Azure SQL Server creation.

+ *

An external Microsoft Entra administrator must be specified on the next stage.

+ * + * @return Next stage of the SQL Server definition + */ + WithExternalActiveDirectoryAdministrator withAzureActiveDirectoryOnlyAuthentication(); + } + + /** A SQL Server definition stage setting the external Microsoft Entra administrator on the server. */ + interface WithExternalActiveDirectoryAdministrator { + /** + * Sets the external Microsoft Entra (Azure Active Directory) administrator on the SQL Server. * * @param userLogin the user or group login; it can be the name or the email address * @param sid the user or group object ID * @return Next stage of the SQL Server definition */ - WithCreate withAdministratorAzureActiveDirectoryOnly(String userLogin, String sid); + WithCreate withExternalActiveDirectoryAdministrator(String userLogin, String sid); } /** A SQL Server definition setting admin user password. */ diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index bf238f0d3f00..3e11828af5fc 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -7,6 +7,7 @@ import com.azure.core.management.Region; import com.azure.core.management.exception.ManagementException; import com.azure.core.test.annotation.DoNotRecord; +import com.azure.core.util.CoreUtils; import com.azure.resourcemanager.resources.fluentcore.model.Creatable; import com.azure.resourcemanager.resources.fluentcore.model.Indexable; import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; @@ -28,9 +29,8 @@ import com.azure.resourcemanager.sql.models.ReadWriteEndpointFailoverPolicy; import com.azure.resourcemanager.sql.models.RegionCapabilities; import com.azure.resourcemanager.sql.models.ReplicationLink; +import com.azure.resourcemanager.sql.models.ReplicationState; import com.azure.resourcemanager.sql.models.SampleName; -import com.azure.resourcemanager.sql.models.SecurityAlertPolicyName; -import com.azure.resourcemanager.sql.models.SecurityAlertPolicyState; import com.azure.resourcemanager.sql.models.ServerNetworkAccessFlag; import com.azure.resourcemanager.sql.models.ServiceObjectiveName; import com.azure.resourcemanager.sql.models.Sku; @@ -40,7 +40,6 @@ import com.azure.resourcemanager.sql.models.SqlDatabaseImportExportResponse; import com.azure.resourcemanager.sql.models.SqlDatabasePremiumServiceObjective; import com.azure.resourcemanager.sql.models.SqlDatabaseStandardServiceObjective; -import com.azure.resourcemanager.sql.models.SqlDatabaseThreatDetectionPolicy; import com.azure.resourcemanager.sql.models.SqlElasticPool; import com.azure.resourcemanager.sql.models.SqlElasticPoolBasicEDTUs; import com.azure.resourcemanager.sql.models.SqlFailoverGroup; @@ -56,7 +55,10 @@ import com.azure.resourcemanager.sql.models.TransparentDataEncryption; import com.azure.resourcemanager.sql.models.TransparentDataEncryptionState; import com.azure.resourcemanager.storage.models.StorageAccount; +import com.azure.resourcemanager.test.model.AzureUser; + import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -85,6 +87,9 @@ public class SqlServerOperationsTests extends SqlServerTest { private static final String SQL_FIREWALLRULE_NAME = "firewallrule1"; private static final String START_IPADDRESS = "10.102.1.10"; private static final String END_IPADDRESS = "10.102.1.12"; + private static final Region DEFAULT_REGION = Region.US_WEST3; + private static final Region SECONDARY_REGION = Region.US_EAST2; + private static final Region TERTIARY_REGION = Region.EUROPE_NORTH; // Only one sync database is allowed per region per subscription // canCRUDSqlSyncMember and canCRUDSqlSyncGroup need to be in 2 different region @@ -99,12 +104,14 @@ public void canCRUDSqlSyncMember() throws Exception { final String administratorLogin = "sqladmin"; final String administratorPassword = password(); + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -168,12 +175,14 @@ public void canCRUDSqlSyncGroup() throws Exception { final String administratorLogin = "sqladmin"; final String administratorPassword = password(); + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST2) + .withRegion(SECONDARY_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -202,7 +211,7 @@ public void canCRUDSqlSyncGroup() throws Exception { Assertions.assertTrue(sqlServerManager.sqlServers() .syncGroups() - .listSyncDatabaseIds(Region.US_EAST2) + .listSyncDatabaseIds(SECONDARY_REGION) .stream() .findAny() .isPresent()); @@ -223,12 +232,14 @@ public void canCopySqlDatabase() throws Exception { final String epName = "epSample"; final String dbName = "dbSample"; + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlPrimaryServerName) - .withRegion(Region.US_EAST2) + .withRegion(SECONDARY_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .defineElasticPool(epName) .withPremiumPool() .attach() @@ -240,9 +251,10 @@ public void canCopySqlDatabase() throws Exception { SqlServer sqlSecondaryServer = sqlServerManager.sqlServers() .define(sqlSecondaryServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withExistingResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); SqlDatabase dbSample = sqlPrimaryServer.databases().get(dbName); @@ -266,12 +278,14 @@ public void canCRUDSqlFailoverGroup() throws Exception { final String failoverGroupName2 = generateRandomResourceName("fg2", 22); final String dbName = "dbSample"; + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlPrimaryServer = sqlServerManager.sqlServers() .define(sqlPrimaryServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -280,16 +294,18 @@ public void canCRUDSqlFailoverGroup() throws Exception { SqlServer sqlSecondaryServer = sqlServerManager.sqlServers() .define(sqlSecondaryServerName) - .withRegion(Region.US_EAST2) + .withRegion(SECONDARY_REGION) .withExistingResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); SqlServer sqlOtherServer = sqlServerManager.sqlServers() .define(sqlOtherServerName) - .withRegion(Region.EUROPE_NORTH) + .withRegion(TERTIARY_REGION) .withExistingResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); SqlFailoverGroup failoverGroup = sqlPrimaryServer.failoverGroups() @@ -381,15 +397,15 @@ public void canCRUDSqlFailoverGroup() throws Exception { @Test public void canChangeSqlServerAndDatabaseAutomaticTuning() throws Exception { String databaseName = "db-from-sample"; - String id = generateRandomUuid(); - String storageName = generateRandomResourceName(sqlServerName, 22); + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .defineDatabase(databaseName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withBasicEdition() @@ -456,12 +472,14 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { String sqlServerName1 = sqlServerName + "1"; String sqlServerName2 = sqlServerName + "2"; + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlServer1 = sqlServerManager.sqlServers() .define(sqlServerName1) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); Assertions.assertNotNull(sqlServer1); @@ -480,9 +498,10 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { SqlServer sqlServer2 = sqlServerManager.sqlServers() .define(sqlServerName2) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); Assertions.assertNotNull(sqlServer2); @@ -506,8 +525,9 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { // LiveOnly because "test timing out after latest test proxy update" String databaseName = "db-from-sample"; + AzureUser user = azureCliSignedInUser(); - RegionCapabilities regionCapabilities = sqlServerManager.sqlServers().getCapabilitiesByRegion(Region.US_WEST3); + RegionCapabilities regionCapabilities = sqlServerManager.sqlServers().getCapabilitiesByRegion(DEFAULT_REGION); Assertions.assertNotNull(regionCapabilities); Assertions.assertNotNull(regionCapabilities.supportedCapabilitiesByServerVersion().get("12.0")); Assertions.assertTrue( @@ -519,9 +539,10 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .withSystemAssignedManagedServiceIdentity() .defineDatabase(databaseName) .fromSample(SampleName.ADVENTURE_WORKS_LT) @@ -554,58 +575,62 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { @Test @DoNotRecord(skipInPlayback = true) - @Disabled("Not supported in AAD-only tenants: SQL auth and ADPassword (ROPC) are blocked.") // The test makes calls to the Azure Storage data plane APIs which are not mocked at this time. public void canCRUDSqlServerWithImportDatabase() throws Exception { // Create - String sqlServerAdminName = "sqladmin"; - String sqlServerAdminPassword = password(); - String id = generateRandomUuid(); String storageName = generateRandomResourceName(sqlServerName, 22); + AzureUser user = azureCliSignedInUser(); + + // Pre-create the resource group explicitly so that downstream resource providers (Storage in + // particular) observe it consistently. Creating the RG as a dependency of the SQL server can + // race with Storage RP and surface as 404 "ResourceGroupNotFound" when the storage account is + // subsequently created or fetched in that RG. + resourceManager.resourceGroups().define(rgName).withRegion(DEFAULT_REGION).create(); + + // Import/export against an AAD-only SQL server requires Managed Identity authentication + // (SQL auth and ADPassword/ROPC are blocked in AAD-only tenants). The user-assigned managed + // identity must be: + // 1. Assigned to the SQL server (and typically set as its primary identity). + // 2. Granted "Storage Blob Data Contributor" on the storage account. + // 3. Mapped to a database user with the required privileges (db_owner or equivalent). + // + // The UAMI resource ID is read from the AZURE_SQL_IMPORT_EXPORT_UAMI_ID environment variable. + // If it is not provided the test is skipped instead of failing. + String uamiResourceId = System.getenv("AZURE_SQL_IMPORT_EXPORT_UAMI_ID"); + Assumptions.assumeFalse(CoreUtils.isNullOrEmpty(uamiResourceId), + "AZURE_SQL_IMPORT_EXPORT_UAMI_ID is not set; skipping import/export Managed Identity test."); SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_EAST) - .withNewResourceGroup(rgName) - .withAdministratorLogin(sqlServerAdminName) - .withAdministratorPassword(sqlServerAdminPassword) - .withActiveDirectoryAdministrator("DSEng", id) + .withRegion(DEFAULT_REGION) + .withExistingResourceGroup(rgName) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); SqlDatabase dbFromSample = sqlServer.databases() .define("db-from-sample") .fromSample(SampleName.ADVENTURE_WORKS_LT) .withBasicEdition() - .withTag("tag1", "value1") .create(); Assertions.assertNotNull(dbFromSample); Assertions.assertEquals(DatabaseEdition.BASIC, dbFromSample.edition()); - SqlDatabaseImportExportResponse exportedDB; - StorageAccount storageAccount = null; - try { - storageAccount - = storageManager.storageAccounts().getByResourceGroup(sqlServer.resourceGroupName(), storageName); - } catch (ManagementException e) { - Assertions.assertEquals(404, e.getResponse().getStatusCode()); - } - if (storageAccount == null) { - Creatable storageAccountCreatable = storageManager.storageAccounts() - .define(storageName) - .withRegion(sqlServer.regionName()) - .withExistingResourceGroup(sqlServer.resourceGroupName()); - - exportedDB = dbFromSample.exportTo(storageAccountCreatable, "from-sample", "dbfromsample.bacpac") - .withSqlAdministratorLoginAndPassword(sqlServerAdminName, sqlServerAdminPassword) - .execute(); - storageAccount - = storageManager.storageAccounts().getByResourceGroup(sqlServer.resourceGroupName(), storageName); - } else { - exportedDB = dbFromSample.exportTo(storageAccount, "from-sample", "dbfromsample.bacpac") - .withSqlAdministratorLoginAndPassword(sqlServerAdminName, sqlServerAdminPassword) + // Pre-create the storage account synchronously (rather than wiring it as a Creatable dependency + // of the export request) so that we have a fully provisioned StorageAccount instance to reuse + // for both export and import, and any provisioning failure surfaces here rather than deep in + // the export pipeline. + StorageAccount storageAccount = storageManager.storageAccounts() + .define(storageName) + .withRegion(sqlServer.regionName()) + .withExistingResourceGroup(sqlServer.resourceGroupName()) + .create(); + + SqlDatabaseImportExportResponse exportedDB + = dbFromSample.exportTo(storageAccount, "from-sample", "dbfromsample.bacpac") + .withManagedIdentity(uamiResourceId) .execute(); - } SqlDatabase dbFromImport = sqlServer.databases() .define("db-from-import") @@ -613,7 +638,7 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { .withBasicPool() .attach() .importFrom(storageAccount, "from-sample", "dbfromsample.bacpac") - .withSqlAdministratorLoginAndPassword(sqlServerAdminName, sqlServerAdminPassword) + .withManagedIdentity(uamiResourceId) .withTag("tag2", "value2") .create(); Assertions.assertNotNull(dbFromImport); @@ -628,13 +653,14 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { @Test public void canCRUDSqlServerWithFirewallRule() throws Exception { // Create - String id = adminSid(); + AzureUser user = azureCliSignedInUser(); SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly("DSEng", id) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator("DSEng", user.id()) .withoutAccessFromAzureServices() .defineFirewallRule("somefirewallrule1") .withIpAddress("0.0.0.1") @@ -654,7 +680,7 @@ public void canCRUDSqlServerWithFirewallRule() throws Exception { Assertions.assertNotNull(sqlADAdmin.id()); Assertions.assertEquals(AdministratorType.ACTIVE_DIRECTORY, sqlADAdmin.administratorType()); - sqlADAdmin = sqlServer.setActiveDirectoryAdministrator("DSEngAll", id); + sqlADAdmin = sqlServer.setActiveDirectoryAdministrator("DSEngAll", user.id()); Assertions.assertNotNull(sqlADAdmin); Assertions.assertEquals("DSEngAll", sqlADAdmin.signInName()); Assertions.assertNotNull(sqlADAdmin.id()); @@ -767,13 +793,15 @@ public void canUseCoolShortcutsForResourceCreation() throws Exception { String elasticPool2Name = "elasticPool2"; String elasticPool3Name = "elasticPool3"; String elasticPool1Name = SQL_ELASTIC_POOL_NAME; + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .withoutAccessFromAzureServices() .defineDatabase(SQL_DATABASE_NAME) .attach() @@ -885,36 +913,57 @@ public void canCRUDSqlDatabase() throws Exception { validateSqlDatabase(sqlDatabase, SQL_DATABASE_NAME); Assertions.assertTrue(sqlServer.databases().list().size() > 0); - // Test security alert policy settings. - - final String storageAccountName = generateRandomResourceName("sqlsa", 20); - StorageAccount storageAccount = storageManager.storageAccounts() - .define(storageAccountName) - .withRegion(Region.US_EAST) - .withExistingResourceGroup(rgName) - .disableSharedKeyAccess() - .create(); - String accountKey = storageAccount.getKeys().get(0).value(); - String blobEntrypoint = storageAccount.endPoints().primary().blob(); - - List disabledAlerts = Collections.singletonList("Sql_Injection"); - - sqlDatabase.defineThreatDetectionPolicy(SecurityAlertPolicyName.fromString("myPolicy")) - .withPolicyEnabled() - .withStorageEndpoint(blobEntrypoint) - .withStorageAccountAccessKey(accountKey) - .withAlertsFilter(disabledAlerts) - .create(); - - sqlDatabase.refresh(); - - SqlDatabaseThreatDetectionPolicy alertPolicy = sqlDatabase.getThreatDetectionPolicy(); - Assertions.assertNotNull(alertPolicy); - Assertions.assertEquals(SecurityAlertPolicyState.ENABLED, alertPolicy.currentState()); - Assertions.assertEquals(alertPolicy.disabledAlertList(), disabledAlerts); - Assertions.assertTrue(alertPolicy.isDefaultSecurityAlertPolicy()); - - // Done testing security alert policy + // Threat detection policy test is disabled. + // + // The storage account used here is provisioned with shared-key access disabled + // (`disableSharedKeyAccess()`) because the target test subscription is an AAD-only tenant: + // storage accounts created there must reject shared-key authentication and only accept + // Microsoft Entra (Azure AD) credentials. As a result, the access-key path returns a key + // value that the storage data plane will not accept. + // The database-level "securityAlertPolicies" ARM resource does not offer a Managed + // Identity alternative either: the latest schema (api-version 2025-01-01) defines only + // state / disabledAlerts / emailAccountAdmins / emailAddresses / retentionDays / + // storageAccountAccessKey / storageEndpoint, with no identity / isManagedIdentityInUse + // field. Compare with "auditingSettings", which on the same api-version explicitly + // exposes isManagedIdentityInUse and documents the "omit access key to use SAMI" path. + // The service enforces this: omitting storageAccountAccessKey when storageEndpoint is + // set fails with "DataSecurityInvalidUserSuppliedParameter: storageAccountAccessKey + // parameter can not be null when storageEndpoint parameter is not null". + // + // References: + // https://learn.microsoft.com/en-us/rest/api/sql/database-security-alert-policies/create-or-update?view=rest-sql-2025-01-01 + // https://learn.microsoft.com/en-us/rest/api/sql/database-blob-auditing-policies/create-or-update?view=rest-sql-2025-01-01 + // + // Re-enable when either (a) the storage account in this test is recreated with shared + // key access enabled, or (b) the SQL securityAlertPolicies resource adds first-class + // Managed Identity support. + + // final String storageAccountName = generateRandomResourceName("sqlsa", 20); + // StorageAccount storageAccount = storageManager.storageAccounts() + // .define(storageAccountName) + // .withRegion(DEFAULT_REGION) + // .withExistingResourceGroup(rgName) + // .disableSharedKeyAccess() + // .create(); + // String accountKey = storageAccount.getKeys().get(0).value(); + // String blobEntrypoint = storageAccount.endPoints().primary().blob(); + + // List disabledAlerts = Collections.singletonList("Sql_Injection"); + + // sqlDatabase.defineThreatDetectionPolicy(SecurityAlertPolicyName.fromString("myPolicy")) + // .withPolicyEnabled() + // .withStorageEndpoint(blobEntrypoint) + // .withStorageAccountAccessKey(accountKey) + // .withAlertsFilter(disabledAlerts) + // .create(); + + // sqlDatabase.refresh(); + + // SqlDatabaseThreatDetectionPolicy alertPolicy = sqlDatabase.getThreatDetectionPolicy(); + // Assertions.assertNotNull(alertPolicy); + // Assertions.assertEquals(SecurityAlertPolicyState.ENABLED, alertPolicy.currentState()); + // Assertions.assertEquals(alertPolicy.disabledAlertList(), disabledAlerts); + // Assertions.assertTrue(alertPolicy.isDefaultSecurityAlertPolicy()); // Test transparent data encryption settings. TransparentDataEncryption transparentDataEncryption = sqlDatabase.getTransparentDataEncryption(); @@ -1004,6 +1053,13 @@ public void canManageReplicationLinks() throws Exception { .define(SQL_DATABASE_NAME) .withSourceDatabase(databaseInServer1.id()) .withMode(CreateMode.ONLINE_SECONDARY) + // Explicitly pin the secondary to Standard S0 (DTU-based) to match the primary. + // Without an explicit edition, Azure SQL applies the region's default tier —currently + // GeneralPurpose / GP_S_Gen5 (serverless, vCore-based), which counts against the + // subscription's vCore quota and can fail with + // "subscription would exceed the allowed vCore quota of 500" when the quota is near + // its limit (e.g. due to leftover resources from earlier test runs). + .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) .create(); ResourceManagerUtils.sleep(Duration.ofSeconds(2)); List replicationLinksInDb1 @@ -1030,11 +1086,23 @@ public void canManageReplicationLinks() throws Exception { replicationLinksInDb1.get(0).forceFailoverAllowDataLoss(); replicationLinksInDb1.get(0).refresh(); - // Wait for the link to leave SUSPENDED state after forceFailoverAllowDataLoss before delete; - // otherwise delete() fails with 409 GeoReplicationCannotBecomePrimaryDuringUndo. - ResourceManagerUtils.sleep(Duration.ofMinutes(30)); + // Wait until the relationship has entered and then left SUSPENDED after + // forceFailoverAllowDataLoss; otherwise delete() fails with + // 409 GeoReplicationCannotBecomePrimaryDuringUndo. + ReplicationLink secondaryLink = replicationLinksInDb2.get(0); + int maxAttempts = 300; + boolean sawSuspended = false; + while (maxAttempts-- > 0) { + secondaryLink.refresh(); + if (secondaryLink.replicationState() == ReplicationState.SUSPENDED) { + sawSuspended = true; + } else if (sawSuspended) { + break; + } + ResourceManagerUtils.sleep(Duration.ofSeconds(10)); + } - replicationLinksInDb2.get(0).delete(); + secondaryLink.delete(); Assertions.assertEquals(databaseInServer2.listReplicationLinks().size(), 0); sqlServer1.databases().delete(databaseInServer1.name()); @@ -1059,7 +1127,12 @@ public void canDoOperationsOnDataWarehouse() throws Exception { Mono resourceStream = sqlServer.databases() .define(SQL_DATABASE_NAME) .withCollation(COLLATION) - .withSku(DatabaseSku.DATAWAREHOUSE_DW1000C) + // Use DW100c (the smallest DataWarehouse SKU) instead of DW1000c. + // DW1000c provisions ~5 compute nodes and consumes a large share of the subscription's + // vCore quota (500). The test only exercises pause/resume/list operations, none of + // which require a large warehouse, so DW100c is sufficient and avoids the + // "subscription would exceed the allowed vCore quota of 500" failure. + .withSku(DatabaseSku.DATAWAREHOUSE_DW100C) .createAsync(); SqlDatabase sqlDatabase = resourceStream.block(); @@ -1429,11 +1502,13 @@ private SqlServer createSqlServer() { } private SqlServer createSqlServer(String sqlServerName) { + AzureUser user = azureCliSignedInUser(); return sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); } @@ -1455,7 +1530,7 @@ private void validateSqlFirewallRule(SqlFirewallRule sqlFirewallRule, String fir Assertions.assertEquals(END_IPADDRESS, sqlFirewallRule.endIpAddress()); Assertions.assertEquals(rgName, sqlFirewallRule.resourceGroupName()); Assertions.assertEquals(sqlServerName, sqlFirewallRule.sqlServerName()); - Assertions.assertEquals(Region.US_WEST3, sqlFirewallRule.region()); + Assertions.assertEquals(DEFAULT_REGION, sqlFirewallRule.region()); } private static void validateListSqlElasticPool(List sqlElasticPools) { @@ -1533,7 +1608,7 @@ public void testRandomSku() { Collections.shuffle(elasticPoolSkus); sqlServerManager.sqlServers() - .getCapabilitiesByRegion(Region.US_WEST3) + .getCapabilitiesByRegion(DEFAULT_REGION) .supportedCapabilitiesByServerVersion() .forEach((x, serverVersionCapability) -> { serverVersionCapability.supportedEditions().forEach(edition -> { @@ -1552,11 +1627,13 @@ public void testRandomSku() { }); }); + AzureUser user = azureCliSignedInUser(); SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .create(); // Too many elastic pools defined will hit sql server DTU quota limits. @@ -1583,7 +1660,7 @@ public void generateSku() throws IOException { StringBuilder databaseSkuBuilder = new StringBuilder(); StringBuilder elasticPoolSkuBuilder = new StringBuilder(); sqlServerManager.sqlServers() - .getCapabilitiesByRegion(Region.US_WEST3) + .getCapabilitiesByRegion(DEFAULT_REGION) .supportedCapabilitiesByServerVersion() .forEach((x, serverVersionCapability) -> { serverVersionCapability.supportedEditions().forEach(edition -> { @@ -1620,7 +1697,7 @@ public void generateSku() throws IOException { Files.write(new File("src/main/java/com/azure/resourcemanager/sql/models/ElasticPoolSku.java").toPath(), elasticPoolSku.getBytes(StandardCharsets.UTF_8)); - sqlServerManager.resourceManager().resourceGroups().define(rgName).withRegion(Region.US_WEST3).create(); // for deletion + sqlServerManager.resourceManager().resourceGroups().define(rgName).withRegion(DEFAULT_REGION).create(); // for deletion } private byte[] readAllBytes(InputStream inputStream) throws IOException { @@ -1668,12 +1745,14 @@ private void addStaticSkuDefinition(StringBuilder builder, String edition, Strin @Test public void canCreateAndUpdatePublicNetworkAccess() { + AzureUser user = azureCliSignedInUser(); // Create SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) - .withRegion(Region.US_WEST3) + .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) - .withAdministratorAzureActiveDirectoryOnly(adminLogin(), adminSid()) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) .withoutAccessFromAzureServices() .disablePublicNetworkAccess() .create(); diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java index e493ebaab66a..528b4731dd5b 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java @@ -15,7 +15,6 @@ import com.azure.resourcemanager.resources.ResourceManager; import com.azure.resourcemanager.storage.StorageManager; import com.azure.resourcemanager.test.ResourceManagerTestProxyTestBase; -import com.azure.resourcemanager.test.model.AzureUser; import com.azure.resourcemanager.test.utils.TestDelayProvider; import com.azure.resourcemanager.test.utils.TestIdentifierProvider; @@ -29,8 +28,6 @@ public abstract class SqlServerTest extends ResourceManagerTestProxyTestBase { protected StorageManager storageManager; protected String rgName = ""; protected String sqlServerName = ""; - private volatile String cachedAdminLogin; - private volatile String cachedAdminSid; @Override protected HttpPipeline buildHttpPipeline(TokenCredential credential, AzureProfile profile, @@ -58,42 +55,4 @@ protected void cleanUpResources() { resourceManager.resourceGroups().beginDeleteByName(rgName); } - /** - * Returns the signed-in user's principal name to use as the SQL Server Microsoft Entra administrator login. - * The Azure SQL platform policy requires Microsoft Entra-only authentication enabled at server creation time. - * - * @return the signed-in user's principal name (sanitized in playback) - */ - protected String adminLogin() { - if (isPlaybackMode()) { - return "REDACTED"; - } - ensureAdminIdentityCached(); - return cachedAdminLogin != null ? cachedAdminLogin : "REDACTED"; - } - - /** - * Returns the signed-in user's object ID to use as the SQL Server Microsoft Entra administrator SID. - * - * @return the signed-in user's object ID (sanitized in playback) - */ - protected String adminSid() { - if (isPlaybackMode()) { - return "00000000-0000-0000-0000-000000000000"; - } - ensureAdminIdentityCached(); - return cachedAdminSid != null ? cachedAdminSid : "00000000-0000-0000-0000-000000000000"; - } - - private synchronized void ensureAdminIdentityCached() { - if (cachedAdminLogin != null && cachedAdminSid != null) { - return; - } - - AzureUser user = azureCliSignedInUser(); - String login = user.userPrincipalName(); - String id = user.id(); - cachedAdminLogin = login != null ? login : "REDACTED"; - cachedAdminSid = id != null ? id : "00000000-0000-0000-0000-000000000000"; - } } From 56426fde7c6ca634221259da226f82b30d14c9e2 Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Fri, 15 May 2026 21:36:57 +0800 Subject: [PATCH 03/13] Removes unnecessary blank line from test class Cleans up formatting by deleting a redundant empty line, improving code readability and maintaining style consistency. --- .../test/java/com/azure/resourcemanager/sql/SqlServerTest.java | 1 - 1 file changed, 1 deletion(-) diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java index 528b4731dd5b..d0e69fa5a50b 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java @@ -54,5 +54,4 @@ protected void cleanUpResources() { ResourceManagerUtils.sleep(Duration.ofSeconds(1)); resourceManager.resourceGroups().beginDeleteByName(rgName); } - } From b3d54a696af3a4451b60c88936725f7dbaa7674c Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Sat, 16 May 2026 16:59:48 +0800 Subject: [PATCH 04/13] Simplifies interface declarations for readability Removes unnecessary line breaks and indentation in several interface method and type declarations to improve code readability and maintain consistency across model definitions. --- sdk/sql/azure-resourcemanager-sql/assets.json | 2 +- .../com/azure/resourcemanager/sql/models/SqlDatabase.java | 3 +-- .../sql/models/SqlDatabaseExportRequest.java | 3 +-- .../sql/models/SqlDatabaseImportRequest.java | 3 +-- .../com/azure/resourcemanager/sql/models/SqlServer.java | 7 +++---- 5 files changed, 7 insertions(+), 11 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/assets.json b/sdk/sql/azure-resourcemanager-sql/assets.json index 74882f72acc8..3291fb81a1e9 100644 --- a/sdk/sql/azure-resourcemanager-sql/assets.json +++ b/sdk/sql/azure-resourcemanager-sql/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/sql/azure-resourcemanager-sql", - "Tag": "java/sql/azure-resourcemanager-sql_2dccdeacd3" + "Tag": "java/sql/azure-resourcemanager-sql_dbed07f81b" } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java index 6a65753373c2..7c4b17557e9b 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java @@ -607,8 +607,7 @@ interface WithAuthenticationAfterElasticPool { * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabase.DefinitionStages.WithAttachFinal - withManagedIdentity(String managedIdentityResourceId); + SqlDatabase.DefinitionStages.WithAttachFinal withManagedIdentity(String managedIdentityResourceId); } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java index 10201e18333d..c65aac7de02a 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java @@ -109,8 +109,7 @@ interface WithAuthenticationTypeAndLoginPassword { * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabaseExportRequest.DefinitionStages.WithExecute - withManagedIdentity(String managedIdentityResourceId); + SqlDatabaseExportRequest.DefinitionStages.WithExecute withManagedIdentity(String managedIdentityResourceId); } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java index 1f9f7d772f2b..f0e1ff4dffe7 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java @@ -98,8 +98,7 @@ interface WithAuthenticationTypeAndLoginPassword { * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabaseImportRequest.DefinitionStages.WithExecute - withManagedIdentity(String managedIdentityResourceId); + SqlDatabaseImportRequest.DefinitionStages.WithExecute withManagedIdentity(String managedIdentityResourceId); } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java index 8a34964b0ca2..e72f0676dc02 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java @@ -234,10 +234,9 @@ public interface SqlServer **************************************************************/ /** Container interface for all the definitions that need to be implemented. */ - interface Definition - extends DefinitionStages.Blank, DefinitionStages.WithGroup, DefinitionStages.WithAdministratorLogin, - DefinitionStages.WithAdministratorPassword, DefinitionStages.WithExternalActiveDirectoryAdministrator, - DefinitionStages.WithCreate { + interface Definition extends DefinitionStages.Blank, DefinitionStages.WithGroup, + DefinitionStages.WithAdministratorLogin, DefinitionStages.WithAdministratorPassword, + DefinitionStages.WithExternalActiveDirectoryAdministrator, DefinitionStages.WithCreate { } /** Grouping of all the storage account definition stages. */ From 1d944f320fd05c35b286bc9b94ed8dcef136fe96 Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Sat, 16 May 2026 23:16:09 +0800 Subject: [PATCH 05/13] Documents new managed identity auth in SQL db fluent APIs Allows user-assigned managed identity authentication for SQL database import, export, and creation stages in the fluent API, clarifying that these enhancements impact only SDK-implemented interfaces and do not break user implementations. --- eng/lintingconfigs/revapi/track2/revapi.json | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/eng/lintingconfigs/revapi/track2/revapi.json b/eng/lintingconfigs/revapi/track2/revapi.json index 4bf3cceba77c..08f451706aae 100644 --- a/eng/lintingconfigs/revapi/track2/revapi.json +++ b/eng/lintingconfigs/revapi/track2/revapi.json @@ -1516,6 +1516,14 @@ }, "justification": "Output-only immutable models' constructors are now private." }, + { + "code": "java.method.addedToInterface", + "new" : { + "matcher": "regex", + "match": "method .* com\\.azure\\.resourcemanager\\.sql\\.models\\.(SqlDatabase|SqlDatabaseOperations|SqlDatabaseExportRequest|SqlDatabaseImportRequest)\\.DefinitionStages\\.(WithAuthentication|WithAuthenticationAfterElasticPool|WithAuthenticationTypeAndLoginPassword)()?::withManagedIdentity\\(java\\.lang\\.String\\)" + }, + "justification": "Adds user-assigned managed identity authentication option to SQL database import/export and creation fluent stages. These DefinitionStages interfaces are only meant to be implemented by the SDK itself." + }, { "code": "java.method.visibilityReduced", "old" : { From 769d1f09be2a5e4220292051fb3eefb0bd6f287e Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Sun, 24 May 2026 19:08:35 +0800 Subject: [PATCH 06/13] Enhance SQL Database and Server APIs with user-assigned managed identity support and update dependencies --- eng/lintingconfigs/revapi/track2/revapi.json | 8 --- sdk/sql/azure-resourcemanager-sql/assets.json | 2 +- sdk/sql/azure-resourcemanager-sql/pom.xml | 12 ++++ .../sql/implementation/SqlServerImpl.java | 45 ++++++++++++-- .../sql/models/SqlDatabase.java | 39 ++++++++---- .../sql/models/SqlDatabaseExportRequest.java | 19 ++++-- .../sql/models/SqlDatabaseImportRequest.java | 19 ++++-- .../sql/models/SqlDatabaseOperations.java | 40 ++++++++----- .../resourcemanager/sql/models/SqlServer.java | 35 ++++++++++- .../sql/SqlServerOperationsTests.java | 60 +++++++++---------- .../resourcemanager/sql/SqlServerTest.java | 6 ++ 11 files changed, 197 insertions(+), 88 deletions(-) diff --git a/eng/lintingconfigs/revapi/track2/revapi.json b/eng/lintingconfigs/revapi/track2/revapi.json index 08f451706aae..4bf3cceba77c 100644 --- a/eng/lintingconfigs/revapi/track2/revapi.json +++ b/eng/lintingconfigs/revapi/track2/revapi.json @@ -1516,14 +1516,6 @@ }, "justification": "Output-only immutable models' constructors are now private." }, - { - "code": "java.method.addedToInterface", - "new" : { - "matcher": "regex", - "match": "method .* com\\.azure\\.resourcemanager\\.sql\\.models\\.(SqlDatabase|SqlDatabaseOperations|SqlDatabaseExportRequest|SqlDatabaseImportRequest)\\.DefinitionStages\\.(WithAuthentication|WithAuthenticationAfterElasticPool|WithAuthenticationTypeAndLoginPassword)()?::withManagedIdentity\\(java\\.lang\\.String\\)" - }, - "justification": "Adds user-assigned managed identity authentication option to SQL database import/export and creation fluent stages. These DefinitionStages interfaces are only meant to be implemented by the SDK itself." - }, { "code": "java.method.visibilityReduced", "old" : { diff --git a/sdk/sql/azure-resourcemanager-sql/assets.json b/sdk/sql/azure-resourcemanager-sql/assets.json index 3291fb81a1e9..2a5656d0c5a3 100644 --- a/sdk/sql/azure-resourcemanager-sql/assets.json +++ b/sdk/sql/azure-resourcemanager-sql/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/sql/azure-resourcemanager-sql", - "Tag": "java/sql/azure-resourcemanager-sql_dbed07f81b" + "Tag": "java/sql/azure-resourcemanager-sql_3f522469a6" } diff --git a/sdk/sql/azure-resourcemanager-sql/pom.xml b/sdk/sql/azure-resourcemanager-sql/pom.xml index 7a8efaf5b219..9a094ea57323 100644 --- a/sdk/sql/azure-resourcemanager-sql/pom.xml +++ b/sdk/sql/azure-resourcemanager-sql/pom.xml @@ -70,6 +70,18 @@ azure-resourcemanager-storage 2.55.5 + + com.azure.resourcemanager + azure-resourcemanager-msi + 2.53.8 + test + + + com.azure.resourcemanager + azure-resourcemanager-authorization + 2.53.9 + test + org.junit.jupiter junit-jupiter-engine diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index dfbe3ec1e9ce..df4ff7d7de59 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -20,8 +20,8 @@ import com.azure.resourcemanager.sql.fluent.models.ServerUsageInner; import com.azure.resourcemanager.sql.models.AdministratorName; import com.azure.resourcemanager.sql.models.AdministratorType; -import com.azure.resourcemanager.sql.models.IdentityType; import com.azure.resourcemanager.sql.models.PrincipalType; +import com.azure.resourcemanager.sql.models.IdentityType; import com.azure.resourcemanager.sql.models.ResourceIdentity; import com.azure.resourcemanager.sql.models.ServerExternalAdministrator; import com.azure.resourcemanager.sql.models.ServerMetric; @@ -40,11 +40,15 @@ import com.azure.resourcemanager.sql.models.SqlServerSecurityAlertPolicyOperations; import com.azure.resourcemanager.sql.models.SqlVirtualNetworkRule; import com.azure.resourcemanager.sql.models.SqlVirtualNetworkRuleOperations; +import com.azure.resourcemanager.sql.models.UserIdentity; import reactor.core.publisher.Mono; import java.util.ArrayList; import java.util.Collections; +import java.util.HashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.UUID; /** Implementation for SqlServer and its parent interfaces. */ @@ -382,11 +386,15 @@ public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, } else { this.innerModel().administrators().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY); } - this.innerModel() - .administrators() - .withLogin(userLogin) - .withSid(UUID.fromString(sid)) - .withPrincipalType(PrincipalType.USER); + this.innerModel().administrators().withLogin(userLogin).withSid(UUID.fromString(sid)); + return this; + } + + @Override + public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, String sid, + PrincipalType principalType) { + withExternalActiveDirectoryAdministrator(userLogin, sid); + this.innerModel().administrators().withPrincipalType(principalType); return this; } @@ -520,6 +528,31 @@ public SqlServerImpl withSystemAssignedManagedServiceIdentity() { return this; } + @Override + public SqlServerImpl withPrimaryUserAssignedManagedServiceIdentity(String identityResourceId) { + Objects.requireNonNull(identityResourceId, "'identityResourceId' cannot be null."); + ResourceIdentity existing = this.innerModel().identity(); + IdentityType type; + Map identities; + if (existing == null || existing.type() == null || existing.type() == IdentityType.NONE) { + type = IdentityType.USER_ASSIGNED; + identities = new HashMap<>(); + } else if (existing.type() == IdentityType.SYSTEM_ASSIGNED) { + type = IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED; + identities = new HashMap<>(); + } else { + type = existing.type(); + identities = existing.userAssignedIdentities() == null + ? new HashMap<>() + : new HashMap<>(existing.userAssignedIdentities()); + } + identities.put(identityResourceId, new UserIdentity()); + this.innerModel() + .withIdentity(new ResourceIdentity().withType(type).withUserAssignedIdentities(identities)) + .withPrimaryUserAssignedIdentityId(identityResourceId); + return this; + } + @Override public SqlServerImpl enablePublicNetworkAccess() { this.innerModel().withPublicNetworkAccess(ServerNetworkAccessFlag.ENABLED); diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java index 7c4b17557e9b..ba9d730a1a55 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabase.java @@ -505,18 +505,24 @@ interface WithAuthentication { withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); /** - * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * Specifies the user-assigned managed identity (UAMI) used to authenticate to the SQL database for import. * - *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as - * its primary identity), the identity must be granted the appropriate role on the source storage account - * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required - * privileges. When this method is used, no administrator password is sent to the service.

+ *

The SQL server must have the specified UAMI assigned (and typically set as its primary identity), the + * UAMI must be granted the appropriate role on the source storage account (e.g. {@code Storage Blob Data + * Contributor}), and it must be mapped to a database user with the required privileges. When this method + * is used, no administrator password is sent to the service.

+ * + *

This method is for user-assigned managed identity. To use the server's system-assigned managed + * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in + * user's object ID is not a managed identity and cannot be used here.

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabase.DefinitionStages.WithAttachAllOptions - withManagedIdentity(String managedIdentityResourceId); + default SqlDatabase.DefinitionStages.WithAttachAllOptions + withManagedIdentity(String managedIdentityResourceId) { + throw new UnsupportedOperationException("[withManagedIdentity] is not supported in " + getClass()); + } } /** @@ -597,17 +603,24 @@ interface WithAuthenticationAfterElasticPool { withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); /** - * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * Specifies the user-assigned managed identity (UAMI) used to authenticate to the SQL database for import. + * + *

The SQL server must have the specified UAMI assigned (and typically set as its primary identity), the + * UAMI must be granted the appropriate role on the source storage account (e.g. {@code Storage Blob Data + * Contributor}), and it must be mapped to a database user with the required privileges. When this method + * is used, no administrator password is sent to the service.

* - *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as - * its primary identity), the identity must be granted the appropriate role on the source storage account - * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required - * privileges. When this method is used, no administrator password is sent to the service.

+ *

This method is for user-assigned managed identity. To use the server's system-assigned managed + * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in + * user's object ID is not a managed identity and cannot be used here.

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabase.DefinitionStages.WithAttachFinal withManagedIdentity(String managedIdentityResourceId); + default SqlDatabase.DefinitionStages.WithAttachFinal + withManagedIdentity(String managedIdentityResourceId) { + throw new UnsupportedOperationException("[withManagedIdentity] is not supported in " + getClass()); + } } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java index c65aac7de02a..8d18780e0b31 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java @@ -99,17 +99,24 @@ interface WithAuthenticationTypeAndLoginPassword { withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); /** - * Sets the user-assigned managed identity used to authenticate to the SQL database for export. + * Sets the user-assigned managed identity (UAMI) used to authenticate to the SQL database for export. * - *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as - * its primary identity), the identity must be granted the appropriate role on the target storage account - * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required - * privileges. When this method is used, no administrator password is sent to the service.

+ *

The SQL server must have the specified UAMI assigned (and typically set as its primary identity), the + * UAMI must be granted the appropriate role on the target storage account (e.g. {@code Storage Blob Data + * Contributor}), and it must be mapped to a database user with the required privileges. When this method + * is used, no administrator password is sent to the service.

+ * + *

This method is for user-assigned managed identity. To use the server's system-assigned managed + * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in + * user's object ID is not a managed identity and cannot be used here.

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabaseExportRequest.DefinitionStages.WithExecute withManagedIdentity(String managedIdentityResourceId); + default SqlDatabaseExportRequest.DefinitionStages.WithExecute + withManagedIdentity(String managedIdentityResourceId) { + throw new UnsupportedOperationException("[withManagedIdentity] is not supported in " + getClass()); + } } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java index f0e1ff4dffe7..c684bbeb6700 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java @@ -88,17 +88,24 @@ interface WithAuthenticationTypeAndLoginPassword { withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); /** - * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * Specifies the user-assigned managed identity (UAMI) used to authenticate to the SQL database for import. * - *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as - * its primary identity), the identity must be granted the appropriate role on the source storage account - * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required - * privileges. When this method is used, no administrator password is sent to the service.

+ *

The SQL server must have the specified UAMI assigned (and typically set as its primary identity), the + * UAMI must be granted the appropriate role on the source storage account (e.g. {@code Storage Blob Data + * Contributor}), and it must be mapped to a database user with the required privileges. When this method + * is used, no administrator password is sent to the service.

+ * + *

This method is for user-assigned managed identity. To use the server's system-assigned managed + * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in + * user's object ID is not a managed identity and cannot be used here.

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabaseImportRequest.DefinitionStages.WithExecute withManagedIdentity(String managedIdentityResourceId); + default SqlDatabaseImportRequest.DefinitionStages.WithExecute + withManagedIdentity(String managedIdentityResourceId) { + throw new UnsupportedOperationException("[withManagedIdentity] is not supported in " + getClass()); + } } /** diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java index f5a0331c21f5..9003f4b66525 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java @@ -189,18 +189,24 @@ interface WithAuthentication { withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); /** - * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * Specifies the user-assigned managed identity (UAMI) used to authenticate to the SQL database for import. * - *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as - * its primary identity), the identity must be granted the appropriate role on the source storage account - * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required - * privileges. When this method is used, no administrator password is sent to the service.

+ *

The SQL server must have the specified UAMI assigned (and typically set as its primary identity), the + * UAMI must be granted the appropriate role on the source storage account (e.g. {@code Storage Blob Data + * Contributor}), and it must be mapped to a database user with the required privileges. When this method + * is used, no administrator password is sent to the service.

+ * + *

This method is for user-assigned managed identity. To use the server's system-assigned managed + * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in + * user's object ID is not a managed identity and cannot be used here.

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabaseOperations.DefinitionStages.WithEditionDefaults - withManagedIdentity(String managedIdentityResourceId); + default SqlDatabaseOperations.DefinitionStages.WithEditionDefaults + withManagedIdentity(String managedIdentityResourceId) { + throw new UnsupportedOperationException("[withManagedIdentity] is not supported in " + getClass()); + } } /** The SQL Database definition to import a BACPAC file as the source database. */ @@ -269,18 +275,24 @@ interface WithAuthenticationAfterElasticPool { withActiveDirectoryLoginAndPassword(String administratorLogin, String administratorPassword); /** - * Specifies the user-assigned managed identity used to authenticate to the SQL database for import. + * Specifies the user-assigned managed identity (UAMI) used to authenticate to the SQL database for import. * - *

The SQL server must have the specified user-assigned managed identity assigned (and typically set as - * its primary identity), the identity must be granted the appropriate role on the source storage account - * (e.g. {@code Storage Blob Data Contributor}), and it must be mapped to a database user with the required - * privileges. When this method is used, no administrator password is sent to the service.

+ *

The SQL server must have the specified UAMI assigned (and typically set as its primary identity), the + * UAMI must be granted the appropriate role on the source storage account (e.g. {@code Storage Blob Data + * Contributor}), and it must be mapped to a database user with the required privileges. When this method + * is used, no administrator password is sent to the service.

+ * + *

This method is for user-assigned managed identity. To use the server's system-assigned managed + * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in + * user's object ID is not a managed identity and cannot be used here.

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use * @return next definition stage */ - SqlDatabaseOperations.DefinitionStages.WithCreateAfterElasticPoolOptions - withManagedIdentity(String managedIdentityResourceId); + default SqlDatabaseOperations.DefinitionStages.WithCreateAfterElasticPoolOptions + withManagedIdentity(String managedIdentityResourceId) { + throw new UnsupportedOperationException("[withManagedIdentity] is not supported in " + getClass()); + } } /** The SQL Database definition to set a restorable dropped database as the source database. */ diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java index e72f0676dc02..87f52f30dac2 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java @@ -283,11 +283,25 @@ interface WithExternalActiveDirectoryAdministrator { /** * Sets the external Microsoft Entra (Azure Active Directory) administrator on the SQL Server. * + *

The principal type is inferred by the service from the SID.

+ * * @param userLogin the user or group login; it can be the name or the email address * @param sid the user or group object ID * @return Next stage of the SQL Server definition */ WithCreate withExternalActiveDirectoryAdministrator(String userLogin, String sid); + + /** + * Sets the external Microsoft Entra (Azure Active Directory) administrator on the SQL Server + * with an explicit principal type. + * + * @param userLogin the user, group, or application login name + * @param sid the user, group, or application object ID + * @param principalType the principal type (User, Group, or Application) + * @return Next stage of the SQL Server definition + */ + WithCreate withExternalActiveDirectoryAdministrator(String userLogin, String sid, + PrincipalType principalType); } /** A SQL Server definition setting admin user password. */ @@ -326,6 +340,22 @@ interface WithSystemAssignedManagedServiceIdentity { WithCreate withSystemAssignedManagedServiceIdentity(); } + /** A SQL Server definition setting a user-assigned managed service identity as the primary identity. */ + interface WithUserAssignedManagedServiceIdentity { + /** + * Sets the specified user-assigned managed identity (UAMI) on the SQL server and marks it as the + * primary identity. If a system-assigned identity is also enabled, the resulting identity type is + * {@code SystemAssigned,UserAssigned}; otherwise it is {@code UserAssigned}. + * + * @param identityResourceId the Azure resource ID of the user-assigned managed identity + * @return Next stage of the SQL Server definition + */ + default WithCreate withPrimaryUserAssignedManagedServiceIdentity(String identityResourceId) { + throw new UnsupportedOperationException( + "[withPrimaryUserAssignedManagedServiceIdentity] is not supported in " + getClass()); + } + } + /** A SQL Server definition for specifying elastic pool. */ interface WithElasticPool { /** @@ -395,8 +425,9 @@ interface WithPublicNetworkAccess { * A SQL Server definition with sufficient inputs to create a new SQL Server in the cloud, but exposing * additional optional inputs to specify. */ - interface WithCreate extends Creatable, WithActiveDirectoryAdministrator, - WithSystemAssignedManagedServiceIdentity, WithElasticPool, WithDatabase, WithFirewallRule, + interface WithCreate + extends Creatable, WithActiveDirectoryAdministrator, WithSystemAssignedManagedServiceIdentity, + WithUserAssignedManagedServiceIdentity, WithElasticPool, WithDatabase, WithFirewallRule, WithVirtualNetworkRule, WithPublicNetworkAccess, DefinitionWithTags { } } diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index 3e11828af5fc..7dcb5e9452bb 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -7,7 +7,8 @@ import com.azure.core.management.Region; import com.azure.core.management.exception.ManagementException; import com.azure.core.test.annotation.DoNotRecord; -import com.azure.core.util.CoreUtils; +import com.azure.resourcemanager.authorization.models.BuiltInRole; +import com.azure.resourcemanager.msi.models.Identity; import com.azure.resourcemanager.resources.fluentcore.model.Creatable; import com.azure.resourcemanager.resources.fluentcore.model.Indexable; import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; @@ -58,7 +59,6 @@ import com.azure.resourcemanager.test.model.AzureUser; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Assumptions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import reactor.core.publisher.Flux; @@ -577,29 +577,33 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { @DoNotRecord(skipInPlayback = true) // The test makes calls to the Azure Storage data plane APIs which are not mocked at this time. public void canCRUDSqlServerWithImportDatabase() throws Exception { - // Create - String storageName = generateRandomResourceName(sqlServerName, 22); + String uamiName = generateRandomResourceName("uami", 18); AzureUser user = azureCliSignedInUser(); - // Pre-create the resource group explicitly so that downstream resource providers (Storage in - // particular) observe it consistently. Creating the RG as a dependency of the SQL server can - // race with Storage RP and surface as 404 "ResourceGroupNotFound" when the storage account is - // subsequently created or fetched in that RG. resourceManager.resourceGroups().define(rgName).withRegion(DEFAULT_REGION).create(); - // Import/export against an AAD-only SQL server requires Managed Identity authentication - // (SQL auth and ADPassword/ROPC are blocked in AAD-only tenants). The user-assigned managed - // identity must be: - // 1. Assigned to the SQL server (and typically set as its primary identity). - // 2. Granted "Storage Blob Data Contributor" on the storage account. - // 3. Mapped to a database user with the required privileges (db_owner or equivalent). - // - // The UAMI resource ID is read from the AZURE_SQL_IMPORT_EXPORT_UAMI_ID environment variable. - // If it is not provided the test is skipped instead of failing. - String uamiResourceId = System.getenv("AZURE_SQL_IMPORT_EXPORT_UAMI_ID"); - Assumptions.assumeFalse(CoreUtils.isNullOrEmpty(uamiResourceId), - "AZURE_SQL_IMPORT_EXPORT_UAMI_ID is not set; skipping import/export Managed Identity test."); + Identity uami = msiManager.identities() + .define(uamiName) + .withRegion(DEFAULT_REGION) + .withExistingResourceGroup(rgName) + .create(); + + StorageAccount storageAccount = storageManager.storageAccounts() + .define(storageName) + .withRegion(DEFAULT_REGION) + .withExistingResourceGroup(rgName) + .disableSharedKeyAccess() + .create(); + + authorizationManager.roleAssignments() + .define(generateRandomUuid()) + .forObjectId(uami.principalId()) + .withBuiltInRole(BuiltInRole.STORAGE_BLOB_DATA_CONTRIBUTOR) + .withResourceScope(storageAccount) + .create(); + + ResourceManagerUtils.sleep(Duration.ofMinutes(1)); SqlServer sqlServer = sqlServerManager.sqlServers() .define(sqlServerName) @@ -607,6 +611,7 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { .withExistingResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withPrimaryUserAssignedManagedServiceIdentity(uami.id()) .create(); SqlDatabase dbFromSample = sqlServer.databases() @@ -617,20 +622,11 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { Assertions.assertNotNull(dbFromSample); Assertions.assertEquals(DatabaseEdition.BASIC, dbFromSample.edition()); - // Pre-create the storage account synchronously (rather than wiring it as a Creatable dependency - // of the export request) so that we have a fully provisioned StorageAccount instance to reuse - // for both export and import, and any provisioning failure surfaces here rather than deep in - // the export pipeline. - StorageAccount storageAccount = storageManager.storageAccounts() - .define(storageName) - .withRegion(sqlServer.regionName()) - .withExistingResourceGroup(sqlServer.resourceGroupName()) - .create(); - SqlDatabaseImportExportResponse exportedDB = dbFromSample.exportTo(storageAccount, "from-sample", "dbfromsample.bacpac") - .withManagedIdentity(uamiResourceId) + .withManagedIdentity(uami.id()) .execute(); + Assertions.assertNotNull(exportedDB); SqlDatabase dbFromImport = sqlServer.databases() .define("db-from-import") @@ -638,7 +634,7 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { .withBasicPool() .attach() .importFrom(storageAccount, "from-sample", "dbfromsample.bacpac") - .withManagedIdentity(uamiResourceId) + .withManagedIdentity(uami.id()) .withTag("tag2", "value2") .create(); Assertions.assertNotNull(dbFromImport); diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java index d0e69fa5a50b..96df532f8dc3 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerTest.java @@ -10,6 +10,8 @@ import com.azure.core.http.policy.HttpPipelinePolicy; import com.azure.core.http.policy.RetryPolicy; import com.azure.core.management.profile.AzureProfile; +import com.azure.resourcemanager.authorization.AuthorizationManager; +import com.azure.resourcemanager.msi.MsiManager; import com.azure.resourcemanager.resources.fluentcore.utils.HttpPipelineProvider; import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; import com.azure.resourcemanager.resources.ResourceManager; @@ -26,6 +28,8 @@ public abstract class SqlServerTest extends ResourceManagerTestProxyTestBase { protected ResourceManager resourceManager; protected SqlServerManager sqlServerManager; protected StorageManager storageManager; + protected MsiManager msiManager; + protected AuthorizationManager authorizationManager; protected String rgName = ""; protected String sqlServerName = ""; @@ -45,6 +49,8 @@ protected void initializeClients(HttpPipeline httpPipeline, AzureProfile profile internalContext.setIdentifierFunction(name -> new TestIdentifierProvider(testResourceNamer)); sqlServerManager = buildManager(SqlServerManager.class, httpPipeline, profile); storageManager = buildManager(StorageManager.class, httpPipeline, profile); + msiManager = buildManager(MsiManager.class, httpPipeline, profile); + authorizationManager = buildManager(AuthorizationManager.class, httpPipeline, profile); resourceManager = sqlServerManager.resourceManager(); setInternalContext(internalContext, sqlServerManager); } From c0bd6947661edfb725e6095cb44a9bb711edb5db Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Sun, 24 May 2026 21:20:53 +0800 Subject: [PATCH 07/13] Add module read permissions for authorization and managed identity in pom.xml --- sdk/sql/azure-resourcemanager-sql/pom.xml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/sdk/sql/azure-resourcemanager-sql/pom.xml b/sdk/sql/azure-resourcemanager-sql/pom.xml index 9a094ea57323..523e118f5156 100644 --- a/sdk/sql/azure-resourcemanager-sql/pom.xml +++ b/sdk/sql/azure-resourcemanager-sql/pom.xml @@ -41,6 +41,9 @@ 0.10 + --add-reads com.azure.resourcemanager.sql=com.azure.resourcemanager.authorization + --add-reads com.azure.resourcemanager.sql=com.azure.resourcemanager.msi + --add-opens com.azure.resourcemanager.sql/com.azure.resourcemanager.sql=ALL-UNNAMED --add-opens com.azure.resourcemanager.resources/com.azure.resourcemanager.resources=ALL-UNNAMED --add-opens com.azure.resourcemanager.resources/com.azure.resourcemanager.resources.fluentcore.arm=ALL-UNNAMED From 24958d09796fa75d367bdbf2dcdf2a01e3b816ee Mon Sep 17 00:00:00 2001 From: Hui Zhu Date: Tue, 26 May 2026 13:56:15 +0800 Subject: [PATCH 08/13] Add support for explicit principal type in external Active Directory administrator configuration --- .../azure-resourcemanager-sql/CHANGELOG.md | 10 +++--- .../sql/implementation/SqlServerImpl.java | 17 ++++----- .../resourcemanager/sql/models/SqlServer.java | 15 ++------ .../sql/SqlServerOperationsTests.java | 35 ++++++++++--------- 4 files changed, 32 insertions(+), 45 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md index 36cdfff3864d..109933b3bc2d 100644 --- a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md +++ b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md @@ -4,7 +4,7 @@ ### Features Added -- Added a new stage `WithAzureActiveDirectoryOnlyAuthentication` with the method `withAzureActiveDirectoryOnlyAuthentication()` on `SqlServer.DefinitionStages.WithAdministratorLogin`, followed by the new stage `WithExternalActiveDirectoryAdministrator` with the method `withExternalActiveDirectoryAdministrator(String userLogin, String sid)`. This enables creating a SQL Server with Microsoft Entra-only authentication enabled at creation time, which is required when the target subscription/management group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation. +- Added a new stage `WithAzureActiveDirectoryOnlyAuthentication` with the method `withAzureActiveDirectoryOnlyAuthentication()` on `SqlServer.DefinitionStages.WithAdministratorLogin`, followed by the new stage `WithExternalActiveDirectoryAdministrator` with the method `withExternalActiveDirectoryAdministrator(String userLogin, String sid, PrincipalType principalType)`. This enables creating a SQL Server with Microsoft Entra-only authentication enabled at creation time, which is required when the target subscription/management group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation. The `principalType` must be specified explicitly because the service does not reliably infer it from the SID. - Added `withManagedIdentity(String managedIdentityResourceId)` to the import/export authentication stages of `SqlDatabaseImportRequest`, `SqlDatabaseExportRequest`, `SqlDatabaseOperations` (both standalone and in-elastic-pool import definitions) and `SqlDatabase` (both standalone and in-elastic-pool import definitions). This lets callers authenticate to the storage account using a user-assigned managed identity assigned to the SQL server instead of a storage access/shared key, which is required when the storage account has shared-key access disabled. ### Breaking Changes @@ -366,21 +366,21 @@ ### Breaking Changes -- Removed `NEW` from `SecurityAlertPolicyState`. The constant is non-functional. +- Removed `NEW` from `SecurityAlertPolicyState`. The constant is non-functional. - Removed `withPolicyNew` method from `SqlDatabaseThreatDetectionPolicy` since `NEW` is no longer supported in `SecurityAlertPolicyState`. - Removed `nextResetTime` and `resourceName` methods from `ServerMetric` and `SqlDatabase`. The methods are non-functional. - Removed `listMetricsDefinitions` and `listMetrics` methods from `SqlDatabase`. Metrics in SQL have been replaced by the Azure monitor shoebox metrics API. Not in SQL any more. - Removed `listServiceTierAdvisors` method from `SqlDatabase`. It's no longer supported. - Removed class `ElasticPoolDatabaseActivity`. It's removed from service definition. -- Removed `listDatabaseActivities`, `listDatabaseMetricDefinitions` and `listDatabaseMetrics` methods from `SqlElasticPool`. -- Removed `elasticPoolName` and `serviceLevelObjective` methods from `SqlRestorableDroppedDatabase`. +- Removed `listDatabaseActivities`, `listDatabaseMetricDefinitions` and `listDatabaseMetrics` methods from `SqlElasticPool`. +- Removed `elasticPoolName` and `serviceLevelObjective` methods from `SqlRestorableDroppedDatabase`. - Removed `getServiceObjective`, `listRecommendedElasticPools`, `listServiceObjectives` methods from `SqlServer`. - Removed `withCreationDate` and `withThumbprint` from SqlServerKeyOperations. The properties are no longer mutable. - Renamed class from `TransparentDataEncryptionInner` to `LogicalDatabaseTransparentDataEncryptionInner`. - Removed class `TransparentDataEncryptionActivity`. The class is no longer functional. - Removed `listActivities` from `TransparentDataEncryption` since `TransparentDataEncryptionActivity` is removed. - Renamed `TransparentDataEncryptionStatus` to `TransparentDataEncryptionState`. -- Removed `location`, `requestedDatabaseDtuCap`, `requestedDatabaseDtuGuarantee`, `requestedDatabaseDtuMax`, `requestedDatabaseDtuMin`, +- Removed `location`, `requestedDatabaseDtuCap`, `requestedDatabaseDtuGuarantee`, `requestedDatabaseDtuMax`, `requestedDatabaseDtuMin`, `requestedDtu`, `requestedDtuGuarantee` and `requestedElasticPoolName`, `requestedStorageLimitInGB` and `requestedStorageLimitInMB` methods from `ElasticPoolActivity`. The properties are no longer functional. - Renamed class from `ElasticPoolActivityInner` to `ElasticPoolOperationInner`. - Removed `readReplicaCount` and `withReadReplicaCount` from `DatabaseUpdate`. The property is non-functional. diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index df4ff7d7de59..419d085df97e 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -377,7 +377,8 @@ public SqlServerImpl withAzureActiveDirectoryOnlyAuthentication() { } @Override - public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, String sid) { + public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, String sid, + PrincipalType principalType) { if (this.innerModel().administrators() == null) { this.innerModel() .withAdministrators( @@ -386,15 +387,11 @@ public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, } else { this.innerModel().administrators().withAdministratorType(AdministratorType.ACTIVE_DIRECTORY); } - this.innerModel().administrators().withLogin(userLogin).withSid(UUID.fromString(sid)); - return this; - } - - @Override - public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, String sid, - PrincipalType principalType) { - withExternalActiveDirectoryAdministrator(userLogin, sid); - this.innerModel().administrators().withPrincipalType(principalType); + this.innerModel() + .administrators() + .withLogin(userLogin) + .withSid(UUID.fromString(sid)) + .withPrincipalType(principalType); return this; } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java index 87f52f30dac2..e7f256a54493 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java @@ -283,21 +283,10 @@ interface WithExternalActiveDirectoryAdministrator { /** * Sets the external Microsoft Entra (Azure Active Directory) administrator on the SQL Server. * - *

The principal type is inferred by the service from the SID.

- * - * @param userLogin the user or group login; it can be the name or the email address - * @param sid the user or group object ID - * @return Next stage of the SQL Server definition - */ - WithCreate withExternalActiveDirectoryAdministrator(String userLogin, String sid); - - /** - * Sets the external Microsoft Entra (Azure Active Directory) administrator on the SQL Server - * with an explicit principal type. - * * @param userLogin the user, group, or application login name * @param sid the user, group, or application object ID - * @param principalType the principal type (User, Group, or Application) + * @param principalType the principal type (User, Group, or Application). Must be specified explicitly; + * the service does not reliably infer it from the SID * @return Next stage of the SQL Server definition */ WithCreate withExternalActiveDirectoryAdministrator(String userLogin, String sid, diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index 7dcb5e9452bb..3d0e7c8b8436 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -26,6 +26,7 @@ import com.azure.resourcemanager.sql.models.ElasticPoolEdition; import com.azure.resourcemanager.sql.models.ElasticPoolSku; import com.azure.resourcemanager.sql.models.FailoverGroupReplicationRole; +import com.azure.resourcemanager.sql.models.PrincipalType; import com.azure.resourcemanager.sql.models.ReadOnlyEndpointFailoverPolicy; import com.azure.resourcemanager.sql.models.ReadWriteEndpointFailoverPolicy; import com.azure.resourcemanager.sql.models.RegionCapabilities; @@ -111,7 +112,7 @@ public void canCRUDSqlSyncMember() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -182,7 +183,7 @@ public void canCRUDSqlSyncGroup() throws Exception { .withRegion(SECONDARY_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -239,7 +240,7 @@ public void canCopySqlDatabase() throws Exception { .withRegion(SECONDARY_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .defineElasticPool(epName) .withPremiumPool() .attach() @@ -254,7 +255,7 @@ public void canCopySqlDatabase() throws Exception { .withRegion(DEFAULT_REGION) .withExistingResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); SqlDatabase dbSample = sqlPrimaryServer.databases().get(dbName); @@ -285,7 +286,7 @@ public void canCRUDSqlFailoverGroup() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .defineDatabase(dbName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -297,7 +298,7 @@ public void canCRUDSqlFailoverGroup() throws Exception { .withRegion(SECONDARY_REGION) .withExistingResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); SqlServer sqlOtherServer = sqlServerManager.sqlServers() @@ -305,7 +306,7 @@ public void canCRUDSqlFailoverGroup() throws Exception { .withRegion(TERTIARY_REGION) .withExistingResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); SqlFailoverGroup failoverGroup = sqlPrimaryServer.failoverGroups() @@ -405,7 +406,7 @@ public void canChangeSqlServerAndDatabaseAutomaticTuning() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .defineDatabase(databaseName) .fromSample(SampleName.ADVENTURE_WORKS_LT) .withBasicEdition() @@ -479,7 +480,7 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); Assertions.assertNotNull(sqlServer1); @@ -501,7 +502,7 @@ public void canCreateAndAquireServerDnsAlias() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); Assertions.assertNotNull(sqlServer2); @@ -542,7 +543,7 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .withSystemAssignedManagedServiceIdentity() .defineDatabase(databaseName) .fromSample(SampleName.ADVENTURE_WORKS_LT) @@ -610,7 +611,7 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { .withRegion(DEFAULT_REGION) .withExistingResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .withPrimaryUserAssignedManagedServiceIdentity(uami.id()) .create(); @@ -656,7 +657,7 @@ public void canCRUDSqlServerWithFirewallRule() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator("DSEng", user.id()) + .withExternalActiveDirectoryAdministrator("DSEng", user.id(), PrincipalType.USER) .withoutAccessFromAzureServices() .defineFirewallRule("somefirewallrule1") .withIpAddress("0.0.0.1") @@ -797,7 +798,7 @@ public void canUseCoolShortcutsForResourceCreation() throws Exception { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .withoutAccessFromAzureServices() .defineDatabase(SQL_DATABASE_NAME) .attach() @@ -1504,7 +1505,7 @@ private SqlServer createSqlServer(String sqlServerName) { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); } @@ -1629,7 +1630,7 @@ public void testRandomSku() { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .create(); // Too many elastic pools defined will hit sql server DTU quota limits. @@ -1748,7 +1749,7 @@ public void canCreateAndUpdatePublicNetworkAccess() { .withRegion(DEFAULT_REGION) .withNewResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id()) + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) .withoutAccessFromAzureServices() .disablePublicNetworkAccess() .create(); From ca8fabda531c5754876430cf5a3b6d12337a2158 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Fri, 29 May 2026 19:02:09 +0800 Subject: [PATCH 09/13] fix import/export --- .../azure-resourcemanager-sql/CHANGELOG.md | 4 +- .../SqlDatabaseExportRequestImpl.java | 11 +- .../sql/implementation/SqlDatabaseImpl.java | 12 +- .../sql/implementation/SqlServerImpl.java | 11 +- .../sql/models/SqlDatabaseExportRequest.java | 6 +- .../sql/models/SqlDatabaseOperations.java | 6 +- .../resourcemanager/sql/models/SqlServer.java | 16 ++- .../sql/SqlServerOperationsTests.java | 110 +++++++++--------- 8 files changed, 104 insertions(+), 72 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md index 109933b3bc2d..d9186f7887b6 100644 --- a/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md +++ b/sdk/sql/azure-resourcemanager-sql/CHANGELOG.md @@ -4,8 +4,8 @@ ### Features Added -- Added a new stage `WithAzureActiveDirectoryOnlyAuthentication` with the method `withAzureActiveDirectoryOnlyAuthentication()` on `SqlServer.DefinitionStages.WithAdministratorLogin`, followed by the new stage `WithExternalActiveDirectoryAdministrator` with the method `withExternalActiveDirectoryAdministrator(String userLogin, String sid, PrincipalType principalType)`. This enables creating a SQL Server with Microsoft Entra-only authentication enabled at creation time, which is required when the target subscription/management group enforces a policy that mandates Microsoft Entra-only authentication on Azure SQL Server creation. The `principalType` must be specified explicitly because the service does not reliably infer it from the SID. -- Added `withManagedIdentity(String managedIdentityResourceId)` to the import/export authentication stages of `SqlDatabaseImportRequest`, `SqlDatabaseExportRequest`, `SqlDatabaseOperations` (both standalone and in-elastic-pool import definitions) and `SqlDatabase` (both standalone and in-elastic-pool import definitions). This lets callers authenticate to the storage account using a user-assigned managed identity assigned to the SQL server instead of a storage access/shared key, which is required when the storage account has shared-key access disabled. +- Supported `withAzureActiveDirectoryOnlyAuthentication` and `withExternalActiveDirectoryAdministrator` for `SqlServer`. +- Supported `withManagedIdentity` for `SqlDatabase` import and export. ### Breaking Changes diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java index c8446a1f3316..7f347ddf266a 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java @@ -3,6 +3,7 @@ package com.azure.resourcemanager.sql.implementation; import com.azure.core.management.exception.ManagementException; +import com.azure.core.util.CoreUtils; import com.azure.resourcemanager.resources.fluentcore.dag.FunctionalTaskItem; import com.azure.resourcemanager.resources.fluentcore.model.Creatable; import com.azure.resourcemanager.resources.fluentcore.model.Indexable; @@ -71,8 +72,10 @@ private Mono getOrCreateStorageAccountContainer(final StorageAccount .flatMap(storageAccountKey -> { self.inner.withStorageUri( String.format("%s%s/%s", storageAccount.endPoints().primary().blob(), containerName, fileName)); - self.inner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); - self.inner.withStorageKey(storageAccountKey.value()); + if (storageAccount.isSharedKeyAccessAllowed() && CoreUtils.isNullOrEmpty(self.inner.storageKey())) { + self.inner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); + self.inner.withStorageKey(storageAccountKey.value()); + } BlobContainers blobContainers = this.sqlServerManager.storageManager().blobContainers(); return blobContainers.getAsync(parent().resourceGroupName(), storageAccount.name(), containerName) .onErrorResume(error -> { @@ -165,6 +168,10 @@ public SqlDatabaseExportRequestImpl withManagedIdentity(String managedIdentityRe this.inner.withAdministratorLogin(managedIdentityResourceId); // No administrator password is required for managed identity authentication. this.inner.withAdministratorLoginPassword(null); + + // Use the same MI for storage account access. + this.inner.withStorageKeyType(StorageKeyType.MANAGED_IDENTITY); + this.inner.withStorageKey(managedIdentityResourceId); return this; } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java index 0357afef97fe..74fdc4b5379e 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImpl.java @@ -6,6 +6,7 @@ import com.azure.core.http.rest.PagedFlux; import com.azure.core.http.rest.PagedIterable; import com.azure.core.management.Region; +import com.azure.core.util.CoreUtils; import com.azure.resourcemanager.resources.fluentcore.arm.ResourceId; import com.azure.resourcemanager.resources.fluentcore.arm.ResourceUtils; import com.azure.resourcemanager.resources.fluentcore.arm.models.implementation.ExternalChildResourceImpl; @@ -717,8 +718,11 @@ public SqlDatabaseImpl importFrom(final StorageAccount storageAccount, final Str .flatMap(storageAccountKey -> { self.importRequestInner.withStorageUri( String.format("%s%s/%s", storageAccount.endPoints().primary().blob(), containerName, fileName)); - self.importRequestInner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); - self.importRequestInner.withStorageKey(storageAccountKey.value()); + if (storageAccount.isSharedKeyAccessAllowed() + && CoreUtils.isNullOrEmpty(self.importRequestInner.storageKey())) { + self.importRequestInner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); + self.importRequestInner.withStorageKey(storageAccountKey.value()); + } return context.voidMono(); })); return this; @@ -763,6 +767,10 @@ public SqlDatabaseImpl withManagedIdentity(String managedIdentityResourceId) { this.importRequestInner.withAdministratorLogin(managedIdentityResourceId); // No administrator password is required for managed identity authentication. this.importRequestInner.withAdministratorLoginPassword(null); + + // Use the same MI for storage account access. + this.importRequestInner.withStorageKeyType(StorageKeyType.MANAGED_IDENTITY); + this.importRequestInner.withStorageKey(managedIdentityResourceId); return this; } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index 419d085df97e..3998dfe1ea9b 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -12,6 +12,7 @@ import com.azure.resourcemanager.resources.fluentcore.arm.models.implementation.GroupableResourceImpl; import com.azure.resourcemanager.resources.fluentcore.dag.FunctionalTaskItem; import com.azure.resourcemanager.resources.fluentcore.utils.PagedConverter; +import com.azure.resourcemanager.resources.fluentcore.utils.ResourceManagerUtils; import com.azure.resourcemanager.sql.SqlServerManager; import com.azure.resourcemanager.sql.fluent.models.RestorableDroppedDatabaseInner; import com.azure.resourcemanager.sql.fluent.models.ServerAutomaticTuningInner; @@ -327,6 +328,12 @@ public void removeActiveDirectoryAdministrator() { .block(); } + @Override + public boolean isAzureActiveDirectoryOnlyAuthenticationEnabled() { + return this.innerModel().administrators() != null + && ResourceManagerUtils.toPrimitiveBoolean(this.innerModel().administrators().azureADOnlyAuthentication()); + } + @Override public SqlServerAutomaticTuning getServerAutomaticTuning() { ServerAutomaticTuningInner serverAutomaticTuningInner @@ -377,7 +384,7 @@ public SqlServerImpl withAzureActiveDirectoryOnlyAuthentication() { } @Override - public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, String sid, + public SqlServerImpl withExternalActiveDirectoryAdministrator(String adminLogin, String sid, PrincipalType principalType) { if (this.innerModel().administrators() == null) { this.innerModel() @@ -389,7 +396,7 @@ public SqlServerImpl withExternalActiveDirectoryAdministrator(String userLogin, } this.innerModel() .administrators() - .withLogin(userLogin) + .withLogin(adminLogin) .withSid(UUID.fromString(sid)) .withPrincipalType(principalType); return this; diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java index 8d18780e0b31..0a4d71cdcc57 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseExportRequest.java @@ -106,11 +106,11 @@ interface WithAuthenticationTypeAndLoginPassword { * Contributor}), and it must be mapped to a database user with the required privileges. When this method * is used, no administrator password is sent to the service.

* - *

This method is for user-assigned managed identity. To use the server's system-assigned managed - * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in - * user's object ID is not a managed identity and cannot be used here.

+ *

This method is for user-assigned managed identity. System-assigned managed identity is not supported. + * See Limitations

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * for both SQL and storage access * @return next definition stage */ default SqlDatabaseExportRequest.DefinitionStages.WithExecute diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java index 9003f4b66525..a1dd28fa1016 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseOperations.java @@ -282,11 +282,11 @@ interface WithAuthenticationAfterElasticPool { * Contributor}), and it must be mapped to a database user with the required privileges. When this method * is used, no administrator password is sent to the service.

* - *

This method is for user-assigned managed identity. To use the server's system-assigned managed - * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in - * user's object ID is not a managed identity and cannot be used here.

+ *

This method is for user-assigned managed identity. System-assigned managed identity is not supported. + * See Limitations

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * for both SQL and storage access * @return next definition stage */ default SqlDatabaseOperations.DefinitionStages.WithCreateAfterElasticPoolOptions diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java index e7f256a54493..b270e0bbd331 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlServer.java @@ -157,6 +157,16 @@ public interface SqlServer /** Removes the Active Directory administrator from this server. */ void removeActiveDirectoryAdministrator(); + /** + * Checks whether Azure Active Directory (AAD) only authentication enabled. + * + * @return true if Azure Active Directory (AAD) only authentication enabled + */ + default boolean isAzureActiveDirectoryOnlyAuthenticationEnabled() { + throw new UnsupportedOperationException( + "[isAzureActiveDirectoryOnlyAuthenticationEnabled] is not supported in " + getClass()); + } + /** * Gets a SQL server automatic tuning state and options. * @@ -283,13 +293,13 @@ interface WithExternalActiveDirectoryAdministrator { /** * Sets the external Microsoft Entra (Azure Active Directory) administrator on the SQL Server. * - * @param userLogin the user, group, or application login name + * @param adminLogin the user, group, or application login name * @param sid the user, group, or application object ID - * @param principalType the principal type (User, Group, or Application). Must be specified explicitly; + * @param principalType the principal type (User, Group, or Application). Must be specified explicitly, since * the service does not reliably infer it from the SID * @return Next stage of the SQL Server definition */ - WithCreate withExternalActiveDirectoryAdministrator(String userLogin, String sid, + WithCreate withExternalActiveDirectoryAdministrator(String adminLogin, String sid, PrincipalType principalType); } diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index 3d0e7c8b8436..1d774104fbf4 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -33,6 +33,8 @@ import com.azure.resourcemanager.sql.models.ReplicationLink; import com.azure.resourcemanager.sql.models.ReplicationState; import com.azure.resourcemanager.sql.models.SampleName; +import com.azure.resourcemanager.sql.models.SecurityAlertPolicyName; +import com.azure.resourcemanager.sql.models.SecurityAlertPolicyState; import com.azure.resourcemanager.sql.models.ServerNetworkAccessFlag; import com.azure.resourcemanager.sql.models.ServiceObjectiveName; import com.azure.resourcemanager.sql.models.Sku; @@ -42,6 +44,7 @@ import com.azure.resourcemanager.sql.models.SqlDatabaseImportExportResponse; import com.azure.resourcemanager.sql.models.SqlDatabasePremiumServiceObjective; import com.azure.resourcemanager.sql.models.SqlDatabaseStandardServiceObjective; +import com.azure.resourcemanager.sql.models.SqlDatabaseThreatDetectionPolicy; import com.azure.resourcemanager.sql.models.SqlElasticPool; import com.azure.resourcemanager.sql.models.SqlElasticPoolBasicEDTUs; import com.azure.resourcemanager.sql.models.SqlFailoverGroup; @@ -577,10 +580,9 @@ public void canGetSqlServerCapabilitiesAndCreateIdentity() throws Exception { @Test @DoNotRecord(skipInPlayback = true) // The test makes calls to the Azure Storage data plane APIs which are not mocked at this time. - public void canCRUDSqlServerWithImportDatabase() throws Exception { + public void canCRUDSqlServerWithImportDatabase() { String storageName = generateRandomResourceName(sqlServerName, 22); String uamiName = generateRandomResourceName("uami", 18); - AzureUser user = azureCliSignedInUser(); resourceManager.resourceGroups().define(rgName).withRegion(DEFAULT_REGION).create(); @@ -611,10 +613,12 @@ public void canCRUDSqlServerWithImportDatabase() throws Exception { .withRegion(DEFAULT_REGION) .withExistingResourceGroup(rgName) .withAzureActiveDirectoryOnlyAuthentication() - .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) + .withExternalActiveDirectoryAdministrator(uamiName, uami.principalId(), PrincipalType.APPLICATION) .withPrimaryUserAssignedManagedServiceIdentity(uami.id()) .create(); + Assertions.assertTrue(sqlServer.isAzureActiveDirectoryOnlyAuthenticationEnabled()); + SqlDatabase dbFromSample = sqlServer.databases() .define("db-from-sample") .fromSample(SampleName.ADVENTURE_WORKS_LT) @@ -897,9 +901,37 @@ public void canUseCoolShortcutsForResourceCreation() throws Exception { } @Test + @Disabled("Legacy threat detection policy doesn't support MI. The new Advanced Threat Protection(ATP) " + + "does not require storage account any more. " + + "See Oury Ba's answer: https://learn.microsoft.com/answers/questions/2276392/how-to-create-microsoft-sql-servers-securityalertp") public void canCRUDSqlDatabase() throws Exception { + final String storageAccountName = generateRandomResourceName("sqlsa", 20); + AzureUser user = azureCliSignedInUser(); + // Create - SqlServer sqlServer = createSqlServer(); + SqlServer sqlServer = sqlServerManager.sqlServers() + .define(sqlServerName) + .withRegion(DEFAULT_REGION) + .withNewResourceGroup(rgName) + .withAzureActiveDirectoryOnlyAuthentication() + .withExternalActiveDirectoryAdministrator(user.userPrincipalName(), user.id(), PrincipalType.USER) + .withSystemAssignedManagedServiceIdentity() + .create(); + + StorageAccount storageAccount = storageManager.storageAccounts() + .define(storageAccountName) + .withRegion(DEFAULT_REGION) + .withExistingResourceGroup(rgName) + .disableSharedKeyAccess() + .create(); + + authorizationManager.roleAssignments() + .define(generateRandomUuid()) + .forObjectId(sqlServer.systemAssignedManagedServiceIdentityPrincipalId()) + .withBuiltInRole(BuiltInRole.STORAGE_BLOB_DATA_CONTRIBUTOR) + .withResourceScope(storageAccount) + .create(); + Mono resourceStream = sqlServer.databases() .define(SQL_DATABASE_NAME) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -910,57 +942,25 @@ public void canCRUDSqlDatabase() throws Exception { validateSqlDatabase(sqlDatabase, SQL_DATABASE_NAME); Assertions.assertTrue(sqlServer.databases().list().size() > 0); - // Threat detection policy test is disabled. - // - // The storage account used here is provisioned with shared-key access disabled - // (`disableSharedKeyAccess()`) because the target test subscription is an AAD-only tenant: - // storage accounts created there must reject shared-key authentication and only accept - // Microsoft Entra (Azure AD) credentials. As a result, the access-key path returns a key - // value that the storage data plane will not accept. - // The database-level "securityAlertPolicies" ARM resource does not offer a Managed - // Identity alternative either: the latest schema (api-version 2025-01-01) defines only - // state / disabledAlerts / emailAccountAdmins / emailAddresses / retentionDays / - // storageAccountAccessKey / storageEndpoint, with no identity / isManagedIdentityInUse - // field. Compare with "auditingSettings", which on the same api-version explicitly - // exposes isManagedIdentityInUse and documents the "omit access key to use SAMI" path. - // The service enforces this: omitting storageAccountAccessKey when storageEndpoint is - // set fails with "DataSecurityInvalidUserSuppliedParameter: storageAccountAccessKey - // parameter can not be null when storageEndpoint parameter is not null". - // - // References: - // https://learn.microsoft.com/en-us/rest/api/sql/database-security-alert-policies/create-or-update?view=rest-sql-2025-01-01 - // https://learn.microsoft.com/en-us/rest/api/sql/database-blob-auditing-policies/create-or-update?view=rest-sql-2025-01-01 - // - // Re-enable when either (a) the storage account in this test is recreated with shared - // key access enabled, or (b) the SQL securityAlertPolicies resource adds first-class - // Managed Identity support. - - // final String storageAccountName = generateRandomResourceName("sqlsa", 20); - // StorageAccount storageAccount = storageManager.storageAccounts() - // .define(storageAccountName) - // .withRegion(DEFAULT_REGION) - // .withExistingResourceGroup(rgName) - // .disableSharedKeyAccess() - // .create(); - // String accountKey = storageAccount.getKeys().get(0).value(); - // String blobEntrypoint = storageAccount.endPoints().primary().blob(); - - // List disabledAlerts = Collections.singletonList("Sql_Injection"); - - // sqlDatabase.defineThreatDetectionPolicy(SecurityAlertPolicyName.fromString("myPolicy")) - // .withPolicyEnabled() - // .withStorageEndpoint(blobEntrypoint) - // .withStorageAccountAccessKey(accountKey) - // .withAlertsFilter(disabledAlerts) - // .create(); - - // sqlDatabase.refresh(); - - // SqlDatabaseThreatDetectionPolicy alertPolicy = sqlDatabase.getThreatDetectionPolicy(); - // Assertions.assertNotNull(alertPolicy); - // Assertions.assertEquals(SecurityAlertPolicyState.ENABLED, alertPolicy.currentState()); - // Assertions.assertEquals(alertPolicy.disabledAlertList(), disabledAlerts); - // Assertions.assertTrue(alertPolicy.isDefaultSecurityAlertPolicy()); + String blobEntrypoint = storageAccount.endPoints().primary().blob(); + + List disabledAlerts = Collections.singletonList("Sql_Injection"); + + sqlDatabase.defineThreatDetectionPolicy(SecurityAlertPolicyName.fromString("myPolicy")) + .withPolicyEnabled() + .withStorageEndpoint(blobEntrypoint) + // use system-assigned MI + .withStorageAccountAccessKey(null) + .withAlertsFilter(disabledAlerts) + .create(); + + sqlDatabase.refresh(); + + SqlDatabaseThreatDetectionPolicy alertPolicy = sqlDatabase.getThreatDetectionPolicy(); + Assertions.assertNotNull(alertPolicy); + Assertions.assertEquals(SecurityAlertPolicyState.ENABLED, alertPolicy.currentState()); + Assertions.assertEquals(alertPolicy.disabledAlertList(), disabledAlerts); + Assertions.assertTrue(alertPolicy.isDefaultSecurityAlertPolicy()); // Test transparent data encryption settings. TransparentDataEncryption transparentDataEncryption = sqlDatabase.getTransparentDataEncryption(); From 2b82ab33ee001b451b37b50efeecee6129c3e3c9 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Mon, 1 Jun 2026 13:31:29 +0800 Subject: [PATCH 10/13] minor fix, recording --- sdk/sql/azure-resourcemanager-sql/assets.json | 2 +- .../SqlDatabaseExportRequestImpl.java | 47 +++++++----- .../SqlDatabaseImportRequestImpl.java | 12 ++- .../sql/SqlServerOperationsTests.java | 73 +++++++++---------- 4 files changed, 75 insertions(+), 59 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/assets.json b/sdk/sql/azure-resourcemanager-sql/assets.json index 2a5656d0c5a3..da5a62b25723 100644 --- a/sdk/sql/azure-resourcemanager-sql/assets.json +++ b/sdk/sql/azure-resourcemanager-sql/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/sql/azure-resourcemanager-sql", - "Tag": "java/sql/azure-resourcemanager-sql_3f522469a6" + "Tag": "java/sql/azure-resourcemanager-sql_4145a7592c" } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java index 7f347ddf266a..b6d1dfadb82c 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseExportRequestImpl.java @@ -67,28 +67,37 @@ public SqlDatabaseExportRequestImpl exportTo(String storageUri) { private Mono getOrCreateStorageAccountContainer(final StorageAccount storageAccount, final String containerName, final String fileName, final FunctionalTaskItem.Context context) { final SqlDatabaseExportRequestImpl self = this; + self.inner.withStorageUri( + String.format("%s%s/%s", storageAccount.endPoints().primary().blob(), containerName, fileName)); + BlobContainers blobContainers = this.sqlServerManager.storageManager().blobContainers(); + Mono container + = blobContainers.getAsync(parent().resourceGroupName(), storageAccount.name(), containerName) + .map(blobContainer -> (Indexable) blobContainer) + .onErrorResume(error -> { + if (error instanceof ManagementException) { + if (((ManagementException) error).getResponse().getStatusCode() == 404) { + return blobContainers.defineContainer(containerName) + .withExistingStorageAccount(parent().resourceGroupName(), storageAccount.name()) + .withPublicAccess(PublicAccess.NONE) + .createAsync() + .map(blobContainer -> (Indexable) blobContainer); + } + } + return Mono.error(error); + }); + + if (!storageAccount.isSharedKeyAccessAllowed() + // self.inner.storageKey could be set before, e.g. with managed identity ID + || !CoreUtils.isNullOrEmpty(self.inner.storageKey())) { + return container; + } + return storageAccount.getKeysAsync() .flatMap(storageAccountKeys -> Mono.justOrEmpty(storageAccountKeys.stream().findFirst())) .flatMap(storageAccountKey -> { - self.inner.withStorageUri( - String.format("%s%s/%s", storageAccount.endPoints().primary().blob(), containerName, fileName)); - if (storageAccount.isSharedKeyAccessAllowed() && CoreUtils.isNullOrEmpty(self.inner.storageKey())) { - self.inner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); - self.inner.withStorageKey(storageAccountKey.value()); - } - BlobContainers blobContainers = this.sqlServerManager.storageManager().blobContainers(); - return blobContainers.getAsync(parent().resourceGroupName(), storageAccount.name(), containerName) - .onErrorResume(error -> { - if (error instanceof ManagementException) { - if (((ManagementException) error).getResponse().getStatusCode() == 404) { - return blobContainers.defineContainer(containerName) - .withExistingStorageAccount(parent().resourceGroupName(), storageAccount.name()) - .withPublicAccess(PublicAccess.NONE) - .createAsync(); - } - } - return Mono.error(error); - }); + self.inner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); + self.inner.withStorageKey(storageAccountKey.value()); + return container; }); } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java index 28622af50773..289110bc740d 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java @@ -2,6 +2,7 @@ // Licensed under the MIT License. package com.azure.resourcemanager.sql.implementation; +import com.azure.core.util.CoreUtils; import com.azure.resourcemanager.resources.fluentcore.dag.FunctionalTaskItem; import com.azure.resourcemanager.resources.fluentcore.model.Indexable; import com.azure.resourcemanager.resources.fluentcore.model.implementation.ExecutableImpl; @@ -55,11 +56,18 @@ public Mono executeWorkAsync() { private Mono getOrCreateStorageAccountContainer(final StorageAccount storageAccount, final String containerName, final String fileName, final FunctionalTaskItem.Context context) { final SqlDatabaseImportRequestImpl self = this; + self.inner.withStorageUri( + String.format("%s%s/%s", storageAccount.endPoints().primary().blob(), containerName, fileName)); + + if (!storageAccount.isSharedKeyAccessAllowed() + // self.inner.storageKey could be set before, e.g. with managed identity ID + || !CoreUtils.isNullOrEmpty(self.inner.storageKey())) { + return context.voidMono(); + } + return storageAccount.getKeysAsync() .flatMap(storageAccountKeys -> Mono.justOrEmpty(storageAccountKeys.stream().findFirst())) .flatMap(storageAccountKey -> { - self.inner.withStorageUri( - String.format("%s%s/%s", storageAccount.endPoints().primary().blob(), containerName, fileName)); self.inner.withStorageKeyType(StorageKeyType.STORAGE_ACCESS_KEY); self.inner.withStorageKey(storageAccountKey.value()); return context.voidMono(); diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index 1d774104fbf4..18dfed06ed81 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -901,10 +901,7 @@ public void canUseCoolShortcutsForResourceCreation() throws Exception { } @Test - @Disabled("Legacy threat detection policy doesn't support MI. The new Advanced Threat Protection(ATP) " - + "does not require storage account any more. " - + "See Oury Ba's answer: https://learn.microsoft.com/answers/questions/2276392/how-to-create-microsoft-sql-servers-securityalertp") - public void canCRUDSqlDatabase() throws Exception { + public void canCRUDSqlDatabase() { final String storageAccountName = generateRandomResourceName("sqlsa", 20); AzureUser user = azureCliSignedInUser(); @@ -918,20 +915,6 @@ public void canCRUDSqlDatabase() throws Exception { .withSystemAssignedManagedServiceIdentity() .create(); - StorageAccount storageAccount = storageManager.storageAccounts() - .define(storageAccountName) - .withRegion(DEFAULT_REGION) - .withExistingResourceGroup(rgName) - .disableSharedKeyAccess() - .create(); - - authorizationManager.roleAssignments() - .define(generateRandomUuid()) - .forObjectId(sqlServer.systemAssignedManagedServiceIdentityPrincipalId()) - .withBuiltInRole(BuiltInRole.STORAGE_BLOB_DATA_CONTRIBUTOR) - .withResourceScope(storageAccount) - .create(); - Mono resourceStream = sqlServer.databases() .define(SQL_DATABASE_NAME) .withStandardEdition(SqlDatabaseStandardServiceObjective.S0) @@ -942,25 +925,41 @@ public void canCRUDSqlDatabase() throws Exception { validateSqlDatabase(sqlDatabase, SQL_DATABASE_NAME); Assertions.assertTrue(sqlServer.databases().list().size() > 0); - String blobEntrypoint = storageAccount.endPoints().primary().blob(); - - List disabledAlerts = Collections.singletonList("Sql_Injection"); - - sqlDatabase.defineThreatDetectionPolicy(SecurityAlertPolicyName.fromString("myPolicy")) - .withPolicyEnabled() - .withStorageEndpoint(blobEntrypoint) - // use system-assigned MI - .withStorageAccountAccessKey(null) - .withAlertsFilter(disabledAlerts) - .create(); - - sqlDatabase.refresh(); - - SqlDatabaseThreatDetectionPolicy alertPolicy = sqlDatabase.getThreatDetectionPolicy(); - Assertions.assertNotNull(alertPolicy); - Assertions.assertEquals(SecurityAlertPolicyState.ENABLED, alertPolicy.currentState()); - Assertions.assertEquals(alertPolicy.disabledAlertList(), disabledAlerts); - Assertions.assertTrue(alertPolicy.isDefaultSecurityAlertPolicy()); + // Legacy threat detection policy doesn't support MI. The new Advanced Threat Protection(ATP) does not require + // storage account any more. See Oury Ba's answer: + // https://learn.microsoft.com/answers/questions/2276392/how-to-create-microsoft-sql-servers-securityalertp + // StorageAccount storageAccount = storageManager.storageAccounts() + // .define(storageAccountName) + // .withRegion(DEFAULT_REGION) + // .withExistingResourceGroup(rgName) + // .disableSharedKeyAccess() + // .create(); + // + // authorizationManager.roleAssignments() + // .define(generateRandomUuid()) + // .forObjectId(sqlServer.systemAssignedManagedServiceIdentityPrincipalId()) + // .withBuiltInRole(BuiltInRole.STORAGE_BLOB_DATA_CONTRIBUTOR) + // .withResourceScope(storageAccount) + // .create(); + // String blobEntrypoint = storageAccount.endPoints().primary().blob(); + // + // List disabledAlerts = Collections.singletonList("Sql_Injection"); + // + // sqlDatabase.defineThreatDetectionPolicy(SecurityAlertPolicyName.fromString("myPolicy")) + // .withPolicyEnabled() + // .withStorageEndpoint(blobEntrypoint) + // // use system-assigned MI + // .withStorageAccountAccessKey(null) + // .withAlertsFilter(disabledAlerts) + // .create(); + // + // sqlDatabase.refresh(); + // + // SqlDatabaseThreatDetectionPolicy alertPolicy = sqlDatabase.getThreatDetectionPolicy(); + // Assertions.assertNotNull(alertPolicy); + // Assertions.assertEquals(SecurityAlertPolicyState.ENABLED, alertPolicy.currentState()); + // Assertions.assertEquals(alertPolicy.disabledAlertList(), disabledAlerts); + // Assertions.assertTrue(alertPolicy.isDefaultSecurityAlertPolicy()); // Test transparent data encryption settings. TransparentDataEncryption transparentDataEncryption = sqlDatabase.getTransparentDataEncryption(); From aa51b4834956dc215d30a410d49b9cdc643e4241 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Mon, 1 Jun 2026 14:15:03 +0800 Subject: [PATCH 11/13] imports --- .../azure/resourcemanager/sql/SqlServerOperationsTests.java | 4 ---- 1 file changed, 4 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index 18dfed06ed81..b5003fe8570f 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -33,8 +33,6 @@ import com.azure.resourcemanager.sql.models.ReplicationLink; import com.azure.resourcemanager.sql.models.ReplicationState; import com.azure.resourcemanager.sql.models.SampleName; -import com.azure.resourcemanager.sql.models.SecurityAlertPolicyName; -import com.azure.resourcemanager.sql.models.SecurityAlertPolicyState; import com.azure.resourcemanager.sql.models.ServerNetworkAccessFlag; import com.azure.resourcemanager.sql.models.ServiceObjectiveName; import com.azure.resourcemanager.sql.models.Sku; @@ -44,7 +42,6 @@ import com.azure.resourcemanager.sql.models.SqlDatabaseImportExportResponse; import com.azure.resourcemanager.sql.models.SqlDatabasePremiumServiceObjective; import com.azure.resourcemanager.sql.models.SqlDatabaseStandardServiceObjective; -import com.azure.resourcemanager.sql.models.SqlDatabaseThreatDetectionPolicy; import com.azure.resourcemanager.sql.models.SqlElasticPool; import com.azure.resourcemanager.sql.models.SqlElasticPoolBasicEDTUs; import com.azure.resourcemanager.sql.models.SqlFailoverGroup; @@ -61,7 +58,6 @@ import com.azure.resourcemanager.sql.models.TransparentDataEncryptionState; import com.azure.resourcemanager.storage.models.StorageAccount; import com.azure.resourcemanager.test.model.AzureUser; - import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; From 67fd6e0dc82f79c0e69661c1d6f4d2c99b364cb0 Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Tue, 2 Jun 2026 17:30:13 +0800 Subject: [PATCH 12/13] address comments --- .../SqlDatabaseImportRequestImpl.java | 4 ++ .../sql/implementation/SqlServerImpl.java | 43 +++++++++++-------- .../sql/models/SqlDatabaseImportRequest.java | 6 +-- .../sql/SqlServerOperationsTests.java | 33 +++++++++++++- 4 files changed, 65 insertions(+), 21 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java index 289110bc740d..ad9c4a23f888 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlDatabaseImportRequestImpl.java @@ -132,6 +132,10 @@ public SqlDatabaseImportRequestImpl withManagedIdentity(String managedIdentityRe this.inner.withAdministratorLogin(managedIdentityResourceId); // No administrator password is required for managed identity authentication. this.inner.withAdministratorLoginPassword(null); + + // Use the same MI for storage account access. + this.inner.withStorageKeyType(StorageKeyType.MANAGED_IDENTITY); + this.inner.withStorageKey(managedIdentityResourceId); return this; } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index 3998dfe1ea9b..56b9e5b072cb 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -528,33 +528,42 @@ public SqlServerImpl withoutDatabase(String databaseName) { @Override public SqlServerImpl withSystemAssignedManagedServiceIdentity() { - this.innerModel().withIdentity(new ResourceIdentity().withType(IdentityType.SYSTEM_ASSIGNED)); + initIdentity(IdentityType.SYSTEM_ASSIGNED); return this; } @Override public SqlServerImpl withPrimaryUserAssignedManagedServiceIdentity(String identityResourceId) { Objects.requireNonNull(identityResourceId, "'identityResourceId' cannot be null."); + initIdentity(IdentityType.USER_ASSIGNED); + this.innerModel().identity().userAssignedIdentities().put(identityResourceId, new UserIdentity()); + this.innerModel().withPrimaryUserAssignedIdentityId(identityResourceId); + return this; + } + + /** + * Initializes or merges the identity on the inner model so that the requested + * {@code desiredType} is present. When both SYSTEM_ASSIGNED and USER_ASSIGNED + * are requested (in any order), the resulting type is SYSTEM_ASSIGNED_USER_ASSIGNED. + * Existing user-assigned identities are preserved across calls. + */ + private void initIdentity(IdentityType desiredType) { ResourceIdentity existing = this.innerModel().identity(); - IdentityType type; - Map identities; if (existing == null || existing.type() == null || existing.type() == IdentityType.NONE) { - type = IdentityType.USER_ASSIGNED; - identities = new HashMap<>(); - } else if (existing.type() == IdentityType.SYSTEM_ASSIGNED) { - type = IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED; - identities = new HashMap<>(); + this.innerModel() + .withIdentity(new ResourceIdentity().withType(desiredType).withUserAssignedIdentities(new HashMap<>())); + } else if (existing.type() == desiredType || existing.type() == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED) { + // Already has the desired type (or already combined) — nothing to change. + if (existing.userAssignedIdentities() == null) { + existing.withUserAssignedIdentities(new HashMap<>()); + } } else { - type = existing.type(); - identities = existing.userAssignedIdentities() == null - ? new HashMap<>() - : new HashMap<>(existing.userAssignedIdentities()); + // One of SYSTEM_ASSIGNED + USER_ASSIGNED → combine + existing.withType(IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED); + if (existing.userAssignedIdentities() == null) { + existing.withUserAssignedIdentities(new HashMap<>()); + } } - identities.put(identityResourceId, new UserIdentity()); - this.innerModel() - .withIdentity(new ResourceIdentity().withType(type).withUserAssignedIdentities(identities)) - .withPrimaryUserAssignedIdentityId(identityResourceId); - return this; } @Override diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java index c684bbeb6700..356b4baaa732 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/models/SqlDatabaseImportRequest.java @@ -95,11 +95,11 @@ interface WithAuthenticationTypeAndLoginPassword { * Contributor}), and it must be mapped to a database user with the required privileges. When this method * is used, no administrator password is sent to the service.

* - *

This method is for user-assigned managed identity. To use the server's system-assigned managed - * identity, enable SAMI on the server and pass the SAMI's resource identifier as well. The signed-in - * user's object ID is not a managed identity and cannot be used here.

+ *

This method is for user-assigned managed identity. System-assigned managed identity is not supported. + * See Limitations

* * @param managedIdentityResourceId the Azure resource ID of the user-assigned managed identity to use + * for both SQL and storage access * @return next definition stage */ default SqlDatabaseImportRequest.DefinitionStages.WithExecute diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index b5003fe8570f..9448a79d47be 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -26,6 +26,7 @@ import com.azure.resourcemanager.sql.models.ElasticPoolEdition; import com.azure.resourcemanager.sql.models.ElasticPoolSku; import com.azure.resourcemanager.sql.models.FailoverGroupReplicationRole; +import com.azure.resourcemanager.sql.models.IdentityType; import com.azure.resourcemanager.sql.models.PrincipalType; import com.azure.resourcemanager.sql.models.ReadOnlyEndpointFailoverPolicy; import com.azure.resourcemanager.sql.models.ReadWriteEndpointFailoverPolicy; @@ -128,6 +129,14 @@ public void canCRUDSqlSyncMember() throws Exception { SqlDatabase dbSync = sqlPrimaryServer.databases().get(dbSyncName); SqlDatabase dbMember = sqlPrimaryServer.databases().get(dbMemberName); + // SQL Data Sync requires SQL authentication for connections to the hub and member databases. Microsoft Entra (Azure AD) + // authentication isn't supported by SQL Data Sync. Because SQL authentication relies on static passwords, + // it doesn't benefit from modern protections like multifactor authentication (MFA), Conditional Access, or managed identities. + // This can increase exposure for the entire SQL instance to credential theft, brute‑force attacks, and operational + // overhead for password rotation and policy enforcement. Where possible, prefer solutions that support Microsoft + // Entra authentication or managed identities. Since SQL Data Sync is scheduled for retirement, migrate to an + // alternative that aligns with your organization's security standards. + // See https://learn.microsoft.com/azure/azure-sql/database/sql-data-sync-data-sql-server-sql-database?view=azuresql SqlSyncGroup sqlSyncGroup = dbSync.syncGroups() .define(syncGroupName) .withSyncDatabaseId(dbSource.id()) @@ -195,6 +204,14 @@ public void canCRUDSqlSyncGroup() throws Exception { SqlDatabase dbSource = sqlPrimaryServer.databases().get(dbName); SqlDatabase dbSync = sqlPrimaryServer.databases().get(dbSyncName); + // SQL Data Sync requires SQL authentication for connections to the hub and member databases. Microsoft Entra (Azure AD) + // authentication isn't supported by SQL Data Sync. Because SQL authentication relies on static passwords, + // it doesn't benefit from modern protections like multifactor authentication (MFA), Conditional Access, or managed identities. + // This can increase exposure for the entire SQL instance to credential theft, brute‑force attacks, and operational + // overhead for password rotation and policy enforcement. Where possible, prefer solutions that support Microsoft + // Entra authentication or managed identities. Since SQL Data Sync is scheduled for retirement, migrate to an + // alternative that aligns with your organization's security standards. + // See https://learn.microsoft.com/azure/azure-sql/database/sql-data-sync-data-sql-server-sql-database?view=azuresql SqlSyncGroup sqlSyncGroup = dbSync.syncGroups() .define(syncGroupName) .withSyncDatabaseId(dbSource.id()) @@ -611,9 +628,11 @@ public void canCRUDSqlServerWithImportDatabase() { .withAzureActiveDirectoryOnlyAuthentication() .withExternalActiveDirectoryAdministrator(uamiName, uami.principalId(), PrincipalType.APPLICATION) .withPrimaryUserAssignedManagedServiceIdentity(uami.id()) + .withSystemAssignedManagedServiceIdentity() .create(); Assertions.assertTrue(sqlServer.isAzureActiveDirectoryOnlyAuthenticationEnabled()); + Assertions.assertSame(IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED, sqlServer.managedServiceIdentityType()); SqlDatabase dbFromSample = sqlServer.databases() .define("db-from-sample") @@ -641,6 +660,18 @@ public void canCRUDSqlServerWithImportDatabase() { Assertions.assertNotNull(dbFromImport); Assertions.assertEquals("ep1", dbFromImport.elasticPoolName()); + // Test importBacpac with managed identity on an existing empty database + SqlDatabase dbForImportBacpac + = sqlServer.databases().define("db-for-import-bacpac").withBasicEdition().create(); + Assertions.assertNotNull(dbForImportBacpac); + + SqlDatabaseImportExportResponse importBacpacResponse + = dbForImportBacpac.importBacpac(storageAccount, "from-sample", "dbfromsample.bacpac") + .withManagedIdentity(uami.id()) + .execute(); + Assertions.assertNotNull(importBacpacResponse); + + dbForImportBacpac.delete(); dbFromImport.delete(); dbFromSample.delete(); sqlServer.elasticPools().delete("ep1"); @@ -898,7 +929,6 @@ public void canUseCoolShortcutsForResourceCreation() throws Exception { @Test public void canCRUDSqlDatabase() { - final String storageAccountName = generateRandomResourceName("sqlsa", 20); AzureUser user = azureCliSignedInUser(); // Create @@ -921,6 +951,7 @@ public void canCRUDSqlDatabase() { validateSqlDatabase(sqlDatabase, SQL_DATABASE_NAME); Assertions.assertTrue(sqlServer.databases().list().size() > 0); +// final String storageAccountName = generateRandomResourceName("sqlsa", 20); // Legacy threat detection policy doesn't support MI. The new Advanced Threat Protection(ATP) does not require // storage account any more. See Oury Ba's answer: // https://learn.microsoft.com/answers/questions/2276392/how-to-create-microsoft-sql-servers-securityalertp From 82c846a737658ac0bc2959e139deaf42365639ed Mon Sep 17 00:00:00 2001 From: "Xiaofei Cao (from Dev Box)" Date: Tue, 2 Jun 2026 20:25:03 +0800 Subject: [PATCH 13/13] record --- sdk/sql/azure-resourcemanager-sql/assets.json | 2 +- .../sql/implementation/SqlServerImpl.java | 17 +++++++++++------ .../sql/SqlServerOperationsTests.java | 2 +- 3 files changed, 13 insertions(+), 8 deletions(-) diff --git a/sdk/sql/azure-resourcemanager-sql/assets.json b/sdk/sql/azure-resourcemanager-sql/assets.json index da5a62b25723..ac2251c2df50 100644 --- a/sdk/sql/azure-resourcemanager-sql/assets.json +++ b/sdk/sql/azure-resourcemanager-sql/assets.json @@ -2,5 +2,5 @@ "AssetsRepo": "Azure/azure-sdk-assets", "AssetsRepoPrefixPath": "java", "TagPrefix": "java/sql/azure-resourcemanager-sql", - "Tag": "java/sql/azure-resourcemanager-sql_4145a7592c" + "Tag": "java/sql/azure-resourcemanager-sql_a84e1b066f" } diff --git a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java index 56b9e5b072cb..927f72ecd469 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java +++ b/sdk/sql/azure-resourcemanager-sql/src/main/java/com/azure/resourcemanager/sql/implementation/SqlServerImpl.java @@ -21,11 +21,12 @@ import com.azure.resourcemanager.sql.fluent.models.ServerUsageInner; import com.azure.resourcemanager.sql.models.AdministratorName; import com.azure.resourcemanager.sql.models.AdministratorType; -import com.azure.resourcemanager.sql.models.PrincipalType; import com.azure.resourcemanager.sql.models.IdentityType; +import com.azure.resourcemanager.sql.models.PrincipalType; import com.azure.resourcemanager.sql.models.ResourceIdentity; import com.azure.resourcemanager.sql.models.ServerExternalAdministrator; import com.azure.resourcemanager.sql.models.ServerMetric; +import com.azure.resourcemanager.sql.models.ServerNetworkAccessFlag; import com.azure.resourcemanager.sql.models.SqlDatabaseOperations; import com.azure.resourcemanager.sql.models.SqlElasticPoolOperations; import com.azure.resourcemanager.sql.models.SqlEncryptionProtectorOperations; @@ -34,7 +35,6 @@ import com.azure.resourcemanager.sql.models.SqlFirewallRuleOperations; import com.azure.resourcemanager.sql.models.SqlRestorableDroppedDatabase; import com.azure.resourcemanager.sql.models.SqlServer; -import com.azure.resourcemanager.sql.models.ServerNetworkAccessFlag; import com.azure.resourcemanager.sql.models.SqlServerAutomaticTuning; import com.azure.resourcemanager.sql.models.SqlServerDnsAliasOperations; import com.azure.resourcemanager.sql.models.SqlServerKeyOperations; @@ -48,7 +48,6 @@ import java.util.Collections; import java.util.HashMap; import java.util.List; -import java.util.Map; import java.util.Objects; import java.util.UUID; @@ -550,11 +549,17 @@ public SqlServerImpl withPrimaryUserAssignedManagedServiceIdentity(String identi private void initIdentity(IdentityType desiredType) { ResourceIdentity existing = this.innerModel().identity(); if (existing == null || existing.type() == null || existing.type() == IdentityType.NONE) { - this.innerModel() - .withIdentity(new ResourceIdentity().withType(desiredType).withUserAssignedIdentities(new HashMap<>())); + ResourceIdentity identity = new ResourceIdentity().withType(desiredType); + if (desiredType == IdentityType.USER_ASSIGNED + || desiredType == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED) { + identity.withUserAssignedIdentities(new HashMap<>()); + } + this.innerModel().withIdentity(identity); } else if (existing.type() == desiredType || existing.type() == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED) { // Already has the desired type (or already combined) — nothing to change. - if (existing.userAssignedIdentities() == null) { + if (existing.userAssignedIdentities() == null + && (existing.type() == IdentityType.USER_ASSIGNED + || existing.type() == IdentityType.SYSTEM_ASSIGNED_USER_ASSIGNED)) { existing.withUserAssignedIdentities(new HashMap<>()); } } else { diff --git a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java index 9448a79d47be..c8dadeb79038 100644 --- a/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java +++ b/sdk/sql/azure-resourcemanager-sql/src/test/java/com/azure/resourcemanager/sql/SqlServerOperationsTests.java @@ -951,7 +951,7 @@ public void canCRUDSqlDatabase() { validateSqlDatabase(sqlDatabase, SQL_DATABASE_NAME); Assertions.assertTrue(sqlServer.databases().list().size() > 0); -// final String storageAccountName = generateRandomResourceName("sqlsa", 20); + // final String storageAccountName = generateRandomResourceName("sqlsa", 20); // Legacy threat detection policy doesn't support MI. The new Advanced Threat Protection(ATP) does not require // storage account any more. See Oury Ba's answer: // https://learn.microsoft.com/answers/questions/2276392/how-to-create-microsoft-sql-servers-securityalertp