Skip to content

Commit 4b1108a

Browse files
[Feature] Enable get-token and bootstrap certificate from Key Vault (#613)
This pull request adds two new features to Virtual Client: a get-token subcommand to retrieve Azure access tokens for Key Vault authentication, and enhanced bootstrap command functionality to support certificate installation from Azure Key Vault. The implementation allows users to either use default Azure credential flows or explicitly provide an access token for Key Vault operations. Changes: Added get-token subcommand that acquires Azure access tokens using interactive browser authentication with device-code fallback Enhanced bootstrap subcommand to support certificate installation from Azure Key Vault in addition to package installation Introduced new components KeyVaultAccessToken and CertificateInstallation for token acquisition and certificate management --------- authored-by: Nirjan Chapagain <nchapagain@example.com>
1 parent e0610a6 commit 4b1108a

28 files changed

Lines changed: 2724 additions & 94 deletions

VERSION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
2.1.57
1+
2.1.58
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
namespace VirtualClient.Actions
5+
{
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Linq;
9+
using System.Threading;
10+
using System.Threading.Tasks;
11+
using Moq;
12+
using NUnit.Framework;
13+
using VirtualClient.Common;
14+
using VirtualClient.Contracts;
15+
16+
[TestFixture]
17+
[Category("Functional")]
18+
public class GetAccessTokenProfileTests
19+
{
20+
private DependencyFixture dependencyFixture;
21+
22+
[OneTimeSetUp]
23+
public void SetupFixture()
24+
{
25+
this.dependencyFixture = new DependencyFixture();
26+
ComponentTypeCache.Instance.LoadComponentTypes(TestDependencies.TestDirectory);
27+
}
28+
29+
[Test]
30+
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Unix)]
31+
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Win32NT)]
32+
public void GetAccessTokenProfileParametersAreInlinedCorrectly(string profile, PlatformID platform)
33+
{
34+
this.dependencyFixture.Setup(platform);
35+
using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.dependencyFixture.Dependencies))
36+
{
37+
WorkloadAssert.ParameterReferencesInlined(executor.Profile);
38+
}
39+
}
40+
41+
[Test]
42+
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Unix)]
43+
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Win32NT)]
44+
public void GetAccessTokenProfileParametersAreAvailable(string profile, PlatformID platform)
45+
{
46+
this.dependencyFixture.Setup(platform);
47+
48+
var mandatoryParameters = new List<string> { "KeyVaultUri", "TenantId" };
49+
using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.dependencyFixture.Dependencies))
50+
{
51+
Assert.IsEmpty(executor.Profile.Actions);
52+
Assert.AreEqual(1, executor.Profile.Dependencies.Count);
53+
54+
var dependencyBlock = executor.Profile.Dependencies.FirstOrDefault();
55+
56+
foreach (var parameters in mandatoryParameters)
57+
{
58+
Assert.IsTrue(dependencyBlock.Parameters.ContainsKey(parameters));
59+
}
60+
}
61+
}
62+
}
63+
}

src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,4 +53,4 @@ public DependencyKeyVaultStore(string storeName, Uri endpointUri, TokenCredentia
5353
/// </summary>
5454
public TokenCredential Credentials { get; }
5555
}
56-
}
56+
}

