Skip to content

Commit ef161d4

Browse files
committed
Support Azure SqlServer role assignments on app specific managed identity
Adding role assignment support for PostgreSQL following the pattern set in dotnet#8140 Fix dotnet#6161
1 parent a5ee0bb commit ef161d4

File tree

7 files changed

+134
-21
lines changed

7 files changed

+134
-21
lines changed

src/Aspire.Hosting.Azure.Sql/AzureSqlExtensions.cs

Lines changed: 37 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,8 @@ public static IResourceBuilder<AzureSqlServerResource> AddAzureSqlServer(this ID
8585
};
8686

8787
var resource = new AzureSqlServerResource(name, configureInfrastructure);
88-
var azureSqlServer = builder.AddResource(resource);
88+
var azureSqlServer = builder.AddResource(resource)
89+
.WithAnnotation(new DefaultRoleAssignmentsAnnotation(new HashSet<RoleDefinition>()));
8990

9091
return azureSqlServer;
9192
}
@@ -203,6 +204,10 @@ private static void CreateSqlServer(
203204
var principalNameParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalName, typeof(string));
204205
infrastructure.Add(principalNameParameter);
205206

207+
var azureResource = (AzureSqlServerResource)infrastructure.AspireResource;
208+
var addAdminRole = azureResource.TryGetLastAnnotation<AppliedRoleAssignmentsAnnotation>(out _) ||
209+
azureResource.InnerResource is not null; // the obsolete AsAzureSqlDatabase use this as well, ensure we generate the role assignment.
210+
206211
var sqlServer = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infrastructure,
207212
(identifier, name) =>
208213
{
@@ -212,35 +217,35 @@ private static void CreateSqlServer(
212217
},
213218
(infrastructure) =>
214219
{
215-
return new SqlServer(infrastructure.AspireResource.GetBicepIdentifier())
220+
var sqlServer = new SqlServer(infrastructure.AspireResource.GetBicepIdentifier())
221+
{
222+
Version = "12.0",
223+
PublicNetworkAccess = ServerNetworkAccessFlag.Enabled,
224+
MinTlsVersion = SqlMinimalTlsVersion.Tls1_2,
225+
Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
226+
};
227+
228+
if (addAdminRole)
216229
{
217-
Administrators = new ServerExternalAdministrator()
230+
sqlServer.Administrators = new ServerExternalAdministrator()
218231
{
219232
AdministratorType = SqlAdministratorType.ActiveDirectory,
220233
IsAzureADOnlyAuthenticationEnabled = true,
221234
Sid = principalIdParameter,
222235
Login = principalNameParameter,
223236
TenantId = BicepFunction.GetSubscription().TenantId
224-
},
225-
Version = "12.0",
226-
PublicNetworkAccess = ServerNetworkAccessFlag.Enabled,
227-
MinTlsVersion = SqlMinimalTlsVersion.Tls1_2,
228-
Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } }
229-
};
237+
};
238+
}
239+
240+
return sqlServer;
230241
});
231242

232243
// If the resource is an existing resource, we model the administrator access
233244
// for the managed identity as an "edge" between the parent SqlServer resource
234245
// and a custom SqlServerAzureADAdministrator resource.
235-
if (sqlServer.IsExistingResource)
246+
if (addAdminRole && sqlServer.IsExistingResource)
236247
{
237-
var admin = new SqlServerAzureADAdministratorWorkaround($"{sqlServer.BicepIdentifier}_admin")
238-
{
239-
ParentOverride = sqlServer,
240-
LoginOverride = principalNameParameter,
241-
SidOverride = principalIdParameter
242-
};
243-
infrastructure.Add(admin);
248+
AddActiveDirectoryAdministrator(infrastructure, sqlServer, principalIdParameter, principalNameParameter);
244249
}
245250

