diff --git a/Directory.Packages.props b/Directory.Packages.props index a3b5f9ab58b..f361a6fc491 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -49,7 +49,7 @@ - + @@ -180,7 +180,7 @@ - + diff --git a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs index da27cf443dd..15a086038e9 100644 --- a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs +++ b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIExtensions.cs @@ -75,6 +75,8 @@ public static IResourceBuilder AddAzureOpenAI(this IDistrib // We need to output name to externalize role assignments. infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = cogServicesAccount.Name.ToBicepExpression() }); + infrastructure.Add(new ProvisioningOutput("id", typeof(string)) { Value = cogServicesAccount.Id.ToBicepExpression() }); + var resource = (AzureOpenAIResource)infrastructure.AspireResource; CognitiveServicesAccountDeployment? dependency = null; diff --git a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIResource.cs b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIResource.cs index 733c6feda16..a129ae4d9ef 100644 --- a/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIResource.cs +++ b/src/Aspire.Hosting.Azure.CognitiveServices/AzureOpenAIResource.cs @@ -1,5 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using Aspire.Hosting.Azure; using Azure.Provisioning.CognitiveServices; using Azure.Provisioning.Primitives; @@ -13,7 +16,7 @@ namespace Aspire.Hosting.ApplicationModel; /// Configures the underlying Azure resource using Azure.Provisioning. public class AzureOpenAIResource(string name, Action configureInfrastructure) : AzureProvisioningResource(name, configureInfrastructure), - IResourceWithConnectionString + IResourceWithConnectionString, IAzureNspAssociationTarget { [Obsolete("Use AzureOpenAIDeploymentResource instead.")] private readonly List _deployments = []; @@ -34,6 +37,11 @@ public class AzureOpenAIResource(string name, Action public BicepOutputReference NameOutputReference => new("name", this); + /// + /// Gets the "id" output reference for the resource. + /// + public BicepOutputReference Id => new("id", this); + /// /// Gets the connection URI expression for the Azure OpenAI endpoint. /// diff --git a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs index 0ccf61bc58b..0968b8a88ec 100644 --- a/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs +++ b/src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs @@ -21,7 +21,8 @@ public class AzureCosmosDBResource(string name, Action Databases { get; } = []; diff --git a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs index 717f7818fbc..f04ad528171 100644 --- a/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs +++ b/src/Aspire.Hosting.Azure.EventHubs/AzureEventHubsResource.cs @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Azure; /// The name of the resource. /// Callback to configure the Azure Event Hubs resource. public class AzureEventHubsResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithAzureFunctionsConfig, IAzurePrivateEndpointTarget + : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithAzureFunctionsConfig, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget { private static readonly string[] s_eventHubClientNames = [ diff --git a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs index e1371df42f2..5b3024da6ce 100644 --- a/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs +++ b/src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Azure; /// The name of the resource. /// Callback to configure the Azure resources. public class AzureKeyVaultResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzureKeyVaultResource, IAzurePrivateEndpointTarget + : AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzureKeyVaultResource, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget { /// /// The secrets for this Key Vault. diff --git a/src/Aspire.Hosting.Azure.Network/AzureNetworkSecurityPerimeterExtensions.cs b/src/Aspire.Hosting.Azure.Network/AzureNetworkSecurityPerimeterExtensions.cs new file mode 100644 index 00000000000..93c33f58f22 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Network/AzureNetworkSecurityPerimeterExtensions.cs @@ -0,0 +1,257 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Azure; +using Azure.Core; +using Azure.Provisioning; +using Azure.Provisioning.Network; +using Azure.Provisioning.Resources; + +namespace Aspire.Hosting; + +/// +/// Provides extension methods for adding Azure Network Security Perimeter resources to the application model. +/// +public static class AzureNetworkSecurityPerimeterExtensions +{ + /// + /// Adds an Azure Network Security Perimeter to the application model. + /// + /// The builder for the distributed application. + /// The name of the Network Security Perimeter resource. + /// A reference to the . + /// + /// This example adds a Network Security Perimeter and associates a storage resource: + /// + /// var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + /// var storage = builder.AddAzureStorage("storage"); + /// storage.WithNetworkSecurityPerimeter(nsp); + /// + /// + [AspireExport(Description = "Adds an Azure Network Security Perimeter resource to the application model.")] + public static IResourceBuilder AddNetworkSecurityPerimeter( + this IDistributedApplicationBuilder builder, + [ResourceName] string name) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentException.ThrowIfNullOrEmpty(name); + + builder.AddAzureProvisioning(); + + var resource = new AzureNetworkSecurityPerimeterResource(name, ConfigureNetworkSecurityPerimeter); + + if (builder.ExecutionContext.IsRunMode) + { + return builder.CreateResourceBuilder(resource); + } + + return builder.AddResource(resource); + } + + /// + /// Adds an access rule to the Network Security Perimeter. + /// + /// The Network Security Perimeter resource builder. + /// The access rule configuration. + /// A reference to the for chaining. + /// + /// This example adds inbound and outbound access rules: + /// + /// var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + /// .WithAccessRule(new AzureNspAccessRule + /// { + /// Name = "allow-my-ip", + /// Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + /// AddressPrefixes = { "203.0.113.0/24" } + /// }) + /// .WithAccessRule(new AzureNspAccessRule + /// { + /// Name = "allow-outbound-fqdn", + /// Direction = NetworkSecurityPerimeterAccessRuleDirection.Outbound, + /// FullyQualifiedDomainNames = { "*.blob.core.windows.net" } + /// }); + /// + /// + [AspireExport(Description = "Adds an access rule to an Azure Network Security Perimeter resource.")] + public static IResourceBuilder WithAccessRule( + this IResourceBuilder builder, + AzureNspAccessRule rule) + { + ArgumentNullException.ThrowIfNull(builder); + ArgumentNullException.ThrowIfNull(rule); + ArgumentException.ThrowIfNullOrEmpty(rule.Name); + + if (builder.Resource.AccessRules.Any(existing => string.Equals(existing.Name, rule.Name, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + $"An access rule named '{rule.Name}' already exists in Network Security Perimeter '{builder.Resource.Name}'.", + nameof(rule)); + } + + builder.Resource.AccessRules.Add(rule); + return builder; + } + + /// + /// Associates an Azure PaaS resource with a Network Security Perimeter. + /// + /// The target PaaS resource builder to associate. + /// The Network Security Perimeter to associate with. + /// + /// The access mode for the association. Defaults to . + /// Use to log violations without blocking traffic. + /// + /// + /// An optional name for the association. If not provided, defaults to "{resourceName}-assoc". + /// + /// A reference to the target resource builder for chaining. + /// + /// + /// In mode, resources within the + /// perimeter can communicate with each other, but public access is restricted to the rules defined + /// in the perimeter profile. + /// + /// + /// In mode, traffic that would + /// be blocked by the perimeter rules is logged but not denied. This is useful when onboarding + /// resources to identify required access rules before switching to enforced mode. + /// + /// + /// + /// This example associates storage and key vault resources with an NSP: + /// + /// var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + /// var storage = builder.AddAzureStorage("storage"); + /// var keyVault = builder.AddAzureKeyVault("kv"); + /// + /// storage.WithNetworkSecurityPerimeter(nsp); + /// keyVault.WithNetworkSecurityPerimeter(nsp, NetworkSecurityPerimeterAssociationAccessMode.Learning); + /// + /// + [AspireExport("associateWithNetworkSecurityPerimeter", Description = "Associates an Azure PaaS resource with a Network Security Perimeter.")] + public static IResourceBuilder WithNetworkSecurityPerimeter( + this IResourceBuilder target, + IResourceBuilder nsp, + NetworkSecurityPerimeterAssociationAccessMode accessMode = NetworkSecurityPerimeterAssociationAccessMode.Enforced, + string? associationName = null) where T : IResource, IAzureNspAssociationTarget + { + ArgumentNullException.ThrowIfNull(target); + ArgumentNullException.ThrowIfNull(nsp); + + associationName ??= $"{target.Resource.Name}-assoc"; + + if (nsp.Resource.Associations.Any(a => string.Equals(a.Name, associationName, StringComparison.OrdinalIgnoreCase))) + { + throw new ArgumentException( + $"An association named '{associationName}' already exists in Network Security Perimeter '{nsp.Resource.Name}'.", + nameof(associationName)); + } + + nsp.Resource.Associations.Add(new AzureNetworkSecurityPerimeterResource.NspAssociationConfig( + associationName, + target.Resource.Id, + accessMode)); + + return target; + } + + private static void ConfigureNetworkSecurityPerimeter(AzureResourceInfrastructure infra) + { + var azureResource = (AzureNetworkSecurityPerimeterResource)infra.AspireResource; + + var nsp = AzureProvisioningResource.CreateExistingOrNewProvisionableResource(infra, + (identifier, name) => + { + var resource = NetworkSecurityPerimeter.FromExisting(identifier); + resource.Name = name; + return resource; + }, + (infrastructure) => + { + return new NetworkSecurityPerimeter(infrastructure.AspireResource.GetBicepIdentifier()) + { + Tags = { { "aspire-resource-name", infrastructure.AspireResource.Name } } + }; + }); + + // Create a default profile + var profileIdentifier = Infrastructure.NormalizeBicepIdentifier($"{nsp.BicepIdentifier}_profile"); + var profile = new NetworkSecurityPerimeterProfile(profileIdentifier) + { + Name = "defaultProfile", + Parent = nsp, + }; + infra.Add(profile); + + // Add access rules to the profile + foreach (var rule in azureResource.AccessRules) + { + var ruleIdentifier = Infrastructure.NormalizeBicepIdentifier($"{profileIdentifier}_{rule.Name}"); + var accessRule = new NetworkSecurityPerimeterAccessRule(ruleIdentifier) + { + Name = rule.Name, + Direction = rule.Direction, + Parent = profile, + }; + + foreach (var prefix in rule.AddressPrefixes) + { + accessRule.AddressPrefixes.Add(prefix); + } + + foreach (var prefixReference in rule.AddressPrefixReferences) + { + accessRule.AddressPrefixes.Add(prefixReference.AsProvisioningParameter(infra)); + } + + foreach (var sub in rule.Subscriptions) + { + accessRule.Subscriptions.Add(new WritableSubResource { Id = new ResourceIdentifier(sub) }); + } + + foreach (var subReference in rule.SubscriptionReferences) + { + accessRule.Subscriptions.Add(new WritableSubResource { Id = subReference.AsProvisioningParameter(infra) }); + } + + foreach (var fqdn in rule.FullyQualifiedDomainNames) + { + accessRule.FullyQualifiedDomainNames.Add(fqdn); + } + + foreach (var fqdnReference in rule.FullyQualifiedDomainNameReferences) + { + accessRule.FullyQualifiedDomainNames.Add(fqdnReference.AsProvisioningParameter(infra)); + } + + infra.Add(accessRule); + } + + // Add resource associations + foreach (var association in azureResource.Associations) + { + var assocIdentifier = Infrastructure.NormalizeBicepIdentifier($"{nsp.BicepIdentifier}_{association.Name}"); + var nspAssociation = new NetworkSecurityPerimeterAssociation(assocIdentifier) + { + Name = association.Name, + Parent = nsp, + AccessMode = association.AccessMode, + PrivateLinkResourceId = association.TargetResourceId.AsProvisioningParameter(infra), + ProfileId = profile.Id, + }; + + infra.Add(nspAssociation); + } + + infra.Add(new ProvisioningOutput("id", typeof(string)) + { + Value = nsp.Id + }); + + infra.Add(new ProvisioningOutput("name", typeof(string)) + { + Value = nsp.Name + }); + } +} diff --git a/src/Aspire.Hosting.Azure.Network/AzureNetworkSecurityPerimeterResource.cs b/src/Aspire.Hosting.Azure.Network/AzureNetworkSecurityPerimeterResource.cs new file mode 100644 index 00000000000..b88b749ac57 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Network/AzureNetworkSecurityPerimeterResource.cs @@ -0,0 +1,70 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Azure.Provisioning.Network; +using Azure.Provisioning.Primitives; + +namespace Aspire.Hosting.Azure; + +/// +/// Represents an Azure Network Security Perimeter resource. +/// +/// +/// +/// A Network Security Perimeter groups PaaS resources (such as Storage, Key Vault, Cosmos DB, and SQL) +/// into a logical security boundary. Resources within the perimeter can communicate with each other, +/// while public access is controlled by the perimeter's access mode and rules. +/// +/// +/// Use +/// to configure specific properties. +/// +/// +/// The name of the resource. +/// Callback to configure the Azure Network Security Perimeter resource. +public class AzureNetworkSecurityPerimeterResource(string name, Action configureInfrastructure) + : AzureProvisioningResource(name, configureInfrastructure) +{ + /// + /// Gets the "id" output reference from the Azure Network Security Perimeter resource. + /// + public BicepOutputReference Id => new("id", this); + + /// + /// Gets the "name" output reference for the resource. + /// + public BicepOutputReference NameOutputReference => new("name", this); + + internal List AccessRules { get; } = []; + + internal List Associations { get; } = []; + + /// + public override ProvisionableResource AddAsExistingResource(AzureResourceInfrastructure infra) + { + var bicepIdentifier = this.GetBicepIdentifier(); + var resources = infra.GetProvisionableResources(); + + var existing = resources.OfType().SingleOrDefault(r => r.BicepIdentifier == bicepIdentifier); + + if (existing is not null) + { + return existing; + } + + var nsp = NetworkSecurityPerimeter.FromExisting(bicepIdentifier); + + if (!TryApplyExistingResourceAnnotation(this, infra, nsp)) + { + nsp.Name = NameOutputReference.AsProvisioningParameter(infra); + } + + infra.Add(nsp); + return nsp; + } + + internal sealed record NspAssociationConfig( + string Name, + BicepOutputReference TargetResourceId, + NetworkSecurityPerimeterAssociationAccessMode AccessMode); +} diff --git a/src/Aspire.Hosting.Azure.Network/AzureNspAccessRule.cs b/src/Aspire.Hosting.Azure.Network/AzureNspAccessRule.cs new file mode 100644 index 00000000000..7f30187e2f4 --- /dev/null +++ b/src/Aspire.Hosting.Azure.Network/AzureNspAccessRule.cs @@ -0,0 +1,82 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Aspire.Hosting.ApplicationModel; +using Azure.Provisioning.Network; + +namespace Aspire.Hosting.Azure; + +/// +/// Represents an access rule configuration for an Azure Network Security Perimeter. +/// +/// +/// Access rules control how traffic flows into and out of the network security perimeter. +/// Inbound rules specify which external sources (IP ranges or subscriptions) can access +/// resources within the perimeter. Outbound rules specify which external destinations (FQDNs) +/// resources within the perimeter can communicate with. +/// +[AspireDto] +public sealed class AzureNspAccessRule +{ + /// + /// Gets or sets the name of the access rule. This name must be unique within the perimeter profile. + /// + public required string Name { get; set; } + + /// + /// Gets or sets the direction of the rule. + /// + public required NetworkSecurityPerimeterAccessRuleDirection Direction { get; set; } + + /// + /// Gets the list of inbound address prefixes (CIDR ranges) allowed by this rule. + /// + /// + /// Only applicable for rules. + /// + public List AddressPrefixes { get; } = []; + + /// + /// Gets the list of inbound address prefixes (CIDR ranges) values allowed by this rule. + /// + /// + /// Only applicable for rules. + /// Values are resolved at deploy time and combined with . + /// + public List AddressPrefixReferences { get; } = []; + + /// + /// Gets the list of subscription IDs allowed by this rule. + /// + /// + /// Only applicable for rules. + /// Subscription IDs should be in the format of a resource ID: /subscriptions/{subscriptionId}. + /// + public List Subscriptions { get; } = []; + + /// + /// Gets the subscription resource ID values allowed by this rule. + /// + /// + /// Only applicable for rules. + /// Values are resolved at deploy time and combined with . + /// + public List SubscriptionReferences { get; } = []; + + /// + /// Gets the list of fully qualified domain names (FQDNs) allowed by this rule. + /// + /// + /// Only applicable for rules. + /// + public List FullyQualifiedDomainNames { get; } = []; + + /// + /// Gets the fully qualified domain name values allowed by this rule. + /// + /// + /// Only applicable for rules. + /// Values are resolved at deploy time and combined with . + /// + public List FullyQualifiedDomainNameReferences { get; } = []; +} diff --git a/src/Aspire.Hosting.Azure.Network/README.md b/src/Aspire.Hosting.Azure.Network/README.md index 6dbe2aa538b..8051b420701 100644 --- a/src/Aspire.Hosting.Azure.Network/README.md +++ b/src/Aspire.Hosting.Azure.Network/README.md @@ -1,6 +1,6 @@ # Aspire.Hosting.Azure.Network library -Provides extension methods and resource definitions for an Aspire AppHost to configure Azure Virtual Networks, Subnets, NAT Gateways, Public IP Addresses, Network Security Groups, and Private Endpoints. +Provides extension methods and resource definitions for an Aspire AppHost to configure Azure Virtual Networks, Subnets, NAT Gateways, Public IP Addresses, Network Security Groups, Network Security Perimeters, and Private Endpoints. ## Getting started @@ -116,6 +116,33 @@ var subnet = vnet.AddSubnet("web-subnet", "10.0.1.0/24") A single NSG can be shared across multiple subnets. +### Adding Network Security Perimeters + +A Network Security Perimeter (NSP) groups PaaS resources into a logical security boundary. Resources within the perimeter can communicate with each other, while public access is restricted by access rules: + +```csharp +var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-my-ip", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + AddressPrefixes = { "203.0.113.0/24" } + }); + +var storage = builder.AddAzureStorage("storage"); +var keyVault = builder.AddAzureKeyVault("kv"); + +storage.WithNetworkSecurityPerimeter(nsp); +keyVault.WithNetworkSecurityPerimeter(nsp); +``` + +Associations use `Enforced` access mode by default, which blocks non-compliant public traffic. Use `Learning` mode to log violations without blocking, which is useful when onboarding resources to identify required access rules: + +```csharp +// Learning mode — logs violations without blocking traffic +storage.WithNetworkSecurityPerimeter(nsp, NetworkSecurityPerimeterAssociationAccessMode.Learning); +``` + ### Adding Private Endpoints Create a private endpoint to securely connect to Azure resources over a private network: @@ -155,6 +182,7 @@ storage.ConfigureInfrastructure(infra => * https://learn.microsoft.com/azure/virtual-network/ * https://learn.microsoft.com/azure/nat-gateway/ * https://learn.microsoft.com/azure/private-link/ +* https://learn.microsoft.com/azure/private-link/network-security-perimeter-concepts ## Feedback & contributing diff --git a/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceResource.cs b/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceResource.cs index a01986e278d..cbbf00df5cb 100644 --- a/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceResource.cs +++ b/src/Aspire.Hosting.Azure.OperationalInsights/AzureLogAnalyticsWorkspaceResource.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using Azure.Provisioning.OperationalInsights; using Azure.Provisioning.Primitives; @@ -12,13 +14,18 @@ namespace Aspire.Hosting.Azure; /// The resource name. /// Callback to configure the Azure Log Analytics Workspace resource. public class AzureLogAnalyticsWorkspaceResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure) + : AzureProvisioningResource(name, configureInfrastructure), IAzureNspAssociationTarget { /// /// Gets the "name" output reference for the resource. /// public BicepOutputReference NameOutputReference => new("name", this); + /// + /// The identifier associated with the Azure Log Analytics Workspace resource. + /// + public BicepOutputReference Id => new("logAnalyticsWorkspaceId", this); + /// /// Gets the "logAnalyticsWorkspaceId" output reference for the Azure Log Analytics Workspace resource. /// diff --git a/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs b/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs index 01c1d91615c..00b727e1556 100644 --- a/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs +++ b/src/Aspire.Hosting.Azure.Search/AzureSearchResource.cs @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Azure; /// The name of the resource /// Callback to configure the Azure AI Search resource. public class AzureSearchResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IAzurePrivateEndpointTarget + : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget { /// /// Gets the "connectionString" output reference from the Azure AI Search resource. diff --git a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs index b7549ecca2c..b7464da2e7a 100644 --- a/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs +++ b/src/Aspire.Hosting.Azure.ServiceBus/AzureServiceBusResource.cs @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Azure; /// The name of the resource. /// Callback to configure the Azure Service Bus resource. public class AzureServiceBusResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IResourceWithAzureFunctionsConfig, IResourceWithEndpoints, IAzurePrivateEndpointTarget + : AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IResourceWithAzureFunctionsConfig, IResourceWithEndpoints, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget { internal List Queues { get; } = []; internal List Topics { get; } = []; diff --git a/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs b/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs index 5c01b66d442..7148a893d7c 100644 --- a/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs +++ b/src/Aspire.Hosting.Azure.Sql/AzureSqlServerResource.cs @@ -27,7 +27,7 @@ namespace Aspire.Hosting.Azure; /// Represents an Azure Sql Server resource. /// [AspireExport(ExposeProperties = true)] -public class AzureSqlServerResource : AzureProvisioningResource, IResourceWithConnectionString, IAzurePrivateEndpointTarget, IAzurePrivateEndpointTargetNotification +public class AzureSqlServerResource : AzureProvisioningResource, IResourceWithConnectionString, IAzurePrivateEndpointTarget, IAzurePrivateEndpointTargetNotification, IAzureNspAssociationTarget { private const string AciSubnetDelegationServiceId = "Microsoft.ContainerInstance/containerGroups"; diff --git a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs index f8b7c3901c1..78f029ec0a5 100644 --- a/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs +++ b/src/Aspire.Hosting.Azure.Storage/AzureStorageResource.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using Aspire.Hosting.ApplicationModel; using Azure.Provisioning.Primitives; using Azure.Provisioning.Storage; @@ -13,7 +15,7 @@ namespace Aspire.Hosting.Azure; /// The name of the resource. /// Callback to configure the Azure resources. public class AzureStorageResource(string name, Action configureInfrastructure) - : AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithAzureFunctionsConfig + : AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithAzureFunctionsConfig, IAzureNspAssociationTarget { internal const string BlobsConnectionKeyPrefix = "Aspire__Azure__Storage__Blobs"; internal const string QueuesConnectionKeyPrefix = "Aspire__Azure__Storage__Queues"; diff --git a/src/Aspire.Hosting.Azure/IAzureNspAssociationTarget.cs b/src/Aspire.Hosting.Azure/IAzureNspAssociationTarget.cs new file mode 100644 index 00000000000..059501d5750 --- /dev/null +++ b/src/Aspire.Hosting.Azure/IAzureNspAssociationTarget.cs @@ -0,0 +1,24 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Diagnostics.CodeAnalysis; +using Aspire.Hosting.ApplicationModel; + +namespace Aspire.Hosting.Azure; + +/// +/// Represents an Azure resource that can be associated with a Network Security Perimeter. +/// +/// +/// Implement this interface on PaaS resources (such as Storage, Key Vault, Cosmos DB, SQL) +/// that support Network Security Perimeter association via the +/// Microsoft.Network/networkSecurityPerimeters/resourceAssociations resource type. +/// +[Experimental("ASPIREAZURE003", UrlFormat = "https://aka.ms/dotnet/aspire/diagnostics#{0}")] +public interface IAzureNspAssociationTarget : IResource +{ + /// + /// Gets the "id" output reference from the Azure resource. + /// + BicepOutputReference Id { get; } +} diff --git a/src/Aspire.Hosting.Foundry/FoundryExtensions.cs b/src/Aspire.Hosting.Foundry/FoundryExtensions.cs index 1ec8ecdca5a..5adbef6d418 100644 --- a/src/Aspire.Hosting.Foundry/FoundryExtensions.cs +++ b/src/Aspire.Hosting.Foundry/FoundryExtensions.cs @@ -452,6 +452,8 @@ private static void ConfigureInfrastructure(AzureResourceInfrastructure infrastr infrastructure.Add(new ProvisioningOutput("name", typeof(string)) { Value = cogServicesAccount.Name.ToBicepExpression() }); + infrastructure.Add(new ProvisioningOutput("id", typeof(string)) { Value = cogServicesAccount.Id.ToBicepExpression() }); + var resource = (FoundryResource)infrastructure.AspireResource; if (resource.CapabilityHost != null) diff --git a/src/Aspire.Hosting.Foundry/FoundryResource.cs b/src/Aspire.Hosting.Foundry/FoundryResource.cs index a3d85f5e5b6..8aa6ff82877 100644 --- a/src/Aspire.Hosting.Foundry/FoundryResource.cs +++ b/src/Aspire.Hosting.Foundry/FoundryResource.cs @@ -1,6 +1,8 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + using Aspire.Hosting.ApplicationModel; using Aspire.Hosting.Azure; using Azure.Provisioning.CognitiveServices; @@ -14,7 +16,7 @@ namespace Aspire.Hosting.Foundry; /// The name of the resource. /// Configures the underlying Azure resource using Azure.Provisioning. public class FoundryResource(string name, Action configureInfrastructure) : - AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString + AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzureNspAssociationTarget { internal Uri? EmulatorServiceUri { get; set; } @@ -35,6 +37,11 @@ public class FoundryResource(string name, Action co /// public BicepOutputReference NameOutputReference => new("name", this); + /// + /// Gets the "id" output reference for the resource. + /// + public BicepOutputReference Id => new("id", this); + /// /// Gets the connection URI expression for the Microsoft Foundry endpoint. /// diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureLogAnalyticsWorkspaceExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureLogAnalyticsWorkspaceExtensionsTests.cs index 618a618aed3..2d1a7943048 100644 --- a/tests/Aspire.Hosting.Azure.Tests/AzureLogAnalyticsWorkspaceExtensionsTests.cs +++ b/tests/Aspire.Hosting.Azure.Tests/AzureLogAnalyticsWorkspaceExtensionsTests.cs @@ -15,7 +15,7 @@ public async Task AddLogAnalyticsWorkspace() var logAnalyticsWorkspace = builder.AddAzureLogAnalyticsWorkspace("logAnalyticsWorkspace"); Assert.Equal("logAnalyticsWorkspace", logAnalyticsWorkspace.Resource.Name); - Assert.Equal("{logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId}", logAnalyticsWorkspace.Resource.WorkspaceId.ValueExpression); + Assert.Equal("{logAnalyticsWorkspace.outputs.logAnalyticsWorkspaceId}", logAnalyticsWorkspace.Resource.Id.ValueExpression); var appInsightsManifest = await AzureManifestUtils.GetManifestWithBicep(logAnalyticsWorkspace.Resource); var expectedManifest = """ diff --git a/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityPerimeterExtensionsTests.cs b/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityPerimeterExtensionsTests.cs new file mode 100644 index 00000000000..0ab6d6d987e --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/AzureNetworkSecurityPerimeterExtensionsTests.cs @@ -0,0 +1,205 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +#pragma warning disable ASPIREAZURE003 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. +#pragma warning disable AZPROVISION001 // Type is for evaluation purposes only and is subject to change or removal in future updates. Suppress this diagnostic to proceed. + +using Aspire.Hosting.ApplicationModel; +using Aspire.Hosting.Utils; +using Azure.Provisioning.Network; + +namespace Aspire.Hosting.Azure.Tests; + +public class AzureNetworkSecurityPerimeterExtensionsTests +{ + [Fact] + public void AddNetworkSecurityPerimeter_CreatesResource() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + + Assert.NotNull(nsp); + Assert.Equal("my-nsp", nsp.Resource.Name); + Assert.IsType(nsp.Resource); + } + + [Fact] + public void AddNetworkSecurityPerimeter_InRunMode_DoesNotAddToBuilder() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Run); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + + Assert.DoesNotContain(nsp.Resource, builder.Resources); + } + + [Fact] + public async Task AddNetworkSecurityPerimeter_GeneratesCorrectBicep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(nsp.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } + + [Fact] + public async Task AddNetworkSecurityPerimeter_WithAccessRules_GeneratesCorrectBicep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-my-ip", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + AddressPrefixes = { "203.0.113.0/24" } + }) + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-outbound-fqdn", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Outbound, + FullyQualifiedDomainNames = { "*.blob.core.windows.net" } + }); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(nsp.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } + + [Fact] + public async Task AddNetworkSecurityPerimeter_WithSubscriptionRule_GeneratesCorrectBicep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-subscription", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + Subscriptions = { "/subscriptions/00000000-0000-0000-0000-000000000001" } + }); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(nsp.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } + + [Fact] + public async Task AddNetworkSecurityPerimeter_WithParameterBasedAccessRules_GeneratesCorrectBicep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var inboundAddressPrefix = builder.AddParameter("inboundAddressPrefix"); + var allowedSubscription = builder.AddParameter("allowedSubscription"); + var outboundFqdn = builder.AddParameter("outboundFqdn"); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-my-ip", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + AddressPrefixes = { "203.0.113.0/24" }, + AddressPrefixReferences = { ReferenceExpression.Create($"{inboundAddressPrefix}") } + }) + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-subscription", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + Subscriptions = { "/subscriptions/00000000-0000-0000-0000-000000000001" }, + SubscriptionReferences = { ReferenceExpression.Create($"{allowedSubscription}") } + }) + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-outbound-fqdn", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Outbound, + FullyQualifiedDomainNames = { "*.blob.core.windows.net" }, + FullyQualifiedDomainNameReferences = { ReferenceExpression.Create($"{outboundFqdn}") } + }); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(nsp.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } + + [Fact] + public async Task AddNetworkSecurityPerimeter_WithStorageAssociation_GeneratesCorrectBicep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + var storage = builder.AddAzureStorage("storage"); + + storage.WithNetworkSecurityPerimeter(nsp); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(nsp.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } + + [Fact] + public async Task AddNetworkSecurityPerimeter_WithMultipleAssociations_GeneratesCorrectBicep() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-my-ip", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + AddressPrefixes = { "203.0.113.0/24" } + }); + + var storage = builder.AddAzureStorage("storage"); + var keyVault = builder.AddAzureKeyVault("kv"); + + storage.WithNetworkSecurityPerimeter(nsp); + keyVault.WithNetworkSecurityPerimeter(nsp, NetworkSecurityPerimeterAssociationAccessMode.Learning); + + var manifest = await AzureManifestUtils.GetManifestWithBicep(nsp.Resource); + + await Verify(manifest.BicepText, extension: "bicep"); + } + + [Fact] + public void WithAccessRule_DuplicateName_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp") + .WithAccessRule(new AzureNspAccessRule + { + Name = "allow-my-ip", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + AddressPrefixes = { "203.0.113.0/24" } + }); + + var exception = Assert.Throws(() => nsp.WithAccessRule(new AzureNspAccessRule + { + Name = "ALLOW-MY-IP", + Direction = NetworkSecurityPerimeterAccessRuleDirection.Inbound, + AddressPrefixes = { "10.0.0.0/8" } + })); + + Assert.Contains("allow-my-ip", exception.Message, StringComparison.OrdinalIgnoreCase); + } + + [Fact] + public void AssociateWith_DuplicateAssociationName_Throws() + { + using var builder = TestDistributedApplicationBuilder.Create(DistributedApplicationOperation.Publish); + + var nsp = builder.AddNetworkSecurityPerimeter("my-nsp"); + var storage = builder.AddAzureStorage("storage"); + + storage.WithNetworkSecurityPerimeter(nsp); + + var exception = Assert.Throws(() => storage.WithNetworkSecurityPerimeter(nsp)); + + Assert.Contains("storage-assoc", exception.Message, StringComparison.OrdinalIgnoreCase); + } + +} diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_GeneratesCorrectBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_GeneratesCorrectBicep.verified.bicep new file mode 100644 index 00000000000..2dea4f39146 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_GeneratesCorrectBicep.verified.bicep @@ -0,0 +1,19 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource my_nsp 'Microsoft.Network/networkSecurityPerimeters@2025-05-01' = { + name: take('mynsp${uniqueString(resourceGroup().id)}', 24) + location: location + tags: { + 'aspire-resource-name': 'my-nsp' + } +} + +resource my_nsp_profile 'Microsoft.Network/networkSecurityPerimeters/profiles@2025-05-01' = { + name: 'defaultProfile' + parent: my_nsp +} + +output id string = my_nsp.id + +output name string = my_nsp.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithAccessRules_GeneratesCorrectBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithAccessRules_GeneratesCorrectBicep.verified.bicep new file mode 100644 index 00000000000..e262cd9f677 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithAccessRules_GeneratesCorrectBicep.verified.bicep @@ -0,0 +1,41 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource my_nsp 'Microsoft.Network/networkSecurityPerimeters@2025-05-01' = { + name: take('mynsp${uniqueString(resourceGroup().id)}', 24) + location: location + tags: { + 'aspire-resource-name': 'my-nsp' + } +} + +resource my_nsp_profile 'Microsoft.Network/networkSecurityPerimeters/profiles@2025-05-01' = { + name: 'defaultProfile' + parent: my_nsp +} + +resource my_nsp_profile_allow_my_ip 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-my-ip' + properties: { + addressPrefixes: [ + '203.0.113.0/24' + ] + direction: 'Inbound' + } + parent: my_nsp_profile +} + +resource my_nsp_profile_allow_outbound_fqdn 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-outbound-fqdn' + properties: { + direction: 'Outbound' + fullyQualifiedDomainNames: [ + '*.blob.core.windows.net' + ] + } + parent: my_nsp_profile +} + +output id string = my_nsp.id + +output name string = my_nsp.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithMultipleAssociations_GeneratesCorrectBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithMultipleAssociations_GeneratesCorrectBicep.verified.bicep new file mode 100644 index 00000000000..52da9697591 --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithMultipleAssociations_GeneratesCorrectBicep.verified.bicep @@ -0,0 +1,62 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param storage_outputs_id string + +param kv_outputs_id string + +resource my_nsp 'Microsoft.Network/networkSecurityPerimeters@2025-05-01' = { + name: take('mynsp${uniqueString(resourceGroup().id)}', 24) + location: location + tags: { + 'aspire-resource-name': 'my-nsp' + } +} + +resource my_nsp_profile 'Microsoft.Network/networkSecurityPerimeters/profiles@2025-05-01' = { + name: 'defaultProfile' + parent: my_nsp +} + +resource my_nsp_profile_allow_my_ip 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-my-ip' + properties: { + addressPrefixes: [ + '203.0.113.0/24' + ] + direction: 'Inbound' + } + parent: my_nsp_profile +} + +resource my_nsp_storage_assoc 'Microsoft.Network/networkSecurityPerimeters/resourceAssociations@2025-05-01' = { + name: 'storage-assoc' + properties: { + accessMode: 'Enforced' + privateLinkResource: { + id: storage_outputs_id + } + profile: { + id: my_nsp_profile.id + } + } + parent: my_nsp +} + +resource my_nsp_kv_assoc 'Microsoft.Network/networkSecurityPerimeters/resourceAssociations@2025-05-01' = { + name: 'kv-assoc' + properties: { + accessMode: 'Learning' + privateLinkResource: { + id: kv_outputs_id + } + profile: { + id: my_nsp_profile.id + } + } + parent: my_nsp +} + +output id string = my_nsp.id + +output name string = my_nsp.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithParameterBasedAccessRules_GeneratesCorrectBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithParameterBasedAccessRules_GeneratesCorrectBicep.verified.bicep new file mode 100644 index 00000000000..c2ddd41d20f --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithParameterBasedAccessRules_GeneratesCorrectBicep.verified.bicep @@ -0,0 +1,65 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param inboundaddressprefix_value string + +param allowedsubscription_value string + +param outboundfqdn_value string + +resource my_nsp 'Microsoft.Network/networkSecurityPerimeters@2025-05-01' = { + name: take('mynsp${uniqueString(resourceGroup().id)}', 24) + location: location + tags: { + 'aspire-resource-name': 'my-nsp' + } +} + +resource my_nsp_profile 'Microsoft.Network/networkSecurityPerimeters/profiles@2025-05-01' = { + name: 'defaultProfile' + parent: my_nsp +} + +resource my_nsp_profile_allow_my_ip 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-my-ip' + properties: { + addressPrefixes: [ + '203.0.113.0/24' + inboundaddressprefix_value + ] + direction: 'Inbound' + } + parent: my_nsp_profile +} + +resource my_nsp_profile_allow_subscription 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-subscription' + properties: { + direction: 'Inbound' + subscriptions: [ + { + id: '/subscriptions/00000000-0000-0000-0000-000000000001' + } + { + id: allowedsubscription_value + } + ] + } + parent: my_nsp_profile +} + +resource my_nsp_profile_allow_outbound_fqdn 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-outbound-fqdn' + properties: { + direction: 'Outbound' + fullyQualifiedDomainNames: [ + '*.blob.core.windows.net' + outboundfqdn_value + ] + } + parent: my_nsp_profile +} + +output id string = my_nsp.id + +output name string = my_nsp.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithStorageAssociation_GeneratesCorrectBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithStorageAssociation_GeneratesCorrectBicep.verified.bicep new file mode 100644 index 00000000000..437382be81b --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithStorageAssociation_GeneratesCorrectBicep.verified.bicep @@ -0,0 +1,35 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +param storage_outputs_id string + +resource my_nsp 'Microsoft.Network/networkSecurityPerimeters@2025-05-01' = { + name: take('mynsp${uniqueString(resourceGroup().id)}', 24) + location: location + tags: { + 'aspire-resource-name': 'my-nsp' + } +} + +resource my_nsp_profile 'Microsoft.Network/networkSecurityPerimeters/profiles@2025-05-01' = { + name: 'defaultProfile' + parent: my_nsp +} + +resource my_nsp_storage_assoc 'Microsoft.Network/networkSecurityPerimeters/resourceAssociations@2025-05-01' = { + name: 'storage-assoc' + properties: { + accessMode: 'Enforced' + privateLinkResource: { + id: storage_outputs_id + } + profile: { + id: my_nsp_profile.id + } + } + parent: my_nsp +} + +output id string = my_nsp.id + +output name string = my_nsp.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithSubscriptionRule_GeneratesCorrectBicep.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithSubscriptionRule_GeneratesCorrectBicep.verified.bicep new file mode 100644 index 00000000000..09d646ba33d --- /dev/null +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureNetworkSecurityPerimeterExtensionsTests.AddNetworkSecurityPerimeter_WithSubscriptionRule_GeneratesCorrectBicep.verified.bicep @@ -0,0 +1,32 @@ +@description('The location for the resource(s) to be deployed.') +param location string = resourceGroup().location + +resource my_nsp 'Microsoft.Network/networkSecurityPerimeters@2025-05-01' = { + name: take('mynsp${uniqueString(resourceGroup().id)}', 24) + location: location + tags: { + 'aspire-resource-name': 'my-nsp' + } +} + +resource my_nsp_profile 'Microsoft.Network/networkSecurityPerimeters/profiles@2025-05-01' = { + name: 'defaultProfile' + parent: my_nsp +} + +resource my_nsp_profile_allow_subscription 'Microsoft.Network/networkSecurityPerimeters/profiles/accessRules@2025-05-01' = { + name: 'allow-subscription' + properties: { + direction: 'Inbound' + subscriptions: [ + { + id: '/subscriptions/00000000-0000-0000-0000-000000000001' + } + ] + } + parent: my_nsp_profile +} + +output id string = my_nsp.id + +output name string = my_nsp.name \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=False.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=False.verified.bicep index 708188314a4..dc2213a27f2 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=False.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=False.verified.bicep @@ -57,4 +57,6 @@ output connectionString string = 'Endpoint=${openai.properties.endpoint}' output endpoint string = openai.properties.endpoint -output name string = openai.name \ No newline at end of file +output name string = openai.name + +output id string = openai.id \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=True.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=True.verified.bicep index 708188314a4..dc2213a27f2 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=True.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=False_useObsoleteApis=True.verified.bicep @@ -57,4 +57,6 @@ output connectionString string = 'Endpoint=${openai.properties.endpoint}' output endpoint string = openai.properties.endpoint -output name string = openai.name \ No newline at end of file +output name string = openai.name + +output id string = openai.id \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=False.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=False.verified.bicep index 1fd922b65a0..0edd4fc7c02 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=False.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=False.verified.bicep @@ -57,4 +57,6 @@ output connectionString string = 'Endpoint=${openai.properties.endpoint}' output endpoint string = openai.properties.endpoint -output name string = openai.name \ No newline at end of file +output name string = openai.name + +output id string = openai.id \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=True.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=True.verified.bicep index 1fd922b65a0..0edd4fc7c02 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=True.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/AzureOpenAIExtensionsTests.AddAzureOpenAI_overrideLocalAuthDefault=True_useObsoleteApis=True.verified.bicep @@ -57,4 +57,6 @@ output connectionString string = 'Endpoint=${openai.properties.endpoint}' output endpoint string = openai.properties.endpoint -output name string = openai.name \ No newline at end of file +output name string = openai.name + +output id string = openai.id \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingAzureOpenAIWithResourceGroup.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingAzureOpenAIWithResourceGroup.verified.bicep index be02f6b588a..67b2abb9525 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingAzureOpenAIWithResourceGroup.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/ExistingAzureResourceTests.SupportsExistingAzureOpenAIWithResourceGroup.verified.bicep @@ -27,4 +27,6 @@ output connectionString string = 'Endpoint=${openAI.properties.endpoint}' output endpoint string = openAI.properties.endpoint -output name string = openAI.name \ No newline at end of file +output name string = openAI.name + +output id string = openAI.id \ No newline at end of file diff --git a/tests/Aspire.Hosting.Azure.Tests/Snapshots/FoundryExtensionsTests.AddFoundry_GeneratesValidBicep#00.verified.bicep b/tests/Aspire.Hosting.Azure.Tests/Snapshots/FoundryExtensionsTests.AddFoundry_GeneratesValidBicep#00.verified.bicep index 5eb1d61b16f..f059d2e9ea7 100644 --- a/tests/Aspire.Hosting.Azure.Tests/Snapshots/FoundryExtensionsTests.AddFoundry_GeneratesValidBicep#00.verified.bicep +++ b/tests/Aspire.Hosting.Azure.Tests/Snapshots/FoundryExtensionsTests.AddFoundry_GeneratesValidBicep#00.verified.bicep @@ -89,4 +89,6 @@ output aiFoundryApiEndpoint string = foundry.properties.endpoints['AI Foundry AP output endpoint string = foundry.properties.endpoint -output name string = foundry.name \ No newline at end of file +output name string = foundry.name + +output id string = foundry.id \ No newline at end of file