src/VirtualClient/VirtualClient.Core.UnitTests/EndpointUtilityTests.cs

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -188,7 +188,7 @@ public void EndpointUtilityThrowsWhenCreatingBlobStoreReferenceForCDNUriIfUriIsV
188188
"https://anystorage.blob.core.windows.net/")]
189189
//
190190
[TestCase(
191-
"https://anystorage.blob.core.windows.net?sv=2022-11-02&ss=b&srt=co&sp=rtf&se=2024-07-02T05:15:29Z&st=2024-07-01T21:15:29Z&spr=https",
191+
"https://anystorage.blob.core.windows.net?sv=2022-11-02&ss=b&srt=co&sp=rtf&se=2024-07-02T05:15:29Z&st=2024-07-01T21:15:29Z&spr=https",
192192
"https://anystorage.blob.core.windows.net/?sv=2022-11-02&ss=b&srt=co&sp=rtf&se=2024-07-02T05:15:29Z&st=2024-07-01T21:15:29Z&spr=https")]
193193
//
194194
[TestCase(
@@ -230,7 +230,7 @@ public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForConnectionStri
230230

231231
[Test]
232232
[TestCase("https://any.service.azure.com?miid=307591a4-abb2-4559-af59-b47177d140cf", "https://any.service.azure.com")]
233-
[TestCase("https://any.service.azure.com/?miid=307591a4-abb2-4559-af59-b47177d140cf","https://any.service.azure.com/")]
233+
[TestCase("https://any.service.azure.com/?miid=307591a4-abb2-4559-af59-b47177d140cf", "https://any.service.azure.com/")]
234234
public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForUrisReferencingManagedIdentities(string uri, string expectedUri)
235235
{
236236
DependencyBlobStore store = EndpointUtility.CreateBlobStoreReference(
@@ -338,7 +338,7 @@ public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForConnectionStri
338338
Assert.IsNotNull(store.Credentials);
339339
Assert.IsInstanceOf<ClientCertificateCredential>(store.Credentials);
340340
}
341-
341+
342342
[Test]
343343
[TestCase("https://any.service.azure.com/?cid=307591a4-abb2-4559-af59-b47177d140cf&tid=985bbc17-e3a5-4fec-b0cb-40dbb8bc5959&crti=ABC&crts=any.domain.com", "https://any.service.azure.com/")]
344344
[TestCase("https://any.service.azure.com/?cid=307591a4-abb2-4559-af59-b47177d140cf&tid=985bbc17-e3a5-4fec-b0cb-40dbb8bc5959&crti=ABC CA 01&crts=any.domain.com", "https://any.service.azure.com/")]
@@ -854,5 +854,37 @@ public void CreateKeyVaultStoreReference_ConnectionString_ThrowsOnInvalid()
854854
"InvalidConnectionString",
855855
this.mockFixture.CertificateManager.Object));
856856
}
857+
858+
[Test]
859+
[TestCase("https://anyvault.vault.azure.net/?cid=123456&tid=654321")]
860+
[TestCase("https://anycontentstorage.blob.core.windows.net?cid=123456&tid=654321")]
861+
[TestCase("https://anypackagestorage.blob.core.windows.net?tid=654321")]
862+
[TestCase("https://anynamespace.servicebus.windows.net?cid=123456&tid=654321")]
863+
[TestCase("https://my-keyvault.vault.azure.net/?;tid=654321")]
864+
public void TryParseMicrosoftEntraTenantIdReference_Uri_WorksAsExpected(string input)
865+
{
866+
// Arrange
867+
Uri uri = new Uri(input);
868+
bool result = EndpointUtility.TryParseMicrosoftEntraTenantIdReference(uri, out string actualTenantId);
869+
870+
// Assert
871+
Assert.True(result);
872+
Assert.AreEqual("654321", actualTenantId);
873+
}
874+
875+
[Test]
876+
[TestCase("https://anycontentstorage.blob.core.windows.net?cid=123456&tenantId=654321")]
877+
[TestCase("https://anypackagestorage.blob.core.windows.net?miid=654321")]
878+
[TestCase("https://my-keyvault.vault.azure.net/;cid=654321")]
879+
public void TryParseMicrosoftEntraTenantIdReference_Uri_ReturnFalseWhenInvalid(string input)
880+
{
881+
// Arrange
882+
Uri uri = new Uri(input);
883+
bool result = EndpointUtility.TryParseMicrosoftEntraTenantIdReference(uri, out string actualTenantId);
884+
885+
// Assert
886+
Assert.IsFalse(result);
887+
Assert.IsNull(actualTenantId);
888+
}
857889
}
858890
}

src/VirtualClient/VirtualClient.Core/EndpointUtility.cs

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -398,6 +398,26 @@ public static bool TryParseCertificateReference(Uri uri, out string issuer, out
398398
return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
399399
}
400400