246251
infrastructure.Add(new SqlFirewallRule("sqlFirewallRule_AllowAllAzureIps")
@@ -285,6 +290,21 @@ private static void CreateSqlServer(
285290
}
286291

287292
infrastructure.Add(new ProvisioningOutput("sqlServerFqdn", typeof(string)) { Value = sqlServer.FullyQualifiedDomainName });
293+
294+
// We need to output name to externalize role assignments.
295+
infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = sqlServer.Name });
296+
}
297+
298+
internal static SqlServerAzureADAdministrator AddActiveDirectoryAdministrator(AzureResourceInfrastructure infra, SqlServer sqlServer, BicepValue<Guid> principalId, BicepValue<string> principalName)
299+
{
300+
var admin = new SqlServerAzureADAdministratorWorkaround($"{sqlServer.BicepIdentifier}_admin")
301+
{
302+
ParentOverride = sqlServer,
303+
LoginOverride = principalName,
304+
SidOverride = principalId
305+
};
306+
infra.Add(admin);
307+
return admin;
288308
}
289309

290310
/// <remarks>

src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22
// The .NET Foundation licenses this file to you under the MIT license.
33

44
using Aspire.Hosting.ApplicationModel;
5+
using Azure.Provisioning.Primitives;
6+
using Azure.Provisioning.Sql;
57

68
namespace Aspire.Hosting.Azure;
79

@@ -39,6 +41,8 @@ public AzureSqlServerResource(SqlServerServerResource innerResource, Action<Azur
3941
/// </summary>
4042
public BicepOutputReference FullyQualifiedDomainName => new("sqlServerFqdn", this);
4143

44+
private BicepOutputReference NameOutputReference => new("name", this);
45+
4246
/// <summary>
4347
/// Gets the connection template for the manifest for the Azure SQL Server resource.
4448
/// </summary>
@@ -90,4 +94,25 @@ internal void SetInnerResource(SqlServerServerResource innerResource)
9094

9195
InnerResource = innerResource;
9296
}
97+
98+
/// <inheritdoc/>
99+
public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra)
100+
{
101+
var store = SqlServer.FromExisting(this.GetBicepIdentifier());
102+
store.Name = NameOutputReference.AsProvisioningParameter(infra);
103+
infra.Add(store);
104+
return store;
105+
}
106+
107+
/// <inheritdoc/>
108+
public override void AddRoleAssignments(IAddRoleAssignmentsContext roleAssignmentContext)
109+
{
110+
var infra = roleAssignmentContext.Infrastructure;
111+
var postgres = (SqlServer)AddAsExistingResource(infra);
112+
113+
var principalId = roleAssignmentContext.PrincipalId;
114+
var principalName = roleAssignmentContext.PrincipalName;
115+
116+
AzureSqlExtensions.AddActiveDirectoryAdministrator(infra, postgres, principalId, principalName);
117+
}
93118
}

tests/Aspire.Hosting.Azure.Tests/AzureBicepResourceTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1397,6 +1397,8 @@ param principalType string
13971397
}
13981398
13991399
output sqlServerFqdn string = sql.properties.fullyQualifiedDomainName
1400+
1401+
output name string = sql.name
14001402
""";
14011403
output.WriteLine(manifest.BicepText);
14021404
Assert.Equal(expectedBicep, manifest.BicepText);
@@ -1478,6 +1480,8 @@ param principalName string
14781480
}
14791481
14801482
output sqlServerFqdn string = sql.properties.fullyQualifiedDomainName
1483+
1484+
output name string = sql.name
14811485
""";
14821486
output.WriteLine(manifest.BicepText);
14831487
Assert.Equal(expectedBicep, manifest.BicepText);

