From 9a0064285d879b0da9381c0715c983d3314eaf7f Mon Sep 17 00:00:00 2001 From: Pavel Krymets Date: Tue, 31 Oct 2017 10:13:53 -0700 Subject: [PATCH] KeyVault HostingStartup (#114) --- .../AssemblyInfo.cs | 6 + ...r.cs => AzureAppServicesHostingStartup.cs} | 13 +- .../AzureKeyVaultHostingStartup.cs | 70 +++++++++ .../HostingStartupConfigurationExtensions.cs | 33 ++++ ...ore.AzureAppServices.HostingStartup.csproj | 8 + ...Core.AzureAppServices.SiteExtension.csproj | 2 +- .../HostingStartupTests.cs | 144 ++++++++++++++++++ ...e.AzureAppServicesIntegration.Tests.csproj | 1 + 8 files changed, 272 insertions(+), 5 deletions(-) create mode 100644 src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AssemblyInfo.cs rename src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/{AzureStartupLoader.cs => AzureAppServicesHostingStartup.cs} (65%) create mode 100644 src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureKeyVaultHostingStartup.cs create mode 100644 src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/HostingStartupConfigurationExtensions.cs create mode 100644 test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/HostingStartupTests.cs diff --git a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AssemblyInfo.cs b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AssemblyInfo.cs new file mode 100644 index 0000000000..6852ee94a9 --- /dev/null +++ b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AssemblyInfo.cs @@ -0,0 +1,6 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Microsoft.AspNetCore.AzureAppServicesIntegration.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")] diff --git a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureStartupLoader.cs b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureAppServicesHostingStartup.cs similarity index 65% rename from src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureStartupLoader.cs rename to src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureAppServicesHostingStartup.cs index 6bc3616701..5987e7cb58 100644 --- a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureStartupLoader.cs +++ b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureAppServicesHostingStartup.cs @@ -5,9 +5,6 @@ using Microsoft.AspNetCore.Hosting; [assembly: HostingStartup(typeof(Microsoft.AspNetCore.AzureAppServices.HostingStartup.AzureAppServicesHostingStartup))] -// To be able to build as Exe -internal class Program { public static void Main() { } } - namespace Microsoft.AspNetCore.AzureAppServices.HostingStartup { /// @@ -15,13 +12,21 @@ namespace Microsoft.AspNetCore.AzureAppServices.HostingStartup /// public class AzureAppServicesHostingStartup : IHostingStartup { + private const string HostingStartupName = "AppServices"; + private const string DiagnosticsFeatureName = "DiagnosticsEnabled"; + /// /// Calls UseAzureAppServices /// /// public void Configure(IWebHostBuilder builder) { - builder.UseAzureAppServices(); + var baseConfiguration = HostingStartupConfigurationExtensions.GetBaseConfiguration(); + + if (baseConfiguration.IsEnabled(HostingStartupName, DiagnosticsFeatureName)) + { + builder.UseAzureAppServices(); + } } } } diff --git a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureKeyVaultHostingStartup.cs b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureKeyVaultHostingStartup.cs new file mode 100644 index 0000000000..e9c2766669 --- /dev/null +++ b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/AzureKeyVaultHostingStartup.cs @@ -0,0 +1,70 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.AspNetCore.DataProtection; +using Microsoft.AspNetCore.Hosting; +using Microsoft.Azure.KeyVault; +using Microsoft.Azure.Services.AppAuthentication; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.AzureKeyVault; +using Microsoft.Extensions.DependencyInjection; + +[assembly: HostingStartup(typeof(Microsoft.AspNetCore.AzureKeyVault.HostingStartup.AzureKeyVaultHostingStartup))] + +namespace Microsoft.AspNetCore.AzureKeyVault.HostingStartup +{ + /// + /// A dynamic KeyVault lightup experience + /// + public class AzureKeyVaultHostingStartup : IHostingStartup + { + private const string HostingStartupName = "KeyVault"; + private const string ConfigurationFeatureName = "ConfigurationEnabled"; + private const string ConfigurationVaultName = "ConfigurationVault"; + private const string DataProtectionFeatureName = "DataProtectionEnabled"; + private const string DataProtectionKeyName = "DataProtectionKey"; + + /// + public void Configure(IWebHostBuilder builder) + { + var azureServiceTokenProvider = new AzureServiceTokenProvider(); + var authenticationCallback = new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback); + var keyVaultClient = new KeyVaultClient(authenticationCallback); + + var baseConfiguration = HostingStartupConfigurationExtensions.GetBaseConfiguration(); + + builder.ConfigureServices((context, collection) => + { + var configuration = new ConfigurationBuilder() + .AddConfiguration(baseConfiguration) + .AddConfiguration(context.Configuration) + .Build(); + + if (configuration.IsEnabled(HostingStartupName, DataProtectionFeatureName) && + configuration.TryGetOption(HostingStartupName, DataProtectionKeyName, out var protectionKey)) + { + AddDataProtection(collection, keyVaultClient, protectionKey); + } + }); + + if (baseConfiguration.IsEnabled(HostingStartupName, ConfigurationFeatureName) && + baseConfiguration.TryGetOption(HostingStartupName, ConfigurationVaultName, out var vault)) + { + builder.ConfigureAppConfiguration((context, configurationBuilder) => + { + AddConfiguration(configurationBuilder, keyVaultClient, vault); + }); + } + } + + internal virtual void AddDataProtection(IServiceCollection serviceCollection, KeyVaultClient client, string protectionKey) + { + serviceCollection.AddDataProtection().ProtectKeysWithAzureKeyVault(client, protectionKey); + } + + internal virtual void AddConfiguration(IConfigurationBuilder configurationBuilder, KeyVaultClient client, string keyVault) + { + configurationBuilder.AddAzureKeyVault(keyVault, client, new DefaultKeyVaultSecretManager()); + } + } +} diff --git a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/HostingStartupConfigurationExtensions.cs b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/HostingStartupConfigurationExtensions.cs new file mode 100644 index 0000000000..55ffdc6710 --- /dev/null +++ b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/HostingStartupConfigurationExtensions.cs @@ -0,0 +1,33 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using Microsoft.Extensions.Configuration; + +namespace Microsoft.AspNetCore.Hosting +{ + internal static class HostingStartupConfigurationExtensions + { + public static IConfiguration GetBaseConfiguration() + { + return new ConfigurationBuilder() + .AddEnvironmentVariables(prefix: "ASPNETCORE_") + .Build(); + } + public static bool IsEnabled(this IConfiguration configuration, string hostingStartupName, string featureName) + { + if (configuration.TryGetOption(hostingStartupName, featureName, out var value)) + { + value = value.ToLowerInvariant(); + return value != "false" && value != "0"; + } + + return true; + } + + public static bool TryGetOption(this IConfiguration configuration, string hostingStartupName, string featureName, out string value) + { + value = configuration[$"HostingStartup:{hostingStartupName}:{featureName}"]; + return !string.IsNullOrEmpty(value); + } + } +} \ No newline at end of file diff --git a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/Microsoft.AspNetCore.AzureAppServices.HostingStartup.csproj b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/Microsoft.AspNetCore.AzureAppServices.HostingStartup.csproj index 140db603d0..6b2d556083 100644 --- a/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/Microsoft.AspNetCore.AzureAppServices.HostingStartup.csproj +++ b/src/Microsoft.AspNetCore.AzureAppServices.HostingStartup/Microsoft.AspNetCore.AzureAppServices.HostingStartup.csproj @@ -17,4 +17,12 @@ + + + + + + + + diff --git a/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj b/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj index c286c14893..71fc124024 100644 --- a/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj +++ b/src/Microsoft.AspNetCore.AzureAppServices.SiteExtension/Microsoft.AspNetCore.AzureAppServices.SiteExtension.csproj @@ -5,7 +5,7 @@ This extension enables additional functionality for ASP.NET Core on Azure WebSites, such as enabling Azure logging. net461 false - aspnet;logging;aspnetcore;AzureSiteExtension + aspnet;logging;aspnetcore;AzureSiteExtension;keyvault;configuration;dataprotection AzureSiteExtension false content diff --git a/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/HostingStartupTests.cs b/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/HostingStartupTests.cs new file mode 100644 index 0000000000..62bd412f41 --- /dev/null +++ b/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/HostingStartupTests.cs @@ -0,0 +1,144 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Hosting; +using Microsoft.AspNetCore.TestHost; +using Microsoft.Azure.KeyVault; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Microsoft.AspNetCore.AzureKeyVault.HostingStartup.Tests +{ + public class HostinStartupTests + { + [Fact] + public void Configure_AddsDataProtection() + { + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__DataProtectionEnabled", null); + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__DataProtectionKey", "http://vault"); + + var callbackCalled = false; + var builder = new WebHostBuilder().Configure(app => { }); + var mockHostingStartup = new MockAzureKeyVaultHostingStartup( + (collection, client, key) => + { + callbackCalled = true; + Assert.NotNull(collection); + Assert.NotNull(client); + Assert.Equal("http://vault", key); + }, + (configurationBuilder, client, vault) => {} + ); + + mockHostingStartup.Configure(builder); + var _ = new TestServer(builder); + + Assert.True(callbackCalled); + } + + [Theory] + [InlineData("0")] + [InlineData("FALSE")] + [InlineData("false")] + public void Configure_SkipsAddsDataProtection_IfDisabled(string value) + { + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__DataProtectionEnabled", value); + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__DataProtectionKey", "http://vault"); + + var callbackCalled = false; + var builder = new WebHostBuilder().Configure(app => { }); + var mockHostingStartup = new MockAzureKeyVaultHostingStartup( + (collection, client, key) => + { + callbackCalled = true; + }, + (configurationBuilder, client, vault) => {} + ); + + mockHostingStartup.Configure(builder); + var _ = new TestServer(builder); + + Assert.False(callbackCalled); + } + + [Fact] + public void Configure_AddsConfiguration() + { + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__ConfigurationEnabled", null); + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__ConfigurationVault", "http://vault"); + + var callbackCalled = false; + var builder = new WebHostBuilder().Configure(app => { }); + + var mockHostingStartup = new MockAzureKeyVaultHostingStartup( + (collection, client, key) => { }, + (configurationBuilder, client, vault) => + { + callbackCalled = true; + Assert.NotNull(configurationBuilder); + Assert.NotNull(client); + Assert.Equal("http://vault", vault); + } + ); + + mockHostingStartup.Configure(builder); + var _ = new TestServer(builder); + + Assert.True(callbackCalled); + } + + [Theory] + [InlineData("0")] + [InlineData("FALSE")] + [InlineData("false")] + public void Configure_SkipsConfiguration_IfDisabled(string value) + { + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__ConfigurationEnabled", value); + Environment.SetEnvironmentVariable("ASPNETCORE_HostingStartup__KeyVault__ConfigurationVault", "http://vault"); + + var callbackCalled = false; + var builder = new WebHostBuilder().Configure(app => { }); + + var mockHostingStartup = new MockAzureKeyVaultHostingStartup( + (collection, client, key) => { }, + (configurationBuilder, client, vault) => + { + callbackCalled = true; + } + ); + + mockHostingStartup.Configure(builder); + var _ = new TestServer(builder); + + Assert.False(callbackCalled); + } + + private class MockAzureKeyVaultHostingStartup : AzureKeyVaultHostingStartup + { + private readonly Action _dataProtectionCallback; + + private readonly Action _configurationCallback; + + public MockAzureKeyVaultHostingStartup( + Action dataProtectionCallback, + Action configurationCallback) + { + _dataProtectionCallback = dataProtectionCallback; + _configurationCallback = configurationCallback; + } + + internal override void AddDataProtection(IServiceCollection serviceCollection, KeyVaultClient client, string protectionKey) + { + _dataProtectionCallback(serviceCollection, client, protectionKey); + } + + internal override void AddConfiguration(IConfigurationBuilder configurationBuilder, KeyVaultClient client, string keyVault) + { + _configurationCallback(configurationBuilder, client, keyVault); + } + } + } +} diff --git a/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests.csproj b/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests.csproj index 6e199d7fe2..8168b6f5a7 100644 --- a/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests.csproj +++ b/test/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests/Microsoft.AspNetCore.AzureAppServicesIntegration.Tests.csproj @@ -7,6 +7,7 @@ +