Skip to content
Draft
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

namespace VirtualClient.Actions
{
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using System.Threading.Tasks;
using Moq;
using NUnit.Framework;
using VirtualClient.Common;
using VirtualClient.Contracts;

[TestFixture]
[Category("Functional")]
public class GetAccessTokenProfileTests
{
private DependencyFixture dependencyFixture;

[OneTimeSetUp]
public void SetupFixture()
{
this.dependencyFixture = new DependencyFixture();
ComponentTypeCache.Instance.LoadComponentTypes(TestDependencies.TestDirectory);
}

[Test]
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Unix)]
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Win32NT)]
public void GetAccessTokenProfileParametersAreInlinedCorrectly(string profile, PlatformID platform)
{
this.dependencyFixture.Setup(platform);
using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.dependencyFixture.Dependencies))
{
WorkloadAssert.ParameterReferencesInlined(executor.Profile);
}
}

[Test]
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Unix)]
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Win32NT)]
public void GetAccessTokenProfileParametersAreAvailable(string profile, PlatformID platform)
{
this.dependencyFixture.Setup(platform);

var mandatoryParameters = new List<string> { "KeyVaultUri", "TenantId" };
using (ProfileExecutor executor = TestDependencies.CreateProfileExecutor(profile, this.dependencyFixture.Dependencies))
{
Assert.IsEmpty(executor.Profile.Actions);
Assert.AreEqual(1, executor.Profile.Dependencies.Count);

var dependencyBlock = executor.Profile.Dependencies.FirstOrDefault();

foreach (var parameters in mandatoryParameters)
{
Assert.IsTrue(dependencyBlock.Parameters.ContainsKey(parameters));
}
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,4 +53,4 @@ public DependencyKeyVaultStore(string storeName, Uri endpointUri, TokenCredentia
/// </summary>
public TokenCredential Credentials { get; }
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -188,7 +188,7 @@ public void EndpointUtilityThrowsWhenCreatingBlobStoreReferenceForCDNUriIfUriIsV
"https://anystorage.blob.core.windows.net/")]
//
[TestCase(
"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",
"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",
"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")]
//
[TestCase(
Expand Down Expand Up @@ -230,7 +230,7 @@ public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForConnectionStri

[Test]
[TestCase("https://any.service.azure.com?miid=307591a4-abb2-4559-af59-b47177d140cf", "https://any.service.azure.com")]
[TestCase("https://any.service.azure.com/?miid=307591a4-abb2-4559-af59-b47177d140cf","https://any.service.azure.com/")]
[TestCase("https://any.service.azure.com/?miid=307591a4-abb2-4559-af59-b47177d140cf", "https://any.service.azure.com/")]
public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForUrisReferencingManagedIdentities(string uri, string expectedUri)
{
DependencyBlobStore store = EndpointUtility.CreateBlobStoreReference(
Expand Down Expand Up @@ -338,7 +338,7 @@ public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForConnectionStri
Assert.IsNotNull(store.Credentials);
Assert.IsInstanceOf<ClientCertificateCredential>(store.Credentials);
}

[Test]
[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/")]
[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/")]
Expand Down Expand Up @@ -854,5 +854,37 @@ public void CreateKeyVaultStoreReference_ConnectionString_ThrowsOnInvalid()
"InvalidConnectionString",
this.mockFixture.CertificateManager.Object));
}

[Test]
[TestCase("https://anyvault.vault.azure.net/?cid=123456&tid=654321")]
[TestCase("https://anycontentstorage.blob.core.windows.net?cid=123456&tid=654321")]
[TestCase("https://anypackagestorage.blob.core.windows.net?tid=654321")]
[TestCase("https://anynamespace.servicebus.windows.net?cid=123456&tid=654321")]
[TestCase("https://my-keyvault.vault.azure.net/;tid=654321")]
public void TryParseMicrosoftEntraTenantIdReference_Uri_WorksAsExpected(string input)
{
// Arrange
Uri uri = new Uri(input);
bool result = EndpointUtility.TryParseMicrosoftEntraTenantIdReference(uri, out string actualTenantId);

// Assert
Assert.True(result);
Assert.AreEqual("654321", actualTenantId);
}

[Test]
[TestCase("https://anycontentstorage.blob.core.windows.net?cid=123456&tenantId=654321")]
[TestCase("https://anypackagestorage.blob.core.windows.net?miid=654321")]
[TestCase("https://my-keyvault.vault.azure.net/;cid=654321")]
public void TryParseMicrosoftEntraTenantIdReference_Uri_ReturnFalseWhenInvalid(string input)
{
// Arrange
Uri uri = new Uri(input);
bool result = EndpointUtility.TryParseMicrosoftEntraTenantIdReference(uri, out string actualTenantId);

// Assert
Assert.IsFalse(result);
Assert.IsNull(actualTenantId);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -115,20 +115,13 @@ public async Task KeyVaultManagerReturnsExpectedKey()
}

[Test]
[TestCase(true)]
[TestCase(false)]
public async Task KeyVaultManagerReturnsExpectedCertificate(bool retrieveWithPrivateKey)
[TestCase(PlatformID.Unix)]
[TestCase(PlatformID.Win32NT)]
public async Task KeyVaultManagerReturnsExpectedCertificate(PlatformID platform)
{
var result = await this.keyVaultManager.GetCertificateAsync("mycert", CancellationToken.None, "https://myvault.vault.azure.net/", retrieveWithPrivateKey);
var result = await this.keyVaultManager.GetCertificateAsync(platform, "mycert", CancellationToken.None, "https://myvault.vault.azure.net/");
Assert.IsNotNull(result);
if (retrieveWithPrivateKey)
{
Assert.IsTrue(result.HasPrivateKey);
}
else
{
Assert.IsFalse(result.HasPrivateKey);
}
Assert.IsTrue(result.HasPrivateKey);
}

[Test]
Expand Down
38 changes: 38 additions & 0 deletions src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
Original file line number Diff line number Diff line change
Expand Up @@ -398,6 +398,26 @@ public static bool TryParseCertificateReference(Uri uri, out string issuer, out
return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
}

/// <summary>
/// Tries to parse the Microsoft Entra reference information from the provided uri. If the uri does not contain the correctly formatted client ID
/// and tenant ID information the method will return false, and keep the two out parameters as null.
/// Ex. https://anystore.blob.core.windows.net?cid={clientId};tid={tenantId}
/// </summary>
/// <param name="uri">The uri to attempt to parse the values from.</param>
/// <param name="tenantId">The tenant ID from the Microsoft Entra reference.</param>
/// <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>
public static bool TryParseMicrosoftEntraTenantIdReference(Uri uri, out string tenantId)
{
string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");

IDictionary<string, string> queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
entry => entry.Key,
entry => entry.Value?.ToString(),
StringComparer.OrdinalIgnoreCase);

return TryGetMicrosoftEntraTenantId(queryParameters, out tenantId);
}

/// <summary>
/// Returns the endpoint by verifying package uri checks.
/// if the endpoint is a package uri without http or https protocols then append the protocol else return the endpoint value.
Expand Down Expand Up @@ -1292,5 +1312,23 @@ private static bool TryGetMicrosoftEntraReferenceForUri(IDictionary<string, stri

return parametersDefined;
}

private static bool TryGetMicrosoftEntraTenantId(IDictionary<string, string> uriParameters, out string tenantId)
{
bool parametersDefined = false;
tenantId = null;

if (uriParameters?.Any() == true)
{
if (uriParameters.TryGetValue(UriParameter.TenantId, out string microsoftEntraTenantId)
&& !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
{
tenantId = microsoftEntraTenantId;
parametersDefined = true;
}
}

return parametersDefined;
}
}
}
5 changes: 3 additions & 2 deletions src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

namespace VirtualClient
{
using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
Expand Down Expand Up @@ -60,10 +61,10 @@ Task<KeyVaultKey> GetKeyAsync(
/// <summary>
/// Retrieves a certificate from the Azure Key Vault.
/// </summary>
/// <param name="platform">The operating system platform (e.g. Windows, Linux).</param>
/// <param name="certName">The name of the certificate to be retrieved</param>
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
/// <param name="keyVaultUri">The URI of the Azure Key Vault.</param>
/// <param name="retrieveWithPrivateKey">flag to decode whether to retrieve certificate with private key</param>
/// <param name="retryPolicy">A policy to use for handling retries when transient errors/failures happen.</param>
/// <returns>
/// A <see cref="X509Certificate2"/> containing the certificate.
Expand All @@ -72,10 +73,10 @@ Task<KeyVaultKey> GetKeyAsync(
/// Thrown if the certificate is not found, access is denied, or another error occurs.
/// </exception>
Task<X509Certificate2> GetCertificateAsync(
PlatformID platform,
string certName,
CancellationToken cancellationToken,
string keyVaultUri = null,
bool retrieveWithPrivateKey = false,
IAsyncPolicy retryPolicy = null);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
namespace VirtualClient.Identity
{
using System;
using System.Threading;
using System.Threading.Tasks;
using Azure.Core;
using VirtualClient.Common.Extensions;

/// <summary>
/// A <see cref="TokenCredential"/> implementation that uses a pre-acquired
/// access token.
/// </summary>
public class AccessTokenCredential : TokenCredential
{
/// <summary>
/// Creates a new instance of the <see cref="AccessTokenCredential"/> class.
/// </summary>
/// <param name="token">
/// The credential provider that will be used to get access tokens.
/// </param>
public AccessTokenCredential(string token)
{
token.ThrowIfNull(nameof(token));
this.AccessToken = new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1));
}

/// <summary>
/// The access token to use for authentication.
/// </summary>
public AccessToken AccessToken { get; }

/// <summary>
/// Gets an access token using the underlying credentials.
/// </summary>
/// <param name="requestContext">Context information used when getting the access token.</param>
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
/// <returns>
/// An access token that can be used to authenticate with Azure resources.
/// </returns>
public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return this.AccessToken;
}

/// <summary>
/// Gets an access token using the underlying credentials.
/// </summary>
/// <param name="requestContext">Context information used when getting the access token.</param>
/// <param name="cancellationToken">A token that can be used to cancel the operation.</param>
/// <returns>
/// An access token that can be used to authenticate with Azure resources.
/// </returns>
public override ValueTask<AccessToken> GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
{
return new ValueTask<AccessToken>(this.AccessToken);
}
}
}
Loading
Loading