Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Large diffs are not rendered by default.

184 changes: 184 additions & 0 deletions src/VirtualClient/VirtualClient.Dependencies/KeyVaultAccessToken.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Virtual Client component that acquires an access token for an Azure Key Vault
/// using interactive browser or device-code authentication.
/// </summary>
public class KeyVaultAccessToken : VirtualClientComponent
{
private IFileSystem fileSystem;

/// <summary>
/// Initializes a new instance of the <see cref="KeyVaultAccessToken"/> class.
/// </summary>
/// <param name="dependencies">Provides all of the required dependencies to the Virtual Client component.</param>
/// <param name="parameters">Parameters to the Virtual Client component.</param>
public KeyVaultAccessToken(IServiceCollection dependencies, IDictionary<string, IConvertible> parameters = null)
: base(dependencies, parameters)
{
this.fileSystem = dependencies.GetService<IFileSystem>();
this.fileSystem.ThrowIfNull(nameof(this.fileSystem));
}

/// <summary>
/// The Azure tenant ID used when requesting an access token for the Key Vault.
/// </summary>
protected string TenantId
{
get
{
return this.Parameters.GetValue<string>(nameof(this.TenantId));
}
}

/// <summary>
/// The Azure Key Vault URI for which an access token will be requested.
/// Example: https://anyvault.vault.azure.net/
/// </summary>
protected string KeyVaultUri
{
get
{
return this.Parameters.GetValue<string>(nameof(this.KeyVaultUri));
}
}

/// <summary>
/// The full file path where the acquired access token will be written,
/// when configured via <see cref="VirtualClientComponent.LogFileName"/> / <see cref="VirtualClientComponent.LogFolderName"/>.
/// </summary>
protected string AccessTokenPath { get; set; }

/// <summary>
/// Initializes the component for execution, including resolving the access token
/// output path and removing any existing token file if configured.
/// </summary>
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);
}
}
}

/// <summary>
/// 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.
/// </summary>
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);
}
}
}
}
196 changes: 196 additions & 0 deletions src/VirtualClient/VirtualClient.Main/CertificateInstallation.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Command resets the Virtual Client environment for "first time" run scenarios.
/// </summary>
public class CertificateInstallation : CommandBase
{
/// <summary>
/// Logger
/// </summary>
protected ILogger Logger { get; set; }

/// <summary>
/// Certificate Name.
/// </summary>
protected string CertificateName { get; set; }

/// <summary>
/// Executes the operations to reset the environment.
/// Installs certificates required by Virtual Client on the local system.
/// </summary>
/// <param name="args">The arguments provided to the application on the command line.</param>
/// <param name="cancellationTokenSource">Provides a token that can be used to cancel the command operations.</param>
/// <returns>The exit code for the command operations.</returns>
public override async Task<int> ExecuteAsync(string[] args, CancellationTokenSource cancellationTokenSource)
{
CancellationToken cancellationToken = cancellationTokenSource.Token;
IServiceCollection dependencies = this.InitializeDependencies(args);
ISystemManagement systemManagement = dependencies.GetService<ISystemManagement>();
ILogger logger = dependencies.GetService<ILogger>();

PlatformSpecifics platformSpecifics = dependencies.GetService<PlatformSpecifics>();
PlatformID platform = platformSpecifics.Platform;

IKeyVaultManager keyVault = dependencies.GetService<IKeyVaultManager>();

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;
}

/// <summary>
/// Installs the certificate in the appropriate certificate store on a Unix/Linux system.
/// Handles both root and sudo scenarios.
/// </summary>
protected virtual async Task InstallCertificateOnUnixAsync(X509Certificate2 certificate, IServiceCollection dependencies, CancellationToken cancellationToken)
{
PlatformSpecifics platformSpecifics = dependencies.GetService<PlatformSpecifics>();
ProcessManager processManager = dependencies.GetService<ProcessManager>();
IFileSystem fileSystem = dependencies.GetService<IFileSystem>();

// 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<DependencyException>();
}
}
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.");
}
}

/// <summary>
/// Installs the certificate in the appropriate certificate store on a Windows system.
/// </summary>
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();
}
});
}
}
}
Loading
Loading