Skip to content

Commit 6900eeb

Browse files
authored
Fix Azure Functions Role Assignments to Host Storage (dotnet#8290)
* Fix Azure Functions Role Assignments to Host Storage With dotnet#8127, a mistake was made in that WithRoleAssignments only works if the app is using ACA infrastructure. If the app isn't using our ACA infrastructure, WithRoleAssignments throws an exception at startup. To make Azure Functions work correctly both with and without ACA infrastructure we use the internal WithDefaultRoleAssignments method to set the default role assignments on the Host Storage, which will be respected for both. * Add FunctionsTest without AzureContainerApps
1 parent 85566cf commit 6900eeb

File tree

4 files changed

+63
-35
lines changed

4 files changed

+63
-35
lines changed

playground/AzureFunctionsEndToEnd/AzureFunctionsEndToEnd.AppHost/aspire-manifest.json

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,11 +53,11 @@
5353
},
5454
"mydatabase": {
5555
"type": "value.v0",
56-
"connectionString": "{cosmosdb.outputs.connectionString}"
56+
"connectionString": "AccountEndpoint={cosmosdb.outputs.connectionString};Database=mydatabase"
5757
},
5858
"mycontainer": {
5959
"type": "value.v0",
60-
"connectionString": "{cosmosdb.outputs.connectionString}"
60+
"connectionString": "AccountEndpoint={cosmosdb.outputs.connectionString};Database=mydatabase;Container=mycontainer"
6161
},
6262
"funcstorage67c6c": {
6363
"type": "azure.bicep.v0",
@@ -98,8 +98,8 @@
9898
"messaging__fullyQualifiedNamespace": "{messaging.outputs.serviceBusEndpoint}",
9999
"Aspire__Azure__Messaging__ServiceBus__messaging__FullyQualifiedNamespace": "{messaging.outputs.serviceBusEndpoint}",
100100
"cosmosdb__accountEndpoint": "{cosmosdb.outputs.connectionString}",
101-
"Aspire__Microsoft__Azure__Cosmos__cosmosdb__AccountEndpoint": "{cosmosdb.outputs.connectionString}",
102101
"Aspire__Microsoft__EntityFrameworkCore__Cosmos__cosmosdb__AccountEndpoint": "{cosmosdb.outputs.connectionString}",
102+
"Aspire__Microsoft__Azure__Cosmos__cosmosdb__AccountEndpoint": "{cosmosdb.outputs.connectionString}",
103103
"blob__blobServiceUri": "{storage.outputs.blobEndpoint}",
104104
"blob__queueServiceUri": "{storage.outputs.queueEndpoint}",
105105
"Aspire__Azure__Storage__Blobs__blob__ServiceUri": "{storage.outputs.blobEndpoint}",

src/Aspire.Hosting.Azure.Functions/Aspire.Hosting.Azure.Functions.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
</ItemGroup>
1717

1818
<ItemGroup>
19+
<Compile Include="$(SharedDir)AzureRoleAssignmentUtils.cs" />
1920
<Compile Include="$(SharedDir)CommandLineArgsParser.cs" />
2021
<Compile Include="$(SharedDir)\LaunchProfiles\*.cs" />
2122
</ItemGroup>

src/Aspire.Hosting.Azure.Functions/AzureFunctionsProjectResourceExtensions.cs

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,19 @@ public static IResourceBuilder<AzureFunctionsProjectResource> AddAzureFunctionsP
4646
.OfType<AzureStorageResource>()
4747
.FirstOrDefault(r => r.Name == storageResourceName);
4848

49-
storage ??= builder.AddAzureStorage(storageResourceName).RunAsEmulator().Resource;
49+
if (storage is null)
50+
{
51+
storage = builder.AddAzureStorage(storageResourceName)
52+
// Azure Functions blob triggers require StorageAccountContributor access to the host storage
53+
// account when deployed. We assign this role to the implicit host storage resource.
54+
.WithDefaultRoleAssignments(StorageBuiltInRole.GetBuiltInRoleName,
55+
StorageBuiltInRole.StorageBlobDataContributor,
56+
StorageBuiltInRole.StorageTableDataContributor,
57+
StorageBuiltInRole.StorageQueueDataContributor,
58+
StorageBuiltInRole.StorageAccountContributor)
59+
.RunAsEmulator()
60+
.Resource;
61+
}
5062

5163
builder.Eventing.Subscribe<BeforeStartEvent>((data, token) =>
5264
{
@@ -59,17 +71,6 @@ public static IResourceBuilder<AzureFunctionsProjectResource> AddAzureFunctionsP
5971
{
6072
removeStorage = false;
6173
}
62-
else
63-
{
64-
// Remove the role assignments associated with the implicit host storage resource
65-
// if an explicit one has been provided. Assume that users must configure their
66-
// own role assignments for the explicit storage resource.
67-
if (item.TryGetAnnotationsOfType<RoleAssignmentAnnotation>(out var roleAssignments) &&
68-
roleAssignments.SingleOrDefault(assignment => assignment.Target == storage) is { } roleAssignmentAnnotation)
69-
{
70-
item.Annotations.Remove(roleAssignmentAnnotation);
71-
}
72-
}
7374
}
7475

7576
if (removeStorage)
@@ -105,13 +106,6 @@ public static IResourceBuilder<AzureFunctionsProjectResource> AddAzureFunctionsP
105106
// Set the storage connection string.
106107
((IResourceWithAzureFunctionsConfig)resource.HostStorage).ApplyAzureFunctionsConfiguration(context.EnvironmentVariables, "AzureWebJobsStorage");
107108
})
108-
// Azure Functions blob triggers require StorageAccountContributor access to the host storage
109-
// account when deployed. We assign this role to the implicit host storage resource when running in publish mode.
110-
.WithRoleAssignments(builder.CreateResourceBuilder(storage),
111-
StorageBuiltInRole.StorageBlobDataContributor,
112-
StorageBuiltInRole.StorageQueueDataContributor,
113-
StorageBuiltInRole.StorageTableDataContributor,
114-
StorageBuiltInRole.StorageAccountContributor)
115109
.WithOtlpExporter()
116110
.WithFunctionsHttpEndpoint();
117111
}

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

Lines changed: 46 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -256,6 +256,39 @@ public async Task AddAzureFunctionsProject_WiresUpHttpsEndpointCorrectly_WhenOnl
256256
);
257257
}
258258

259+
[Fact]
260+
public async Task AddAzureFunctionsProject_CanGetStorageManifestSuccessfully()
261+
{
262+
using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
263+
264+
// hardcoded sha256 to make the storage name deterministic
265+
builder.Configuration["AppHost:Sha256"] = "634f8";
266+
var project = builder.AddAzureFunctionsProject<TestProjectWithHttpsNoPort>("funcapp");
267+
268+
var app = builder.Build();
269+
270+
var model = app.Services.GetRequiredService<DistributedApplicationModel>();
271+
272+
await ExecuteBeforeStartHooksAsync(app, default);
273+
274+
var storage = Assert.Single(model.Resources.OfType<AzureProvisioningResource>().Where(r => r.Name == $"funcstorage634f8"));
275+
276+
var (storageManifest, _) = await GetManifestWithBicep(storage);
277+
278+
var expectedRolesManifest =
279+
"""
280+
{
281+
"type": "azure.bicep.v0",
282+
"path": "funcstorage634f8.module.bicep",
283+
"params": {
284+
"principalType": "",
285+
"principalId": ""
286+
}
287+
}
288+
""";
289+
Assert.Equal(expectedRolesManifest, storageManifest.ToString());
290+
}
291+
259292
[Fact]
260293
public async Task AddAzureFunctionsProject_WorksWithAddAzureContainerAppsInfrastructure()
261294
{
@@ -312,21 +345,21 @@ param principalId string
312345
scope: funcstorage634f8
313346
}
314347
315-
resource funcstorage634f8_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
316-
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
348+
resource funcstorage634f8_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
349+
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
317350
properties: {
318351
principalId: principalId
319-
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
352+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
320353
principalType: 'ServicePrincipal'
321354
}
322355
scope: funcstorage634f8
323356
}
324-
325-
resource funcstorage634f8_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
326-
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
357+
358+
resource funcstorage634f8_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
359+
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
327360
properties: {
328361
principalId: principalId
329-
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
362+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
330363
principalType: 'ServicePrincipal'
331364
}
332365
scope: funcstorage634f8
@@ -593,21 +626,21 @@ param principalId string
593626
scope: funcstorage634f8
594627
}
595628
596-
resource funcstorage634f8_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
597-
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
629+
resource funcstorage634f8_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
630+
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
598631
properties: {
599632
principalId: principalId
600-
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
633+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
601634
principalType: 'ServicePrincipal'
602635
}
603636
scope: funcstorage634f8
604637
}
605638
606-
resource funcstorage634f8_StorageTableDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
607-
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3'))
639+
resource funcstorage634f8_StorageQueueDataContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
640+
name: guid(funcstorage634f8.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88'))
608641
properties: {
609642
principalId: principalId
610-
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '0a9a7e1f-b9d0-4cc4-a60d-0319b160aaa3')
643+
roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '974c5e8b-45b9-4653-ba55-5f855dd0fb88')
611644
principalType: 'ServicePrincipal'
612645
}
613646
scope: funcstorage634f8

0 commit comments

Comments
 (0)