Skip to content

Commit 7187153

Browse files
[release/9.2] Fixed resolving secrets for keyvault references in run mode (#8755)
* Fixed resolving secrets for keyvault refernces in run mode - Moved token credential resolution out of the AzureProvisioner so it can shared. - Configure the secret resolving in both configure and provision cases. * Update src/Aspire.Hosting.Azure/Provisioning/Provisioners/TokenCredentialHolder.cs --------- Co-authored-by: David Fowler <[email protected]>
1 parent 8de8d4b commit 7187153

File tree

4 files changed

+80
-45
lines changed

4 files changed

+80
-45
lines changed

src/Aspire.Hosting.Azure/Provisioning/AzureProvisionerExtensions.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ public static IDistributedApplicationBuilder AddAzureProvisioning(this IDistribu
2929
.ValidateDataAnnotations()
3030
.ValidateOnStart();
3131

32+
builder.Services.AddSingleton<TokenCredentialHolder>();
33+
3234
builder.AddAzureProvisioner<AzureBicepResource, BicepProvisioner>();
3335

3436
return builder;

src/Aspire.Hosting.Azure/Provisioning/Provisioners/AzureProvisioner.cs

Lines changed: 4 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
using Aspire.Hosting.Lifecycle;
1414
using Azure;
1515
using Azure.Core;
16-
using Azure.Identity;
1716
using Azure.ResourceManager;
1817
using Azure.ResourceManager.Resources;
1918
using Microsoft.Extensions.Configuration;
@@ -35,7 +34,8 @@ internal sealed class AzureProvisioner(
3534
IServiceProvider serviceProvider,
3635
ResourceNotificationService notificationService,
3736
ResourceLoggerService loggerService,
38-
IDistributedApplicationEventing eventing
37+
IDistributedApplicationEventing eventing,
38+
TokenCredentialHolder tokenCredentialHolder
3939
) : IDistributedApplicationLifecycleHook
4040
{
4141
internal const string AspireResourceNameTag = "aspire-resource-name";
@@ -219,7 +219,7 @@ private async Task ProvisionAzureResources(
219219
var userSecretsLazy = new Lazy<Task<JsonObject>>(() => GetUserSecretsAsync(userSecretsPath, cancellationToken));
220220

221221
// Make resources wait on the same provisioning context
222-
var provisioningContextLazy = new Lazy<Task<ProvisioningContext>>(() => GetProvisioningContextAsync(userSecretsLazy, cancellationToken));
222+
var provisioningContextLazy = new Lazy<Task<ProvisioningContext>>(() => GetProvisioningContextAsync(tokenCredentialHolder.Credential, userSecretsLazy, cancellationToken));
223223

224224
var tasks = new List<Task>();
225225

@@ -366,39 +366,8 @@ async Task PublishConnectionStringAvailableEventAsync()
366366
}
367367
}
368368

369-
private async Task<ProvisioningContext> GetProvisioningContextAsync(Lazy<Task<JsonObject>> userSecretsLazy, CancellationToken cancellationToken)
369+
private async Task<ProvisioningContext> GetProvisioningContextAsync(TokenCredential credential, Lazy<Task<JsonObject>> userSecretsLazy, CancellationToken cancellationToken)
370370
{
371-
// Optionally configured in AppHost appSettings under "Azure" : { "CredentialSource": "AzureCli" }
372-
var credentialSetting = _options.CredentialSource;
373-
374-
TokenCredential credential = credentialSetting switch
375-
{
376-
"AzureCli" => new AzureCliCredential(),
377-
"AzurePowerShell" => new AzurePowerShellCredential(),
378-
"VisualStudio" => new VisualStudioCredential(),
379-
"VisualStudioCode" => new VisualStudioCodeCredential(),
380-
"AzureDeveloperCli" => new AzureDeveloperCliCredential(),
381-
"InteractiveBrowser" => new InteractiveBrowserCredential(),
382-
_ => new DefaultAzureCredential(new DefaultAzureCredentialOptions()
383-
{
384-
ExcludeManagedIdentityCredential = true,
385-
ExcludeWorkloadIdentityCredential = true,
386-
ExcludeAzurePowerShellCredential = true,
387-
CredentialProcessTimeout = TimeSpan.FromSeconds(15)
388-
})
389-
};
390-
391-
if (credential.GetType() == typeof(DefaultAzureCredential))
392-
{
393-
logger.LogInformation(
394-
"Using DefaultAzureCredential for provisioning. This may not work in all environments. " +
395-
"See https://aka.ms/azsdk/net/identity/credential-chains#defaultazurecredential-overview for more information.");
396-
}
397-
else
398-
{
399-
logger.LogInformation("Using {credentialType} for provisioning.", credential.GetType().Name);
400-
}
401-
402371
var subscriptionId = _options.SubscriptionId ?? throw new MissingConfigurationException("An Azure subscription id is required. Set the Azure:SubscriptionId configuration value.");
403372

404373
var armClient = new ArmClient(credential, subscriptionId);

src/Aspire.Hosting.Azure/Provisioning/Provisioners/BicepProvisioner.cs

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,8 @@ namespace Aspire.Hosting.Azure.Provisioning;
1919

2020
internal sealed class BicepProvisioner(
2121
ResourceNotificationService notificationService,
22-
ResourceLoggerService loggerService) : AzureResourceProvisioner<AzureBicepResource>
22+
ResourceLoggerService loggerService,
23+
TokenCredentialHolder tokenCredentialHolder) : AzureResourceProvisioner<AzureBicepResource>
2324
{
2425
public override bool ShouldProvision(IConfiguration configuration, AzureBicepResource resource)
2526
=> !resource.IsContainer();
@@ -67,6 +68,12 @@ public override async Task<bool> ConfigureResourceAsync(IConfiguration configura
6768
}
6869
}
6970

71+
if (resource is IAzureKeyVaultResource kvr)
72+
{
73+
ConfigureSecretResolver(kvr);
74+
}
75+
76+
// Populate secret outputs from key vault (if any)
7077
foreach (var item in section.GetSection("SecretOutputs").GetChildren())
7178
{
7279
resource.SecretOutputs[item.Key] = item.Value;
@@ -280,15 +287,7 @@ await notificationService.PublishUpdateAsync(resource, state =>
280287
// Populate secret outputs from key vault (if any)
281288
if (resource is IAzureKeyVaultResource kvr)
282289
{
283-
var vaultUri = resource.Outputs[kvr.VaultUriOutputReference.Name] as string ?? throw new InvalidOperationException($"{kvr.VaultUriOutputReference.Name} not found in outputs.");
284-
285-
// Set the client for resolving secrets at runtime
286-
var client = new SecretClient(new(vaultUri), context.Credential);
287-
kvr.SecretResolver = async (secretRef, ct) =>
288-
{
289-
var secret = await client.GetSecretAsync(secretRef.SecretName, cancellationToken: ct).ConfigureAwait(false);
290-
return secret.Value.Value;
291-
};
290+
ConfigureSecretResolver(kvr);
292291
}
293292

294293
await notificationService.PublishUpdateAsync(resource, state =>
@@ -308,6 +307,21 @@ await notificationService.PublishUpdateAsync(resource, state =>
308307
.ConfigureAwait(false);
309308
}
310309

310+
private void ConfigureSecretResolver(IAzureKeyVaultResource kvr)
311+
{
312+
var resource = (AzureBicepResource)kvr;
313+
314+
var vaultUri = resource.Outputs[kvr.VaultUriOutputReference.Name] as string ?? throw new InvalidOperationException($"{kvr.VaultUriOutputReference.Name} not found in outputs.");
315+
316+
// Set the client for resolving secrets at runtime
317+
var client = new SecretClient(new(vaultUri), tokenCredentialHolder.Credential);
318+
kvr.SecretResolver = async (secretRef, ct) =>
319+
{
320+
var secret = await client.GetSecretAsync(secretRef.SecretName, cancellationToken: ct).ConfigureAwait(false);
321+
return secret.Value.Value;
322+
};
323+
}
324+
311325
private static void PopulateWellKnownParameters(AzureBicepResource resource, ProvisioningContext context)
312326
{
313327
if (resource.Parameters.TryGetValue(AzureBicepResource.KnownParameters.PrincipalId, out var principalId) && principalId is null)
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using Azure.Core;
5+
using Azure.Identity;
6+
using Microsoft.Extensions.Logging;
7+
using Microsoft.Extensions.Options;
8+
9+
namespace Aspire.Hosting.Azure.Provisioning;
10+
11+
internal class TokenCredentialHolder
12+
{
13+
public TokenCredentialHolder(ILogger<AzureProvisioner> logger, IOptions<AzureProvisionerOptions> options)
14+
{
15+
// Optionally configured in AppHost appSettings under "Azure" : { "CredentialSource": "AzureCli" }
16+
var credentialSetting = options.Value.CredentialSource;
17+
18+
TokenCredential credential = credentialSetting switch
19+
{
20+
"AzureCli" => new AzureCliCredential(),
21+
"AzurePowerShell" => new AzurePowerShellCredential(),
22+
"VisualStudio" => new VisualStudioCredential(),
23+
"VisualStudioCode" => new VisualStudioCodeCredential(),
24+
"AzureDeveloperCli" => new AzureDeveloperCliCredential(),
25+
"InteractiveBrowser" => new InteractiveBrowserCredential(),
26+
_ => new DefaultAzureCredential(new DefaultAzureCredentialOptions()
27+
{
28+
ExcludeManagedIdentityCredential = true,
29+
ExcludeWorkloadIdentityCredential = true,
30+
ExcludeAzurePowerShellCredential = true,
31+
CredentialProcessTimeout = TimeSpan.FromSeconds(15)
32+
})
33+
};
34+
35+
if (credential.GetType() == typeof(DefaultAzureCredential))
36+
{
37+
logger.LogInformation(
38+
"Using DefaultAzureCredential for provisioning. This may not work in all environments. " +
39+
"See https://aka.ms/azsdk/net/identity/credential-chains#defaultazurecredential-overview for more information.");
40+
}
41+
else
42+
{
43+
logger.LogInformation("Using {credentialType} for provisioning.", credential.GetType().Name);
44+
}
45+
46+
Credential = credential;
47+
}
48+
49+
public TokenCredential Credential { get; }
50+
}

0 commit comments

Comments
 (0)