diff --git a/src/VirtualClient/VirtualClient.Dependencies/DependencyCertificateInstallation.cs b/src/VirtualClient/VirtualClient.Dependencies/DependencyCertificateInstallation.cs new file mode 100644 index 0000000000..93ce21bd86 --- /dev/null +++ b/src/VirtualClient/VirtualClient.Dependencies/DependencyCertificateInstallation.cs @@ -0,0 +1,368 @@ +// 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.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Polly; + using VirtualClient.Common; + using VirtualClient.Common.Extensions; + using VirtualClient.Common.Telemetry; + using VirtualClient.Configuration; + using VirtualClient.Contracts; + using VirtualClient.Logging; + + /// + /// Provides functionality for downloading and installing dependency certificates from + /// a cloud keyvault store onto the system. + /// + public class DependencyCertificateInstallation : VirtualClientComponent + { + /// + /// Initializes a new instance of the class. + /// + /// Provides all of the required dependencies to the Virtual Client component. + /// + /// Parameters defined in the execution profile or supplied to the Virtual Client on the command line. + /// + public DependencyCertificateInstallation(IServiceCollection dependencies, IDictionary parameters = null) + : base(dependencies, parameters) + { + } + + /// + /// The type of archival file format the blob is in. + /// + public ArchiveType ArchiveType + { + get + { + return this.Parameters.GetEnumValue(nameof(DependencyCertificateInstallation.ArchiveType), ArchiveType.Zip); + } + } + + /// + /// The name of the blob package in the storage account. + /// + public string CertificateName + { + get + { + return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.CertificateName)); + } + + set + { + this.Parameters[nameof(DependencyCertificateInstallation.CertificateName)] = value; + } + } + + /// + /// The name of the container in which the blob package exists in + /// the storage account. + /// + public string KeyVaultName + { + get + { + return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.KeyVaultName)); + } + + set + { + this.Parameters[nameof(DependencyCertificateInstallation.KeyVaultName)] = value; + } + } + + /// + /// The path in which to install the package. + /// + public string InstallationPath + { + get + { + return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.InstallationPath), string.Empty); + } + + set + { + this.Parameters[nameof(DependencyCertificateInstallation.InstallationPath)] = value; + } + } + + /// + /// Whether the blob should be extracted. + /// + public bool Extract + { + get + { + return this.Parameters.GetValue(nameof(DependencyCertificateInstallation.Extract), true); + } + + set + { + this.Parameters[nameof(DependencyCertificateInstallation.Extract)] = value; + } + } + + /// + /// Defines an access token to use for authentication + authorization with + /// Azure resources. + /// + public string AccessToken { get; set; } + + /// + /// Flag indicates whether the installer is running unattended. This scenario + /// is used with installations via other automation to ensure the installer does + /// not block on exiting. + /// + public bool Unattended { get; set; } + + /// + /// A retry policy to apply to transient issues with accessing secrets in + /// an Azure Key Vault. + /// + protected IAsyncPolicy KeyVaultAccessRetryPolicy { get; set; } + + /// + /// Executes the blob package download/installation operation. + /// + protected override async Task ExecuteAsync(EventContext telemetryContext, CancellationToken cancellationToken) + { + IPackageManager packageManager = this.Dependencies.GetService(); + IFileSystem fileSystem = this.Dependencies.GetService(); + + DependencyPath existingPackage = await packageManager.GetPackageAsync(this.PackageName, cancellationToken) + .ConfigureAwait(false); + + /* + MetadataContract.Persist( + $"package_{this.PackageName}", + this.BlobName, + MetadataContract.DependenciesCategory, + true); + + IPackageManager packageManager = this.Dependencies.GetService(); + IFileSystem fileSystem = this.Dependencies.GetService(); + + DependencyPath existingPackage = await packageManager.GetPackageAsync(this.PackageName, cancellationToken) + .ConfigureAwait(false); + + telemetryContext.AddContext("packageDirectory", this.PlatformSpecifics.GetPackagePath()); + telemetryContext.AddContext("packageExists", existingPackage != null); + telemetryContext.AddContext("package", existingPackage); + + // If a built-in package exists, we do not currently override it with a Blob package + // downloaded to the system. + if (existingPackage == null || !fileSystem.Directory.Exists(existingPackage.Path)) + { + BlobDescriptor packageDescription = new BlobDescriptor + { + Name = this.BlobName, + ContainerName = this.BlobContainer, + PackageName = this.PackageName, + ArchiveType = this.ArchiveType, + Extract = this.Extract + }; + + telemetryContext.AddContext("package", packageDescription); + + string installationPath = null; + if (!string.IsNullOrWhiteSpace(this.InstallationPath)) + { + IDiskManager diskManager = this.Dependencies.GetService(); + IEnumerable disks = await diskManager.GetDisksAsync(cancellationToken).ConfigureAwait(false); + if (!DependencyCertificateInstallation.TryResolveRelativeDiskLocation(disks, this.InstallationPath, this.Platform, out installationPath)) + { + throw new WorkloadException( + $"The installation path provided '{this.InstallationPath}' cannot be resolved", + ErrorReason.DependencyInstallationFailed); + } + } + + if (!this.TryGetPackageStoreManager(out IBlobManager blobManager)) + { + throw new DependencyException( + $"Package store not defined. The package '{packageDescription.Name}' cannot be installed because the package store information " + + $"was not provided to the application on the command line (e.g. --packages).", + ErrorReason.PackageStoreNotDefined); + } + + string packageLocation = await packageManager.InstallPackageAsync(blobManager, packageDescription, cancellationToken, installationPath) + .ConfigureAwait(false); + + if (string.IsNullOrWhiteSpace(packageLocation)) + { + throw new DependencyException( + $"Blob package installation failed for package '{this.BlobName}'.", + ErrorReason.DependencyInstallationFailed); + } + + telemetryContext.AddContext("packageLocation", packageLocation); + } + + */ + } + + /// + /// Downloads the dependency from the container specified to the path defined. + /// + /// The blob manager to use for downloading the package dependency. + /// The dependency description. + /// Provides the location where the package should be downloaded. + /// A token that can be used to cancel the operation. + protected virtual async Task DownloadDependencyPackageAsync(IBlobManager blobManager, DependencyDescriptor description, string downloadPath, CancellationToken cancellationToken) + { + FileStream stream = new FileStream(downloadPath, FileMode.Create, FileAccess.ReadWrite, FileShare.None); + + await blobManager.DownloadBlobAsync(description, stream, cancellationToken) + .ConfigureAwait(false); + + return stream; + } + + /// + /// Installs certificates required by the CRC SDK on the local system. + /// + /// Provides dependencies required to download and install certificates. + /// A token that can be used to cancel the operation. + protected async Task InstallCertificatesAsync(IServiceCollection dependencies, CancellationToken cancellationToken) + { + Console.WriteLine(); + Console.WriteLine($"[Downloading Certificates]"); + // Console.WriteLine($"Certificate = {settings.InstallerCertificateName}"); + + PlatformSpecifics platformSpecifics = dependencies.GetService(); + PlatformID platform = platformSpecifics.Platform; + + X509Certificate2 certificate = null; // await this.DownloadCertificateAsync(settings, platform, cancellationToken); + + try + { + Console.WriteLine(); + Console.WriteLine($"[Installing Certificates]"); + + if (platform == PlatformID.Unix) + { + await this.InstallCertificateOnUnixAsync(certificate, dependencies, cancellationToken); + } + else if (platform == PlatformID.Win32NT) + { + await this.InstallCertificateOnWindowsAsync(certificate, cancellationToken); + } + else + { + throw new DependencyException( + $"Certificate installation for OS platform '{platform}' is not supported.", + ErrorReason.NotSupported); + } + } + catch (CryptographicException exc) when (exc.Message.Contains("access", StringComparison.OrdinalIgnoreCase)) + { + throw new DependencyException( + $"Certificate installation failed. Local certificate store access permissions denied. The CRC SDK installer must be " + + $"run with Administrative privileges in order to install certificates in the current context.", + ErrorReason.DependencyInstallationFailed); + } + } + + /// + /// 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, IServiceCollection dependencies, CancellationToken cancellationToken) + { + PlatformSpecifics platformSpecifics = dependencies.GetService(); + ProcessManager processManager = dependencies.GetService(); + IFileSystem fileSystem = dependencies.GetService(); + + // On Unix/Linux systems, we install ther 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 = platformSpecifics.GetEnvironmentVariable(EnvironmentVariable.USER); + string sudoUser = platformSpecifics.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 (!fileSystem.Directory.Exists(certificateDirectory)) + { + 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 fileSystem.File.WriteAllBytesAsync( + platformSpecifics.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."); + } + } + } +} \ No newline at end of file diff --git a/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs new file mode 100644 index 0000000000..c1a11cfe5b --- /dev/null +++ b/src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs @@ -0,0 +1,184 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient.Dependencies +{ + using System; + using System.Collections.Generic; + using System.Diagnostics; + 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; + 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/CertificateInstallation.cs b/src/VirtualClient/VirtualClient.Main/CertificateInstallation.cs new file mode 100644 index 0000000000..71960099be --- /dev/null +++ b/src/VirtualClient/VirtualClient.Main/CertificateInstallation.cs @@ -0,0 +1,196 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient +{ + using System; + using System.Collections.Generic; + using System.IO.Abstractions; + using System.Linq; + using System.Threading; + using System.Threading.Tasks; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using VirtualClient.Common; + using VirtualClient.Common.Contracts; + using VirtualClient.Common.Extensions; + using VirtualClient.Contracts; + using System.IO; + using System.Runtime.ConstrainedExecution; + using System.Runtime.InteropServices; + using System.Security; + using System.Security.Cryptography; + using System.Security.Cryptography.X509Certificates; + using Azure; + using Azure.Security.KeyVault.Secrets; + using Polly; + using VirtualClient; + using VirtualClient.Identity; + using Serilog.Core; + using VirtualClient.Common.Telemetry; + using Azure.Core; + using Azure.Identity; + + /// + /// Command resets the Virtual Client environment for "first time" run scenarios. + /// + public class CertificateInstallation : CommandBase + { + /// + /// Logger + /// + protected ILogger Logger { get; set; } + + /// + /// Certificate Name. + /// + protected string CertificateName { get; set; } + + /// + /// Executes the operations to reset the environment. + /// Installs certificates required by Virtual Client on the local system. + /// + /// 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 async Task ExecuteAsync(string[] args, CancellationTokenSource cancellationTokenSource) + { + CancellationToken cancellationToken = cancellationTokenSource.Token; + IServiceCollection dependencies = this.InitializeDependencies(args); + ISystemManagement systemManagement = dependencies.GetService(); + ILogger logger = dependencies.GetService(); + + PlatformSpecifics platformSpecifics = dependencies.GetService(); + PlatformID platform = platformSpecifics.Platform; + + IKeyVaultManager keyVault = dependencies.GetService(); + + if (keyVault == null) + { + throw new DependencyException("Key Vault manager is not available.", ErrorReason.DependencyNotFound); + } + + X509Certificate2 certificate = await keyVault.GetCertificateAsync(this.CertificateName, cancellationToken); + + try + { + Program.LogMessage(this.Logger, $"[Installing Certificates]", EventContext.Persisted()); + + if (platform == PlatformID.Unix) + { + await this.InstallCertificateOnUnixAsync(certificate, dependencies, cancellationToken); + } + else if (platform == PlatformID.Win32NT) + { + await this.InstallCertificateOnWindowsAsync(certificate); + } + else + { + throw new DependencyException( + $"Certificate installation for OS platform '{platform}' is not supported.", + ErrorReason.NotSupported); + } + } + catch (CryptographicException exc) when (exc.Message.Contains("access", StringComparison.OrdinalIgnoreCase)) + { + throw new DependencyException( + $"Certificate installation failed. Local certificate store access permissions denied. Virtual Client must be " + + $"running with Administrative privileges in order to install certificates in the current context.", + ErrorReason.DependencyInstallationFailed); + } + + return 0; + } + + /// + /// Installs the certificate in the appropriate certificate store on a Unix/Linux system. + /// Handles both root and sudo scenarios. + /// + protected virtual async Task InstallCertificateOnUnixAsync(X509Certificate2 certificate, IServiceCollection dependencies, CancellationToken cancellationToken) + { + PlatformSpecifics platformSpecifics = dependencies.GetService(); + ProcessManager processManager = dependencies.GetService(); + IFileSystem fileSystem = dependencies.GetService(); + + // On Unix/Linux systems, we install ther 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 = platformSpecifics.GetEnvironmentVariable(EnvironmentVariable.USER); + string sudoUser = platformSpecifics.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"; + } + + Program.LogMessage(this.Logger, $"Certificate Store = {certificateDirectory}", EventContext.Persisted()); + + if (!fileSystem.Directory.Exists(certificateDirectory)) + { + 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 fileSystem.File.WriteAllBytesAsync( + platformSpecifics.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}'. Run Virtual Client with " + + $"sudo/root privileges to install certificates in privileged locations."); + } + } + + /// + /// Installs the certificate in the appropriate certificate store on a Windows system. + /// + protected virtual Task InstallCertificateOnWindowsAsync(X509Certificate2 certificate) + { + return Task.Run(() => + { + Program.LogMessage(this.Logger, $"Certificate Store = CurrentUser/Personal", EventContext.Persisted()); + using (X509Store store = new X509Store(StoreName.My, StoreLocation.CurrentUser, OpenFlags.ReadWrite)) + { + store.Open(OpenFlags.ReadWrite); + store.Add(certificate); + store.Close(); + } + }); + } + } +} \ 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..8c54d2965e --- /dev/null +++ b/src/VirtualClient/VirtualClient.Main/GetAccessTokenCommand.cs @@ -0,0 +1,76 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace VirtualClient +{ + using Azure.Storage.Blobs; + using Microsoft.CodeAnalysis; + using Microsoft.Extensions.DependencyInjection; + using Microsoft.Extensions.Logging; + using Microsoft.Identity.Client.Platforms.Features.DesktopOs.Kerberos; + using Newtonsoft.Json; + using Polly; + using System; + using System.Collections.Generic; + using System.ComponentModel; + using System.Globalization; + using System.IO; + using System.IO.Abstractions; + using System.Linq; + using System.Net; + using System.Runtime.InteropServices; + using System.Security.Cryptography.X509Certificates; + using System.Threading; + using System.Threading.Tasks; + using VirtualClient.Common; + using VirtualClient.Common.Contracts; + using VirtualClient.Common.Extensions; + using VirtualClient.Common.Telemetry; + using VirtualClient.Contracts; + using VirtualClient.Contracts.Metadata; + using VirtualClient.Contracts.Validation; + using VirtualClient.Logging; + using VirtualClient.Metadata; + + /// + /// 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") + }; + + IServiceCollection dependencies = this.InitializeDependencies(args); + this.Parameters = this.ResolveParameters(); + + return base.ExecuteAsync(args, cancellationTokenSource); + } + + private Dictionary ResolveParameters() + { + ////IKeyVaultManager keyVaultManager = dependencies.GetService(); + ////var a = keyVaultManager.StoreDescription; + + var parameters = new Dictionary(StringComparer.OrdinalIgnoreCase); + + if(string.IsNullOrWhiteSpace(this.KeyVault)) + { + Uri baseUri = new Uri(this.KeyVault); + var store = new DependencyKeyVaultStore("KeyVault", baseUri); + } + + return parameters; + } + } +} diff --git a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs index 7ddc9999ef..c9688b3804 100644 --- a/src/VirtualClient/VirtualClient.Main/OptionFactory.cs +++ b/src/VirtualClient/VirtualClient.Main/OptionFactory.cs @@ -598,6 +598,48 @@ public static Option CreateKeyVaultOption(bool required = false, object defaultV return option; } + /// + /// Command line option defines a certificate name to download from Azure keyvault. + /// + /// Sets this option as required. + /// Sets the default value when none is provided. + public static Option CreateCertNameOption(bool required = false, object defaultValue = null) + { + Option option = new Option( + new string[] { "--certName", "--cert-name", "--cert-Name" }) + { + Name = "CertificateName", + Description = "An endpoint URI or connection string to the Key Vault from which secrets and certificates can be accessed.", + ArgumentHelpName = "CertificatesName", + AllowMultipleArgumentsPerToken = false + }; + + OptionFactory.SetOptionRequirements(option, required, defaultValue); + + return option; + } + + /// + /// Command line option defines an access token to authenticate with Azure key vault. + /// + /// Sets this option as required. + /// Sets the default value when none is provided. + public static Option CreateAccessTokenOption(bool required = false, object defaultValue = null) + { + Option option = new Option( + new string[] { "--token", "--access-token" }) + { + Name = "AccessToken", + Description = "An access token to authenticate with Azure key vault.", + ArgumentHelpName = "Access Token", + AllowMultipleArgumentsPerToken = false + }; + + OptionFactory.SetOptionRequirements(option, required, defaultValue); + + return option; + } + /// /// Command line option defines the path to the environment layout file. /// diff --git a/src/VirtualClient/VirtualClient.Main/Program.cs b/src/VirtualClient/VirtualClient.Main/Program.cs index db50e303ed..309a5f68f3 100644 --- a/src/VirtualClient/VirtualClient.Main/Program.cs +++ b/src/VirtualClient/VirtualClient.Main/Program.cs @@ -330,6 +330,11 @@ internal static CommandLineBuilder SetupCommandLine(string[] args, CancellationT bootstrapSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource)); rootCommand.Add(bootstrapSubcommand); + Command getAccessTokenSubcommand = Program.CreateGetTokenSubCommand(settings); + getAccessTokenSubcommand.TreatUnmatchedTokensAsErrors = true; + getAccessTokenSubcommand.Handler = CommandHandler.Create(cmd => cmd.ExecuteAsync(args, cancellationTokenSource)); + rootCommand.Add(getAccessTokenSubcommand); + Command cleanSubcommand = Program.CreateCleanSubcommand(settings); cleanSubcommand.TreatUnmatchedTokensAsErrors = true; cleanSubcommand.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 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..95cf3519bd --- /dev/null +++ b/src/VirtualClient/VirtualClient.Main/profiles/INSTALL-CERTIFICATES.json @@ -0,0 +1,23 @@ +{ + "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", + "CertificatePassword": "$.Parameters.CertificatePassword" + } + } + ] +} \ No newline at end of file