dotnet-user-secrets: add support for json output and piping json input
This commit is contained in:
parent
01d35b0624
commit
a0e164f379
|
|
@ -2,11 +2,10 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
internal class ClearCommand : ICommand
|
public class ClearCommand : ICommand
|
||||||
{
|
{
|
||||||
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
||||||
{
|
{
|
||||||
|
|
@ -19,10 +18,10 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(SecretsStore store, ILogger logger)
|
public void Execute(CommandContext context)
|
||||||
{
|
{
|
||||||
store.Clear();
|
context.SecretStore.Clear();
|
||||||
store.Save();
|
context.SecretStore.Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
// 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.Logging;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class CommandContext
|
||||||
|
{
|
||||||
|
public CommandContext(
|
||||||
|
SecretsStore store,
|
||||||
|
ILogger logger,
|
||||||
|
IConsole console)
|
||||||
|
{
|
||||||
|
SecretStore = store;
|
||||||
|
Logger = logger;
|
||||||
|
Console = console;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConsole Console { get; }
|
||||||
|
public ILogger Logger { get; }
|
||||||
|
public SecretsStore SecretStore { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System.IO;
|
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
|
|
||||||
|
|
@ -9,16 +8,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
public class CommandLineOptions
|
public class CommandLineOptions
|
||||||
{
|
{
|
||||||
|
public string Id { get; set; }
|
||||||
public bool IsVerbose { get; set; }
|
public bool IsVerbose { get; set; }
|
||||||
public bool IsHelp { get; set; }
|
public bool IsHelp { get; set; }
|
||||||
public string Project { get; set; }
|
public string Project { get; set; }
|
||||||
internal ICommand Command { get; set; }
|
public ICommand Command { get; set; }
|
||||||
|
|
||||||
public static CommandLineOptions Parse(string[] args, TextWriter output)
|
public static CommandLineOptions Parse(string[] args, IConsole console)
|
||||||
{
|
{
|
||||||
var app = new CommandLineApplication()
|
var app = new CommandLineApplication()
|
||||||
{
|
{
|
||||||
Out = output,
|
Out = console.Out,
|
||||||
|
Error = console.Error,
|
||||||
Name = "dotnet user-secrets",
|
Name = "dotnet user-secrets",
|
||||||
FullName = "User Secrets Manager",
|
FullName = "User Secrets Manager",
|
||||||
Description = "Manages user secrets"
|
Description = "Manages user secrets"
|
||||||
|
|
@ -33,7 +34,13 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
|
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
|
||||||
CommandOptionType.SingleValue, inherited: true);
|
CommandOptionType.SingleValue, inherited: true);
|
||||||
|
|
||||||
|
// the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one
|
||||||
|
// in the current project
|
||||||
|
var optionId = app.Option("--id", "The user secret id to use.",
|
||||||
|
CommandOptionType.SingleValue, inherited: true);
|
||||||
|
|
||||||
var options = new CommandLineOptions();
|
var options = new CommandLineOptions();
|
||||||
|
|
||||||
app.Command("set", c => SetCommand.Configure(c, options));
|
app.Command("set", c => SetCommand.Configure(c, options));
|
||||||
app.Command("remove", c => RemoveCommand.Configure(c, options));
|
app.Command("remove", c => RemoveCommand.Configure(c, options));
|
||||||
app.Command("list", c => ListCommand.Configure(c, options));
|
app.Command("list", c => ListCommand.Configure(c, options));
|
||||||
|
|
@ -48,6 +55,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
options.Id = optionId.Value();
|
||||||
options.IsHelp = app.IsShowingInformation;
|
options.IsHelp = app.IsShowingInformation;
|
||||||
options.IsVerbose = optionVerbose.HasValue();
|
options.IsVerbose = optionVerbose.HasValue();
|
||||||
options.Project = optionProject.Value();
|
options.Project = optionProject.Value();
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using Microsoft.Extensions.Logging;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
internal interface ICommand
|
public interface ICommand
|
||||||
{
|
{
|
||||||
void Execute(SecretsStore store, ILogger logger);
|
void Execute(CommandContext context);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,15 @@
|
||||||
|
// 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.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public interface IConsole
|
||||||
|
{
|
||||||
|
TextWriter Out { get; }
|
||||||
|
TextWriter Error { get; }
|
||||||
|
TextReader In { get; }
|
||||||
|
bool IsInputRedirected { get; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -3,35 +3,67 @@
|
||||||
|
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
internal class ListCommand : ICommand
|
public class ListCommand : ICommand
|
||||||
{
|
{
|
||||||
|
private readonly bool _jsonOutput;
|
||||||
|
|
||||||
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
||||||
{
|
{
|
||||||
command.Description = "Lists all the application secrets";
|
command.Description = "Lists all the application secrets";
|
||||||
command.HelpOption();
|
command.HelpOption();
|
||||||
|
|
||||||
|
var optJson = command.Option("--json", "Use json output. JSON is wrapped by '//BEGIN' and '//END'",
|
||||||
|
CommandOptionType.NoValue);
|
||||||
|
|
||||||
command.OnExecute(() =>
|
command.OnExecute(() =>
|
||||||
{
|
{
|
||||||
options.Command = new ListCommand();
|
options.Command = new ListCommand(optJson.HasValue());
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(SecretsStore store, ILogger logger)
|
public ListCommand(bool jsonOutput)
|
||||||
{
|
{
|
||||||
if (store.Count == 0)
|
_jsonOutput = jsonOutput;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Execute(CommandContext context)
|
||||||
|
{
|
||||||
|
if (_jsonOutput)
|
||||||
{
|
{
|
||||||
logger.LogInformation(Resources.Error_No_Secrets_Found);
|
ReportJson(context);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (context.SecretStore.Count == 0)
|
||||||
|
{
|
||||||
|
context.Logger.LogInformation(Resources.Error_No_Secrets_Found);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
foreach (var secret in store.AsEnumerable())
|
foreach (var secret in context.SecretStore.AsEnumerable())
|
||||||
{
|
{
|
||||||
logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
|
context.Logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ReportJson(CommandContext context)
|
||||||
|
{
|
||||||
|
var jObject = new JObject();
|
||||||
|
foreach(var item in context.SecretStore.AsEnumerable())
|
||||||
|
{
|
||||||
|
jObject[item.Key] = item.Value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO logger would prefix each line.
|
||||||
|
context.Console.Out.WriteLine("//BEGIN");
|
||||||
|
context.Console.Out.WriteLine(jObject.ToString(Formatting.Indented));
|
||||||
|
context.Console.Out.WriteLine("//END");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -0,0 +1,19 @@
|
||||||
|
// 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 System.IO;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class PhysicalConsole : IConsole
|
||||||
|
{
|
||||||
|
private PhysicalConsole() { }
|
||||||
|
|
||||||
|
public static IConsole Singleton { get; } = new PhysicalConsole();
|
||||||
|
public TextWriter Error => Console.Error;
|
||||||
|
public TextReader In => Console.In;
|
||||||
|
public TextWriter Out => Console.Out;
|
||||||
|
public bool IsInputRedirected => Console.IsInputRedirected;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
// 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.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Configuration.Json;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
{
|
||||||
|
public class ReadableJsonConfigurationProvider : JsonConfigurationProvider
|
||||||
|
{
|
||||||
|
public ReadableJsonConfigurationProvider()
|
||||||
|
: base(new JsonConfigurationSource())
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public IDictionary<string, string> CurrentData => Data;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -6,7 +6,7 @@ using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
internal class RemoveCommand : ICommand
|
public class RemoveCommand : ICommand
|
||||||
{
|
{
|
||||||
private readonly string _keyName;
|
private readonly string _keyName;
|
||||||
|
|
||||||
|
|
@ -33,16 +33,16 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
_keyName = keyName;
|
_keyName = keyName;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(SecretsStore store, ILogger logger)
|
public void Execute(CommandContext context)
|
||||||
{
|
{
|
||||||
if (!store.ContainsKey(_keyName))
|
if (!context.SecretStore.ContainsKey(_keyName))
|
||||||
{
|
{
|
||||||
logger.LogWarning(Resources.Error_Missing_Secret, _keyName);
|
context.Logger.LogWarning(Resources.Error_Missing_Secret, _keyName);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
store.Remove(_keyName);
|
context.SecretStore.Remove(_keyName);
|
||||||
store.Save();
|
context.SecretStore.Save();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,7 @@ using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
internal class SecretsStore
|
public class SecretsStore
|
||||||
{
|
{
|
||||||
private readonly string _secretsFilePath;
|
private readonly string _secretsFilePath;
|
||||||
private IDictionary<string, string> _secrets;
|
private IDictionary<string, string> _secrets;
|
||||||
|
|
@ -27,13 +27,15 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
|
||||||
_secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
|
_secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
|
||||||
logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);
|
logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);
|
||||||
|
_secrets = Load(userSecretsId);
|
||||||
|
}
|
||||||
|
|
||||||
_secrets = new ConfigurationBuilder()
|
public string this[string key]
|
||||||
.AddJsonFile(_secretsFilePath, optional: true)
|
{
|
||||||
.Build()
|
get
|
||||||
.AsEnumerable()
|
{
|
||||||
.Where(i => i.Value != null)
|
return _secrets[key];
|
||||||
.ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase);
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public int Count => _secrets.Count;
|
public int Count => _secrets.Count;
|
||||||
|
|
@ -54,7 +56,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Save()
|
public virtual void Save()
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));
|
Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));
|
||||||
|
|
||||||
|
|
@ -69,5 +71,15 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
|
|
||||||
File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8);
|
File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
protected virtual IDictionary<string, string> Load(string userSecretsId)
|
||||||
|
{
|
||||||
|
return new ConfigurationBuilder()
|
||||||
|
.AddJsonFile(_secretsFilePath, optional: true)
|
||||||
|
.Build()
|
||||||
|
.AsEnumerable()
|
||||||
|
.Where(i => i.Value != null)
|
||||||
|
.ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -1,12 +1,15 @@
|
||||||
// Copyright (c) .NET Foundation. All rights reserved.
|
// 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.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Text;
|
||||||
using Microsoft.Extensions.CommandLineUtils;
|
using Microsoft.Extensions.CommandLineUtils;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
{
|
{
|
||||||
internal class SetCommand : ICommand
|
public class SetCommand : ICommand
|
||||||
{
|
{
|
||||||
private readonly string _keyName;
|
private readonly string _keyName;
|
||||||
private readonly string _keyValue;
|
private readonly string _keyValue;
|
||||||
|
|
@ -14,38 +17,88 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
|
||||||
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
public static void Configure(CommandLineApplication command, CommandLineOptions options)
|
||||||
{
|
{
|
||||||
command.Description = "Sets the user secret to the specified value";
|
command.Description = "Sets the user secret to the specified value";
|
||||||
|
command.ExtendedHelpText = @"
|
||||||
|
Additional Info:
|
||||||
|
This command will also handle piped input. Piped input is expected to be a valid JSON format.
|
||||||
|
|
||||||
|
Examples:
|
||||||
|
dotnet user-secrets set ConnStr ""User ID=bob;Password=***""
|
||||||
|
cat secrets.json | dotnet user-secrets set
|
||||||
|
";
|
||||||
|
|
||||||
command.HelpOption();
|
command.HelpOption();
|
||||||
|
|
||||||
var keyArg = command.Argument("[name]", "Name of the secret");
|
var nameArg = command.Argument("[name]", "Name of the secret");
|
||||||
var valueArg = command.Argument("[value]", "Value of the secret");
|
var valueArg = command.Argument("[value]", "Value of the secret");
|
||||||
|
|
||||||
command.OnExecute(() =>
|
command.OnExecute(() =>
|
||||||
{
|
{
|
||||||
if (keyArg.Value == null)
|
options.Command = new SetCommand(nameArg.Value, valueArg.Value);
|
||||||
{
|
|
||||||
throw new GracefulException("Missing parameter value for 'name'.\nUse the '--help' flag to see info.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (valueArg.Value == null)
|
|
||||||
{
|
|
||||||
throw new GracefulException("Missing parameter value for 'value'.\nUse the '--help' flag to see info.");
|
|
||||||
}
|
|
||||||
|
|
||||||
options.Command = new SetCommand(keyArg.Value, valueArg.Value);
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public SetCommand(string keyName, string keyValue)
|
internal SetCommand(string keyName, string keyValue)
|
||||||
{
|
{
|
||||||
|
Debug.Assert(keyName != null || keyValue == null, "Inconsistent state. keyValue must not be null if keyName is null.");
|
||||||
_keyName = keyName;
|
_keyName = keyName;
|
||||||
_keyValue = keyValue;
|
_keyValue = keyValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Execute(SecretsStore store, ILogger logger)
|
internal SetCommand()
|
||||||
|
{ }
|
||||||
|
|
||||||
|
public void Execute(CommandContext context)
|
||||||
{
|
{
|
||||||
store.Set(_keyName, _keyValue);
|
if (context.Console.IsInputRedirected && _keyName == null)
|
||||||
store.Save();
|
{
|
||||||
logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue);
|
ReadFromInput(context);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
SetFromArguments(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ReadFromInput(CommandContext context)
|
||||||
|
{
|
||||||
|
// parses stdin with the same parser that Microsoft.Extensions.Configuration.Json would use
|
||||||
|
var provider = new ReadableJsonConfigurationProvider();
|
||||||
|
using (var stream = new MemoryStream())
|
||||||
|
{
|
||||||
|
using (var writer = new StreamWriter(stream, Encoding.Unicode, 1024, true))
|
||||||
|
{
|
||||||
|
writer.Write(context.Console.In.ReadToEnd()); // TODO buffer?
|
||||||
|
}
|
||||||
|
|
||||||
|
stream.Seek(0, SeekOrigin.Begin);
|
||||||
|
provider.Load(stream);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var k in provider.CurrentData)
|
||||||
|
{
|
||||||
|
context.SecretStore.Set(k.Key, k.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.Logger.LogInformation(Resources.Message_Saved_Secrets, provider.CurrentData.Count);
|
||||||
|
|
||||||
|
context.SecretStore.Save();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void SetFromArguments(CommandContext context)
|
||||||
|
{
|
||||||
|
if (_keyName == null)
|
||||||
|
{
|
||||||
|
throw new GracefulException(Resources.FormatError_MissingArgument("name"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_keyValue == null)
|
||||||
|
{
|
||||||
|
throw new GracefulException((Resources.FormatError_MissingArgument("value")));
|
||||||
|
}
|
||||||
|
|
||||||
|
context.SecretStore.Set(_keyName, _keyValue);
|
||||||
|
context.SecretStore.Save();
|
||||||
|
context.Logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -18,17 +18,17 @@ namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
{
|
{
|
||||||
private ILogger _logger;
|
private ILogger _logger;
|
||||||
private CommandOutputProvider _loggerProvider;
|
private CommandOutputProvider _loggerProvider;
|
||||||
private readonly TextWriter _consoleOutput;
|
private readonly IConsole _console;
|
||||||
private readonly string _workingDirectory;
|
private readonly string _workingDirectory;
|
||||||
|
|
||||||
public Program()
|
public Program()
|
||||||
: this(Console.Out, Directory.GetCurrentDirectory())
|
: this(PhysicalConsole.Singleton, Directory.GetCurrentDirectory())
|
||||||
{
|
{
|
||||||
}
|
}
|
||||||
|
|
||||||
internal Program(TextWriter consoleOutput, string workingDirectory)
|
internal Program(IConsole console, string workingDirectory)
|
||||||
{
|
{
|
||||||
_consoleOutput = consoleOutput;
|
_console = console;
|
||||||
_workingDirectory = workingDirectory;
|
_workingDirectory = workingDirectory;
|
||||||
|
|
||||||
var loggerFactory = new LoggerFactory();
|
var loggerFactory = new LoggerFactory();
|
||||||
|
|
@ -117,7 +117,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
|
|
||||||
internal int RunInternal(params string[] args)
|
internal int RunInternal(params string[] args)
|
||||||
{
|
{
|
||||||
var options = CommandLineOptions.Parse(args, _consoleOutput);
|
var options = CommandLineOptions.Parse(args, _console);
|
||||||
|
|
||||||
if (options == null)
|
if (options == null)
|
||||||
{
|
{
|
||||||
|
|
@ -136,12 +136,18 @@ namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
|
|
||||||
var userSecretsId = ResolveUserSecretsId(options);
|
var userSecretsId = ResolveUserSecretsId(options);
|
||||||
var store = new SecretsStore(userSecretsId, Logger);
|
var store = new SecretsStore(userSecretsId, Logger);
|
||||||
options.Command.Execute(store, Logger);
|
var context = new CommandContext(store, Logger, _console);
|
||||||
|
options.Command.Execute(context);
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string ResolveUserSecretsId(CommandLineOptions options)
|
private string ResolveUserSecretsId(CommandLineOptions options)
|
||||||
{
|
{
|
||||||
|
if (!string.IsNullOrEmpty(options.Id))
|
||||||
|
{
|
||||||
|
return options.Id;
|
||||||
|
}
|
||||||
|
|
||||||
var projectPath = options.Project ?? _workingDirectory;
|
var projectPath = options.Project ?? _workingDirectory;
|
||||||
|
|
||||||
if (!Path.IsPathRooted(projectPath))
|
if (!Path.IsPathRooted(projectPath))
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,24 @@ namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Missing parameter value for '{name}'.
|
||||||
|
/// Use the '--help' flag to see info.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Error_MissingArgument
|
||||||
|
{
|
||||||
|
get { return GetString("Error_MissingArgument"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Missing parameter value for '{name}'.
|
||||||
|
/// Use the '--help' flag to see info.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatError_MissingArgument(object name)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Cannot find '{key}' in the secret store.
|
/// Cannot find '{key}' in the secret store.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -106,6 +124,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
|
||||||
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully saved {number} secrets to the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string Message_Saved_Secrets
|
||||||
|
{
|
||||||
|
get { return GetString("Message_Saved_Secrets"); }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Successfully saved {number} secrets to the secret store.
|
||||||
|
/// </summary>
|
||||||
|
internal static string FormatMessage_Saved_Secrets(object number)
|
||||||
|
{
|
||||||
|
return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number);
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Secrets file path {secretsFilePath}.
|
/// Secrets file path {secretsFilePath}.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
dotnet-user-secrets
|
||||||
|
===================
|
||||||
|
|
||||||
|
`dotnet-user-secrets` is a command line tool for managing the secrets in a user secret store.
|
||||||
|
|
||||||
|
### How To Install
|
||||||
|
|
||||||
|
Add `Microsoft.Extensions.SecretManager.Tools` to the `tools` section of your `project.json` file:
|
||||||
|
|
||||||
|
```js
|
||||||
|
{
|
||||||
|
..
|
||||||
|
"tools": {
|
||||||
|
"Microsoft.Extensions.SecretManager.Tools": "1.0.0-*"
|
||||||
|
}
|
||||||
|
...
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### How To Use
|
||||||
|
|
||||||
|
Run `dotnet user-secrets --help` for more information about usage.
|
||||||
|
|
@ -120,6 +120,10 @@
|
||||||
<data name="Error_Command_Failed" xml:space="preserve">
|
<data name="Error_Command_Failed" xml:space="preserve">
|
||||||
<value>Command failed : {message}</value>
|
<value>Command failed : {message}</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Error_MissingArgument" xml:space="preserve">
|
||||||
|
<value>Missing parameter value for '{name}'.
|
||||||
|
Use the '--help' flag to see info.</value>
|
||||||
|
</data>
|
||||||
<data name="Error_Missing_Secret" xml:space="preserve">
|
<data name="Error_Missing_Secret" xml:space="preserve">
|
||||||
<value>Cannot find '{key}' in the secret store.</value>
|
<value>Cannot find '{key}' in the secret store.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
@ -135,6 +139,9 @@
|
||||||
<data name="Message_Saved_Secret" xml:space="preserve">
|
<data name="Message_Saved_Secret" xml:space="preserve">
|
||||||
<value>Successfully saved {key} = {value} to the secret store.</value>
|
<value>Successfully saved {key} = {value} to the secret store.</value>
|
||||||
</data>
|
</data>
|
||||||
|
<data name="Message_Saved_Secrets" xml:space="preserve">
|
||||||
|
<value>Successfully saved {number} secrets to the secret store.</value>
|
||||||
|
</data>
|
||||||
<data name="Message_Secret_File_Path" xml:space="preserve">
|
<data name="Message_Secret_File_Path" xml:space="preserve">
|
||||||
<value>Secrets file path {secretsFilePath}.</value>
|
<value>Secrets file path {secretsFilePath}.</value>
|
||||||
</data>
|
</data>
|
||||||
|
|
|
||||||
|
|
@ -8,9 +8,9 @@ using System.Text;
|
||||||
using Microsoft.Extensions.Configuration.UserSecrets;
|
using Microsoft.Extensions.Configuration.UserSecrets;
|
||||||
using Microsoft.Extensions.Configuration.UserSecrets.Tests;
|
using Microsoft.Extensions.Configuration.UserSecrets.Tests;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
using Xunit;
|
using Xunit;
|
||||||
using Xunit.Abstractions;
|
using Xunit.Abstractions;
|
||||||
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
|
||||||
|
|
||||||
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
{
|
{
|
||||||
|
|
@ -48,7 +48,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
public void Error_Project_DoesNotExist()
|
public void Error_Project_DoesNotExist()
|
||||||
{
|
{
|
||||||
var projectPath = Path.Combine(GetTempSecretProject(), "does_not_exist", "project.json");
|
var projectPath = Path.Combine(GetTempSecretProject(), "does_not_exist", "project.json");
|
||||||
var secretManager = new Program(Console.Out, Directory.GetCurrentDirectory()) { Logger = _logger };
|
var secretManager = new Program(new TestConsole(), Directory.GetCurrentDirectory()) { Logger = _logger };
|
||||||
|
|
||||||
var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "--project", projectPath));
|
var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "--project", projectPath));
|
||||||
|
|
||||||
|
|
@ -61,7 +61,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
var projectPath = GetTempSecretProject();
|
var projectPath = GetTempSecretProject();
|
||||||
var cwd = Path.Combine(projectPath, "nested1");
|
var cwd = Path.Combine(projectPath, "nested1");
|
||||||
Directory.CreateDirectory(cwd);
|
Directory.CreateDirectory(cwd);
|
||||||
var secretManager = new Program(Console.Out, cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider };
|
var secretManager = new Program(new TestConsole(), cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider };
|
||||||
secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug;
|
secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug;
|
||||||
|
|
||||||
secretManager.RunInternal("list", "-p", "../", "--verbose");
|
secretManager.RunInternal("list", "-p", "../", "--verbose");
|
||||||
|
|
@ -86,7 +86,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
var dir = fromCurrentDirectory
|
var dir = fromCurrentDirectory
|
||||||
? projectPath
|
? projectPath
|
||||||
: Path.GetTempPath();
|
: Path.GetTempPath();
|
||||||
var secretManager = new Program(Console.Out, dir) { Logger = _logger };
|
var secretManager = new Program(new TestConsole(), dir) { Logger = _logger };
|
||||||
|
|
||||||
foreach (var secret in secrets)
|
foreach (var secret in secrets)
|
||||||
{
|
{
|
||||||
|
|
@ -222,6 +222,27 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
|
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void List_Json()
|
||||||
|
{
|
||||||
|
var output = new StringBuilder();
|
||||||
|
var testConsole = new TestConsole
|
||||||
|
{
|
||||||
|
Out = new StringWriter(output)
|
||||||
|
};
|
||||||
|
string id;
|
||||||
|
var projectPath = GetTempSecretProject(out id);
|
||||||
|
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
|
||||||
|
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
|
||||||
|
var secretManager = new Program(testConsole, Path.GetDirectoryName(projectPath)) { Logger = _logger };
|
||||||
|
secretManager.RunInternal("list", "--id", id, "--json");
|
||||||
|
var stdout = output.ToString();
|
||||||
|
Assert.Contains("//BEGIN", stdout);
|
||||||
|
Assert.Contains(@"""AzureAd:ClientSecret"": ""abcd郩˙î""", stdout);
|
||||||
|
Assert.Contains("//END", stdout);
|
||||||
|
}
|
||||||
|
|
||||||
[Fact]
|
[Fact]
|
||||||
public void Set_Flattens_Nested_Objects()
|
public void Set_Flattens_Nested_Objects()
|
||||||
{
|
{
|
||||||
|
|
@ -265,7 +286,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
? projectPath
|
? projectPath
|
||||||
: Path.GetTempPath();
|
: Path.GetTempPath();
|
||||||
|
|
||||||
var secretManager = new Program(Console.Out, dir) { Logger = _logger };
|
var secretManager = new Program(new TestConsole(), dir) { Logger = _logger };
|
||||||
|
|
||||||
var secrets = new KeyValuePair<string, string>[]
|
var secrets = new KeyValuePair<string, string>[]
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,101 @@
|
||||||
|
// 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.IO;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using Microsoft.Extensions.Logging.Abstractions;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
|
{
|
||||||
|
public class SetCommandTest
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public void SetsFromPipedInput()
|
||||||
|
{
|
||||||
|
var input = @"
|
||||||
|
{
|
||||||
|
""Key1"": ""str value"",
|
||||||
|
""Key2"": 1234,
|
||||||
|
""Key3"": false
|
||||||
|
}";
|
||||||
|
var testConsole = new TestConsole
|
||||||
|
{
|
||||||
|
IsInputRedirected = true,
|
||||||
|
In = new StringReader(input)
|
||||||
|
};
|
||||||
|
var secretStore = new TestSecretsStore();
|
||||||
|
var command = new SetCommand();
|
||||||
|
|
||||||
|
command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole));
|
||||||
|
|
||||||
|
Assert.Equal(3, secretStore.Count);
|
||||||
|
Assert.Equal("str value", secretStore["Key1"]);
|
||||||
|
Assert.Equal("1234", secretStore["Key2"]);
|
||||||
|
Assert.Equal("False", secretStore["Key3"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void ParsesNestedObjects()
|
||||||
|
{
|
||||||
|
var input = @"
|
||||||
|
{
|
||||||
|
""Key1"": {
|
||||||
|
""nested"" : ""value""
|
||||||
|
},
|
||||||
|
""array"": [ 1, 2 ]
|
||||||
|
}";
|
||||||
|
|
||||||
|
var testConsole = new TestConsole
|
||||||
|
{
|
||||||
|
IsInputRedirected = true,
|
||||||
|
In = new StringReader(input)
|
||||||
|
};
|
||||||
|
var secretStore = new TestSecretsStore();
|
||||||
|
var command = new SetCommand();
|
||||||
|
|
||||||
|
command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole));
|
||||||
|
|
||||||
|
Assert.Equal(3, secretStore.Count);
|
||||||
|
Assert.True(secretStore.ContainsKey("Key1:nested"));
|
||||||
|
Assert.Equal("value", secretStore["Key1:nested"]);
|
||||||
|
Assert.Equal("1", secretStore["array:0"]);
|
||||||
|
Assert.Equal("2", secretStore["array:1"]);
|
||||||
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public void OnlyPipesInIfNoArgs()
|
||||||
|
{
|
||||||
|
var testConsole = new TestConsole
|
||||||
|
{
|
||||||
|
IsInputRedirected = true,
|
||||||
|
In = new StringReader("")
|
||||||
|
};
|
||||||
|
var secretStore = new TestSecretsStore();
|
||||||
|
var command = new SetCommand("key", null);
|
||||||
|
|
||||||
|
var ex = Assert.Throws<GracefulException>(
|
||||||
|
() => command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)));
|
||||||
|
Assert.Equal(Resources.FormatError_MissingArgument("value"), ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private class TestSecretsStore : SecretsStore
|
||||||
|
{
|
||||||
|
public TestSecretsStore()
|
||||||
|
: base("xyz", NullLogger.Instance)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
protected override IDictionary<string, string> Load(string userSecretsId)
|
||||||
|
{
|
||||||
|
return new Dictionary<string, string>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Save()
|
||||||
|
{
|
||||||
|
// noop
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,17 @@
|
||||||
|
// 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 System.IO;
|
||||||
|
using Microsoft.Extensions.SecretManager.Tools.Internal;
|
||||||
|
|
||||||
|
namespace Microsoft.Extensions.SecretManager.Tools.Tests
|
||||||
|
{
|
||||||
|
public class TestConsole : IConsole
|
||||||
|
{
|
||||||
|
public TextWriter Error { get; set; } = Console.Error;
|
||||||
|
public TextReader In { get; set; } = Console.In;
|
||||||
|
public TextWriter Out { get; set; } = Console.Out;
|
||||||
|
public bool IsInputRedirected { get; set; } = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -7,7 +7,7 @@ using Newtonsoft.Json;
|
||||||
|
|
||||||
namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
|
namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
|
||||||
{
|
{
|
||||||
internal class UserSecretHelper
|
public class UserSecretHelper
|
||||||
{
|
{
|
||||||
internal static string GetTempSecretProject()
|
internal static string GetTempSecretProject()
|
||||||
{
|
{
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue