Skip to content

[API Proposal]: Customizable IConfigurationRoot.GetDebugView() for hiding the value #60065

@oskrabanek

Description

@oskrabanek

Background and motivation

As the GetDebugView() method will display the whole configuration, we want to have it in logs for debug purposes. This brings a security risk as some secrets such as connection string, Identity credentials and others might be included in the configuration.

API Proposal

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

namespace Microsoft.Extensions.Configuration
{
    /// <summary>
    /// Extension methods for <see cref="IConfigurationRoot"/>.
    /// </summary>
    public static class ConfigurationRootExtensions
    {
        /// <summary>
        /// Generates a human-readable view of the configuration showing where each value came from.
        /// </summary>
        /// <returns> The debug view. </returns>
        public static string GetDebugView(this IConfigurationRoot root)
        {
            return GetDebugView(root, null);
        }

        /// <summary>
        /// Generates a human-readable view of the configuration showing where each value came from.
        /// </summary>
        /// <param name="root">Configuration root</param>
        /// <param name="processValue">
        /// Function for processing the value e.g. hiding secrets
        /// Parameters:
        ///   Key: Key of the current configuration section
        ///   Path: Full path to the configuration section
        ///   Value: Value of the configuration section
        ///   ConfigurationProvider: Provider of the value of the configuration section
        ///   returns: Value is used to assign as the Value of the configuration section
        /// </param>
        /// <returns> The debug view. </returns>
        public static string GetDebugView(this IConfigurationRoot root, Func<string, string, string, IConfigurationProvider, string> processValue)
        {
            void RecurseChildren(
                StringBuilder stringBuilder,
                IEnumerable<IConfigurationSection> children,
                string indent)
            {
                foreach (IConfigurationSection child in children)
                {
                    (string Value, IConfigurationProvider Provider) valueAndProvider = GetValueAndProvider(root, child.Path);

                    if (valueAndProvider.Provider != null)
                    {
                        var value = processValue != null
                            ? processValue(child.Key, child.Path, valueAndProvider.Value, valueAndProvider.Provider)
                            : valueAndProvider.Value;

                        stringBuilder
                            .Append(indent)
                            .Append(child.Key)
                            .Append('=')
                            .Append(value)
                            .Append(" (")
                            .Append(valueAndProvider.Provider)
                            .AppendLine(")");
                    }
                    else
                    {
                        stringBuilder
                            .Append(indent)
                            .Append(child.Key)
                            .AppendLine(":");
                    }

                    RecurseChildren(stringBuilder, child.GetChildren(), indent + "  ");
                }
            }

            var builder = new StringBuilder();

            RecurseChildren(builder, root.GetChildren(), "");

            return builder.ToString();
        }

        private static (string Value, IConfigurationProvider Provider) GetValueAndProvider(
            IConfigurationRoot root,
            string key)
        {
            foreach (IConfigurationProvider provider in root.Providers.Reverse())
            {
                if (provider.TryGet(key, out string value))
                {
                    return (value, provider);
                }
            }

            return (null, null);
        }
    }
}

API Usage

public Startup(IWebHostEnvironment env, IConfiguration configuration)
{
    ...
  
    if (configuration is IConfigurationRoot configurationRoot)
    {
        var startupLoggerFactory = StartupLoggerHelper.CreateStartupLoggerFactory(configuration, env);
        var startupLogger = startupLoggerFactory.CreateLogger<Startup>();
        startupLogger.LogDebug(configurationRoot.GetDebugView(HideSecrets));
    }
}

private string HideSecrets(string key, string path, string value, IConfigurationProvider provider)
{
    var providerName = provider.ToString();
    if (providerName.Contains("KeyVault"))
    {
        return "*** secret ***";
    }

    return value;
}

Risks

None as far as I know.

Metadata

Metadata

Assignees

No one assigned

    Labels

    api-approvedAPI was approved in API review, it can be implementedarea-Extensions-Configurationhelp wanted[up-for-grabs] Good issue for external contributors

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions