Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feature - track Azure Key Vault dependency #207

Merged
merged 18 commits into from
Nov 18, 2020
Merged
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
17 changes: 12 additions & 5 deletions docs/preview/features/secret-store/provider/key-vault.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,28 +30,35 @@ public class Program
.ConfigureSecretStore((context, config, builder) =>
{
// Adding the Azure Key Vault secret provider with the built-in overloads
builder.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri);
// `keyVaultUri`: the URI where the Azure Key Vault is located.
builder.AddAzureKeyVaultWithManagedIdentity(keyVaultUri);

// Several other built-in overloads are available too:
// `AddAzureKeyVaultWithServicePrincipal`
// `AddAzureKeyVaultWithCertificate`

// Or, alternatively using the fully customizable approach.
var vaultAuthentication = new ManagedServiceIdentityAuthentication();
// `clientId`: The client id to authenticate for a user assigned managed identity.
// More information on user assigned managed identities can be found here: https://docs.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/overview#how-a-user-assigned-managed-identity-works-with-an-azure-vm</param>
var vaultAuthentication = new ChainedTokenCredential(new ManagedIdentityCredential(clientId), new EnvironmentCredential());
var vaultConfiguration = new KeyVaultConfiguration(keyVaultUri);

builder.AddAzureKeyVault(vaultAuthentication, vaultConfiguration);

// Adding a default cached variant of the Azure Key Vault provider (default: 5 min caching).
builder.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, allowCaching: true);
builder.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, allowCaching: true);

// Assing a configurable cached variant of the Azure Key Vault provider.
var cacheConfiguration = new CacheConfiguration(TimeSpan.FromMinutes(1));
builder.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, cacheConfiguration);
builder.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, cacheConfiguration);

// Adding the Azure Key Vault secret provider, using `-` instead of `:` when looking up secrets.
// Example - When looking up `ServicePrincipal:ClientKey` it will be changed to `ServicePrincipal-ClientKey`.
builder.AddAzureKeyVaultWithManagedServiceIdentity(keyVaultUri, mutateSecretName: secretName => secretName.Replace(":", "-"));
builder.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, mutateSecretName: secretName => secretName.Replace(":", "-"));

