File: AzureNetworkSecurityPerimeterExtensions.cs
Web Access
Project: src\src\Aspire.Hosting.Azure.Network\Aspire.Hosting.Azure.Network.csproj (Aspire.Hosting.Azure.Network)
// 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;
 
/// <summary>
/// Provides extension methods for adding Azure Network Security Perimeter resources to the application model.
/// </summary>
public static class AzureNetworkSecurityPerimeterExtensions
{
    /// <summary>
    /// Adds an Azure Network Security Perimeter to the application model.
    /// </summary>
    /// <param name="builder">The builder for the distributed application.</param>
    /// <param name="name">The name of the Network Security Perimeter resource.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{AzureNetworkSecurityPerimeterResource}"/>.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <example>
    /// This example adds a Network Security Perimeter and associates a storage resource:
    /// <code>
    /// var nsp = builder.AddNetworkSecurityPerimeter("my-nsp");
    /// var storage = builder.AddAzureStorage("storage");
    /// storage.WithNetworkSecurityPerimeter(nsp);
    /// </code>
    /// </example>
    [AspireExport]
    public static IResourceBuilder<AzureNetworkSecurityPerimeterResource> 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);
    }
 
    /// <summary>
    /// Adds an access rule to the Network Security Perimeter.
    /// </summary>
    /// <param name="builder">The Network Security Perimeter resource builder.</param>
    /// <param name="rule">The access rule configuration.</param>
    /// <returns>A reference to the <see cref="IResourceBuilder{AzureNetworkSecurityPerimeterResource}"/> for chaining.</returns>
    /// <ats-returns>The resource builder.</ats-returns>
    /// <example>
    /// This example adds inbound and outbound access rules:
    /// <code>
    /// 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" }
    ///     });
    /// </code>
    /// </example>
    [AspireExport]
    public static IResourceBuilder<AzureNetworkSecurityPerimeterResource> WithAccessRule(
        this IResourceBuilder<AzureNetworkSecurityPerimeterResource> 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;
    }
 
    /// <summary>
    /// Associates an Azure PaaS resource with a Network Security Perimeter.
    /// </summary>
    /// <param name="target">The target PaaS resource builder to associate.</param>
    /// <param name="nsp">The Network Security Perimeter to associate with.</param>
    /// <param name="accessMode">
    /// The access mode for the association. Defaults to <see cref="NetworkSecurityPerimeterAssociationAccessMode.Enforced"/>.
    /// Use <see cref="NetworkSecurityPerimeterAssociationAccessMode.Learning"/> to log violations without blocking traffic.
    /// </param>
    /// <param name="associationName">
    /// An optional name for the association. If not provided, defaults to <c>"{resourceName}-assoc"</c>.
    /// </param>
    /// <returns>A reference to the target resource builder for chaining.</returns>
    /// <remarks>
    /// <para>
    /// In <see cref="NetworkSecurityPerimeterAssociationAccessMode.Enforced"/> mode, resources within the
    /// perimeter can communicate with each other, but public access is restricted to the rules defined
    /// in the perimeter profile.
    /// </para>
    /// <para>
    /// In <see cref="NetworkSecurityPerimeterAssociationAccessMode.Learning"/> 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.
    /// </para>
    /// </remarks>
    /// <example>
    /// This example associates storage and key vault resources with an NSP:
    /// <code>
    /// var nsp = builder.AddNetworkSecurityPerimeter("my-nsp");
    /// var storage = builder.AddAzureStorage("storage");
    /// var keyVault = builder.AddAzureKeyVault("kv");
    ///
    /// storage.WithNetworkSecurityPerimeter(nsp);
    /// keyVault.WithNetworkSecurityPerimeter(nsp, NetworkSecurityPerimeterAssociationAccessMode.Learning);
    /// </code>
    /// </example>
    [AspireExport("associateWithNetworkSecurityPerimeter", MethodName = "withNetworkSecurityPerimeter")]
    public static IResourceBuilder<T> WithNetworkSecurityPerimeter<T>(
        this IResourceBuilder<T> target,
        IResourceBuilder<AzureNetworkSecurityPerimeterResource> 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
        });
    }
}