Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Directory.Packages.props
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@
<PackageVersion Include="Azure.Provisioning.EventHubs" Version="1.1.0" />
<PackageVersion Include="Azure.Provisioning.KeyVault" Version="1.1.0" />
<PackageVersion Include="Azure.Provisioning.Kusto" Version="1.0.0-beta.1" />
<PackageVersion Include="Azure.Provisioning.Network" Version="1.0.0" />
<PackageVersion Include="Azure.Provisioning.Network" Version="1.1.0-beta.2" />
<PackageVersion Include="Azure.Provisioning.OperationalInsights" Version="1.1.0" />
<PackageVersion Include="Azure.Provisioning.PostgreSql" Version="1.1.1" />
<PackageVersion Include="Azure.Provisioning.PrivateDns" Version="1.0.0" />
Expand Down Expand Up @@ -180,7 +180,7 @@
<!-- playground apps dependencies for javascript -->
<PackageVersion Include="Swashbuckle.AspNetCore" Version="9.0.6" />
<!-- Pinned versions for Component Governance - Remove when root dependencies are updated -->
<PackageVersion Include="Azure.Core" Version="1.51.1" />
<PackageVersion Include="Azure.Core" Version="1.52.0" />
<PackageVersion Include="Azure.Identity" Version="1.18.0" />
<!-- https://github.com/Azure/azure-cosmos-dotnet-v3/pull/3313 -->
<PackageVersion Include="Newtonsoft.Json" Version="13.0.4" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,8 @@ public static IResourceBuilder<AzureOpenAIResource> 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;
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -13,7 +16,7 @@ namespace Aspire.Hosting.ApplicationModel;
/// <param name="configureInfrastructure">Configures the underlying Azure resource using Azure.Provisioning.</param>
public class AzureOpenAIResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
: AzureProvisioningResource(name, configureInfrastructure),
IResourceWithConnectionString
IResourceWithConnectionString, IAzureNspAssociationTarget
{
[Obsolete("Use AzureOpenAIDeploymentResource instead.")]
private readonly List<AzureOpenAIDeployment> _deployments = [];
Expand All @@ -34,6 +37,11 @@ public class AzureOpenAIResource(string name, Action<AzureResourceInfrastructure
/// </summary>
public BicepOutputReference NameOutputReference => new("name", this);

/// <summary>
/// Gets the "id" output reference for the resource.
/// </summary>
public BicepOutputReference Id => new("id", this);

/// <summary>
/// Gets the connection URI expression for the Azure OpenAI endpoint.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/Aspire.Hosting.Azure.CosmosDB/AzureCosmosDBResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,8 @@ public class AzureCosmosDBResource(string name, Action<AzureResourceInfrastructu
IResourceWithConnectionString,
IResourceWithEndpoints,
IResourceWithAzureFunctionsConfig,
IAzurePrivateEndpointTarget
IAzurePrivateEndpointTarget,
IAzureNspAssociationTarget
{
internal List<AzureCosmosDBDatabaseResource> Databases { get; } = [];

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Azure;
/// <param name="name">The name of the resource.</param>
/// <param name="configureInfrastructure">Callback to configure the Azure Event Hubs resource.</param>
public class AzureEventHubsResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
: AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithAzureFunctionsConfig, IAzurePrivateEndpointTarget
: AzureProvisioningResource(name, configureInfrastructure), IResourceWithConnectionString, IResourceWithEndpoints, IResourceWithAzureFunctionsConfig, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget
{
private static readonly string[] s_eventHubClientNames =
[
Expand Down
2 changes: 1 addition & 1 deletion src/Aspire.Hosting.Azure.KeyVault/AzureKeyVaultResource.cs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ namespace Aspire.Hosting.Azure;
/// <param name="name">The name of the resource.</param>
/// <param name="configureInfrastructure">Callback to configure the Azure resources.</param>
public class AzureKeyVaultResource(string name, Action<AzureResourceInfrastructure> configureInfrastructure)
: AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzureKeyVaultResource, IAzurePrivateEndpointTarget
: AzureProvisioningResource(name, configureInfrastructure), IResourceWithEndpoints, IResourceWithConnectionString, IAzureKeyVaultResource, IAzurePrivateEndpointTarget, IAzureNspAssociationTarget
{
/// <summary>
/// The secrets for this Key Vault.
Expand Down
Original file line number Diff line number Diff line change
@@ -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;

/// <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>
/// <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.AssociateWith(nsp);
/// </code>
/// </example>
[AspireExport(Description = "Adds an Azure Network Security Perimeter resource to the application model.")]
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>
/// <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(Description = "Adds an access rule to an Azure Network Security Perimeter resource.")]
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.AssociateWith(nsp);
/// keyVault.AssociateWith(nsp, NetworkSecurityPerimeterAssociationAccessMode.Learning);
/// </code>
/// </example>
[AspireExport("associateWithNsp", Description = "Associates an Azure PaaS resource with a Network Security Perimeter.")]
public static IResourceBuilder<T> AssociateWith<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);
Comment on lines +178 to +185
Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: I'm not currently publicly modeling a "profile" child resource. I'm not sure it is necessary for most usages.

We can easily add it in the future, and keep the existing easy APIs - the existing APIs will still apply to the "default" profile.

Let me know if you think this is a mistake.


// 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
});
}
}
Loading
Loading