From acfbbbeb609b78797c04228b910e4e5961bc8839 Mon Sep 17 00:00:00 2001
From: nchapagain001 <165215502+nchapagain001@users.noreply.github.com>
Date: Wed, 17 Dec 2025 14:58:59 -0800
Subject: [PATCH 1/9] Adding token
---
.../KeyVaultAccessToken.cs | 182 ++++++++++++++++++
.../GetAccessTokenCommand.cs | 40 ++++
.../VirtualClient.Main/Program.cs | 22 +++
.../profiles/GET-ACCESS-TOKEN.json | 19 ++
4 files changed, 263 insertions(+)
create mode 100644 src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
create mode 100644 src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs
create mode 100644 src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json
diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
new file mode 100644
index 0000000000..51cc42cafd
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
@@ -0,0 +1,182 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Dependencies
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.IO.Abstractions;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Azure.Core;
+ using Azure.Identity;
+ using Microsoft.Extensions.DependencyInjection;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Virtual Client component that acquires an access token for an Azure Key Vault
+ /// using interactive browser or device-code authentication.
+ ///
+ public class KeyVaultAccessToken : VirtualClientComponent
+ {
+ private IFileSystem fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Provides all of the required dependencies to the Virtual Client component.
+ /// Parameters to the Virtual Client component.
+ public KeyVaultAccessToken(IServiceCollection dependencies, IDictionary parameters = null)
+ : base(dependencies, parameters)
+ {
+ this.fileSystem = dependencies.GetService();
+ this.fileSystem.ThrowIfNull(nameof(this.fileSystem));
+ }
+
+ ///
+ /// The Azure tenant ID used when requesting an access token for the Key Vault.
+ ///
+ protected string TenantId
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.TenantId));
+ }
+ }
+
+ ///
+ /// The Azure Key Vault URI for which an access token will be requested.
+ /// Example: https://anyvault.vault.azure.net/
+ ///
+ protected string KeyVaultUri
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.KeyVaultUri));
+ }
+ }
+
+ ///
+ /// The full file path where the acquired access token will be written,
+ /// when configured via / .
+ ///
+ protected string AccessTokenPath { get; set; }
+
+ ///
+ /// Initializes the component for execution, including resolving the access token
+ /// output path and removing any existing token file if configured.
+ ///
+ protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ if (!string.IsNullOrWhiteSpace(this.LogFileName))
+ {
+ string directory = !string.IsNullOrWhiteSpace(this.LogFolderName)
+ ? this.LogFolderName
+ : this.fileSystem.Directory.GetCurrentDirectory();
+
+ this.AccessTokenPath = this.fileSystem.Path.GetFullPath(
+ this.fileSystem.Path.Combine(directory, this.LogFileName));
+
+ if (this.fileSystem.File.Exists(this.AccessTokenPath))
+ {
+ await this.fileSystem.File.DeleteAsync(this.AccessTokenPath);
+ }
+ }
+ }
+
+ ///
+ /// Acquires an access token for the configured Key Vault URI using Azure Identity.
+ /// Attempts interactive browser authentication first and falls back to
+ /// device-code authentication when a browser is not available.
+ /// The access token can optionally be written to a file and is always
+ /// written to the console output.
+ ///
+ protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ this.KeyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
+ this.TenantId.ThrowIfNullOrWhiteSpace(nameof(this.TenantId));
+
+ string accessToken = null;
+ if (!cancellationToken.IsCancellationRequested)
+ {
+ string[] installerTenantResourceScopes = new string[]
+ {
+ new Uri(baseUri: new Uri(this.KeyVaultUri), relativeUri: ".default").ToString(),
+ // Example of a specific scope:
+ // "api://56e7ee83-1cf6-4048-a664-c2a08955f825/user_impersonation"
+ };
+
+ TokenRequestContext requestContext = new TokenRequestContext(scopes: installerTenantResourceScopes);
+
+ try
+ {
+ // Attempt an interactive (browser-based) authentication first. On most Windows environments
+ // this will work and is the most convenient for the user. On many Linux systems, there may
+ // not be a GUI and thus no browser. In that case, we fall back to the device code credential
+ // option in the catch block below.
+ InteractiveBrowserCredential credential = new InteractiveBrowserCredential(
+ new InteractiveBrowserCredentialOptions
+ {
+ TenantId = this.TenantId
+ });
+
+ AccessToken response = await credential.GetTokenAsync(requestContext, cancellationToken);
+ accessToken = response.Token;
+ }
+ catch (AuthenticationFailedException exc) when (exc.Message.Contains("Unable to open a web page"))
+ {
+ // Browser-based authentication is unavailable; switch to device code flow and present
+ // the user with a code and URL to complete authentication from another device.
+ DeviceCodeCredential credential = new DeviceCodeCredential(new DeviceCodeCredentialOptions
+ {
+ TenantId = this.TenantId,
+ DeviceCodeCallback = (codeInfo, token) =>
+ {
+ Console.WriteLine(string.Empty);
+ Console.WriteLine("Browser-based authentication unavailable (e.g. no GUI). Using device/code option.");
+ Console.WriteLine(string.Empty);
+ Console.WriteLine("********************** Azure Key Vault Authorization **********************");
+ Console.WriteLine(string.Empty);
+ Console.WriteLine(codeInfo.Message);
+ Console.WriteLine(string.Empty);
+ Console.WriteLine("***************************************************************************");
+ Console.WriteLine(string.Empty);
+
+ return Task.CompletedTask;
+ }
+ });
+
+ AccessToken token = await credential.GetTokenAsync(requestContext, cancellationToken);
+ accessToken = token.Token;
+ }
+
+ if (string.IsNullOrWhiteSpace(accessToken))
+ {
+ throw new AuthenticationFailedException("Authentication failed. No access token could be obtained.");
+ }
+
+ if (!string.IsNullOrEmpty(this.AccessTokenPath))
+ {
+ using (FileSystemStream fileStream = this.fileSystem.FileStream.New(
+ this.AccessTokenPath,
+ FileMode.Create,
+ FileAccess.ReadWrite,
+ FileShare.ReadWrite))
+ {
+ byte[] bytedata = Encoding.Default.GetBytes(accessToken);
+ fileStream.Write(bytedata, 0, bytedata.Length);
+ await fileStream.FlushAsync().ConfigureAwait(false);
+ this.Logger.LogTraceMessage($"Access token saved to file: {this.AccessTokenPath}");
+ }
+ }
+
+ Console.WriteLine("[Access Token]:");
+ Console.WriteLine(accessToken);
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs b/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs
new file mode 100644
index 0000000000..e984d0e7bd
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs
@@ -0,0 +1,40 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using VirtualClient.Contracts;
+
+ ///
+ /// Command that executes a profile to acquire an access token for an Azure Key Vault.
+ ///
+ internal class GetAccessTokenCommand : ExecuteProfileCommand
+ {
+ ///
+ /// Executes the access token acquisition operations using the configured profile.
+ ///
+ /// The arguments provided to the application on the command line.
+ /// Provides a token that can be used to cancel the command operations.
+ /// The exit code for the command operations.
+ public override Task ExecuteAsync(string[] args, CancellationTokenSource cancellationTokenSource)
+ {
+ this.Timeout = ProfileTiming.OneIteration();
+ this.Profiles = new List
+ {
+ new DependencyProfileReference("GET-ACCESS-TOKEN.json")
+ };
+
+ if (this.Parameters == null)
+ {
+ this.Parameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
+ }
+
+ return base.ExecuteAsync(args, cancellationTokenSource);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/Program.cs b/src/VirtualClient/VirtualClient.Main/Program.cs
index db50e303ed..1f458fe89e 100644
--- a/src/VirtualClient/VirtualClient.Main/Program.cs
+++ b/src/VirtualClient/VirtualClient.Main/Program.cs
@@ -325,6 +325,11 @@ internal static CommandLineBuilder SetupCommandLine(string[] args, CancellationT
apiSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
rootCommand.Add(apiSubcommand);
+ Command getAccessTokenSubcommand = Program.CreateGetTokenSubCommand(settings);
+ getAccessTokenSubcommand.TreatUnmatchedTokensAsErrors = true;
+ getAccessTokenSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
+ rootCommand.Add(getAccessTokenSubcommand);
+
Command bootstrapSubcommand = Program.CreateBootstrapSubcommand(settings);
bootstrapSubcommand.TreatUnmatchedTokensAsErrors = true;
bootstrapSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource));
@@ -406,6 +411,23 @@ private static Command CreateApiSubcommand(DefaultSettings settings)
return apiCommand;
}
+ private static Command CreateGetTokenSubCommand(DefaultSettings settings)
+ {
+ Command getAccessTokenCommand = new Command(
+ "get-token",
+ "Get access token for current user to authenticate with Azure Key Vault.")
+ {
+ // OPTIONAL
+ // -------------------------------------------------------------------
+ OptionFactory.CreateParametersOption(required: false),
+
+ // --key-vault
+ OptionFactory.CreateKeyVaultOption(required: false)
+ };
+
+ return getAccessTokenCommand;
+ }
+
private static Command CreateBootstrapSubcommand(DefaultSettings settings)
{
Command bootstrapCommand = new Command(
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json b/src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json
new file mode 100644
index 0000000000..3e6d6f2c4e
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/profiles/GET-ACCESS-TOKEN.json
@@ -0,0 +1,19 @@
+{
+ "Description": "Get access token for the user that can be used to authenticate.",
+ "Parameters": {
+ "KeyVaultUri": null,
+ "TenantId": null,
+ "LogFileName": "AccessToken.txt"
+ },
+ "Dependencies": [
+ {
+ "Type": "KeyVaultAccessToken",
+ "Parameters": {
+ "Scenario": "GetKVAccessToken",
+ "TenantId": "$.Parameters.TenantId",
+ "KeyVaultUri": "$.Parameters.KeyVaultUri",
+ "LogFileName": "$.Parameters.LogFileName"
+ }
+ }
+ ]
+}
\ No newline at end of file
From ffd7423cd6cbc4af989f9f3e4c26ec2a3709544d Mon Sep 17 00:00:00 2001
From: nchapagain001 <165215502+nchapagain001@users.noreply.github.com>
Date: Thu, 18 Dec 2025 12:44:48 -0800
Subject: [PATCH 2/9] Adding UT for AccessToken.
---
.../GetAccessTokenProfileTests.cs | 63 ++++
.../KeyVaultAccessTokenTests.cs | 322 ++++++++++++++++++
.../KeyVaultAccessToken.cs | 71 +++-
3 files changed, 441 insertions(+), 15 deletions(-)
create mode 100644 src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs
create mode 100644 src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs
diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs
new file mode 100644
index 0000000000..87818f841d
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs
@@ -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 async Task GetAccessTokenProfileParametersAreAvailable(string profile, PlatformID platform)
+ {
+ this.dependencyFixture.Setup(platform);
+
+ var mandatoryParameters = new List { "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));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs
new file mode 100644
index 0000000000..426f796446
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs
@@ -0,0 +1,322 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+namespace VirtualClient.Dependencies
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO;
+ using System.Text;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Azure.Core;
+ using Azure.Identity;
+ using Moq;
+ using NUnit.Framework;
+ using Polly;
+ using VirtualClient.Common.Telemetry;
+
+ [TestFixture]
+ [Category("Unit")]
+ public class KeyVaultAccessTokenTests
+ {
+ private MockFixture mockFixture;
+
+ [SetUp]
+ public void Setup()
+ {
+ this.mockFixture = new MockFixture();
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public async Task InitializeWillNotDoAnythingIfLogFileNameIsNotProvided(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ this.SetupWorkingDirectory(platform, out _);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ await component.InitializeAsyncInternal(EventContext.None, CancellationToken.None).ConfigureAwait(false);
+
+ Assert.IsNull(component.AccessTokenPathInternal);
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public async Task InitializeWillEnsureAccessTokenPathIsReadyIfLogFileNameIsProvided(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ this.SetupWorkingDirectory(platform, out string workingDir);
+
+ string expectedPath = this.Combine(workingDir, "AccessToken.txt");
+
+ // Setup: file does not exist initially
+ this.mockFixture.File.Setup(f => f.Exists(expectedPath)).Returns(false);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ component.Parameters["LogFileName"] = "AccessToken.txt";
+
+ await component.InitializeAsyncInternal(EventContext.None, CancellationToken.None).ConfigureAwait(false);
+
+ Assert.AreEqual(expectedPath, component.AccessTokenPathInternal);
+ Assert.IsFalse(this.mockFixture.File.Object.Exists(component.AccessTokenPathInternal), "File should not be created during Initialize.");
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public async Task InitializeWillEnsureOldFileIsDeletedIfPresent(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ this.SetupWorkingDirectory(platform, out string workingDir);
+
+ string tokenPath = this.Combine(workingDir, "AccessToken.txt");
+
+ // Setup: existing token file is present and should be deleted during Initialize.
+ this.mockFixture.File.Setup(f => f.Exists(tokenPath)).Returns(true);
+
+ bool deleteCalled = false;
+ this.mockFixture.FileSystem
+ .Setup(f => f.File.Delete(It.IsAny()))
+ .Callback(() => deleteCalled = true);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ component.Parameters["LogFileName"] = "AccessToken.txt";
+
+ await component.InitializeAsyncInternal(EventContext.None, CancellationToken.None).ConfigureAwait(false);
+
+ Assert.IsTrue(deleteCalled, "Existing token file should be deleted.");
+ this.mockFixture.File.Verify(f => f.Delete(It.IsAny()), Times.Once);
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public void ExecuteAsyncValidatesRequiredParameters(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, parameters))
+ {
+ Assert.ThrowsAsync(() => component.ExecuteAsync(CancellationToken.None));
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public async Task ExecuteAsyncWillWriteTokenToFileWhenLogFileNameIsProvided(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+ this.SetupWorkingDirectory(platform, out string workingDir);
+
+ string tokenContent = Guid.NewGuid().ToString();
+ string expectedPath = this.Combine(workingDir, "AccessToken.txt");
+
+ Mock mockFileStream = new Mock();
+ this.mockFixture.FileStream.Setup(f => f.New(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny()))
+ .Returns(mockFileStream.Object)
+ .Callback((string path, FileMode mode, FileAccess access, FileShare share) =>
+ {
+ Assert.AreEqual(expectedPath, path);
+ Assert.IsTrue(mode == FileMode.Create);
+ Assert.IsTrue(access == FileAccess.ReadWrite);
+ Assert.IsTrue(share == FileShare.ReadWrite);
+ });
+
+ mockFileStream
+ .Setup(x => x.Write(It.IsAny(), It.IsAny(), It.IsAny()))
+ .Callback((byte[] data, int offset, int count) =>
+ {
+ byte[] byteData = Encoding.Default.GetBytes(tokenContent);
+ Assert.AreEqual(offset, 0);
+ Assert.AreEqual(count, byteData.Length);
+ Assert.AreEqual(data, byteData);
+ });
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ component.Parameters["LogFileName"] = "AccessToken.txt";
+ component.InteractiveTokenToReturn = tokenContent;
+
+ await component.InitializeAsyncInternal(EventContext.None, CancellationToken.None).ConfigureAwait(false);
+ await component.ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public void GetTokenRequestContextWillReturnCorrectValue(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ TokenRequestContext ctx = component.GetTokenRequestContextInternal();
+
+ Assert.IsNotNull(ctx);
+ Assert.AreEqual(1, ctx.Scopes.Length);
+ Assert.AreEqual("https://myvault.vault.azure.net/.default", ctx.Scopes[0]);
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public async Task ExecuteAsyncWillUseInteractiveTokenFirst(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ component.InteractiveTokenToReturn = "interactive-ok";
+
+ await component.ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
+
+ Assert.AreEqual(1, component.InteractiveCalls);
+ Assert.AreEqual(0, component.DeviceCodeCalls);
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix)]
+ [TestCase(PlatformID.Win32NT)]
+ public async Task ExecuteAsyncWillUseDeviceLoginIfInteractiveFailsWithExactError(PlatformID platform)
+ {
+ this.mockFixture.Setup(platform);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ component.ThrowBrowserUnavailableAuthenticationFailedException = true;
+ component.DeviceCodeTokenToReturn = "device-code-ok";
+
+ await component.ExecuteAsync(CancellationToken.None).ConfigureAwait(false);
+
+ Assert.AreEqual(1, component.InteractiveCalls);
+ Assert.AreEqual(1, component.DeviceCodeCalls);
+ }
+ }
+
+ [Test]
+ [TestCase(PlatformID.Unix, null)]
+ [TestCase(PlatformID.Win32NT, null)]
+ [TestCase(PlatformID.Unix, "")]
+ [TestCase(PlatformID.Win32NT, "")]
+ [TestCase(PlatformID.Unix, " ")]
+ [TestCase(PlatformID.Win32NT, " ")]
+ [TestCase(PlatformID.Unix, "validToken")]
+ [TestCase(PlatformID.Win32NT, "validToken")]
+ public void ExecuteAsyncWillCheckIfValidTokenIsGenerated(PlatformID platform, string token)
+ {
+ this.mockFixture.Setup(platform);
+
+ using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
+ {
+ component.InteractiveTokenToReturn = token;
+
+ if (string.IsNullOrWhiteSpace(token))
+ {
+ Assert.ThrowsAsync(() => component.ExecuteAsync(CancellationToken.None));
+ }
+ else
+ {
+ Assert.DoesNotThrowAsync(() => component.ExecuteAsync(CancellationToken.None), string.Empty);
+ }
+ }
+ }
+
+ private void SetupWorkingDirectory(PlatformID platform, out string workingDir)
+ {
+ workingDir = platform == PlatformID.Win32NT ? @"C:\home\user" : "/home/user";
+
+ // KeyVaultAccessToken uses ISystemManagement.FileSystem internally, which in unit tests is MockFixture.FileSystem
+ this.mockFixture.Directory.Setup(d => d.GetCurrentDirectory()).Returns(workingDir);
+ }
+
+ private string Combine(string left, string right)
+ {
+ // Avoid relying on host OS behavior; use the path separator expected by the test platform.
+ char sep = this.mockFixture.Platform == PlatformID.Win32NT ? '\\' : '/';
+ return $"{left.TrimEnd(sep)}{sep}{right.TrimStart(sep)}";
+ }
+
+ private IDictionary CreateDefaultParameters()
+ {
+ return new Dictionary(StringComparer.OrdinalIgnoreCase)
+ {
+ { "TenantId", "00000000-0000-0000-0000-000000000000" },
+ { "KeyVaultUri", "https://myvault.vault.azure.net/" }
+ };
+ }
+
+ private sealed class TestKeyVaultAccessToken : KeyVaultAccessToken
+ {
+ public TestKeyVaultAccessToken(Microsoft.Extensions.DependencyInjection.IServiceCollection dependencies, IDictionary parameters)
+ : base(dependencies, parameters)
+ {
+ }
+
+ public string InteractiveTokenToReturn { get; set; } = "interactive-token";
+
+ public string DeviceCodeTokenToReturn { get; set; } = "device-token";
+
+ public bool ThrowBrowserUnavailableAuthenticationFailedException { get; set; }
+
+ public int InteractiveCalls { get; private set; }
+
+ public int DeviceCodeCalls { get; private set; }
+
+ public string AccessTokenPathInternal => this.AccessTokenPath;
+
+ public Task InitializeAsyncInternal(EventContext context, CancellationToken token)
+ {
+ return this.InitializeAsync(context, token);
+ }
+
+ public TokenRequestContext GetTokenRequestContextInternal()
+ {
+ return this.GetTokenRequestContext();
+ }
+
+ protected override async Task AcquireInteractiveTokenAsync(
+ TokenCredential credential,
+ TokenRequestContext requestContext,
+ CancellationToken cancellationToken)
+ {
+ this.InteractiveCalls++;
+
+ if (this.ThrowBrowserUnavailableAuthenticationFailedException)
+ {
+ throw new AuthenticationFailedException("Unable to open a web page");
+ }
+
+ await Task.Yield();
+ return this.InteractiveTokenToReturn;
+ }
+
+ protected override async Task AcquireDeviceCodeTokenAsync(
+ TokenCredential credential,
+ TokenRequestContext requestContext,
+ CancellationToken cancellationToken)
+ {
+ this.DeviceCodeCalls++;
+ await Task.Yield();
+ return this.DeviceCodeTokenToReturn;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
index 51cc42cafd..4f3e9ca116 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
@@ -10,6 +10,7 @@ namespace VirtualClient.Dependencies
using System.Text;
using System.Threading;
using System.Threading.Tasks;
+ using Azure;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.DependencyInjection;
@@ -78,8 +79,7 @@ protected override async Task InitializeAsync(EventContext telemetryContext, Can
? this.LogFolderName
: this.fileSystem.Directory.GetCurrentDirectory();
- this.AccessTokenPath = this.fileSystem.Path.GetFullPath(
- this.fileSystem.Path.Combine(directory, this.LogFileName));
+ this.AccessTokenPath = this.Combine(directory, this.LogFileName);
if (this.fileSystem.File.Exists(this.AccessTokenPath))
{
@@ -103,15 +103,7 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
string accessToken = null;
if (!cancellationToken.IsCancellationRequested)
{
- string[] installerTenantResourceScopes = new string[]
- {
- new Uri(baseUri: new Uri(this.KeyVaultUri), relativeUri: ".default").ToString(),
- // Example of a specific scope:
- // "api://56e7ee83-1cf6-4048-a664-c2a08955f825/user_impersonation"
- };
-
- TokenRequestContext requestContext = new TokenRequestContext(scopes: installerTenantResourceScopes);
-
+ TokenRequestContext requestContext = this.GetTokenRequestContext();
try
{
// Attempt an interactive (browser-based) authentication first. On most Windows environments
@@ -124,8 +116,8 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
TenantId = this.TenantId
});
- AccessToken response = await credential.GetTokenAsync(requestContext, cancellationToken);
- accessToken = response.Token;
+ accessToken = await this.AcquireInteractiveTokenAsync(credential, requestContext, cancellationToken);
+
}
catch (AuthenticationFailedException exc) when (exc.Message.Contains("Unable to open a web page"))
{
@@ -150,8 +142,7 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
}
});
- AccessToken token = await credential.GetTokenAsync(requestContext, cancellationToken);
- accessToken = token.Token;
+ accessToken = await this.AcquireDeviceCodeTokenAsync(credential, requestContext, cancellationToken);
}
if (string.IsNullOrWhiteSpace(accessToken))
@@ -170,6 +161,8 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
byte[] bytedata = Encoding.Default.GetBytes(accessToken);
fileStream.Write(bytedata, 0, bytedata.Length);
await fileStream.FlushAsync().ConfigureAwait(false);
+ fileStream.Close();
+ fileStream.Dispose();
this.Logger.LogTraceMessage($"Access token saved to file: {this.AccessTokenPath}");
}
}
@@ -178,5 +171,53 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
Console.WriteLine(accessToken);
}
}
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected virtual async Task AcquireInteractiveTokenAsync(
+ TokenCredential credential,
+ TokenRequestContext requestContext,
+ CancellationToken cancellationToken)
+ {
+ AccessToken response = await credential.GetTokenAsync(requestContext, cancellationToken);
+ return response.Token;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected virtual async Task AcquireDeviceCodeTokenAsync(
+ TokenCredential credential,
+ TokenRequestContext requestContext,
+ CancellationToken cancellationToken)
+ {
+ AccessToken response = await credential.GetTokenAsync(requestContext, cancellationToken);
+ return response.Token;
+ }
+
+ ///
+ ///
+ ///
+ ///
+ protected virtual TokenRequestContext GetTokenRequestContext()
+ {
+ string[] installerTenantResourceScopes = new string[]
+ {
+ new Uri(baseUri: new Uri(this.KeyVaultUri), relativeUri: ".default").ToString(),
+ // Example of a specific scope:
+ // "api://56e7ee83-1cf6-4048-a664-c2a08955f825/user_impersonation"
+ };
+
+ return new TokenRequestContext(scopes: installerTenantResourceScopes);
+ }
}
}
\ No newline at end of file
From cc08bf05f5e16c9c120e0ba1a5a6eeb9a04c142d Mon Sep 17 00:00:00 2001
From: nchapagain001 <165215502+nchapagain001@users.noreply.github.com>
Date: Thu, 18 Dec 2025 13:13:29 -0800
Subject: [PATCH 3/9] Clean up
---
.../KeyVaultAccessTokenTests.cs | 17 +++---
.../KeyVaultAccessToken.cs | 58 +++++++++----------
2 files changed, 36 insertions(+), 39 deletions(-)
diff --git a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs
index 426f796446..0e4a1ded68 100644
--- a/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies.UnitTests/KeyVaultAccessTokenTests.cs
@@ -13,7 +13,6 @@ namespace VirtualClient.Dependencies
using Azure.Identity;
using Moq;
using NUnit.Framework;
- using Polly;
using VirtualClient.Common.Telemetry;
[TestFixture]
@@ -132,9 +131,9 @@ public async Task ExecuteAsyncWillWriteTokenToFileWhenLogFileNameIsProvided(Plat
.Callback((string path, FileMode mode, FileAccess access, FileShare share) =>
{
Assert.AreEqual(expectedPath, path);
- Assert.IsTrue(mode == FileMode.Create);
- Assert.IsTrue(access == FileAccess.ReadWrite);
- Assert.IsTrue(share == FileShare.ReadWrite);
+ Assert.AreEqual(FileMode.Create, mode);
+ Assert.AreEqual(FileAccess.ReadWrite, access);
+ Assert.AreEqual(FileShare.ReadWrite, share);
});
mockFileStream
@@ -142,9 +141,9 @@ public async Task ExecuteAsyncWillWriteTokenToFileWhenLogFileNameIsProvided(Plat
.Callback((byte[] data, int offset, int count) =>
{
byte[] byteData = Encoding.Default.GetBytes(tokenContent);
- Assert.AreEqual(offset, 0);
- Assert.AreEqual(count, byteData.Length);
- Assert.AreEqual(data, byteData);
+ Assert.AreEqual(0, offset);
+ Assert.AreEqual(byteData.Length, count);
+ CollectionAssert.AreEqual(byteData, data);
});
using (TestKeyVaultAccessToken component = new TestKeyVaultAccessToken(this.mockFixture.Dependencies, this.CreateDefaultParameters()))
@@ -220,7 +219,7 @@ public async Task ExecuteAsyncWillUseDeviceLoginIfInteractiveFailsWithExactError
[TestCase(PlatformID.Win32NT, " ")]
[TestCase(PlatformID.Unix, "validToken")]
[TestCase(PlatformID.Win32NT, "validToken")]
- public void ExecuteAsyncWillCheckIfValidTokenIsGenerated(PlatformID platform, string token)
+ public void ExecuteAsyncThrowsErrorIfTokenIsNullOrWhitespace(PlatformID platform, string token)
{
this.mockFixture.Setup(platform);
@@ -234,7 +233,7 @@ public void ExecuteAsyncWillCheckIfValidTokenIsGenerated(PlatformID platform, st
}
else
{
- Assert.DoesNotThrowAsync(() => component.ExecuteAsync(CancellationToken.None), string.Empty);
+ Assert.DoesNotThrowAsync(() => component.ExecuteAsync(CancellationToken.None));
}
}
}
diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
index 4f3e9ca116..567ac45282 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
@@ -10,7 +10,6 @@ namespace VirtualClient.Dependencies
using System.Text;
using System.Threading;
using System.Threading.Tasks;
- using Azure;
using Azure.Core;
using Azure.Identity;
using Microsoft.Extensions.DependencyInjection;
@@ -19,8 +18,8 @@ namespace VirtualClient.Dependencies
using VirtualClient.Contracts;
///
- /// Virtual Client component that acquires an access token for an Azure Key Vault
- /// using interactive browser or device-code authentication.
+ /// Virtual Client component that acquires an Azure access token for the specified Key Vault
+ /// using interactive browser authentication with a device-code fallback.
///
public class KeyVaultAccessToken : VirtualClientComponent
{
@@ -39,7 +38,7 @@ public KeyVaultAccessToken(IServiceCollection dependencies, IDictionary
- /// The Azure tenant ID used when requesting an access token for the Key Vault.
+ /// Gets the Azure tenant ID used to acquire an access token.
///
protected string TenantId
{
@@ -50,7 +49,7 @@ protected string TenantId
}
///
- /// The Azure Key Vault URI for which an access token will be requested.
+ /// Gets the Azure Key Vault URI for which the access token will be requested.
/// Example: https://anyvault.vault.azure.net/
///
protected string KeyVaultUri
@@ -62,14 +61,15 @@ protected string KeyVaultUri
}
///
- /// The full file path where the acquired access token will be written,
- /// when configured via / .
+ /// Gets or sets the full file path where the acquired access token will be written when file logging is enabled.
+ /// This is resolved during when
+ /// is provided.
///
protected string AccessTokenPath { get; set; }
///
- /// Initializes the component for execution, including resolving the access token
- /// output path and removing any existing token file if configured.
+ /// Resolves the access token output file path
+ /// and removes any existing token file so the current run produces a fresh token output.
///
protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
@@ -90,10 +90,9 @@ protected override async Task InitializeAsync(EventContext telemetryContext, Can
///
/// Acquires an access token for the configured Key Vault URI using Azure Identity.
- /// Attempts interactive browser authentication first and falls back to
- /// device-code authentication when a browser is not available.
- /// The access token can optionally be written to a file and is always
- /// written to the console output.
+ /// The component attempts interactive browser authentication first and falls back to
+ /// device-code authentication when a browser is not available (e.g. headless Linux).
+ /// The token is always written to standard output. Token is also written to a file if AccessTokenPath is resolved.
///
protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
@@ -117,7 +116,6 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
});
accessToken = await this.AcquireInteractiveTokenAsync(credential, requestContext, cancellationToken);
-
}
catch (AuthenticationFailedException exc) when (exc.Message.Contains("Unable to open a web page"))
{
@@ -161,8 +159,6 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
byte[] bytedata = Encoding.Default.GetBytes(accessToken);
fileStream.Write(bytedata, 0, bytedata.Length);
await fileStream.FlushAsync().ConfigureAwait(false);
- fileStream.Close();
- fileStream.Dispose();
this.Logger.LogTraceMessage($"Access token saved to file: {this.AccessTokenPath}");
}
}
@@ -173,15 +169,15 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
}
///
- ///
+ /// Acquires an access token using interactive browser authentication.
///
- ///
- ///
- ///
- ///
+ /// The interactive browser credential to use.
+ /// The request context containing the required scopes.
+ /// A token that can be used to cancel the operation.
+ /// The access token string.
protected virtual async Task AcquireInteractiveTokenAsync(
- TokenCredential credential,
- TokenRequestContext requestContext,
+ TokenCredential credential,
+ TokenRequestContext requestContext,
CancellationToken cancellationToken)
{
AccessToken response = await credential.GetTokenAsync(requestContext, cancellationToken);
@@ -189,12 +185,13 @@ protected virtual async Task AcquireInteractiveTokenAsync(
}
///
- ///
+ /// Acquires an access token using device-code authentication.
+ /// This is used as a fallback when interactive browser authentication is unavailable.
///
- ///
- ///
- ///
- ///
+ /// The device code credential to use.
+ /// The request context containing the required scopes.
+ /// A token that can be used to cancel the operation.
+ /// The access token string.
protected virtual async Task AcquireDeviceCodeTokenAsync(
TokenCredential credential,
TokenRequestContext requestContext,
@@ -205,9 +202,10 @@ protected virtual async Task AcquireDeviceCodeTokenAsync(
}
///
- ///
+ /// Creates the used to request an access token for the target Key Vault resource.
+ /// Uses the Key Vault resource scope: "{KeyVaultUri}/.default".
///
- ///
+ /// The token request context containing the required scopes.
protected virtual TokenRequestContext GetTokenRequestContext()
{
string[] installerTenantResourceScopes = new string[]
From 63af6a1332b3dce230b4a2dc34c50bc3b305c068 Mon Sep 17 00:00:00 2001
From: nchapagain001 <165215502+nchapagain001@users.noreply.github.com>
Date: Fri, 19 Dec 2025 12:56:42 -0800
Subject: [PATCH 4/9] Idea for endpoint parsing.
---
.../Identity/EndPointSettingsExtensions.cs | 302 ++++++++++++++++++
.../Identity/EndpointSettings.cs | 49 +++
2 files changed, 351 insertions(+)
create mode 100644 src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs
create mode 100644 src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs
diff --git a/src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs b/src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs
new file mode 100644
index 0000000000..1dce8ad002
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs
@@ -0,0 +1,302 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Text.RegularExpressions;
+using VirtualClient.Contracts;
+
+namespace VirtualClient.Identity
+{
+ internal static class EndPointSettingsExtensions
+ {
+ internal const string AllowedPackageUri = "https://packages.virtualclient.microsoft.com";
+
+ ///
+ /// Returns true/false whether the value is a custom Virtual Client connection string
+ /// (e.g. EndpointUrl=https://any.blob.core.windows.net;ManagedIdentityId=307591a4-abb2-4559-af59-b47177d140cf).
+ ///
+ /// The value to evaluate.
+ /// True if the value is a custom Virtual Client connection string. False if not.
+ public static bool IsCustomConnectionString(string value)
+ {
+ bool isConnectionString = false;
+ StringComparison ignoreCase = StringComparison.OrdinalIgnoreCase;
+
+ if (value.Contains($"{ConnectionParameter.EndpointUrl}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.EventHubNamespace}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.ClientId}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.TenantId}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.ManagedIdentityId}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.CertificateIssuer}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.CertificateSubject}=", ignoreCase)
+ || value.Contains($"{ConnectionParameter.CertificateThumbprint}=", ignoreCase))
+ {
+ isConnectionString = true;
+ }
+
+ return isConnectionString;
+ }
+
+ ///
+ /// Returns true/false whether the value is a custom Virtual Client URI
+ /// (e.g. https://any.service.azure.com/?cid=307591a4-abb2-4559-af59-b47177d140cf&tid=985bbc17-E3a5-4fec-b0cb-40dbb8bc5959&crti=ABC&crts=any.service.com).
+ ///
+ /// The URI to evaluate.
+ /// True if the URI is a custom Virtual Client URI. False if not.
+ public static bool IsCustomUri(Uri endpointUri)
+ {
+ return Regex.IsMatch(
+ endpointUri.Query,
+ $"{UriParameter.CertificateIssuer}=|{UriParameter.CertificateSubject}=|{UriParameter.CertificateThumbprint}=|{UriParameter.ClientId}=|{UriParameter.ManagedIdentityId}=|{UriParameter.TenantId}=",
+ RegexOptions.IgnoreCase);
+ }
+
+ ///
+ /// Returns true/false whether the value is an Event Hub namespace access policy/connection string.
+ /// (e.g. Endpoint=sb://any.servicebus.windows.net/;SharedAccessKeyName=AnyAccessPolicy;SharedAccessKey=...).
+ ///
+ /// The value to evaluate.
+ /// True if the value is an Event Hub namespace connection string. False if not.
+ public static bool IsEventHubConnectionString(string value)
+ {
+ return Regex.IsMatch(
+ value,
+ $"{ConnectionParameter.Endpoint}=|{ConnectionParameter.SharedAccessKeyName}=",
+ RegexOptions.IgnoreCase);
+ }
+
+ ///
+ /// Returns true/false whether the value is a standard Storage Account connection string.
+ /// (e.g. DefaultEndpointsProtocol=https;AccountName=anystorage;EndpointSuffix=core.windows.net...).
+ ///
+ /// The value to evaluate.
+ /// True if the value is a Storage Account connection string. False if not.
+ public static bool IsStorageAccountConnectionString(string value)
+ {
+ return Regex.IsMatch(
+ value,
+ $"{ConnectionParameter.DefaultEndpointsProtocol}=|{ConnectionParameter.BlobEndpoint}=",
+ RegexOptions.IgnoreCase);
+ }
+
+ ///
+ /// Returns true/false whether the value is a standard Storage Account connection string.
+ /// (e.g. DefaultEndpointsProtocol=https;AccountName=anystorage;EndpointSuffix=core.windows.net...).
+ ///
+ /// The URI to evaluate.
+ /// True if the value is a Storage Account connection string. False if not.
+ public static bool IsStorageAccountSasUri(Uri endpointUri)
+ {
+ return Regex.IsMatch(endpointUri.Query, "sv=|se=|spr=|sig=", RegexOptions.IgnoreCase);
+ }
+
+ ///
+ /// Parses the subject name and issuer from the provided uri. If the uri does not contain the correctly formatted certificate subject name
+ /// and issuer information the method will return false, and keep the two out parameters as null.
+ /// Ex. https://vegaprod01proxyapi.azurewebsites.net?crti=issuerName&crts=certSubject
+ ///
+ /// The uri to attempt to parse the values from.
+ /// The issuer of the certificate.
+ /// The subject of the certificate.
+ /// True/False if the method was able to successfully parse both the subject name and the issuer of the certificate.
+ public static bool TryParseCertificateReference(Uri uri, out string issuer, out string subject)
+ {
+ string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");
+
+ IDictionary queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
+ entry => entry.Key,
+ entry => entry.Value?.ToString(),
+ StringComparer.OrdinalIgnoreCase);
+
+ return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
+ }
+
+ ///
+ /// 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.
+ ///
+ /// endpoint to verify and format
+ ///
+ public static string ValidateAndFormatPackageUri(string endpoint)
+ {
+ string packageUri = new Uri(AllowedPackageUri).Host;
+ return packageUri == endpoint ? $"https://{endpoint}" : endpoint;
+ }
+
+ internal static bool TryGetEndpointForConnection(IDictionary connectionParameters, out string endpoint)
+ {
+ bool endpointDefined = false;
+ endpoint = null;
+
+ if (connectionParameters?.Any() == true)
+ {
+ if ((connectionParameters.TryGetValue(ConnectionParameter.Endpoint, out endpoint) || connectionParameters.TryGetValue(ConnectionParameter.EndpointUrl, out endpoint))
+ && !string.IsNullOrWhiteSpace(endpoint))
+ {
+ endpointDefined = true;
+ }
+ }
+
+ return endpointDefined;
+ }
+
+ internal static bool TryGetCertificateReferenceForConnection(IDictionary uriParameters, out string certificateThumbPrint)
+ {
+ bool parametersDefined = false;
+ certificateThumbPrint = null;
+
+ if (uriParameters?.Any() == true)
+ {
+ if (uriParameters.TryGetValue(ConnectionParameter.CertificateThumbprint, out string thumbprint)
+ && !string.IsNullOrWhiteSpace(thumbprint))
+ {
+ certificateThumbPrint = thumbprint;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetCertificateReferenceForConnection(IDictionary connectionParameters, out string certificateIssuer, out string certificateSubject)
+ {
+ bool parametersDefined = false;
+ certificateIssuer = null;
+ certificateSubject = null;
+
+ if (connectionParameters?.Any() == true)
+ {
+ if (connectionParameters.TryGetValue(ConnectionParameter.CertificateIssuer, out string issuer)
+ && connectionParameters.TryGetValue(ConnectionParameter.CertificateSubject, out string subject)
+ && !string.IsNullOrWhiteSpace(issuer)
+ && !string.IsNullOrWhiteSpace(subject))
+ {
+ certificateIssuer = issuer;
+ certificateSubject = subject;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetManagedIdentityReferenceForConnection(IDictionary connectionParameters, out string managedIdentityId)
+ {
+ bool parametersDefined = false;
+ managedIdentityId = null;
+
+ if (connectionParameters?.Any() == true)
+ {
+ if (connectionParameters.TryGetValue(ConnectionParameter.ManagedIdentityId, out string managedIdentityClientId)
+ && !string.IsNullOrWhiteSpace(managedIdentityClientId))
+ {
+ managedIdentityId = managedIdentityClientId;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetMicrosoftEntraReferenceForConnection(IDictionary connectionParameters, out string clientId, out string tenantId)
+ {
+ bool parametersDefined = false;
+ clientId = null;
+ tenantId = null;
+
+ if (connectionParameters?.Any() == true)
+ {
+ if (connectionParameters.TryGetValue(ConnectionParameter.ClientId, out string microsoftEntraClientId)
+ && connectionParameters.TryGetValue(ConnectionParameter.TenantId, out string microsoftEntraTenantId)
+ && !string.IsNullOrWhiteSpace(microsoftEntraClientId)
+ && !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
+ {
+ clientId = microsoftEntraClientId;
+ tenantId = microsoftEntraTenantId;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetCertificateReferenceForUri(IDictionary uriParameters, out string certificateThumbPrint)
+ {
+ bool parametersDefined = false;
+ certificateThumbPrint = null;
+
+ if (uriParameters?.Any() == true)
+ {
+ if (uriParameters.TryGetValue(UriParameter.CertificateThumbprint, out string thumbprint)
+ && !string.IsNullOrWhiteSpace(thumbprint))
+ {
+ certificateThumbPrint = thumbprint;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetCertificateReferenceForUri(IDictionary uriParameters, out string certificateIssuer, out string certificateSubject)
+ {
+ bool parametersDefined = false;
+ certificateIssuer = null;
+ certificateSubject = null;
+
+ if (uriParameters?.Any() == true)
+ {
+ if (uriParameters.TryGetValue(UriParameter.CertificateIssuer, out string issuer)
+ && uriParameters.TryGetValue(UriParameter.CertificateSubject, out string subject)
+ && !string.IsNullOrWhiteSpace(issuer)
+ && !string.IsNullOrWhiteSpace(subject))
+ {
+ certificateIssuer = issuer;
+ certificateSubject = subject;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetManagedIdentityReferenceForUri(IDictionary uriParameters, out string managedIdentityId)
+ {
+ bool parametersDefined = false;
+ managedIdentityId = null;
+
+ if (uriParameters?.Any() == true)
+ {
+ if (uriParameters.TryGetValue(UriParameter.ManagedIdentityId, out string managedIdentityClientId)
+ && !string.IsNullOrWhiteSpace(managedIdentityClientId))
+ {
+ managedIdentityId = managedIdentityClientId;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
+ internal static bool TryGetMicrosoftEntraReferenceForUri(IDictionary uriParameters, out string clientId, out string tenantId)
+ {
+ bool parametersDefined = false;
+ clientId = null;
+ tenantId = null;
+
+ if (uriParameters?.Any() == true)
+ {
+ if (uriParameters.TryGetValue(UriParameter.ClientId, out string microsoftEntraClientId)
+ && uriParameters.TryGetValue(UriParameter.TenantId, out string microsoftEntraTenantId)
+ && !string.IsNullOrWhiteSpace(microsoftEntraClientId)
+ && !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
+ {
+ clientId = microsoftEntraClientId;
+ tenantId = microsoftEntraTenantId;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs b/src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs
new file mode 100644
index 0000000000..de4a555ab0
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs
@@ -0,0 +1,49 @@
+namespace VirtualClient.Identity
+{
+ using System;
+ using VirtualClient.Common.Extensions;
+
+ internal class EndpointSettings
+ {
+ private static readonly char[] TrimChars = { '\'', '"', ' ' };
+
+ public EndpointSettings(string endpoint)
+ {
+ endpoint.ThrowIfNullOrWhiteSpace(nameof(endpoint));
+
+ endpoint = EndPointSettingsExtensions.ValidateAndFormatPackageUri(endpoint);
+ string argumentValue = endpoint.Trim(EndpointSettings.TrimChars);
+
+ // this.IsBlobStoreConnectionString = EndPointSettingsExtensions.IsBlobStoreConnectionString(argumentValue);
+
+ // this.EndPoint = new Uri(endpoint);
+ this.IsCustomConnectionString = EndPointSettingsExtensions.IsCustomConnectionString(endpoint);
+
+ this.IsEventHubConnectionString = EndPointSettingsExtensions.IsEventHubConnectionString(endpoint);
+ // this.IsKeyVaultConnectionString = EndPointSettingsExtensions.IsKeyVaultConnectionString(endpoint);
+
+ }
+
+ public Uri EndPoint { get; set; }
+
+ public bool IsCustomConnectionString { get; set; }
+
+ public bool IsBlobStoreConnectionString { get; set; }
+
+ public bool IsEventHubConnectionString { get; set; }
+
+ public bool IsKeyVaultConnectionString { get; set; }
+
+ public string TenantId { get; set; }
+
+ public string ClientId { get; set; }
+
+ public string ManagedIdentityId { get; set; }
+
+ public string CertificateThumbprint { get; set; }
+
+ public string CertificateSubjectName { get; set; }
+
+ public string CertificateIssuerName { get; set; }
+ }
+}
From 910b9c9c35124f773b450fb7f6ad23eefe636e17 Mon Sep 17 00:00:00 2001
From: Nirjan Chapagain
Date: Sat, 10 Jan 2026 10:45:23 -0800
Subject: [PATCH 5/9] saving existing work
---
.../DependencyKeyVaultStore.cs | 15 +-
.../Identity/AccessTokenCredential.cs | 58 +++++
.../CertificateInstallation.cs | 240 ++++++++++++++++++
.../profiles/INSTALL-CERTIFICATES.json | 22 ++
4 files changed, 332 insertions(+), 3 deletions(-)
create mode 100644 src/VirtualClient/VirtualClient.Core/Identity/AccessTokenCredential.cs
create mode 100644 src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
create mode 100644 src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
diff --git a/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs b/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs
index 3f20d96445..ca97281b15 100644
--- a/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs
+++ b/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs
@@ -16,26 +16,30 @@ public class DependencyKeyVaultStore : DependencyStore
/// Initializes an instance of the class.
///
/// The name of the KeyVault store (e.g. KeyVault).
+ /// The Microsoft Entra tenant/directory ID.
/// The URI/SAS for the target Key Vault.
- public DependencyKeyVaultStore(string storeName, Uri endpointUri)
+ public DependencyKeyVaultStore(string storeName, string tenantId, Uri endpointUri)
: base(storeName, DependencyStore.StoreTypeAzureKeyVault)
{
endpointUri.ThrowIfNull(nameof(endpointUri));
this.EndpointUri = endpointUri;
this.KeyVaultNameSpace = endpointUri.Host;
+ this.TenantId = tenantId;
}
///
/// Initializes an instance of the class.
///
/// The name of the KeyVault store (e.g. KeyVault).
+ /// The Microsoft Entra tenant/directory ID.
/// The URI/SAS for the target Key Vault.
/// An identity token credential to use for authentication against the Key Vault.
- public DependencyKeyVaultStore(string storeName, Uri endpointUri, TokenCredential credentials)
- : this(storeName, endpointUri)
+ public DependencyKeyVaultStore(string storeName, string tenantId, Uri endpointUri, TokenCredential credentials)
+ : this(storeName, tenantId, endpointUri)
{
credentials.ThrowIfNull(nameof(credentials));
this.Credentials = credentials;
+ this.TenantId = tenantId;
}
///
@@ -48,6 +52,11 @@ public DependencyKeyVaultStore(string storeName, Uri endpointUri, TokenCredentia
///
public string KeyVaultNameSpace { get; }
+ ///
+ /// The Key Vault tenant ID.
+ ///
+ public string TenantId { get; }
+
///
/// An identity token credential to use for authentication against the Key vault.
///
diff --git a/src/VirtualClient/VirtualClient.Core/Identity/AccessTokenCredential.cs b/src/VirtualClient/VirtualClient.Core/Identity/AccessTokenCredential.cs
new file mode 100644
index 0000000000..8dbf2e87d6
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Core/Identity/AccessTokenCredential.cs
@@ -0,0 +1,58 @@
+namespace VirtualClient.Identity
+{
+ using System;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Azure.Core;
+ using VirtualClient.Common.Extensions;
+
+ ///
+ /// A implementation that uses a pre-acquired
+ /// access token.
+ ///
+ public class AccessTokenCredential : TokenCredential
+ {
+ ///
+ /// Creates a new instance of the class.
+ ///
+ ///
+ /// The credential provider that will be used to get access tokens.
+ ///
+ public AccessTokenCredential(string token)
+ {
+ token.ThrowIfNull(nameof(token));
+ this.AccessToken = new AccessToken(token, DateTimeOffset.UtcNow.AddHours(1));
+ }
+
+ ///
+ /// The access token to use for authentication.
+ ///
+ public AccessToken AccessToken { get; }
+
+ ///
+ /// Gets an access token using the underlying credentials.
+ ///
+ /// Context information used when getting the access token.
+ /// A token that can be used to cancel the operation.
+ ///
+ /// An access token that can be used to authenticate with Azure resources.
+ ///
+ public override AccessToken GetToken(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return this.AccessToken;
+ }
+
+ ///
+ /// Gets an access token using the underlying credentials.
+ ///
+ /// Context information used when getting the access token.
+ /// A token that can be used to cancel the operation.
+ ///
+ /// An access token that can be used to authenticate with Azure resources.
+ ///
+ public override ValueTask GetTokenAsync(TokenRequestContext requestContext, CancellationToken cancellationToken)
+ {
+ return new ValueTask(this.AccessToken);
+ }
+ }
+}
diff --git a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
new file mode 100644
index 0000000000..273ca845de
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
@@ -0,0 +1,240 @@
+namespace VirtualClient.Dependencies
+{
+ using System;
+ using System.Collections.Generic;
+ using System.IO.Abstractions;
+ using System.Security.Cryptography.X509Certificates;
+ using System.Threading;
+ using System.Threading.Tasks;
+ using Microsoft.CodeAnalysis;
+ using Microsoft.Extensions.DependencyInjection;
+ using VirtualClient.Common;
+ using VirtualClient.Common.Extensions;
+ using VirtualClient.Common.Telemetry;
+ using VirtualClient.Contracts;
+ using VirtualClient.Identity;
+
+ ///
+ /// Virtual Client component that acquires an Azure access token for the specified Key Vault
+ /// using interactive browser authentication with a device-code fallback.
+ ///
+ public class CertificateInstallation : VirtualClientComponent
+ {
+ private IFileSystem fileSystem;
+
+ ///
+ /// Initializes a new instance of the class.
+ ///
+ /// Provides all of the required dependencies to the Virtual Client component.
+ /// Parameters to the Virtual Client component.
+ public CertificateInstallation(IServiceCollection dependencies, IDictionary parameters = null)
+ : base(dependencies, parameters)
+ {
+ this.fileSystem = this.Dependencies.GetService();
+ this.fileSystem.ThrowIfNull(nameof(this.fileSystem));
+ }
+
+ ///
+ /// Gets the Azure tenant ID used to acquire an access token.
+ ///
+ protected string TenantId
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.TenantId));
+ }
+ }
+
+ ///
+ /// Gets the Azure Key Vault URI for which the access token will be requested.
+ /// Example: https://anyvault.vault.azure.net/
+ ///
+ protected string KeyVaultUri
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.KeyVaultUri));
+ }
+ }
+
+ ///
+ /// The name of the certificate to be retrieved
+ ///
+ protected string CertificateName
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.KeyVaultUri));
+ }
+ }
+
+ ///
+ /// Acquires an access token for the configured Key Vault URI using Azure Identity.
+ /// The component attempts interactive browser authentication first and falls back to
+ /// device-code authentication when a browser is not available (e.g. headless Linux).
+ /// The token is always written to standard output. Token is also written to a file if AccessTokenPath is resolved.
+ ///
+ protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ this.CertificateName.ThrowIfNullOrWhiteSpace(nameof(this.CertificateName));
+
+ try
+ {
+ IKeyVaultManager keyVault = await this.GetKeyVaultManager(cancellationToken);
+ X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.CertificateName, cancellationToken);
+
+ if (this.Platform == PlatformID.Win32NT)
+ {
+ await this.InstallCertificateOnWindowsAsync(certificate, cancellationToken);
+ }
+ else if (this.Platform == PlatformID.Unix)
+ {
+ await this.InstallCertificateOnUnixAsync(certificate, cancellationToken);
+ }
+ else
+ {
+ throw new PlatformNotSupportedException($"The '{nameof(CertificateInstallation)}' component is not supported on platform '{this.Platform}'.");
+ }
+ }
+ catch (Exception exc)
+ {
+ throw new DependencyException(
+ $"An error occurred installing the certificate '{this.CertificateName}' from Key Vault. See inner exception for details.",
+ exc);
+ }
+ }
+
+ ///
+ /// Installs the certificate in the appropriate certificate store on a Windows system.
+ ///
+ protected virtual Task InstallCertificateOnWindowsAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
+ {
+ return Task.Run(() =>
+ {
+ Console.WriteLine($"Certificate Store = CurrentUser/Personal");
+ using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ store.Close();
+ }
+ });
+ }
+
+ ///
+ /// Installs the certificate in the appropriate certificate store on a Unix/Linux system.
+ ///
+ protected virtual async Task InstallCertificateOnUnixAsync(X509Certificate2 certificate, CancellationToken cancellationToken)
+ {
+ // On Unix/Linux systems, we install the certificate in the default location for the
+ // user as well as in a static location. In the future we will likely use the static location
+ // only.
+ string certificateDirectory = null;
+
+ try
+ {
+ // When "sudo" is used to run the installer, we need to know the logged
+ // in user account. On Linux systems, there is an environment variable 'SUDO_USER'
+ // that defines the logged in user.
+
+ string user = this.GetEnvironmentVariable(EnvironmentVariable.USER);
+ string sudoUser = this.GetEnvironmentVariable(EnvironmentVariable.SUDO_USER);
+ certificateDirectory = $"/home/{user}/.dotnet/corefx/cryptography/x509stores/my";
+
+ if (!string.IsNullOrWhiteSpace(sudoUser))
+ {
+ // The installer is being executed with "sudo" privileges. We want to use the
+ // logged in user profile vs. "root".
+ certificateDirectory = $"/home/{sudoUser}/.dotnet/corefx/cryptography/x509stores/my";
+ }
+ else if (user == "root")
+ {
+ // The installer is being executed from the "root" account on Linux.
+ certificateDirectory = $"/root/.dotnet/corefx/cryptography/x509stores/my";
+ }
+
+ Console.WriteLine($"Certificate Store = {certificateDirectory}");
+
+ if (!this.fileSystem.Directory.Exists(certificateDirectory))
+ {
+ this.fileSystem.Directory.CreateDirectory(certificateDirectory);
+ }
+
+ using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite))
+ {
+ store.Open(OpenFlags.ReadWrite);
+ store.Add(certificate);
+ store.Close();
+ }
+
+ await this.fileSystem.File.WriteAllBytesAsync(
+ this.Combine(certificateDirectory, $"{certificate.Thumbprint}.pfx"),
+ certificate.Export(X509ContentType.Pfx));
+
+ // Permissions 777 (-rwxrwxrwx)
+ // https://linuxhandbook.com/linux-file-permissions/
+ //
+ // User = read, write, execute
+ // Group = read, write, execute
+ // Other = read, write, execute
+ using (IProcessProxy process = processManager.CreateProcess("chmod", $"-R 777 {certificateDirectory}"))
+ {
+ await process.StartAndWaitAsync(cancellationToken);
+ process.ThrowIfErrored();
+ }
+ }
+ catch (UnauthorizedAccessException)
+ {
+ throw new UnauthorizedAccessException(
+ $"Access permissions denied for certificate directory '{certificateDirectory}'. Execute the installer with " +
+ $"sudo/root privileges to install SDK certificates in privileged locations.");
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected async Task GetKeyVaultManager(CancellationToken cancellationToken)
+ {
+ IKeyVaultManager keyVaultManager = this.Dependencies.GetService();
+ keyVaultManager.ThrowIfNull(nameof(keyVaultManager));
+
+ // need better if condition.
+ if (keyVaultManager.StoreDescription != null)
+ {
+ return keyVaultManager;
+ }
+ else if (!string.IsNullOrWhiteSpace(this.LogFileName))
+ {
+ string directory = !string.IsNullOrWhiteSpace(this.LogFolderName)
+ ? this.LogFolderName
+ : this.fileSystem.Directory.GetCurrentDirectory();
+
+ string accessTokenPath = this.Combine(directory, this.LogFileName);
+
+ string keyVaultUri = string.IsNullOrWhiteSpace(this.KeyVaultUri)
+ ? ((DependencyKeyVaultStore)keyVaultManager.StoreDescription).EndpointUri.ToString()
+ : this.KeyVaultUri;
+
+ string tenantId = string.IsNullOrWhiteSpace(this.TenantId)
+ ? ((DependencyKeyVaultStore)keyVaultManager.StoreDescription).TenantId
+ : this.TenantId;
+
+ keyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
+ tenantId.ThrowIfNullOrWhiteSpace(nameof(this.TenantId));
+
+ string token_ = await this.fileSystem.File.ReadAllTextAsync(accessTokenPath, cancellationToken).ConfigureAwait(false);
+ AccessTokenCredential tokenCredential = new AccessTokenCredential(token_);
+
+ DependencyKeyVaultStore dependencyKeyVault = new DependencyKeyVaultStore(DependencyStore.KeyVault, this.TenantId, new Uri(this.KeyVaultUri), tokenCredential);
+ return new KeyVaultManager(dependencyKeyVault);
+ }
+ else
+ {
+ throw new InvalidOperationException($"The Key Vault manager has not been properly initialized. The '{nameof(this.LogFileName)}' parameter must be provided to read the access token from file.");
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
new file mode 100644
index 0000000000..e3f4fc4424
--- /dev/null
+++ b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
@@ -0,0 +1,22 @@
+{
+ "Description": "Installs certificate from a Azure Key Vault.",
+ "Parameters": {
+ "AccessToken": "{access token}",
+ "KeyVaultUri": "https://yourkeyvault.vault.azure.net/",
+ "CertificateName": "cert-01-name",
+ "CertificatePassword": "",
+ "TenantId": ""
+ },
+ "Dependencies": [
+ {
+ "Type": "CertificateInstallation",
+ "Parameters": {
+ "Scenario": "InstallCertificate",
+ "AccessToken": "$.Parameters.AccessToken",
+ "TenantId": "$.Parameters.TenantId",
+ "KeyVaultUri": "$.Parameters.KeyVaultUri",
+ "CertificateName": "$.Parameters.CertificateName"
+ }
+ }
+ ]
+}
\ No newline at end of file
From f113108e7e9dbe2cd74925ee87b9603df1842880 Mon Sep 17 00:00:00 2001
From: Nirjan Chapagain
Date: Sat, 10 Jan 2026 13:59:58 -0800
Subject: [PATCH 6/9] get-token works with tenantId praameter and tid from key
vault uri
---
.../GetAccessTokenProfileTests.cs | 2 +-
.../DependencyKeyVaultStore.cs | 17 ++-----
.../VirtualClient.Core/EndpointUtility.cs | 51 +++++++++++++++++++
.../CertificateInstallation.cs | 18 +++----
.../KeyVaultAccessToken.cs | 35 ++++++-------
5 files changed, 80 insertions(+), 43 deletions(-)
diff --git a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs
index 87818f841d..13b099c53a 100644
--- a/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs
+++ b/src/VirtualClient/VirtualClient.Actions.FunctionalTests/GetAccessTokenProfileTests.cs
@@ -41,7 +41,7 @@ public void GetAccessTokenProfileParametersAreInlinedCorrectly(string profile, P
[Test]
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Unix)]
[TestCase("GET-ACCESS-TOKEN.json", PlatformID.Win32NT)]
- public async Task GetAccessTokenProfileParametersAreAvailable(string profile, PlatformID platform)
+ public void GetAccessTokenProfileParametersAreAvailable(string profile, PlatformID platform)
{
this.dependencyFixture.Setup(platform);
diff --git a/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs b/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs
index ca97281b15..959431724d 100644
--- a/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs
+++ b/src/VirtualClient/VirtualClient.Contracts/DependencyKeyVaultStore.cs
@@ -16,30 +16,26 @@ public class DependencyKeyVaultStore : DependencyStore
/// Initializes an instance of the class.
///
/// The name of the KeyVault store (e.g. KeyVault).
- /// The Microsoft Entra tenant/directory ID.
/// The URI/SAS for the target Key Vault.
- public DependencyKeyVaultStore(string storeName, string tenantId, Uri endpointUri)
+ public DependencyKeyVaultStore(string storeName, Uri endpointUri)
: base(storeName, DependencyStore.StoreTypeAzureKeyVault)
{
endpointUri.ThrowIfNull(nameof(endpointUri));
this.EndpointUri = endpointUri;
this.KeyVaultNameSpace = endpointUri.Host;
- this.TenantId = tenantId;
}
///
/// Initializes an instance of the class.
///
/// The name of the KeyVault store (e.g. KeyVault).
- /// The Microsoft Entra tenant/directory ID.
/// The URI/SAS for the target Key Vault.
/// An identity token credential to use for authentication against the Key Vault.
- public DependencyKeyVaultStore(string storeName, string tenantId, Uri endpointUri, TokenCredential credentials)
- : this(storeName, tenantId, endpointUri)
+ public DependencyKeyVaultStore(string storeName, Uri endpointUri, TokenCredential credentials)
+ : this(storeName, endpointUri)
{
credentials.ThrowIfNull(nameof(credentials));
this.Credentials = credentials;
- this.TenantId = tenantId;
}
///
@@ -52,14 +48,9 @@ public DependencyKeyVaultStore(string storeName, string tenantId, Uri endpointUr
///
public string KeyVaultNameSpace { get; }
- ///
- /// The Key Vault tenant ID.
- ///
- public string TenantId { get; }
-
///
/// An identity token credential to use for authentication against the Key vault.
///
public TokenCredential Credentials { get; }
}
-}
+}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs b/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
index 9294acffa1..86803e9c48 100644
--- a/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
+++ b/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
@@ -398,6 +398,39 @@ public static bool TryParseCertificateReference(Uri uri, out string issuer, out
return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
}
+ ///
+ /// 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}
+ ///
+ /// The uri to attempt to parse the values from.
+ /// The tenant ID from the Microsoft Entra reference.
+ /// True/False if the method was able to successfully parse both the client ID and the tenant ID from the Microsoft Entra reference.
+ public static bool TryParseMicrosoftEntraReference(Uri uri, out string tenantId)
+ {
+ string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");
+
+ IDictionary queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
+ entry => entry.Key,
+ entry => entry.Value?.ToString(),
+ StringComparer.OrdinalIgnoreCase);
+
+ bool parametersDefined = false;
+ tenantId = null;
+
+ if (queryParameters?.Any() == true)
+ {
+ if (queryParameters.TryGetValue(UriParameter.TenantId, out string microsoftEntraTenantId)
+ && !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
+ {
+ tenantId = microsoftEntraTenantId;
+ parametersDefined = true;
+ }
+ }
+
+ return parametersDefined;
+ }
+
///
/// 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.
@@ -1292,5 +1325,23 @@ private static bool TryGetMicrosoftEntraReferenceForUri(IDictionary 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;
+ }
}
}
diff --git a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
index 273ca845de..c0b3f86f8e 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
@@ -20,7 +20,9 @@
///
public class CertificateInstallation : VirtualClientComponent
{
+ private ISystemManagement systemManagement;
private IFileSystem fileSystem;
+ private ProcessManager processManager;
///
/// Initializes a new instance of the class.
@@ -30,8 +32,9 @@ public class CertificateInstallation : VirtualClientComponent
public CertificateInstallation(IServiceCollection dependencies, IDictionary parameters = null)
: base(dependencies, parameters)
{
- this.fileSystem = this.Dependencies.GetService();
- this.fileSystem.ThrowIfNull(nameof(this.fileSystem));
+ this.systemManagement = dependencies.GetService();
+ this.fileSystem = this.systemManagement.FileSystem;
+ this.processManager = this.systemManagement.ProcessManager;
}
///
@@ -176,8 +179,8 @@ await this.fileSystem.File.WriteAllBytesAsync(
//
// User = read, write, execute
// Group = read, write, execute
- // Other = read, write, execute
- using (IProcessProxy process = processManager.CreateProcess("chmod", $"-R 777 {certificateDirectory}"))
+ // Other = read, write, execute+
+ using (IProcessProxy process = this.processManager.CreateProcess("chmod", $"-R 777 {certificateDirectory}"))
{
await process.StartAndWaitAsync(cancellationToken);
process.ThrowIfErrored();
@@ -217,18 +220,13 @@ protected async Task GetKeyVaultManager(CancellationToken canc
string keyVaultUri = string.IsNullOrWhiteSpace(this.KeyVaultUri)
? ((DependencyKeyVaultStore)keyVaultManager.StoreDescription).EndpointUri.ToString()
: this.KeyVaultUri;
-
- string tenantId = string.IsNullOrWhiteSpace(this.TenantId)
- ? ((DependencyKeyVaultStore)keyVaultManager.StoreDescription).TenantId
- : this.TenantId;
keyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
- tenantId.ThrowIfNullOrWhiteSpace(nameof(this.TenantId));
string token_ = await this.fileSystem.File.ReadAllTextAsync(accessTokenPath, cancellationToken).ConfigureAwait(false);
AccessTokenCredential tokenCredential = new AccessTokenCredential(token_);
- DependencyKeyVaultStore dependencyKeyVault = new DependencyKeyVaultStore(DependencyStore.KeyVault, this.TenantId, new Uri(this.KeyVaultUri), tokenCredential);
+ DependencyKeyVaultStore dependencyKeyVault = new DependencyKeyVaultStore(DependencyStore.KeyVault, new Uri(this.KeyVaultUri), tokenCredential);
return new KeyVaultManager(dependencyKeyVault);
}
else
diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
index 567ac45282..d19c8f2c5d 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
@@ -38,27 +38,15 @@ public KeyVaultAccessToken(IServiceCollection dependencies, IDictionary
- /// Gets the Azure tenant ID used to acquire an access token.
+ /// Gets the Azure Key Vault URI for which the access token will be requested.
+ /// Example: https://anyvault.vault.azure.net/
///
- protected string TenantId
- {
- get
- {
- return this.Parameters.GetValue(nameof(this.TenantId));
- }
- }
+ protected Uri KeyVaultUri { get; set; }
///
- /// Gets the Azure Key Vault URI for which the access token will be requested.
- /// Example: https://anyvault.vault.azure.net/
+ /// Gets the Azure tenant ID used to acquire an access token.
///
- protected string KeyVaultUri
- {
- get
- {
- return this.Parameters.GetValue(nameof(this.KeyVaultUri));
- }
- }
+ protected string TenantId { get; set; }
///
/// Gets or sets the full file path where the acquired access token will be written when file logging is enabled.
@@ -96,7 +84,16 @@ protected override async Task InitializeAsync(EventContext telemetryContext, Can
///
protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken)
{
- this.KeyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
+ this.KeyVaultUri = new Uri(this.Parameters.GetValue(nameof(this.KeyVaultUri)));
+ this.KeyVaultUri.ThrowIfNull(nameof(this.KeyVaultUri));
+
+ this.TenantId = this.Parameters.GetValue(nameof(this.TenantId));
+ if (string.IsNullOrWhiteSpace(this.TenantId))
+ {
+ EndpointUtility.TryParseMicrosoftEntraReference(this.KeyVaultUri, out string tenant);
+ this.TenantId = tenant;
+ }
+
this.TenantId.ThrowIfNullOrWhiteSpace(nameof(this.TenantId));
string accessToken = null;
@@ -210,7 +207,7 @@ protected virtual TokenRequestContext GetTokenRequestContext()
{
string[] installerTenantResourceScopes = new string[]
{
- new Uri(baseUri: new Uri(this.KeyVaultUri), relativeUri: ".default").ToString(),
+ new Uri(baseUri: this.KeyVaultUri, relativeUri: ".default").ToString(),
// Example of a specific scope:
// "api://56e7ee83-1cf6-4048-a664-c2a08955f825/user_impersonation"
};
From c4c452cdc523f9e4daa5a73cdf9d2a0b27d17296 Mon Sep 17 00:00:00 2001
From: Nirjan Chapagain
Date: Sat, 10 Jan 2026 14:08:31 -0800
Subject: [PATCH 7/9] Foo
---
.../VirtualClient.Core/EndpointUtility.cs | 19 +++----------------
.../KeyVaultAccessToken.cs | 2 +-
2 files changed, 4 insertions(+), 17 deletions(-)
diff --git a/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs b/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
index 86803e9c48..dff6394fb1 100644
--- a/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
+++ b/src/VirtualClient/VirtualClient.Core/EndpointUtility.cs
@@ -406,7 +406,7 @@ public static bool TryParseCertificateReference(Uri uri, out string issuer, out
/// The uri to attempt to parse the values from.
/// The tenant ID from the Microsoft Entra reference.
/// True/False if the method was able to successfully parse both the client ID and the tenant ID from the Microsoft Entra reference.
- public static bool TryParseMicrosoftEntraReference(Uri uri, out string tenantId)
+ public static bool TryParseMicrosoftEntraTenantIdReference(Uri uri, out string tenantId)
{
string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");
@@ -415,20 +415,7 @@ public static bool TryParseMicrosoftEntraReference(Uri uri, out string tenantId)
entry => entry.Value?.ToString(),
StringComparer.OrdinalIgnoreCase);
- bool parametersDefined = false;
- tenantId = null;
-
- if (queryParameters?.Any() == true)
- {
- if (queryParameters.TryGetValue(UriParameter.TenantId, out string microsoftEntraTenantId)
- && !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
- {
- tenantId = microsoftEntraTenantId;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
+ return TryGetMicrosoftEntraTenantId(queryParameters, out tenantId);
}
///
@@ -1326,7 +1313,7 @@ private static bool TryGetMicrosoftEntraReferenceForUri(IDictionary uriParameters, out string tenantId)
+ private static bool TryGetMicrosoftEntraTenantId(IDictionary uriParameters, out string tenantId)
{
bool parametersDefined = false;
tenantId = null;
diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
index d19c8f2c5d..166026fafe 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
@@ -90,7 +90,7 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
this.TenantId = this.Parameters.GetValue(nameof(this.TenantId));
if (string.IsNullOrWhiteSpace(this.TenantId))
{
- EndpointUtility.TryParseMicrosoftEntraReference(this.KeyVaultUri, out string tenant);
+ EndpointUtility.TryParseMicrosoftEntraTenantIdReference(this.KeyVaultUri, out string tenant);
this.TenantId = tenant;
}
From 4121ce89222c6b4f3cffb853e1906e0ff070ee52 Mon Sep 17 00:00:00 2001
From: Nirjan Chapagain
Date: Thu, 15 Jan 2026 08:39:52 -0800
Subject: [PATCH 8/9] Working version
---
.../KeyVaultManagerTests.cs | 2 +-
.../VirtualClient.Core/IKeyVaultManager.cs | 3 +
.../VirtualClient.Core/KeyVaultManager.cs | 60 +++++++++++--------
.../CertificateInstallation.cs | 57 ++++++++++++------
4 files changed, 78 insertions(+), 44 deletions(-)
diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs
index c3c6c6ac40..aefc00d3a4 100644
--- a/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs
+++ b/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs
@@ -119,7 +119,7 @@ public async Task KeyVaultManagerReturnsExpectedKey()
[TestCase(false)]
public async Task KeyVaultManagerReturnsExpectedCertificate(bool retrieveWithPrivateKey)
{
- var result = await this.keyVaultManager.GetCertificateAsync("mycert", CancellationToken.None, "https://myvault.vault.azure.net/", retrieveWithPrivateKey);
+ var result = await this.keyVaultManager.GetCertificateAsync(PlatformID.Unix, "mycert", CancellationToken.None, "https://myvault.vault.azure.net/", retrieveWithPrivateKey);
Assert.IsNotNull(result);
if (retrieveWithPrivateKey)
{
diff --git a/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs b/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
index 4a4d19329a..292347405d 100644
--- a/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
+++ b/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
@@ -3,6 +3,7 @@
namespace VirtualClient
{
+ using System;
using System.Security.Cryptography.X509Certificates;
using System.Threading;
using System.Threading.Tasks;
@@ -60,6 +61,7 @@ Task GetKeyAsync(
///
/// Retrieves a certificate from the Azure Key Vault.
///
+ /// The operating system platform (e.g. Windows, Linux).
/// The name of the certificate to be retrieved
/// A token that can be used to cancel the operation.
/// The URI of the Azure Key Vault.
@@ -72,6 +74,7 @@ Task GetKeyAsync(
/// Thrown if the certificate is not found, access is denied, or another error occurs.
///
Task GetCertificateAsync(
+ PlatformID platform,
string certName,
CancellationToken cancellationToken,
string keyVaultUri = null,
diff --git a/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs b/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
index de1b82a6a6..8676792d8e 100644
--- a/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
+++ b/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
@@ -15,7 +15,6 @@ namespace VirtualClient
using Azure.Security.KeyVault.Secrets;
using Polly;
using VirtualClient.Common.Extensions;
- using VirtualClient.Contracts;
///
/// Provides methods for retrieving secrets, keys, and certificates from an Azure Key Vault.
@@ -190,6 +189,7 @@ public async Task GetKeyAsync(
///
/// Retrieves a certificate from the Azure Key Vault.
///
+ ///
/// The name of the certificate to be retrieved
/// A token that can be used to cancel the operation.
/// The URI of the Azure Key Vault.
@@ -202,13 +202,13 @@ public async Task GetKeyAsync(
/// Thrown if the certificate is not found, access is denied, or another error occurs.
///
public async Task GetCertificateAsync(
+ PlatformID platform,
string certName,
CancellationToken cancellationToken,
string keyVaultUri = null,
bool retrieveWithPrivateKey = false,
IAsyncPolicy retryPolicy = null)
{
- this.ValidateKeyVaultStore();
this.StoreDescription.ThrowIfNull(nameof(this.StoreDescription));
certName.ThrowIfNullOrWhiteSpace(nameof(certName), "The certificate name cannot be null or empty.");
@@ -217,37 +217,47 @@ public async Task GetCertificateAsync(
? new Uri(keyVaultUri)
: ((DependencyKeyVaultStore)this.StoreDescription).EndpointUri;
- CertificateClient client = this.CreateCertificateClient(vaultUri, ((DependencyKeyVaultStore)this.StoreDescription).Credentials);
-
try
{
- return await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
+ KeyVaultSecret keyVaultSecret = await (retryPolicy ?? KeyVaultManager.DefaultRetryPolicy).ExecuteAsync(async () =>
{
- // Get the full certificate with private key (PFX) if requested
- if (retrieveWithPrivateKey)
- {
- X509Certificate2 privateKeyCert = await client
- .DownloadCertificateAsync(certName, cancellationToken: cancellationToken)
- .ConfigureAwait(false);
+ SecretClient secretsClient = new SecretClient(vaultUri, ((DependencyKeyVaultStore)this.StoreDescription).Credentials);
+ Response response = await secretsClient.GetSecretAsync(certName, version: null, cancellationToken);
- if (privateKeyCert is null || !privateKeyCert.HasPrivateKey)
- {
- throw new DependencyException("Failed to retrieve certificate content with private key.");
- }
+ return response.Value;
+ }).ConfigureAwait(false);
+
+ byte[] privateKeyBytes = Convert.FromBase64String(keyVaultSecret.Value);
+ X509Certificate2 certificate = null;
+
+ var keyStorageFlags = X509KeyStorageFlags.MachineKeySet | X509KeyStorageFlags.PersistKeySet;
- return privateKeyCert;
- }
- else
- {
- // If private key not needed, load cert from PublicBytes
- KeyVaultCertificateWithPolicy cert = await client.GetCertificateAsync(certName, cancellationToken: cancellationToken);
#if NET9_0_OR_GREATER
- return X509CertificateLoader.LoadCertificate(cert.Cer);
+ if (platform == PlatformID.Unix)
+ {
+ certificate = X509CertificateLoader.LoadPkcs12(privateKeyBytes, null, X509KeyStorageFlags.PersistKeySet);
+ }
+ else if (platform == PlatformID.Win32NT)
+ {
+ certificate = X509CertificateLoader.LoadPkcs12(privateKeyBytes, null, keyStorageFlags);
+ }
#elif NET8_0_OR_GREATER
- return new X509Certificate2(cert.Cer);
+ if (platform == PlatformID.Unix)
+ {
+ certificate = new X509Certificate2(privateKeyBytes, (string)null, X509KeyStorageFlags.PersistKeySet);
+ }
+ else if (platform == PlatformID.Win32NT)
+ {
+ certificate = new X509Certificate2(privateKeyBytes, (string)null, keyStorageFlags);
+ }
#endif
- }
- }).ConfigureAwait(false);
+
+ if (certificate is null || !certificate.HasPrivateKey)
+ {
+ throw new DependencyException("Failed to retrieve certificate content with private key.");
+ }
+
+ return certificate;
}
catch (RequestFailedException ex) when (ex.Status == (int)HttpStatusCode.Forbidden)
{
diff --git a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
index c0b3f86f8e..453eb5fa22 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
@@ -67,7 +67,39 @@ protected string CertificateName
{
get
{
- return this.Parameters.GetValue(nameof(this.KeyVaultUri));
+ return this.Parameters.GetValue(nameof(this.CertificateName));
+ }
+ }
+
+ ///
+ /// Gets the access token used to authenticate with Azure services.
+ ///
+ protected string AccessToken { get; set; }
+
+ ///
+ /// Gets the path to the file where the access token is saved.
+ ///
+ protected string AccessTokenPath
+ {
+ get
+ {
+ return this.Parameters.GetValue(nameof(this.AccessTokenPath));
+ }
+ }
+
+ ///
+ ///
+ ///
+ ///
+ ///
+ ///
+ protected override async Task InitializeAsync(EventContext telemetryContext, CancellationToken cancellationToken)
+ {
+ this.AccessToken = this.Parameters.GetValue(nameof(this.AccessToken), string.Empty);
+
+ if (string.IsNullOrWhiteSpace(this.AccessToken) && !string.IsNullOrWhiteSpace(this.AccessTokenPath))
+ {
+ this.AccessToken = await this.fileSystem.File.ReadAllTextAsync(this.AccessTokenPath);
}
}
@@ -83,8 +115,8 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
try
{
- IKeyVaultManager keyVault = await this.GetKeyVaultManager(cancellationToken);
- X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.CertificateName, cancellationToken);
+ IKeyVaultManager keyVault = this.GetKeyVaultManager(cancellationToken);
+ X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.Platform, this.CertificateName, cancellationToken);
if (this.Platform == PlatformID.Win32NT)
{
@@ -199,7 +231,7 @@ await this.fileSystem.File.WriteAllBytesAsync(
///
///
///
- protected async Task GetKeyVaultManager(CancellationToken cancellationToken)
+ protected IKeyVaultManager GetKeyVaultManager(CancellationToken cancellationToken)
{
IKeyVaultManager keyVaultManager = this.Dependencies.GetService();
keyVaultManager.ThrowIfNull(nameof(keyVaultManager));
@@ -209,22 +241,11 @@ protected async Task GetKeyVaultManager(CancellationToken canc
{
return keyVaultManager;
}
- else if (!string.IsNullOrWhiteSpace(this.LogFileName))
+ else if (!string.IsNullOrWhiteSpace(this.AccessToken))
{
- string directory = !string.IsNullOrWhiteSpace(this.LogFolderName)
- ? this.LogFolderName
- : this.fileSystem.Directory.GetCurrentDirectory();
-
- string accessTokenPath = this.Combine(directory, this.LogFileName);
-
- string keyVaultUri = string.IsNullOrWhiteSpace(this.KeyVaultUri)
- ? ((DependencyKeyVaultStore)keyVaultManager.StoreDescription).EndpointUri.ToString()
- : this.KeyVaultUri;
-
- keyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
+ this.KeyVaultUri.ThrowIfNullOrWhiteSpace(nameof(this.KeyVaultUri));
- string token_ = await this.fileSystem.File.ReadAllTextAsync(accessTokenPath, cancellationToken).ConfigureAwait(false);
- AccessTokenCredential tokenCredential = new AccessTokenCredential(token_);
+ AccessTokenCredential tokenCredential = new AccessTokenCredential(this.AccessToken);
DependencyKeyVaultStore dependencyKeyVault = new DependencyKeyVaultStore(DependencyStore.KeyVault, new Uri(this.KeyVaultUri), tokenCredential);
return new KeyVaultManager(dependencyKeyVault);
From 14c5f5702f947c6732c15c3a21d3aafaa8906167 Mon Sep 17 00:00:00 2001
From: Nirjan Chapagain
Date: Thu, 15 Jan 2026 14:26:38 -0800
Subject: [PATCH 9/9] foo
---
.../EndpointUtilityTests.cs | 38 ++-
.../KeyVaultManagerTests.cs | 17 +-
.../VirtualClient.Core/IKeyVaultManager.cs | 2 -
.../Identity/EndPointSettingsExtensions.cs | 302 ------------------
.../Identity/EndpointSettings.cs | 49 ---
.../VirtualClient.Core/KeyVaultManager.cs | 4 +-
.../CertificateInstallation.cs | 9 +-
.../KeyVaultManagerTests.cs | 12 -
.../VirtualClient.Main/Program.cs | 17 +-
.../profiles/INSTALL-CERTIFICATES.json | 22 --
.../CommandLineOptionTests.cs | 39 +++
11 files changed, 98 insertions(+), 413 deletions(-)
delete mode 100644 src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs
delete mode 100644 src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs
delete mode 100644 src/VirtualClient/VirtualClient.IntegrationTests/KeyVaultManagerTests.cs
delete mode 100644 src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/EndpointUtilityTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/EndpointUtilityTests.cs
index 960f157e7d..34fd05b29f 100644
--- a/src/VirtualClient/VirtualClient.Core.UnitTests/EndpointUtilityTests.cs
+++ b/src/VirtualClient/VirtualClient.Core.UnitTests/EndpointUtilityTests.cs
@@ -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(
@@ -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(
@@ -338,7 +338,7 @@ public void EndpointUtilityCreatesTheExpectedBlobStoreReferenceForConnectionStri
Assert.IsNotNull(store.Credentials);
Assert.IsInstanceOf(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/")]
@@ -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);
+ }
}
}
diff --git a/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs b/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs
index aefc00d3a4..91aaf29636 100644
--- a/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs
+++ b/src/VirtualClient/VirtualClient.Core.UnitTests/KeyVaultManagerTests.cs
@@ -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(PlatformID.Unix, "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]
diff --git a/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs b/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
index 292347405d..13d4a2b8c4 100644
--- a/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
+++ b/src/VirtualClient/VirtualClient.Core/IKeyVaultManager.cs
@@ -65,7 +65,6 @@ Task GetKeyAsync(
/// The name of the certificate to be retrieved
/// A token that can be used to cancel the operation.
/// The URI of the Azure Key Vault.
- /// flag to decode whether to retrieve certificate with private key
/// A policy to use for handling retries when transient errors/failures happen.
///
/// A containing the certificate.
@@ -78,7 +77,6 @@ Task GetCertificateAsync(
string certName,
CancellationToken cancellationToken,
string keyVaultUri = null,
- bool retrieveWithPrivateKey = false,
IAsyncPolicy retryPolicy = null);
}
}
diff --git a/src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs b/src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs
deleted file mode 100644
index 1dce8ad002..0000000000
--- a/src/VirtualClient/VirtualClient.Core/Identity/EndPointSettingsExtensions.cs
+++ /dev/null
@@ -1,302 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text.RegularExpressions;
-using VirtualClient.Contracts;
-
-namespace VirtualClient.Identity
-{
- internal static class EndPointSettingsExtensions
- {
- internal const string AllowedPackageUri = "https://packages.virtualclient.microsoft.com";
-
- ///
- /// Returns true/false whether the value is a custom Virtual Client connection string
- /// (e.g. EndpointUrl=https://any.blob.core.windows.net;ManagedIdentityId=307591a4-abb2-4559-af59-b47177d140cf).
- ///
- /// The value to evaluate.
- /// True if the value is a custom Virtual Client connection string. False if not.
- public static bool IsCustomConnectionString(string value)
- {
- bool isConnectionString = false;
- StringComparison ignoreCase = StringComparison.OrdinalIgnoreCase;
-
- if (value.Contains($"{ConnectionParameter.EndpointUrl}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.EventHubNamespace}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.ClientId}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.TenantId}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.ManagedIdentityId}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.CertificateIssuer}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.CertificateSubject}=", ignoreCase)
- || value.Contains($"{ConnectionParameter.CertificateThumbprint}=", ignoreCase))
- {
- isConnectionString = true;
- }
-
- return isConnectionString;
- }
-
- ///
- /// Returns true/false whether the value is a custom Virtual Client URI
- /// (e.g. https://any.service.azure.com/?cid=307591a4-abb2-4559-af59-b47177d140cf&tid=985bbc17-E3a5-4fec-b0cb-40dbb8bc5959&crti=ABC&crts=any.service.com).
- ///
- /// The URI to evaluate.
- /// True if the URI is a custom Virtual Client URI. False if not.
- public static bool IsCustomUri(Uri endpointUri)
- {
- return Regex.IsMatch(
- endpointUri.Query,
- $"{UriParameter.CertificateIssuer}=|{UriParameter.CertificateSubject}=|{UriParameter.CertificateThumbprint}=|{UriParameter.ClientId}=|{UriParameter.ManagedIdentityId}=|{UriParameter.TenantId}=",
- RegexOptions.IgnoreCase);
- }
-
- ///
- /// Returns true/false whether the value is an Event Hub namespace access policy/connection string.
- /// (e.g. Endpoint=sb://any.servicebus.windows.net/;SharedAccessKeyName=AnyAccessPolicy;SharedAccessKey=...).
- ///
- /// The value to evaluate.
- /// True if the value is an Event Hub namespace connection string. False if not.
- public static bool IsEventHubConnectionString(string value)
- {
- return Regex.IsMatch(
- value,
- $"{ConnectionParameter.Endpoint}=|{ConnectionParameter.SharedAccessKeyName}=",
- RegexOptions.IgnoreCase);
- }
-
- ///
- /// Returns true/false whether the value is a standard Storage Account connection string.
- /// (e.g. DefaultEndpointsProtocol=https;AccountName=anystorage;EndpointSuffix=core.windows.net...).
- ///
- /// The value to evaluate.
- /// True if the value is a Storage Account connection string. False if not.
- public static bool IsStorageAccountConnectionString(string value)
- {
- return Regex.IsMatch(
- value,
- $"{ConnectionParameter.DefaultEndpointsProtocol}=|{ConnectionParameter.BlobEndpoint}=",
- RegexOptions.IgnoreCase);
- }
-
- ///
- /// Returns true/false whether the value is a standard Storage Account connection string.
- /// (e.g. DefaultEndpointsProtocol=https;AccountName=anystorage;EndpointSuffix=core.windows.net...).
- ///
- /// The URI to evaluate.
- /// True if the value is a Storage Account connection string. False if not.
- public static bool IsStorageAccountSasUri(Uri endpointUri)
- {
- return Regex.IsMatch(endpointUri.Query, "sv=|se=|spr=|sig=", RegexOptions.IgnoreCase);
- }
-
- ///
- /// Parses the subject name and issuer from the provided uri. If the uri does not contain the correctly formatted certificate subject name
- /// and issuer information the method will return false, and keep the two out parameters as null.
- /// Ex. https://vegaprod01proxyapi.azurewebsites.net?crti=issuerName&crts=certSubject
- ///
- /// The uri to attempt to parse the values from.
- /// The issuer of the certificate.
- /// The subject of the certificate.
- /// True/False if the method was able to successfully parse both the subject name and the issuer of the certificate.
- public static bool TryParseCertificateReference(Uri uri, out string issuer, out string subject)
- {
- string queryString = Uri.UnescapeDataString(uri.Query).Trim('?').Replace("&", ",,,");
-
- IDictionary queryParameters = TextParsingExtensions.ParseDelimitedValues(queryString)?.ToDictionary(
- entry => entry.Key,
- entry => entry.Value?.ToString(),
- StringComparer.OrdinalIgnoreCase);
-
- return TryGetCertificateReferenceForUri(queryParameters, out issuer, out subject);
- }
-
- ///
- /// 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.
- ///
- /// endpoint to verify and format
- ///
- public static string ValidateAndFormatPackageUri(string endpoint)
- {
- string packageUri = new Uri(AllowedPackageUri).Host;
- return packageUri == endpoint ? $"https://{endpoint}" : endpoint;
- }
-
- internal static bool TryGetEndpointForConnection(IDictionary connectionParameters, out string endpoint)
- {
- bool endpointDefined = false;
- endpoint = null;
-
- if (connectionParameters?.Any() == true)
- {
- if ((connectionParameters.TryGetValue(ConnectionParameter.Endpoint, out endpoint) || connectionParameters.TryGetValue(ConnectionParameter.EndpointUrl, out endpoint))
- && !string.IsNullOrWhiteSpace(endpoint))
- {
- endpointDefined = true;
- }
- }
-
- return endpointDefined;
- }
-
- internal static bool TryGetCertificateReferenceForConnection(IDictionary uriParameters, out string certificateThumbPrint)
- {
- bool parametersDefined = false;
- certificateThumbPrint = null;
-
- if (uriParameters?.Any() == true)
- {
- if (uriParameters.TryGetValue(ConnectionParameter.CertificateThumbprint, out string thumbprint)
- && !string.IsNullOrWhiteSpace(thumbprint))
- {
- certificateThumbPrint = thumbprint;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetCertificateReferenceForConnection(IDictionary connectionParameters, out string certificateIssuer, out string certificateSubject)
- {
- bool parametersDefined = false;
- certificateIssuer = null;
- certificateSubject = null;
-
- if (connectionParameters?.Any() == true)
- {
- if (connectionParameters.TryGetValue(ConnectionParameter.CertificateIssuer, out string issuer)
- && connectionParameters.TryGetValue(ConnectionParameter.CertificateSubject, out string subject)
- && !string.IsNullOrWhiteSpace(issuer)
- && !string.IsNullOrWhiteSpace(subject))
- {
- certificateIssuer = issuer;
- certificateSubject = subject;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetManagedIdentityReferenceForConnection(IDictionary connectionParameters, out string managedIdentityId)
- {
- bool parametersDefined = false;
- managedIdentityId = null;
-
- if (connectionParameters?.Any() == true)
- {
- if (connectionParameters.TryGetValue(ConnectionParameter.ManagedIdentityId, out string managedIdentityClientId)
- && !string.IsNullOrWhiteSpace(managedIdentityClientId))
- {
- managedIdentityId = managedIdentityClientId;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetMicrosoftEntraReferenceForConnection(IDictionary connectionParameters, out string clientId, out string tenantId)
- {
- bool parametersDefined = false;
- clientId = null;
- tenantId = null;
-
- if (connectionParameters?.Any() == true)
- {
- if (connectionParameters.TryGetValue(ConnectionParameter.ClientId, out string microsoftEntraClientId)
- && connectionParameters.TryGetValue(ConnectionParameter.TenantId, out string microsoftEntraTenantId)
- && !string.IsNullOrWhiteSpace(microsoftEntraClientId)
- && !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
- {
- clientId = microsoftEntraClientId;
- tenantId = microsoftEntraTenantId;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetCertificateReferenceForUri(IDictionary uriParameters, out string certificateThumbPrint)
- {
- bool parametersDefined = false;
- certificateThumbPrint = null;
-
- if (uriParameters?.Any() == true)
- {
- if (uriParameters.TryGetValue(UriParameter.CertificateThumbprint, out string thumbprint)
- && !string.IsNullOrWhiteSpace(thumbprint))
- {
- certificateThumbPrint = thumbprint;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetCertificateReferenceForUri(IDictionary uriParameters, out string certificateIssuer, out string certificateSubject)
- {
- bool parametersDefined = false;
- certificateIssuer = null;
- certificateSubject = null;
-
- if (uriParameters?.Any() == true)
- {
- if (uriParameters.TryGetValue(UriParameter.CertificateIssuer, out string issuer)
- && uriParameters.TryGetValue(UriParameter.CertificateSubject, out string subject)
- && !string.IsNullOrWhiteSpace(issuer)
- && !string.IsNullOrWhiteSpace(subject))
- {
- certificateIssuer = issuer;
- certificateSubject = subject;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetManagedIdentityReferenceForUri(IDictionary uriParameters, out string managedIdentityId)
- {
- bool parametersDefined = false;
- managedIdentityId = null;
-
- if (uriParameters?.Any() == true)
- {
- if (uriParameters.TryGetValue(UriParameter.ManagedIdentityId, out string managedIdentityClientId)
- && !string.IsNullOrWhiteSpace(managedIdentityClientId))
- {
- managedIdentityId = managedIdentityClientId;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
-
- internal static bool TryGetMicrosoftEntraReferenceForUri(IDictionary uriParameters, out string clientId, out string tenantId)
- {
- bool parametersDefined = false;
- clientId = null;
- tenantId = null;
-
- if (uriParameters?.Any() == true)
- {
- if (uriParameters.TryGetValue(UriParameter.ClientId, out string microsoftEntraClientId)
- && uriParameters.TryGetValue(UriParameter.TenantId, out string microsoftEntraTenantId)
- && !string.IsNullOrWhiteSpace(microsoftEntraClientId)
- && !string.IsNullOrWhiteSpace(microsoftEntraTenantId))
- {
- clientId = microsoftEntraClientId;
- tenantId = microsoftEntraTenantId;
- parametersDefined = true;
- }
- }
-
- return parametersDefined;
- }
- }
-}
diff --git a/src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs b/src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs
deleted file mode 100644
index de4a555ab0..0000000000
--- a/src/VirtualClient/VirtualClient.Core/Identity/EndpointSettings.cs
+++ /dev/null
@@ -1,49 +0,0 @@
-namespace VirtualClient.Identity
-{
- using System;
- using VirtualClient.Common.Extensions;
-
- internal class EndpointSettings
- {
- private static readonly char[] TrimChars = { '\'', '"', ' ' };
-
- public EndpointSettings(string endpoint)
- {
- endpoint.ThrowIfNullOrWhiteSpace(nameof(endpoint));
-
- endpoint = EndPointSettingsExtensions.ValidateAndFormatPackageUri(endpoint);
- string argumentValue = endpoint.Trim(EndpointSettings.TrimChars);
-
- // this.IsBlobStoreConnectionString = EndPointSettingsExtensions.IsBlobStoreConnectionString(argumentValue);
-
- // this.EndPoint = new Uri(endpoint);
- this.IsCustomConnectionString = EndPointSettingsExtensions.IsCustomConnectionString(endpoint);
-
- this.IsEventHubConnectionString = EndPointSettingsExtensions.IsEventHubConnectionString(endpoint);
- // this.IsKeyVaultConnectionString = EndPointSettingsExtensions.IsKeyVaultConnectionString(endpoint);
-
- }
-
- public Uri EndPoint { get; set; }
-
- public bool IsCustomConnectionString { get; set; }
-
- public bool IsBlobStoreConnectionString { get; set; }
-
- public bool IsEventHubConnectionString { get; set; }
-
- public bool IsKeyVaultConnectionString { get; set; }
-
- public string TenantId { get; set; }
-
- public string ClientId { get; set; }
-
- public string ManagedIdentityId { get; set; }
-
- public string CertificateThumbprint { get; set; }
-
- public string CertificateSubjectName { get; set; }
-
- public string CertificateIssuerName { get; set; }
- }
-}
diff --git a/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs b/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
index 8676792d8e..4f26375608 100644
--- a/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
+++ b/src/VirtualClient/VirtualClient.Core/KeyVaultManager.cs
@@ -189,11 +189,10 @@ public async Task GetKeyAsync(
///
/// Retrieves a certificate from the Azure Key Vault.
///
- ///
+ /// The operating system platform.
/// The name of the certificate to be retrieved
/// A token that can be used to cancel the operation.
/// The URI of the Azure Key Vault.
- /// flag to decode whether to retrieve certificate with private key
/// A policy to use for handling retries when transient errors/failures happen.
///
/// A containing the certificate
@@ -206,7 +205,6 @@ public async Task GetCertificateAsync(
string certName,
CancellationToken cancellationToken,
string keyVaultUri = null,
- bool retrieveWithPrivateKey = false,
IAsyncPolicy retryPolicy = null)
{
this.StoreDescription.ThrowIfNull(nameof(this.StoreDescription));
diff --git a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
index 453eb5fa22..e9dd3e2c46 100644
--- a/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
+++ b/src/VirtualClient/VirtualClient.Dependencies/CertificateInstallation.cs
@@ -115,7 +115,7 @@ protected override async Task ExecuteAsync(EventContext telemetryContext, Cancel
try
{
- IKeyVaultManager keyVault = this.GetKeyVaultManager(cancellationToken);
+ IKeyVaultManager keyVault = this.GetKeyVaultManager();
X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.Platform, this.CertificateName, cancellationToken);
if (this.Platform == PlatformID.Win32NT)
@@ -227,16 +227,13 @@ await this.fileSystem.File.WriteAllBytesAsync(
}
///
- ///
+ /// Gets the Key Vault manager to use to retrieve certificates from Key Vault.
///
- ///
- ///
- protected IKeyVaultManager GetKeyVaultManager(CancellationToken cancellationToken)
+ protected IKeyVaultManager GetKeyVaultManager()
{
IKeyVaultManager keyVaultManager = this.Dependencies.GetService();
keyVaultManager.ThrowIfNull(nameof(keyVaultManager));
- // need better if condition.
if (keyVaultManager.StoreDescription != null)
{
return keyVaultManager;
diff --git a/src/VirtualClient/VirtualClient.IntegrationTests/KeyVaultManagerTests.cs b/src/VirtualClient/VirtualClient.IntegrationTests/KeyVaultManagerTests.cs
deleted file mode 100644
index a9c1666faa..0000000000
--- a/src/VirtualClient/VirtualClient.IntegrationTests/KeyVaultManagerTests.cs
+++ /dev/null
@@ -1,12 +0,0 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-
-namespace VirtualClient
-{
- internal class KeyVaultManagerTests
- {
- }
-}
diff --git a/src/VirtualClient/VirtualClient.Main/Program.cs b/src/VirtualClient/VirtualClient.Main/Program.cs
index 1f458fe89e..43c452b112 100644
--- a/src/VirtualClient/VirtualClient.Main/Program.cs
+++ b/src/VirtualClient/VirtualClient.Main/Program.cs
@@ -419,10 +419,23 @@ private static Command CreateGetTokenSubCommand(DefaultSettings settings)
{
// OPTIONAL
// -------------------------------------------------------------------
- OptionFactory.CreateParametersOption(required: false),
+ // --clean
+ OptionFactory.CreateCleanOption(required: false),
+
+ // --client-id
+ OptionFactory.CreateClientIdOption(required: false, Guid.NewGuid().ToString()),
+
+ // --experiment-id
+ OptionFactory.CreateExperimentIdOption(required: false, Guid.NewGuid().ToString()),
// --key-vault
- OptionFactory.CreateKeyVaultOption(required: false)
+ OptionFactory.CreateKeyVaultOption(required: false),
+
+ // --parameters
+ OptionFactory.CreateParametersOption(required: false),
+
+ // --verbose
+ OptionFactory.CreateVerboseFlag(required: false, false)
};
return getAccessTokenCommand;
diff --git a/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
deleted file mode 100644
index e3f4fc4424..0000000000
--- a/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json
+++ /dev/null
@@ -1,22 +0,0 @@
-{
- "Description": "Installs certificate from a Azure Key Vault.",
- "Parameters": {
- "AccessToken": "{access token}",
- "KeyVaultUri": "https://yourkeyvault.vault.azure.net/",
- "CertificateName": "cert-01-name",
- "CertificatePassword": "",
- "TenantId": ""
- },
- "Dependencies": [
- {
- "Type": "CertificateInstallation",
- "Parameters": {
- "Scenario": "InstallCertificate",
- "AccessToken": "$.Parameters.AccessToken",
- "TenantId": "$.Parameters.TenantId",
- "KeyVaultUri": "$.Parameters.KeyVaultUri",
- "CertificateName": "$.Parameters.CertificateName"
- }
- }
- ]
-}
\ No newline at end of file
diff --git a/src/VirtualClient/VirtualClient.UnitTests/CommandLineOptionTests.cs b/src/VirtualClient/VirtualClient.UnitTests/CommandLineOptionTests.cs
index 8135448868..0cc22e5707 100644
--- a/src/VirtualClient/VirtualClient.UnitTests/CommandLineOptionTests.cs
+++ b/src/VirtualClient/VirtualClient.UnitTests/CommandLineOptionTests.cs
@@ -608,6 +608,45 @@ public void VirtualClientCommandLineSupportsResponseFiles()
}
}
+ [Test]
+ [TestCase("--agentId", "AgentID")]
+ [TestCase("--client-id", "AgentID")]
+ [TestCase("--c", "AgentID")]
+ [TestCase("--clean", null)]
+ [TestCase("--clean", "logs")]
+ [TestCase("--clean", "logs,packages,state,temp")]
+ [TestCase("--experimentId", "0B692DEB-411E-4AC1-80D5-AF539AE1D6B2")]
+ [TestCase("--experiment-id", "0B692DEB-411E-4AC1-80D5-AF539AE1D6B2")]
+ [TestCase("--e", "0B692DEB-411E-4AC1-80D5-AF539AE1D6B2")]
+ [TestCase("--kv", "https://anyvault.vault.azure.net/?cid=1...&tid=2")]
+ [TestCase("--key-vault", "testingKV")]
+ [TestCase("--parameters", "helloWorld=123,,,TenantId=789203498")]
+ [TestCase("--pm", "testing")]
+ [TestCase("--verbose", null)]
+ public void VirtualClientGetTokenCommandSupportsOnlyExpectedOptions(string option, string value)
+ {
+ using (CancellationTokenSource cancellationSource = new CancellationTokenSource())
+ {
+ List arguments = new List()
+ {
+ "get-token"
+ };
+
+ arguments.Add(option);
+ if (value != null)
+ {
+ arguments.Add(value);
+ }
+
+ Assert.DoesNotThrow(() =>
+ {
+ ParseResult result = Program.SetupCommandLine(arguments.ToArray(), cancellationSource).Build().Parse(arguments);
+ Assert.IsFalse(result.Errors.Any());
+ result.ThrowOnUsageError();
+ }, $"Option '{option}' is not supported.");
+ }
+ }
+
private class TestExecuteCommand : ExecuteCommand
{
public Action OnExecuteCommand { get; set; }