// Tracking the Azure Key Vault dependency which works well together with Application Insights (default: `false`).
// See https://observability.arcus-azure.net/features/writing-different-telemetry-types#measuring-custom-dependencies for more information.
builder.AddAzureKeyVaultWithManagedIdentity(keyVaultUri, configureOptions: options => options.TrackDependency = true);
})
.ConfigureWebHostDefaults(webBuilder => webBuilder.UseStartup<Startup>());
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
namespace Arcus.Security.Providers.AzureKeyVault.Configuration
{
/// <summary>
/// Represents the available options to configure extra options of the Azure Key Vault for the <see cref="KeyVaultSecretProvider"/>.
/// </summary>
public class KeyVaultOptions
tomkerkhove marked this conversation as resolved.
Show resolved Hide resolved
{
/// <summary>
/// Gets or sets the flag to indicate whether or not the <see cref="KeyVaultSecretProvider"/> should track the Azure Key Vault dependency.
/// </summary>
public bool TrackDependency { get; set; } = false;
}
}

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;
using Arcus.Observability.Telemetry.Core;
using Arcus.Security.Providers.AzureKeyVault.Authentication;
using Arcus.Security.Providers.AzureKeyVault.Configuration;
using Arcus.Security.Core;
Expand All @@ -12,7 +13,6 @@
using GuardNet;
using Microsoft.Azure.KeyVault;
using Microsoft.Azure.KeyVault.Models;
using Microsoft.Rest.TransientFaultHandling;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Abstractions;
using Polly;
Expand All @@ -25,6 +25,11 @@ namespace Arcus.Security.Providers.AzureKeyVault
/// </summary>
public class KeyVaultSecretProvider : ISecretProvider
{
/// <summary>
/// Gets the name of the dependency that can be used to track the Azure Key Vault resource in Application Insights.
/// </summary>
protected const string DependencyName = "Azure key vault";

/// <summary>
/// Gets the pattern which the Azure Key Vault URI should match against. (See https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-and-versioning).
/// </summary>
Expand All @@ -47,6 +52,7 @@ public class KeyVaultSecretProvider : ISecretProvider

private readonly IKeyVaultAuthentication _authentication;
private readonly SecretClient _secretClient;
private readonly KeyVaultOptions _options;
private readonly bool _isUsingAzureSdk;

private IKeyVaultClient _keyVaultClient;
Expand All @@ -60,8 +66,9 @@ public class KeyVaultSecretProvider : ISecretProvider
/// <param name="vaultConfiguration">Configuration related to the Azure Key Vault instance to use</param>
/// <exception cref="ArgumentNullException">The <paramref name="authentication"/> cannot be <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="vaultConfiguration"/> cannot be <c>null</c>.</exception>
[Obsolete("Use the constructor without the " + nameof(IKeyVaultAuthentication) + " but with the Azure SDK " + nameof(TokenCredential) + " instead")]
public KeyVaultSecretProvider(IKeyVaultAuthentication authentication, IKeyVaultConfiguration vaultConfiguration)
: this(authentication, vaultConfiguration, NullLogger<KeyVaultSecretProvider>.Instance)
: this(authentication, vaultConfiguration, new KeyVaultOptions(), NullLogger<KeyVaultSecretProvider>.Instance)
{
}

Expand All @@ -70,10 +77,12 @@ public KeyVaultSecretProvider(IKeyVaultAuthentication authentication, IKeyVaultC
/// </summary>
/// <param name="authentication">.The requested authentication type for connecting to the Azure Key Vault instance.</param>
/// <param name="vaultConfiguration">The configuration related to the Azure Key Vault instance to use.</param>
/// <param name="options">The additional options to configure the provider.</param>
/// <param name="logger">The logger to write diagnostic trace messages during the interaction with the Azure Key Vault.</param>
/// <exception cref="ArgumentNullException">The <paramref name="authentication"/> cannot be <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="vaultConfiguration"/> cannot be <c>null</c>.</exception>
public KeyVaultSecretProvider(IKeyVaultAuthentication authentication, IKeyVaultConfiguration vaultConfiguration, ILogger<KeyVaultSecretProvider> logger)
[Obsolete("Use the constructor without the " + nameof(IKeyVaultAuthentication) + " but with the Azure SDK " + nameof(TokenCredential) + " instead")]
public KeyVaultSecretProvider(IKeyVaultAuthentication authentication, IKeyVaultConfiguration vaultConfiguration, KeyVaultOptions options, ILogger<KeyVaultSecretProvider> logger)
{
Guard.NotNull(vaultConfiguration, nameof(vaultConfiguration), "Requires a Azure Key Vault configuration to setup the secret provider");
Guard.NotNull(authentication, nameof(authentication), "Requires an Azure Key Vault authentication instance to authenticate with the vault");
Expand All @@ -84,6 +93,7 @@ public KeyVaultSecretProvider(IKeyVaultAuthentication authentication, IKeyVaultC
"Requires the Azure Key Vault host to be in the right format, see https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-and-versioning");

_authentication = authentication;
_options = options;
_isUsingAzureSdk = false;

Logger = logger ?? NullLogger<KeyVaultSecretProvider>.Instance;
Expand All @@ -97,7 +107,7 @@ public KeyVaultSecretProvider(IKeyVaultAuthentication authentication, IKeyVaultC
/// <exception cref="ArgumentNullException">The <paramref name="tokenCredential"/> cannot be <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="vaultConfiguration"/> cannot be <c>null</c>.</exception>
public KeyVaultSecretProvider(TokenCredential tokenCredential, IKeyVaultConfiguration vaultConfiguration)
: this(tokenCredential, vaultConfiguration, NullLogger<KeyVaultSecretProvider>.Instance)
: this(tokenCredential, vaultConfiguration, new KeyVaultOptions(), NullLogger<KeyVaultSecretProvider>.Instance)
{
}

Expand All @@ -106,10 +116,11 @@ public KeyVaultSecretProvider(TokenCredential tokenCredential, IKeyVaultConfigur
/// </summary>
/// <param name="tokenCredential">The requested authentication type for connecting to the Azure Key Vault instance</param>
/// <param name="vaultConfiguration">Configuration related to the Azure Key Vault instance to use</param>
/// <param name="options">The additional options to configure the provider.</param>
/// <param name="logger">The logger to write diagnostic trace messages during the interaction with the Azure Key Vault.</param>
/// <exception cref="ArgumentNullException">The <paramref name="tokenCredential"/> cannot be <c>null</c>.</exception>
/// <exception cref="ArgumentNullException">The <paramref name="vaultConfiguration"/> cannot be <c>null</c>.</exception>
public KeyVaultSecretProvider(TokenCredential tokenCredential, IKeyVaultConfiguration vaultConfiguration, ILogger<KeyVaultSecretProvider> logger)
public KeyVaultSecretProvider(TokenCredential tokenCredential, IKeyVaultConfiguration vaultConfiguration, KeyVaultOptions options, ILogger<KeyVaultSecretProvider> logger)
{
Guard.NotNull(vaultConfiguration, nameof(vaultConfiguration), "Requires a Azure Key Vault configuration to setup the secret provider");
Guard.NotNull(tokenCredential, nameof(tokenCredential), "Requires an Azure Key Vault authentication instance to authenticate with the vault");
Expand All @@ -120,6 +131,7 @@ public KeyVaultSecretProvider(TokenCredential tokenCredential, IKeyVaultConfigur
"Requires the Azure Key Vault host to be in the right format, see https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-and-versioning");

_secretClient = new SecretClient(vaultConfiguration.VaultUri, tokenCredential);
_options = options;
_isUsingAzureSdk = true;

Logger = logger ?? NullLogger<KeyVaultSecretProvider>.Instance;
Expand Down Expand Up @@ -167,6 +179,28 @@ public virtual async Task<Secret> GetSecretAsync(string secretName)
Guard.NotNullOrWhitespace(secretName, nameof(secretName), "Requires a non-blank secret name to request a secret in Azure Key Vault");
Guard.For<FormatException>(() => !SecretNameRegex.IsMatch(secretName), "Requires a secret name in the correct format to request a secret in Azure Key Vault, see https://docs.microsoft.com/en-us/azure/key-vault/general/about-keys-secrets-certificates#objects-identifiers-and-versioning");

var isSuccessful = false;
using (DependencyMeasurement measurement = DependencyMeasurement.Start())
{
try
{
Secret secret = await GetSecretCoreAsync(secretName);
isSuccessful = true;

return secret;
}
finally
{
if (_options.TrackDependency)
{
Logger.LogDependency(DependencyName, secretName, VaultUri, isSuccessful, measurement);
}
}
}
}

private async Task<Secret> GetSecretCoreAsync(string secretName)
{
if (_isUsingAzureSdk)
{
Secret secret = await GetSecretUsingSecretClientAsync(secretName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
<ItemGroup>
<PackageReference Include="Guard.Net" Version="1.2.0" />
<PackageReference Include="Arcus.Testing.Logging" Version="0.2.0-preview-1" />
<PackageReference Include="Serilog" Version="2.10.0" />
</ItemGroup>

</Project>
30 changes: 30 additions & 0 deletions src/Arcus.Security.Tests.Core/Stubs/InMemoryLogSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using Serilog.Core;
using Serilog.Events;

namespace Arcus.Security.Tests.Core.Stubs
{
/// <summary>
/// Represents a test <see cref="ILogEventSink"/> implementation to store the Serilog log events in-memory.
/// </summary>
public class InMemoryLogSink : ILogEventSink
{
private readonly ICollection<LogEvent> _events = new Collection<LogEvent>();

/// <summary>
/// Gets the currently logged events; stored in-memory.
/// </summary>
public IEnumerable<LogEvent> LogEvents => _events.AsEnumerable();

/// <summary>
/// Emit the provided log event to the sink.
/// </summary>
/// <param name="logEvent">The log event to write.</param>
public void Emit(LogEvent logEvent)
{
_events.Add(logEvent);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@
<PackageReference Include="Microsoft.Extensions.Logging" Version="2.2.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Arcus.Observability.Telemetry.Serilog.Sinks.ApplicationInsights" Version="1.0.0" />
<PackageReference Include="Guard.Net" Version="1.2.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.7.1" />
<PackageReference Include="Serilog.Extensions.Hosting" Version="3.1.0" />
<PackageReference Include="Vault" Version="0.9.1.3" />
<PackageReference Include="xunit" Version="2.4.1" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.4.3">
Expand Down
37 changes: 37 additions & 0 deletions src/Arcus.Security.Tests.Integration/Fixture/XunitTestLogSink.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using System;
using GuardNet;
using Serilog.Core;
using Serilog.Events;
using Xunit.Abstractions;

namespace Arcus.Security.Tests.Integration.Fixture
{
/// <summary>
/// xUnit test implementation of an Serilog <see cref="ILogEventSink"/> to delegate Serilog events to the xUnit <see cref="ITestOutputHelper"/>.
/// </summary>
public class XunitTestLogSink : ILogEventSink
{
private readonly ITestOutputHelper _outputWriter;

/// <summary>
/// Initializes a new instance of the <see cref="XunitTestLogSink"/> class.
/// </summary>
/// <param name="outputWriter">The xUnit test output helper to delegate the Serilog log events to.</param>
/// <exception cref="ArgumentNullException">Thrown when the <paramref name="outputWriter"/> is <c>null</c>.</exception>
public XunitTestLogSink(ITestOutputHelper outputWriter)
{
Guard.NotNull(outputWriter, nameof(outputWriter), "Requires a xUnit test output helper to write Serilog log events to the xUnit test output");
_outputWriter = outputWriter;
}

/// <summary>
/// Emit the provided log event to the sink.
/// </summary>
/// <param name="logEvent">The log event to write.</param>
public void Emit(LogEvent logEvent)
{
string message = logEvent.RenderMessage();
_outputWriter.WriteLine(message);
}
}
}
32 changes: 27 additions & 5 deletions src/Arcus.Security.Tests.Integration/IntegrationTest.cs
Original file line number Diff line number Diff line change
@@ -1,21 +1,43 @@
using Arcus.Security.Tests.Integration.Fixture;
using System;
using Arcus.Security.Tests.Core.Stubs;
using Arcus.Security.Tests.Integration.Fixture;
using Arcus.Testing.Logging;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Serilog;
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Extensions.Logging;
using Xunit.Abstractions;

namespace Arcus.Security.Tests.Integration
{
public class IntegrationTest
public class IntegrationTest : IDisposable
{
protected TestConfig Configuration { get; }
protected XunitTestLogger Logger { get; }
protected Logger Logger { get; }
protected InMemoryLogSink InMemoryLogSink { get; }

public IntegrationTest(ITestOutputHelper testOutput)
{
Logger = new XunitTestLogger(testOutput);

// The appsettings.local.json allows users to override (gitignored) settings locally for testing purposes
Configuration = TestConfig.Create();
InMemoryLogSink = new InMemoryLogSink();

var configuration = new LoggerConfiguration()
.WriteTo.Sink(new XunitTestLogSink(testOutput))
.WriteTo.Sink(InMemoryLogSink)
.WriteTo.AzureApplicationInsights(Configuration.GetValue<string>("Arcus:ApplicationInsights:InstrumentationKey"));

Logger = configuration.CreateLogger();
}

/// <summary>
/// Performs application-defined tasks associated with freeing, releasing, or resetting unmanaged resources.
/// </summary>
public void Dispose()
{
Logger.Dispose();
}
}
}
Loading