tests/Aspire.Hosting.Azure.Tests/AzureResourceOptionsTests.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,8 @@ param principalName string
130130
}
131131
132132
output sqlServerFqdn string = sql_server.properties.fullyQualifiedDomainName
133+
134+
output name string = sql_server.name
133135
""";
134136
output.WriteLine(actualBicep);
135137
Assert.Equal(expectedBicep, actualBicep);

tests/Aspire.Hosting.Azure.Tests/AzureSqlExtensionsTests.cs

Lines changed: 25 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System.Runtime.CompilerServices;
45
using Aspire.Hosting.ApplicationModel;
56
using Aspire.Hosting.Utils;
67
using Xunit;
@@ -11,9 +12,11 @@ namespace Aspire.Hosting.Azure.Tests;
1112
public class AzureSqlExtensionsTests(ITestOutputHelper output)
1213
{
1314
[Theory]
14-
[InlineData(true)]
15-
[InlineData(false)]
16-
public async Task AddAzureSqlServer(bool publishMode)
15+
// [InlineData(true, true)] this scenario is covered in RoleAssignmentTests.SqlSupport. The output doesn't match the pattern here because the role assignment isn't generated
16+
[InlineData(true, false)]
17+
[InlineData(false, true)]
18+
[InlineData(false, false)]
19+
public async Task AddAzureSqlServer(bool publishMode, bool useAcaInfrastructure)
1720
{
1821
using var builder = TestDistributedApplicationBuilder.Create(publishMode ? DistributedApplicationOperation.Publish : DistributedApplicationOperation.Run);
1922

@@ -22,7 +25,20 @@ public async Task AddAzureSqlServer(bool publishMode)
2225
sql.AddDatabase("db1");
2326
sql.AddDatabase("db2", "db2Name");
2427

25-
var manifest = await AzureManifestUtils.GetManifestWithBicep(sql.Resource);
28+
if (useAcaInfrastructure)
29+
{
30+
builder.AddAzureContainerAppsInfrastructure();
31+
32+
// on ACA infrastructure, if there are no references to the resource,
33+
// then there won't be any roles created. So add a reference here.
34+
builder.AddContainer("api", "myimage")
35+
.WithReference(sql);
36+
}
37+
38+
using var app = builder.Build();
39+
await ExecuteBeforeStartHooksAsync(app, default);
40+
41+
var manifest = await AzureManifestUtils.GetManifestWithBicep(sql.Resource, skipPreparer: true);
2642

2743
var principalTypeParam = "";
2844
if (!publishMode)
@@ -125,6 +141,8 @@ param principalName string
125141
}
126142
127143
output sqlServerFqdn string = sql.properties.fullyQualifiedDomainName
144+
145+
output name string = sql.name
128146
""";
129147
output.WriteLine(manifest.BicepText);
130148
Assert.Equal(expectedBicep, manifest.BicepText);
@@ -237,4 +255,7 @@ private sealed class Dummy1Annotation : IResourceAnnotation
237255
private sealed class Dummy2Annotation : IResourceAnnotation
238256
{
239257
}
258+
259+
[UnsafeAccessor(UnsafeAccessorKind.Method, Name = "ExecuteBeforeStartHooksAsync")]
260+
private static extern Task ExecuteBeforeStartHooksAsync(DistributedApplication app, CancellationToken cancellationToken);
240261
}

tests/Aspire.Hosting.Azure.Tests/ExistingAzureResourceTests.cs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1169,6 +1169,8 @@ param existingResourceName string
11691169
}
11701170
11711171
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName
1172+
1173+
output name string = existingResourceName
11721174
""";
11731175

11741176
output.WriteLine(BicepText);
@@ -1243,6 +1245,8 @@ param existingResourceName string
12431245
}
12441246
12451247
output sqlServerFqdn string = sqlServer.properties.fullyQualifiedDomainName
1248+
1249+
output name string = existingResourceName
12461250
""";
12471251

12481252
output.WriteLine(BicepText);

tests/Aspire.Hosting.Azure.Tests/RoleAssignmentTests.cs

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -389,6 +389,43 @@ param principalName string
389389
includePrincipalName: true);
390390
}
391391

392+
[Fact]
393+
public Task SqlSupport()
394+
{
395+
return RoleAssignmentTest("sql",
396+
builder =>
397+
{
398+
var redis = builder.AddAzureSqlServer("sql");
399+
400+
builder.AddProject<Project>("api", launchProfileName: null)
401+
.WithReference(redis);
402+
},
403+
"""
404+
@description('The location for the resource(s) to be deployed.')
405+
param location string = resourceGroup().location
406+
407+
param sql_outputs_name string
408+
409+
param principalId string
410+
411+
param principalName string
412+
413+
resource sql 'Microsoft.Sql/servers@2021-11-01' existing = {
414+
name: sql_outputs_name
415+
}
416+
417+
resource sql_admin 'Microsoft.Sql/servers/administrators@2021-11-01' = {
418+
name: 'ActiveDirectory'
419+
properties: {
420+
login: principalName
421+
sid: principalId
422+
}
423+
parent: sql
424+
}
425+
""",
426+
includePrincipalName: true);
427+
}
428+
392429
private async Task RoleAssignmentTest(
393430
string azureResourceName,
394431
Action<IDistributedApplicationBuilder> configureBuilder,

0 commit comments

Comments
 (0)