diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/.aspire/settings.json b/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/.aspire/settings.json new file mode 100644 index 00000000..f3018f9a --- /dev/null +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/.aspire/settings.json @@ -0,0 +1,3 @@ +{ + "appHostPath": "../CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost.csproj" +} \ No newline at end of file diff --git a/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/Program.cs b/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/Program.cs index 8a1ebfbb..9b2b0806 100644 --- a/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/Program.cs +++ b/examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/Program.cs @@ -17,6 +17,7 @@ builder.AddProject("servicea") + .WithReference(redis) .PublishAsAzureContainerApp((i,c)=> { }) .WithDaprSidecar(sidecar => sidecar.WithReference(stateStore).WithReference(pubSub)) .WaitFor(redis); diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs index 69208aec..c2dfb1b8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs @@ -4,10 +4,12 @@ using Azure.Provisioning.AppContainers; using Azure.Provisioning.Expressions; using Azure.Provisioning.KeyVault; +using Azure.Provisioning.Redis; using Azure.Provisioning.Roles; using CommunityToolkit.Aspire.Hosting.Azure.Dapr; using CommunityToolkit.Aspire.Hosting.Dapr; -using AzureRedisResource = Azure.Provisioning.Redis.RedisResource; +using CdkRedisResource = Azure.Provisioning.Redis.RedisResource; +using RedisResource = Aspire.Hosting.ApplicationModel.RedisResource; namespace Aspire.Hosting; @@ -91,16 +93,12 @@ private static IResourceBuilder ConfigureRedisPubSubComp private static void ConfigureForManagedIdentityAuthentication(this IResourceBuilder builder, IResourceBuilder redisBuilder, string componentType) { - var principalIdParam = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalId, typeof(string)); - - var configureInfrastructure = (AzureResourceInfrastructure infrastructure) => + var configureInfrastructure = (AzureResourceInfrastructure infrastructure, UserAssignedIdentity daprIdentity) => { var redisHostParam = redisBuilder.GetOutput(daprConnectionStringKey).AsProvisioningParameter(infrastructure, redisHostKey); var provisionableResources = infrastructure.GetProvisionableResources(); - if (provisionableResources.OfType().FirstOrDefault() - is ContainerAppManagedEnvironment managedEnvironment && - provisionableResources.OfType().FirstOrDefault() is UserAssignedIdentity identity) + if (provisionableResources.OfType().FirstOrDefault() is ContainerAppManagedEnvironment managedEnvironment) { var daprComponent = AzureDaprHostingExtensions.CreateDaprComponent( builder.Resource.Name, @@ -115,7 +113,7 @@ is ContainerAppManagedEnvironment managedEnvironment && new() { Name = redisHostKey, Value = redisHostParam }, new() { Name = "enableTLS", Value = "true" }, new() { Name = "useEntraID", Value = "true" }, - new() { Name = "azureClientId", Value = identity.PrincipalId } + new() { Name = "azureClientId", Value = daprIdentity.PrincipalId } }; // Add state-specific metadata @@ -124,6 +122,25 @@ is ContainerAppManagedEnvironment managedEnvironment && metadata.Add(new ContainerAppDaprMetadata { Name = "actorStateStore", Value = "true" }); } + if (redisBuilder.Resource.AddAsExistingResource(infrastructure) is CdkRedisResource redis) + { + var redisBicepIdentifier = redisBuilder.Resource.GetBicepIdentifier(); + var policyBicepIdentifier = $"{redisBicepIdentifier}_contributor"; + if (!infrastructure.GetProvisionableResources().OfType().Any(r => r.BicepIdentifier == policyBicepIdentifier)) + { + + infrastructure.Add(new RedisCacheAccessPolicyAssignment($"{redisBicepIdentifier}_contributor") + { + Name = BicepFunction.CreateGuid(redis.Id, daprIdentity.PrincipalId, "Data Contributor"), + Parent = redis, + AccessPolicyName = "Data Contributor", + ObjectId = daprIdentity.PrincipalId, + ObjectIdAlias = daprIdentity.Name + }); + } + + } + daprComponent.Metadata = [.. metadata]; // Add scopes if any exist @@ -132,15 +149,18 @@ is ContainerAppManagedEnvironment managedEnvironment && infrastructure.Add(daprComponent); infrastructure.TryAdd(redisHostParam); + } }; + + //builder.WithRoleAssignments(redisBuilder, RedisBuiltInRole.GetBuiltInRoleName, [RedisBuiltInRole.RedisCacheContributor]); builder.WithAnnotation(new AzureDaprComponentPublishingAnnotation(configureInfrastructure)); // Configure the Redis resource to output the connection string redisBuilder.ConfigureInfrastructure(infrastructure => { - var redisResource = infrastructure.GetProvisionableResources().OfType().SingleOrDefault(); + var redisResource = infrastructure.GetProvisionableResources().OfType().SingleOrDefault(); var outputExists = infrastructure.GetProvisionableResources().OfType().Any(o => o.BicepIdentifier == daprConnectionStringKey); if (redisResource is not null && !outputExists) @@ -162,7 +182,7 @@ private static void ConfigureForAccessKeyAuthentication(this IResourceBuilder + var configureInfrastructure = (AzureResourceInfrastructure infrastructure, UserAssignedIdentity daprIdentity) => { var redisHostParam = redisBuilder.GetOutput(daprConnectionStringKey).AsProvisioningParameter(infrastructure, redisHostKey); @@ -183,6 +203,7 @@ private static void ConfigureForAccessKeyAuthentication(this IResourceBuilder public static class AzureContainerAppEnvironmentResourceBuilderExtensions { + private const string DaprManagedIdentityKey = "daprManagedIdentity"; + /// /// Configures the Azure Container App Environment resource to use Dapr. + /// This method creates a dedicated managed identity for Dapr components and configures all Dapr components to use it. /// - /// - /// + /// The Azure Container App Environment resource builder. + /// The configured Azure Container App Environment resource builder. public static IResourceBuilder WithDaprComponents( this IResourceBuilder builder) { @@ -53,13 +60,38 @@ public static IResourceBuilder WithDaprCom return builder.ConfigureInfrastructure(infrastructure => { + // Create the Dapr managed identity once + var daprIdentity = new UserAssignedIdentity(DaprManagedIdentityKey); + + infrastructure.Add(daprIdentity); + var daprComponentResources = builder.ApplicationBuilder.Resources.OfType(); foreach (var daprComponentResource in daprComponentResources) { + if (daprComponentResource.TryGetLastAnnotation(out var roleAssignmentAnnotation)) + { + var target = roleAssignmentAnnotation.Target.AddAsExistingResource(infrastructure); + + foreach (var roleDefinition in roleAssignmentAnnotation.Roles) + { + var id = new MemberExpression(new IdentifierExpression(roleAssignmentAnnotation.Target.GetBicepIdentifier()), "id"); + var roleAssignment = new RoleAssignment($"{daprComponentResource.Name}{roleDefinition.Name}") + { + Name = BicepFunction.CreateGuid(id, daprIdentity.Id, BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", roleDefinition.Id)), + RoleDefinitionId = BicepFunction.GetSubscriptionResourceId("Microsoft.Authorization/roleDefinitions", roleDefinition.Id), + PrincipalId = daprIdentity.PrincipalId, + PrincipalType = RoleManagementPrincipalType.ServicePrincipal, + Scope = new IdentifierExpression(target.BicepIdentifier) + }; + + infrastructure.Add(roleAssignment); + } + } + daprComponentResource.TryGetLastAnnotation(out var publishingAnnotation); + publishingAnnotation?.PublishingAction(infrastructure, daprIdentity); - publishingAnnotation?.PublishingAction(infrastructure); } }); } diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprComponentPublishingAnnotation.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprComponentPublishingAnnotation.cs index 46c084b4..f1cffa9a 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprComponentPublishingAnnotation.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprComponentPublishingAnnotation.cs @@ -1,5 +1,6 @@ using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; +using Azure.Provisioning.Roles; namespace CommunityToolkit.Aspire.Hosting.Azure.Dapr; /// @@ -10,4 +11,4 @@ namespace CommunityToolkit.Aspire.Hosting.Azure.Dapr; /// allowing customization of the resource infrastructure. /// The action to be executed on the during the publishing process. This /// action allows for customization of the infrastructure configuration. -public record AzureDaprComponentPublishingAnnotation(Action PublishingAction) : IResourceAnnotation; \ No newline at end of file +public record AzureDaprComponentPublishingAnnotation(Action PublishingAction) : IResourceAnnotation; \ No newline at end of file diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs index 4a8362ed..1efe1aae 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs @@ -38,6 +38,27 @@ public static IResourceBuilder AddAzureDaprResource( .WithManifestPublishingCallback(azureDaprComponentResource.WriteToManifest); } + /// + /// Adds role assignments to the specified Azure resource, allowing the target resource to assume the specified built-in roles. + /// + /// The type of the resource being configured. + /// The type of the target Azure resource to which roles are assigned. + /// The type representing built-in roles. + /// The resource builder for the resource being configured. + /// The resource builder for the target Azure resource to receive role assignments. + /// A function that returns the name of a role given a built-in role value. + /// An array of built-in roles to assign to the target resource. + /// The updated resource builder with role assignments applied. + public static IResourceBuilder WithRoleAssignments(this IResourceBuilder builder, IResourceBuilder target, Func getName, TBuiltInRole[] roles) + where T : IResource + where TTarget : AzureProvisioningResource + where TBuiltInRole : notnull + { + builder.WithAnnotation(new RoleAssignmentAnnotation(target.Resource, CreateRoleDefinitions(roles, getName))); + return builder; + } + + /// /// Adds scopes to the specified Dapr component in a container app managed environment. /// @@ -96,4 +117,10 @@ public static ContainerAppManagedEnvironmentDaprComponent CreateDaprComponent( Version = version }; } + + private static HashSet CreateRoleDefinitions(IReadOnlyList roles, Func getName) + where TBuiltInRole : notnull + { + return [.. roles.Select(r => new RoleDefinition(r.ToString()!, getName(r)))]; + } } diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureKeyVaultDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureKeyVaultDaprHostingExtensions.cs index fa37df4e..6064870c 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureKeyVaultDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureKeyVaultDaprHostingExtensions.cs @@ -3,7 +3,7 @@ using Azure.Provisioning; using Azure.Provisioning.AppContainers; using Azure.Provisioning.Expressions; -using Azure.Provisioning.KeyVault; +using Azure.Provisioning.Roles; using CommunityToolkit.Aspire.Hosting.Azure.Dapr; using CommunityToolkit.Aspire.Hosting.Dapr; @@ -17,6 +17,8 @@ public static class AzureKeyVaultDaprHostingExtensions private const string secretStoreComponentKey = "secretStoreComponent"; private const string secretStore = nameof(secretStore); + + /// /// Configures the Key Vault secret store component for the Dapr component resource. /// @@ -27,9 +29,9 @@ public static IResourceBuilder ConfigureKeyVaultSecretsC { ArgumentNullException.ThrowIfNull(builder, nameof(builder)); - var principalIdParameter = new ProvisioningParameter(AzureBicepResource.KnownParameters.PrincipalId, typeof(string)); + //TODO: We may need to actually add the key vault resource here as well - I'm not sure if aspire automatically adds it anymore or not - var configureInfrastructure = (AzureResourceInfrastructure infrastructure) => + var configureInfrastructure = (AzureResourceInfrastructure infrastructure, UserAssignedIdentity daprIdentity) => { if (infrastructure.GetProvisionableResources().OfType().FirstOrDefault() is ContainerAppManagedEnvironment managedEnvironment) { @@ -43,12 +45,11 @@ public static IResourceBuilder ConfigureKeyVaultSecretsC daprComponent.Scopes = []; daprComponent.Metadata = [ new ContainerAppDaprMetadata { Name = "vaultName", Value = kvNameParam }, - new ContainerAppDaprMetadata { Name = "azureClientId", Value = principalIdParameter } + new ContainerAppDaprMetadata { Name = "azureClientId", Value = daprIdentity.PrincipalId } ]; infrastructure.Add(daprComponent); infrastructure.Add(kvNameParam); - infrastructure.Add(principalIdParameter); infrastructure.Add(new ProvisioningOutput(secretStoreComponentKey, typeof(string)) {