401+
/// <summary>
402+
/// Tries to parse the Microsoft Entra reference information from the provided uri. If the uri does not contain the correctly formatted client ID
403+
/// and tenant ID information the method will return false, and keep the two out parameters as null.
404+
/// Ex. https://anystore.blob.core.windows.net?cid={clientId};tid={tenantId}
405+
/// </summary>
406+
/// <param name="uri">The uri to attempt to parse the values from.</param>
407+
/// <param name="tenantId">The tenant ID from the Microsoft Entra reference.</param>
408+
/// <returns>True/False if the method was able to successfully parse both the client ID and the tenant ID from the Microsoft Entra reference.</returns>
409+
public static bool TryParseMicrosoftEntraTenantIdReference(Uri uri, out string tenantId)
410+
{
411+
string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");
412+
413+
IDictionary<string, string> queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
414+
entry => entry.Key,
415+
entry => entry.Value?.ToString(),
416+
StringComparer.OrdinalIgnoreCase);
417+
418+
return TryGetMicrosoftEntraTenantId(queryParameters, out tenantId);
419+
}
420+
401421
/// <summary>
402422
/// Returns the endpoint by verifying package uri checks.
403423
/// if the endpoint is a package uri without http or https protocols then append the protocol else return the endpoint value.
@@ -1292,5 +1312,23 @@ private static bool TryGetMicrosoftEntraReferenceForUri(IDictionary<string, stri
12921312

12931313
return parametersDefined;
12941314
}
1315+
1316+
private static bool TryGetMicrosoftEntraTenantId(IDictionary<string, string> uriParameters, out string tenantId)
1317+
{
1318+
bool parametersDefined = false;
1319+
tenantId = null;
1320+
1321+
if (uriParameters?.Any() == true)
1322+
{
1323+
if (uriParameters.TryGetValue(UriParameter.TenantId, out string microsoftEntraTenantId)
1324+
&& !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
1325+
{
1326+
tenantId = microsoftEntraTenantId;
1327+
parametersDefined = true;
1328+
}
1329+
}
1330+
1331+
return parametersDefined;
1332+
}
12951333
}
12961334
}

src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
namespace VirtualClient
55
{
6+
using System;
67
using System.Security.Cryptography.X509Certificates;
78
using System.Threading;
89
using System.Threading.Tasks;
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
namespace VirtualClient.Identity
2+
{
3+
using System;
4+
using System.Threading;
5+
using System.Threading.Tasks;
6+
using Azure.Core;
7+
using VirtualClient.Common.Extensions;
8+
9+
/// <summary>
10+
/// A <see cref="TokenCredential"/> implementation that uses a pre-acquired
11+
/// access token.
12+
/// </summary>
13+
public class AccessTokenCredential : TokenCredential
14+
{
15+
/// <summary>
16+
/// Creates a new instance of the <see cref="AccessTokenCredential"/> class.
17+
/// </summary>
18+
/// <param name="token">
19+
/// The access token string to use for authentication.
20+
/// </param>
21+
public AccessTokenCredential(string token)
22+
{
23+
token.ThrowIfNull(nameof(token));
24+
this.AccessToken = new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1));
25+
}
26+
27+
/// <summary>
28+
/// The access token to use for authentication.
29+
/// </summary>
30+
public AccessToken AccessToken { get; }
31+
32+
/// <summary>
33+
/// Gets an access token using the underlying credentials.
34+
/// </summary>
35+
/// <param name="requestContext">Context information used when getting the access token.</param>
36+
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
37+
/// <returns>
38+
/// An access token that can be used to authenticate with Azure resources.
39+
/// </returns>
40+
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
41+
{
42+
return this.AccessToken;
43+
}
44+
45+
/// <summary>
46+
/// Gets an access token using the underlying credentials.
47+
/// </summary>
48+
/// <param name="requestContext">Context information used when getting the access token.</param>
49+
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
50+
/// <returns>
51+
/// An access token that can be used to authenticate with Azure resources.
52+
/// </returns>
53+
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
54+
{
55+
return new ValueTask<AccessToken>(this.AccessToken);
56+
}
57+
}
58+
}

src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ namespace VirtualClient
1515
using Azure.Security.KeyVault.Secrets;
1616
using Polly;
1717
using VirtualClient.Common.Extensions;
18-
using VirtualClient.Contracts;
1918

2019
/// <summary>
2120
/// Provides methods for retrieving secrets, keys, and certificates from an Azure Key Vault.

0 commit comments

Comments
 (0)