diff --git a/src/libraries/Common/tests/Extensions/ConfigurationRootTest.cs b/src/libraries/Common/tests/Extensions/ConfigurationRootTest.cs index d61144d0e4313..27c505ae4441f 100644 --- a/src/libraries/Common/tests/Extensions/ConfigurationRootTest.cs +++ b/src/libraries/Common/tests/Extensions/ConfigurationRootTest.cs @@ -11,6 +11,8 @@ namespace Microsoft.Extensions.Configuration.Test { public class ConfigurationRootTest { + private const string SecretCover = "*****"; + [Fact] public void RootDisposesProviders() { @@ -76,12 +78,56 @@ public void ChainedConfigurationIsDisposed(bool shouldDispose) Assert.Equal(shouldDispose, provider.IsDisposed); } + [Fact] + public void SecretsAreConcealed() + { + var provider = new SecretConfigurationProvider("secret", "secret-value"); + + var config = new ConfigurationRoot(new IConfigurationProvider[] { + provider + }); + + var debugView = config.GetDebugView(ProcessValue); + + Assert.Contains(SecretCover, debugView); + } + + [Fact] + public void NonSecretsAreNotConcealed() + { + var provider = new TestConfigurationProvider("foo", "foo-value"); + + var config = new ConfigurationRoot(new IConfigurationProvider[] { + provider + }); + + var debugView = config.GetDebugView(ProcessValue); + + Assert.DoesNotContain(SecretCover, debugView); + } + + private string ProcessValue(ConfigurationDebugViewContext context) + { + if (context.ConfigurationProvider.ToString() == nameof(SecretConfigurationProvider)) + { + return SecretCover; + } + + return context.Value; + } + private class TestConfigurationProvider : ConfigurationProvider { public TestConfigurationProvider(string key, string value) => Data.Add(key, value); } + private class SecretConfigurationProvider : ConfigurationProvider + { + public SecretConfigurationProvider(string key, string value) + => Data.Add(key, value); + } + private class DisposableTestConfigurationProvider : ConfigurationProvider, IDisposable { public bool IsDisposed { get; set; } diff --git a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs index 6c83ffe225776..ea95f74ac8736 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/ref/Microsoft.Extensions.Configuration.Abstractions.cs @@ -32,6 +32,15 @@ public static partial class ConfigurationPath public static partial class ConfigurationRootExtensions { public static string GetDebugView(this Microsoft.Extensions.Configuration.IConfigurationRoot root) { throw null; } + public static string GetDebugView(this IConfigurationRoot root, System.Func? processValue) { throw null; } + } + public readonly partial struct ConfigurationDebugViewContext + { + public ConfigurationDebugViewContext(string path, string key, string? value, IConfigurationProvider configurationProvider) { throw null; } + public string Path { get; } + public string Key { get; } + public string? Value { get; } + public IConfigurationProvider ConfigurationProvider { get; } } public partial interface IConfiguration { diff --git a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationDebugViewContext.cs b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationDebugViewContext.cs new file mode 100644 index 0000000000000..ec53db48792b7 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationDebugViewContext.cs @@ -0,0 +1,39 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +namespace Microsoft.Extensions.Configuration +{ + /// + /// Provides the data about current item of the configuration. + /// + public readonly struct ConfigurationDebugViewContext + { + public ConfigurationDebugViewContext(string path, string key, string? value, IConfigurationProvider configurationProvider) + { + Path = path; + Key = key; + Value = value; + ConfigurationProvider = configurationProvider; + } + + /// + /// Gets the path of the current item. + /// + public string Path { get; } + + /// + /// Gets the key of the current item. + /// + public string Key { get; } + + /// + /// Gets the value of the current item. + /// + public string? Value { get; } + + /// + /// Gets the that was used to get the value of the current item. + /// + public IConfigurationProvider ConfigurationProvider { get; } + } +} diff --git a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationRootExtensions.cs b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationRootExtensions.cs index b6976a734cfa9..6b677b95c09d2 100644 --- a/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationRootExtensions.cs +++ b/src/libraries/Microsoft.Extensions.Configuration.Abstractions/src/ConfigurationRootExtensions.cs @@ -1,6 +1,7 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using System; using System.Collections.Generic; using System.Linq; using System.Text; @@ -17,6 +18,22 @@ public static class ConfigurationRootExtensions /// /// The debug view. public static string GetDebugView(this IConfigurationRoot root) + { + return GetDebugView(root, processValue: null); + } + + /// + /// Generates a human-readable view of the configuration showing where each value came from. + /// + /// Configuration root + /// + /// Function for processing the value e.g. hiding secrets + /// Parameters: + /// ConfigurationDebugViewContext: Context of the current configuration item + /// returns: A string value is used to assign as the Value of the configuration section + /// + /// The debug view. + public static string GetDebugView(this IConfigurationRoot root, Func? processValue) { void RecurseChildren( StringBuilder stringBuilder, @@ -29,11 +46,15 @@ void RecurseChildren( if (valueAndProvider.Provider != null) { + string? value = processValue != null + ? processValue(new ConfigurationDebugViewContext(child.Key, child.Path, valueAndProvider.Value, valueAndProvider.Provider)) + : valueAndProvider.Value; + stringBuilder .Append(indent) .Append(child.Key) .Append('=') - .Append(valueAndProvider.Value) + .Append(value) .Append(" (") .Append(valueAndProvider.Provider) .AppendLine(")");