From 2f894c5a3e7a4192da22a7854628d6a8248b5af7 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Thu, 2 Oct 2025 09:02:06 +1300 Subject: [PATCH 1/5] Enhance Dapr integration with managed identity support and role assignments --- .../.aspire/settings.json | 3 ++ .../Program.cs | 1 + .../AzureRedisCacheDaprHostingExtensions.cs | 17 ++++---- ...AppEnvironmentResourceBuilderExtensions.cs | 39 +++++++++++++++++-- .../AzureDaprComponentPublishingAnnotation.cs | 3 +- .../AzureDaprHostingExtensions.cs | 27 +++++++++++++ .../AzureKeyVaultDaprHostingExtensions.cs | 11 +++--- 7 files changed, 84 insertions(+), 17 deletions(-) create mode 100644 examples/dapr/CommunityToolkit.Aspire.Hosting.Azure.Dapr.AppHost/.aspire/settings.json 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..db948fce 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs @@ -5,9 +5,11 @@ using Azure.Provisioning.Expressions; using Azure.Provisioning.KeyVault; using Azure.Provisioning.Roles; +using Azure.ResourceManager.Authorization; using CommunityToolkit.Aspire.Hosting.Azure.Dapr; using CommunityToolkit.Aspire.Hosting.Dapr; using AzureRedisResource = Azure.Provisioning.Redis.RedisResource; +using RedisBuiltInRole = Azure.Provisioning.Redis.RedisBuiltInRole; 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 @@ -132,9 +130,11 @@ 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 @@ -162,7 +162,7 @@ private static void ConfigureForAccessKeyAuthentication(this IResourceBuilder + var configureInfrastructure = (AzureResourceInfrastructure infrastructure, UserAssignedIdentity daprIdentity) => { var redisHostParam = redisBuilder.GetOutput(daprConnectionStringKey).AsProvisioningParameter(infrastructure, redisHostKey); @@ -183,6 +183,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,39 @@ 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..486837a4 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); } + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + 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)) { From 8b0e34f62e6dcd09a4a71926018c944b0cd80c1e Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Thu, 2 Oct 2025 09:53:15 +1300 Subject: [PATCH 2/5] Update src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AzureDaprHostingExtensions.cs | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs index 486837a4..1efe1aae 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureDaprHostingExtensions.cs @@ -39,16 +39,16 @@ public static IResourceBuilder AddAzureDaprResource( } /// - /// + /// 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 From db4d4658f0c3c2733c1a7e953bf02d03f3262d7e Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Thu, 2 Oct 2025 09:53:53 +1300 Subject: [PATCH 3/5] Update src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureContainerAppEnvironmentResourceBuilderExtensions.cs Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- .../AzureContainerAppEnvironmentResourceBuilderExtensions.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureContainerAppEnvironmentResourceBuilderExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureContainerAppEnvironmentResourceBuilderExtensions.cs index a5acb9cd..68c05212 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureContainerAppEnvironmentResourceBuilderExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr/AzureContainerAppEnvironmentResourceBuilderExtensions.cs @@ -92,7 +92,6 @@ public static IResourceBuilder WithDaprCom daprComponentResource.TryGetLastAnnotation(out var publishingAnnotation); publishingAnnotation?.PublishingAction(infrastructure, daprIdentity); - } }); } From d6f0cb1db98e49a33ac5a3f4d5149cb81d5ac28d Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Sat, 4 Oct 2025 18:32:22 +1300 Subject: [PATCH 4/5] Refactor Azure Redis integration for Dapr components Updated namespaces and resource models to use `CdkRedisResource` and `RedisResource` instead of `AzureRedisResource` and `RedisBuiltInRole`. Introduced managed identity authentication logic with dynamic `RedisCacheAccessPolicyAssignment` creation for "Data Contributor" access. Commented out legacy role assignment code to align with the new approach. Refactored Redis resource handling to support the updated resource model. Enhanced metadata handling for `state.redis` components, ensuring proper configuration of `actorStateStore`. Maintained infrastructure output logic for Redis connection strings, now operating on the new resource type. --- .../AzureRedisCacheDaprHostingExtensions.cs | 30 +++++++++++++++---- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs index db948fce..811c9fa0 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs @@ -4,12 +4,12 @@ using Azure.Provisioning.AppContainers; using Azure.Provisioning.Expressions; using Azure.Provisioning.KeyVault; +using Azure.Provisioning.Redis; using Azure.Provisioning.Roles; -using Azure.ResourceManager.Authorization; using CommunityToolkit.Aspire.Hosting.Azure.Dapr; using CommunityToolkit.Aspire.Hosting.Dapr; -using AzureRedisResource = Azure.Provisioning.Redis.RedisResource; -using RedisBuiltInRole = Azure.Provisioning.Redis.RedisBuiltInRole; +using CdkRedisResource = Azure.Provisioning.Redis.RedisResource; +using RedisResource = Aspire.Hosting.ApplicationModel.RedisResource; namespace Aspire.Hosting; @@ -122,6 +122,25 @@ private static void ConfigureForManagedIdentityAuthentication(this IResourceBuil metadata.Add(new ContainerAppDaprMetadata { Name = "actorStateStore", Value = "true" }); } + if (redisBuilder.Resource.AddAsExistingResource(infrastructure) is CdkRedisResource redis) + { + var policyName = BicepFunction.CreateGuid(redis.Id, daprIdentity.PrincipalId, "Data Contributor"); + if (!infrastructure.GetProvisionableResources().OfType().Any(r => r.Name == policyName)) + { + var redisBicepIdentifier = redisBuilder.Resource.GetBicepIdentifier(); + + infrastructure.Add(new RedisCacheAccessPolicyAssignment($"{redisBicepIdentifier}_contributor") + { + Name = policyName, + Parent = redis, + AccessPolicyName = "Data Contributor", + ObjectId = daprIdentity.PrincipalId, + ObjectIdAlias = daprIdentity.Name + }); + } + + } + daprComponent.Metadata = [.. metadata]; // Add scopes if any exist @@ -134,13 +153,14 @@ private static void ConfigureForManagedIdentityAuthentication(this IResourceBuil } }; - builder.WithRoleAssignments(redisBuilder, RedisBuiltInRole.GetBuiltInRoleName, [RedisBuiltInRole.RedisCacheContributor]); + + //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) From 127786171d838a458ad558d1ba3cc22698253074 Mon Sep 17 00:00:00 2001 From: Brett Smith Date: Sat, 4 Oct 2025 19:54:29 +1300 Subject: [PATCH 5/5] Refactor Redis access policy assignment logic Updated the `ConfigureForManagedIdentityAuthentication` method to use `BicepIdentifier` for identifying existing `RedisCacheAccessPolicyAssignment` resources instead of the `Name` property. Introduced `policyBicepIdentifier` for consistent resource identification and dynamically generated the `Name` property using `BicepFunction.CreateGuid`. These changes enhance clarity and ensure consistency in resource management. --- .../AzureRedisCacheDaprHostingExtensions.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs index 811c9fa0..c2dfb1b8 100644 --- a/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs +++ b/src/CommunityToolkit.Aspire.Hosting.Azure.Dapr.Redis/AzureRedisCacheDaprHostingExtensions.cs @@ -124,14 +124,14 @@ private static void ConfigureForManagedIdentityAuthentication(this IResourceBuil if (redisBuilder.Resource.AddAsExistingResource(infrastructure) is CdkRedisResource redis) { - var policyName = BicepFunction.CreateGuid(redis.Id, daprIdentity.PrincipalId, "Data Contributor"); - if (!infrastructure.GetProvisionableResources().OfType().Any(r => r.Name == policyName)) + var redisBicepIdentifier = redisBuilder.Resource.GetBicepIdentifier(); + var policyBicepIdentifier = $"{redisBicepIdentifier}_contributor"; + if (!infrastructure.GetProvisionableResources().OfType().Any(r => r.BicepIdentifier == policyBicepIdentifier)) { - var redisBicepIdentifier = redisBuilder.Resource.GetBicepIdentifier(); infrastructure.Add(new RedisCacheAccessPolicyAssignment($"{redisBicepIdentifier}_contributor") { - Name = policyName, + Name = BicepFunction.CreateGuid(redis.Id, daprIdentity.PrincipalId, "Data Contributor"), Parent = redis, AccessPolicyName = "Data Contributor", ObjectId = daprIdentity.PrincipalId,