dotnet / runtime

.NET is a cross-platform runtime for cloud, mobile, desktop, and IoT apps.
https://docs.microsoft.com/dotnet/core/
MIT License
14.97k stars 4.66k forks source link

[API Proposal]: EnvironmentVariablesConfigurationProvider support custom key and value mutation #61169

Open marknadig opened 2 years ago

marknadig commented 2 years ago

Background and motivation

I would like to be able to store environment variables in an encrypted form. To facilitate this I need to be able to mutate the values after they are read. I'm proposing the EnvironmentVariablesConfigurationProvider support an optional translator lambda that accepts a DictionaryKey which would mutate the key/value, or perhaps two ref strings would be cleaner since DictionaryKey's Key and Value are object.

For example, environment variable: SET MyProduct_SecretEnc=RE9UTkVUUk9DS1M=

should result in a key Secret with value DOTNETROCKS with Translator:

(ref key, rev value) => {
    if (key.EndsWith("Enc") {
       key = key.Substring(0, key.Length - 3);
       value = Encoding.UTF8.GetString(Convert.FromBase64String(value));
    }
}

I'd like to see if this is something you'd consider in a PR before I submit.

API Proposal

namespace Microsoft.Extensions.Configuration.EnvironmentVariables
{
    /// <summary>
    /// Represents environment variables as an <see cref="IConfigurationSource"/>.
    /// </summary>
    public class EnvironmentVariablesConfigurationSource : IConfigurationSource
    {
        /// <summary>
        /// A method to translate the key and value before storing in the collection.
        /// </summary>
        public Action<ref string, ref string> Translator { get; set; }

... pass Translator in provider's ctor and apply in AddIfPrefixed()

namespace Microsoft.Extensions.Configuration.EnvironmentVariables
{
    /// <summary>
    /// An environment variable based <see cref="ConfigurationProvider"/>.
    /// </summary>
    public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider
    {

        private void AddIfPrefixed(Dictionary<string, string> data, string key, string value)
        {
            if (key.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase))
            {
                key = key.Substring(_prefix.Length);
                data[key] = value;
                if (_translator is not null)  {
                   _translator(ref key, ref value)
                }
            }
        }

API Usage

public static IHostBuilder CreateHostBuilder(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureAppConfiguration((hostingContext, config) =>
        {
            config.AddEnvironmentVariables(prefix: "MyProduct_", translator: (ref key, rev value) => {
                if (key.EndsWith("Enc") {
                   key = key.Substring(0, key.Length - 3);
                   value = Encoding.UTF8.GetString(Convert.FromBase64String(value));
                }
            });
})

Alternative Designs

No response

Risks

Given this would be a new optional parameter, seems risk is low.

@area-Extensions-Configuration

@maryamariyan @michaelgsharp @safern @tarekgh

dotnet-issue-labeler[bot] commented 2 years ago

I couldn't figure out the best area label to add to this issue. If you have write-permissions please help me learn by adding exactly one area label.

ghost commented 2 years ago

Tagging subscribers to this area: @maryamariyan, @safern See info in area-owners.md if you want to be subscribed.

Issue Details
### Background and motivation I would like to be able to store environment variables in an encrypted form. To facilitate this I need to be able to mutate the values after they are read. I'm proposing the EnvironmentVariablesConfigurationProvider support an optional translator lambda that accepts a DictionaryKey which would mutate the key/value, or perhaps two `ref` strings would be cleaner since DictionaryKey's Key and Value are `object`. For example, environment variable: `SET MyProduct_SecretEnc=RE9UTkVUUk9DS1M=` should result in a key `Secret` with value `DOTNETROCKS` with Translator: ``` (ref key, rev value) => { if (key.EndsWith("Enc") { key = key.Substring(0, key.Length - 3); value = Encoding.UTF8.GetString(Convert.FromBase64String(value)); } } ``` I'd like to see if this is something you'd consider in a PR before I submit. ### API Proposal ```C# namespace Microsoft.Extensions.Configuration.EnvironmentVariables { /// /// Represents environment variables as an . /// public class EnvironmentVariablesConfigurationSource : IConfigurationSource { /// /// A method to translate the key and value before storing in the collection. /// public Action Translator { get; set; } ... pass Translator in provider's ctor and apply in AddIfPrefixed() namespace Microsoft.Extensions.Configuration.EnvironmentVariables { /// /// An environment variable based . /// public class EnvironmentVariablesConfigurationProvider : ConfigurationProvider { private void AddIfPrefixed(Dictionary data, string key, string value) { if (key.StartsWith(_prefix, StringComparison.OrdinalIgnoreCase)) { key = key.Substring(_prefix.Length); data[key] = value; if (_translator is not null) { _translator(ref key, ref value) } } } ``` ### API Usage ```C# public static IHostBuilder CreateHostBuilder(string[] args) => Host.CreateDefaultBuilder(args) .ConfigureAppConfiguration((hostingContext, config) => { config.AddEnvironmentVariables(prefix: "MyProduct_", translator: (ref key, rev value) => { if (key.EndsWith("Enc") { key = key.Substring(0, key.Length - 3); value = Encoding.UTF8.GetString(Convert.FromBase64String(value)); } }); }) ``` ### Alternative Designs _No response_ ### Risks Given this would be a new optional parameter, seems risk is low. @area-Extensions-Configuration @maryamariyan @michaelgsharp @safern @tarekgh
Author: marknadig
Assignees: -
Labels: `api-suggestion`, `untriaged`, `area-Extensions-Configuration`
Milestone: -
davidfowl commented 2 years ago

This seems like it should be more general purpose than environment variables.

eerhardt commented 2 years ago

I would like to be able to store environment variables in an encrypted form.

I don't fully understand the motivation here. If you have secret stored in an environment variable of the process, what's the value in encrypting it elsewhere? The secret is still available in the environment variable.

KalleOlaviNiemitalo commented 2 years ago

Perhaps this can be implemented as a wrapper similar to ChainedConfigurationProvider. The IConfigurationProvider.Set(string key, string value) implementation would have to decide whether to encode the value if neither the original key nor the "Enc" suffixed key exists already.

drothmaler commented 1 year ago

Another use case for this would be, a casing conversion (or at least '_', '-' or '.' replacement) of a key...

Especially with option binding, I would normally want to configure record ExportOptions(string FileFormat) using {"Export":{"FileFormat":"pdf"}} from JSON based appsettings, but would like to use MY_APP__EXPORT__FILE_FORMAT=txt as an env variable.