File: RoleAssignmentTests.cs
Web Access
Project: src\tests\Aspire.Hosting.Azure.Tests\Aspire.Hosting.Azure.Tests.csproj (Aspire.Hosting.Azure.Tests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System.Text.Json.Nodes;
using Aspire.Hosting.ApplicationModel;
using Aspire.Hosting.Utils;
using Azure.Provisioning.AppConfiguration;
using Azure.Provisioning.CognitiveServices;
using Azure.Provisioning.EventHubs;
using Azure.Provisioning.KeyVault;
using Azure.Provisioning.Search;
using Azure.Provisioning.ServiceBus;
using Azure.Provisioning.SignalR;
using Azure.Provisioning.WebPubSub;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
using static Aspire.Hosting.Utils.AzureManifestUtils;
 
namespace Aspire.Hosting.Azure.Tests;
 
public class RoleAssignmentTests(ITestOutputHelper output)
{
    [Fact]
    public Task ServiceBusSupport()
    {
        return RoleAssignmentTest("sb",
            builder =>
            {
                var sb = builder.AddAzureServiceBus("sb");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(sb, ServiceBusBuiltInRole.AzureServiceBusDataReceiver, ServiceBusBuiltInRole.AzureServiceBusDataSender);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param sb_outputs_name string
 
            param principalId string
 
            resource sb 'Microsoft.ServiceBus/namespaces@2024-01-01' existing = {
              name: sb_outputs_name
            }
 
            resource sb_AzureServiceBusDataReceiver 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(sb.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4f6d3b9b-027b-4f4c-9142-0e5a2a2247e0')
                principalType: 'ServicePrincipal'
              }
              scope: sb
            }
 
            resource sb_AzureServiceBusDataSender 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(sb.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '69a216fc-b8fb-44d8-bc22-1f3c2cd27a39')
                principalType: 'ServicePrincipal'
              }
              scope: sb
            }
            """);
    }
 
    [Fact]
    public Task AppConfigurationSupport()
    {
        return RoleAssignmentTest("config",
            builder =>
            {
                var config = builder.AddAzureAppConfiguration("config");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(config, AppConfigurationBuiltInRole.AppConfigurationDataReader);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
            
            param config_outputs_name string
            
            param principalId string
            
            resource config 'Microsoft.AppConfiguration/configurationStores@2024-05-01' existing = {
              name: config_outputs_name
            }
            
            resource config_AppConfigurationDataReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(config.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '516239f1-63e1-4d78-a4de-a74fb236a071')
                principalType: 'ServicePrincipal'
              }
              scope: config
            }
            """);
    }
 
    [Fact]
    public Task OpenAISupport()
    {
        return RoleAssignmentTest("openai",
            builder =>
            {
                var openai = builder.AddAzureOpenAI("openai");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(openai, CognitiveServicesBuiltInRole.CognitiveServicesOpenAIUser);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param openai_outputs_name string
 
            param principalId string
 
            resource openai 'Microsoft.CognitiveServices/accounts@2024-10-01' existing = {
              name: openai_outputs_name
            }
 
            resource openai_CognitiveServicesOpenAIUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(openai.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '5e0bd9bd-7b93-4f28-af87-19fc36ad61bd')
                principalType: 'ServicePrincipal'
              }
              scope: openai
            }
            """);
    }
 
    [Fact]
    public Task EventHubsSupport()
    {
        return RoleAssignmentTest("eventhubs",
            builder =>
            {
                var eventhubs = builder.AddAzureEventHubs("eventhubs");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(eventhubs, EventHubsBuiltInRole.AzureEventHubsDataReceiver);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param eventhubs_outputs_name string
 
            param principalId string
 
            resource eventhubs 'Microsoft.EventHub/namespaces@2024-01-01' existing = {
              name: eventhubs_outputs_name
            }
 
            resource eventhubs_AzureEventHubsDataReceiver 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(eventhubs.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a638d3c7-ab3a-418d-83e6-5f17a39d4fde'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'a638d3c7-ab3a-418d-83e6-5f17a39d4fde')
                principalType: 'ServicePrincipal'
              }
              scope: eventhubs
            }
            """);
    }
 
    [Fact]
    public Task KeyVaultSupport()
    {
        return RoleAssignmentTest("keyvault",
            builder =>
            {
                var keyvault = builder.AddAzureKeyVault("keyvault");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(keyvault, KeyVaultBuiltInRole.KeyVaultSecretsUser);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param keyvault_outputs_name string
 
            param principalId string
 
            resource keyvault 'Microsoft.KeyVault/vaults@2023-07-01' existing = {
              name: keyvault_outputs_name
            }
 
            resource keyvault_KeyVaultSecretsUser 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(keyvault.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '4633458b-17de-408a-b874-0445c86b69e6')
                principalType: 'ServicePrincipal'
              }
              scope: keyvault
            }
            """);
    }
 
    [Fact]
    public Task SearchSupport()
    {
        return RoleAssignmentTest("search",
            builder =>
            {
                var search = builder.AddAzureSearch("search");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(search, SearchBuiltInRole.SearchIndexDataReader);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param search_outputs_name string
 
            param principalId string
 
            resource search 'Microsoft.Search/searchServices@2023-11-01' existing = {
              name: search_outputs_name
            }
 
            resource search_SearchIndexDataReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(search.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '1407120a-92aa-4202-b7e9-c0e197c71c8f')
                principalType: 'ServicePrincipal'
              }
              scope: search
            }
            """);
    }
 
    [Fact]
    public Task SignalRSupport()
    {
        return RoleAssignmentTest("signalr",
            builder =>
            {
                var signalr = builder.AddAzureSignalR("signalr");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(signalr, SignalRBuiltInRole.SignalRContributor);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param signalr_outputs_name string
 
            param principalId string
 
            resource signalr 'Microsoft.SignalRService/signalR@2024-03-01' existing = {
              name: signalr_outputs_name
            }
 
            resource signalr_SignalRContributor 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(signalr.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8cf5e20a-e4b2-4e9d-b3a1-5ceb692c2761'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', '8cf5e20a-e4b2-4e9d-b3a1-5ceb692c2761')
                principalType: 'ServicePrincipal'
              }
              scope: signalr
            }
            """);
    }
 
    [Fact]
    public Task WebPubSubSupport()
    {
        return RoleAssignmentTest("webpubsub",
            builder =>
            {
                var webpubsub = builder.AddAzureWebPubSub("webpubsub");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithRoleAssignments(webpubsub, WebPubSubBuiltInRole.WebPubSubServiceReader);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param webpubsub_outputs_name string
 
            param principalId string
 
            resource webpubsub 'Microsoft.SignalRService/webPubSub@2024-03-01' existing = {
              name: webpubsub_outputs_name
            }
 
            resource webpubsub_WebPubSubServiceReader 'Microsoft.Authorization/roleAssignments@2022-04-01' = {
              name: guid(webpubsub.id, principalId, subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'bfb1c7d2-fb1a-466b-b2ba-aee63b92deaf'))
              properties: {
                principalId: principalId
                roleDefinitionId: subscriptionResourceId('Microsoft.Authorization/roleDefinitions', 'bfb1c7d2-fb1a-466b-b2ba-aee63b92deaf')
                principalType: 'ServicePrincipal'
              }
              scope: webpubsub
            }
            """);
    }
 
    [Fact]
    public Task CosmosDBSupport()
    {
        return RoleAssignmentTest("cosmos",
            builder =>
            {
                var redis = builder.AddAzureCosmosDB("cosmos");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithReference(redis);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param cosmos_outputs_name string
 
            param principalId string
 
            resource cosmos 'Microsoft.DocumentDB/databaseAccounts@2024-08-15' existing = {
              name: cosmos_outputs_name
            }
 
            resource cosmos_roleDefinition 'Microsoft.DocumentDB/databaseAccounts/sqlRoleDefinitions@2024-08-15' existing = {
              name: '00000000-0000-0000-0000-000000000002'
              parent: cosmos
            }
 
            resource cosmos_roleAssignment 'Microsoft.DocumentDB/databaseAccounts/sqlRoleAssignments@2024-08-15' = {
              name: guid(principalId, cosmos_roleDefinition.id, cosmos.id)
              properties: {
                principalId: principalId
                roleDefinitionId: cosmos_roleDefinition.id
                scope: cosmos.id
              }
              parent: cosmos
            }
            """);
    }
 
    [Fact]
    public Task RedisSupport()
    {
        return RoleAssignmentTest("redis",
            builder =>
            {
                var redis = builder.AddAzureRedis("redis");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithReference(redis);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param redis_outputs_name string
 
            param principalId string
 
            param principalName string
 
            resource redis 'Microsoft.Cache/redis@2024-03-01' existing = {
              name: redis_outputs_name
            }
 
            resource redis_contributor 'Microsoft.Cache/redis/accessPolicyAssignments@2024-03-01' = {
              name: guid(redis.id, principalId, 'Data Contributor')
              properties: {
                accessPolicyName: 'Data Contributor'
                objectId: principalId
                objectIdAlias: principalName
              }
              parent: redis
            }
            """,
            includePrincipalName: true);
    }
 
    [Fact]
    public Task PostgresSupport()
    {
        return RoleAssignmentTest("postgres",
            builder =>
            {
                var redis = builder.AddAzurePostgresFlexibleServer("postgres");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithReference(redis);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param postgres_outputs_name string
 
            param principalId string
 
            param principalName string
 
            resource postgres 'Microsoft.DBforPostgreSQL/flexibleServers@2024-08-01' existing = {
              name: postgres_outputs_name
            }
 
            resource postgres_admin 'Microsoft.DBforPostgreSQL/flexibleServers/administrators@2024-08-01' = {
              name: principalId
              properties: {
                principalName: principalName
                principalType: 'ServicePrincipal'
              }
              parent: postgres
            }
            """,
            includePrincipalName: true);
    }
 
    [Fact]
    public Task SqlSupport()
    {
        return RoleAssignmentTest("sql",
            builder =>
            {
                var redis = builder.AddAzureSqlServer("sql");
 
                builder.AddProject<Project>("api", launchProfileName: null)
                    .WithReference(redis);
            },
            """
            @description('The location for the resource(s) to be deployed.')
            param location string = resourceGroup().location
 
            param sql_outputs_name string
 
            param principalId string
 
            param principalName string
 
            resource sql 'Microsoft.Sql/servers@2021-11-01' existing = {
              name: sql_outputs_name
            }
            
            resource sql_admin 'Microsoft.Sql/servers/administrators@2021-11-01' = {
              name: 'ActiveDirectory'
              properties: {
                login: principalName
                sid: principalId
              }
              parent: sql
            }
            """,
            includePrincipalName: true);
    }
 
    private async Task RoleAssignmentTest(
        string azureResourceName,
        Action<IDistributedApplicationBuilder> configureBuilder,
        string expectedRolesBicep,
        bool includePrincipalName = false)
    {
        var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish);
        builder.AddAzureContainerAppEnvironment("env");
 
        configureBuilder(builder);
 
        var app = builder.Build();
 
        var model = app.Services.GetRequiredService<DistributedApplicationModel>();
 
        await ExecuteBeforeStartHooksAsync(app, default);
 
        var projRoles = Assert.Single(model.Resources.OfType<AzureProvisioningResource>().Where(r => r.Name == $"api-roles-{azureResourceName}"));
 
        var (rolesManifest, rolesBicep) = await GetManifestWithBicep(projRoles);
 
        var principalNameSegment = includePrincipalName ? """
            ,
                "principalName": "{api-identity.outputs.principalName}"
            """ : null;
 
        var expectedRolesManifest =
            $$"""
            {
              "type": "azure.bicep.v0",
              "path": "api-roles-{{azureResourceName}}.module.bicep",
              "params": {
                "{{azureResourceName}}_outputs_name": "{{{azureResourceName}}.outputs.name}",
                "principalId": "{api-identity.outputs.principalId}"{{principalNameSegment}}
              }
            }
            """;
        Assert.Equal(expectedRolesManifest, rolesManifest.ToString());
 
        output.WriteLine(rolesBicep);
        Assert.Equal(expectedRolesBicep, rolesBicep);
    }
 
    private static Task<(JsonNode ManifestNode, string BicepText)> GetManifestWithBicep(IResource resource) =>
        AzureManifestUtils.GetManifestWithBicep(resource, skipPreparer: true);
 
    private sealed class Project : IProjectMetadata
    {
        public string ProjectPath => "project";
    }
}