diff --git a/Microsoft.DotNet.Watcher.Tools.sln b/DotNetTools.sln
similarity index 80%
rename from Microsoft.DotNet.Watcher.Tools.sln
rename to DotNetTools.sln
index 658ce28d54..e600c724c1 100644
--- a/Microsoft.DotNet.Watcher.Tools.sln
+++ b/DotNetTools.sln
@@ -1,7 +1,7 @@
Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
-VisualStudioVersion = 14.0.25123.0
+VisualStudioVersion = 14.0.25420.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}"
EndProject
@@ -29,6 +29,10 @@ Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "AppWithDeps", "test\TestApp
EndProject
Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Dependency", "test\TestApps\Dependency\Dependency.xproj", "{2F48041A-F7D1-478F-9C38-D41F0F05E8CA}"
EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.SecretManager.Tools", "src\Microsoft.Extensions.SecretManager.Tools\Microsoft.Extensions.SecretManager.Tools.xproj", "{8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}"
+EndProject
+Project("{8BB2217D-0F2D-49D1-97BC-3654ED321F3B}") = "Microsoft.Extensions.SecretManager.Tools.Tests", "test\Microsoft.Extensions.SecretManager.Tools.Tests\Microsoft.Extensions.SecretManager.Tools.Tests.xproj", "{7B331122-83B1-4F08-A119-DC846959844C}"
+EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
@@ -63,6 +67,14 @@ Global
{2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Debug|Any CPU.Build.0 = Debug|Any CPU
{2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Release|Any CPU.ActiveCfg = Release|Any CPU
{2F48041A-F7D1-478F-9C38-D41F0F05E8CA}.Release|Any CPU.Build.0 = Release|Any CPU
+ {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Release|Any CPU.Build.0 = Release|Any CPU
+ {7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
@@ -76,5 +88,7 @@ Global
{2AB1A28B-2022-49EA-AF77-AC8A875915CC} = {2876B12E-5841-4792-85A8-2929AEE11885}
{F7734E61-F510-41E0-AD15-301A64081CD1} = {2876B12E-5841-4792-85A8-2929AEE11885}
{2F48041A-F7D1-478F-9C38-D41F0F05E8CA} = {2876B12E-5841-4792-85A8-2929AEE11885}
+ {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E} = {66517987-2A5A-4330-B130-207039378FD4}
+ {7B331122-83B1-4F08-A119-DC846959844C} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134}
EndGlobalSection
EndGlobal
diff --git a/NuGetPackageVerifier.json b/NuGetPackageVerifier.json
index 447bab830f..932af6fb14 100644
--- a/NuGetPackageVerifier.json
+++ b/NuGetPackageVerifier.json
@@ -5,7 +5,8 @@
],
"packages": {
"Microsoft.DotNet.Watcher.Tools": { },
- "Microsoft.DotNet.Watcher.Core": { }
+ "Microsoft.DotNet.Watcher.Core": { },
+ "Microsoft.Extensions.SecretManager.Tools": { }
}
},
"Default": { // Rules to run for packages not listed in any other set.
diff --git a/README.md b/README.md
index 11b1f68db4..64eb8a2cee 100644
--- a/README.md
+++ b/README.md
@@ -1,48 +1,12 @@
-dotnet-watch
-===
-`dotnet-watch` is a file watcher for `dotnet` that restarts the specified application when changes in the source code are detected.
+DotNetTools
+===========
-### How To Install
+[](https://travis-ci.org/aspnet/dotnet-watch/branches)
+[](https://ci.appveyor.com/project/aspnetci/dnx-watch/branch/dev)
-Add `Microsoft.DotNet.Watcher.Tools` to the `tools` section of your `project.json` file:
+The project contains command-line tools for the .NET Core SDK.
-```
-{
-...
- "tools": {
- "Microsoft.DotNet.Watcher.Tools": {
- "version": "1.0.0-*",
- "imports": "portable-net451+win8"
- }
- }
-...
-}
-```
-
-### How To Use
-
- dotnet watch [dotnet arguments]
-
-Add `watch` after `dotnet` in the command that you want to run:
-
-| What you want to run | Dotnet watch command |
-| ---------------------------------------------- | -------------------------------------------------------- |
-| dotnet run | dotnet **watch** run |
-| dotnet run --arg1 value1 | dotnet **watch** run --arg1 value |
-| dotnet run --framework net451 -- --arg1 value1 | dotnet **watch** run --framework net451 -- --arg1 value1 |
-| dotnet test | dotnet **watch** test |
-
-### Advanced configuration options
-
-Configuration options can be passed to `dotnet watch` through environment variables. The available variables are:
-
-| Variable | Effect |
-| ---------------------------------------------- | -------------------------------------------------------- |
-| DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. |
-| DOTNET_WATCH_LOG_LEVEL | Used to set the logging level for messages coming from `dotnet watch`. Accepted values `None`, `Trace`, `Debug`, `Information`, `Warning`, `Error`, `Critical`. Default: `Information`. |
-
-AppVeyor: [](https://ci.appveyor.com/project/aspnetci/dnx-watch/branch/dev)
-
-Travis: [](https://travis-ci.org/aspnet/dotnet-watch)
+ - [dotnet-watch](src/Microsoft.DotNet.Watcher.Tools/)
+ - [dotnet-user-secrets](src/Microsoft.Extensions.SecretManager.Tools/)
This project is part of ASP.NET Core. You can find samples, documentation and getting started instructions for ASP.NET Core at the [Home](https://github.com/aspnet/home) repo.
diff --git a/src/Microsoft.DotNet.Watcher.Tools/README.md b/src/Microsoft.DotNet.Watcher.Tools/README.md
new file mode 100644
index 0000000000..b244177440
--- /dev/null
+++ b/src/Microsoft.DotNet.Watcher.Tools/README.md
@@ -0,0 +1,39 @@
+dotnet-watch
+============
+`dotnet-watch` is a file watcher for `dotnet` that restarts the specified application when changes in the source code are detected.
+
+### How To Install
+
+Add `Microsoft.DotNet.Watcher.Tools` to the `tools` section of your `project.json` file:
+
+```
+{
+...
+ "tools": {
+ "Microsoft.DotNet.Watcher.Tools": "1.0.0-*"
+ }
+...
+}
+```
+
+### How To Use
+
+ dotnet watch [dotnet arguments]
+
+Add `watch` after `dotnet` in the command that you want to run:
+
+| What you want to run | Dotnet watch command |
+| ---------------------------------------------- | -------------------------------------------------------- |
+| dotnet run | dotnet **watch** run |
+| dotnet run --arg1 value1 | dotnet **watch** run --arg1 value |
+| dotnet run --framework net451 -- --arg1 value1 | dotnet **watch** run --framework net451 -- --arg1 value1 |
+| dotnet test | dotnet **watch** test |
+
+### Advanced configuration options
+
+Configuration options can be passed to `dotnet watch` through environment variables. The available variables are:
+
+| Variable | Effect |
+| ---------------------------------------------- | -------------------------------------------------------- |
+| DOTNET_USE_POLLING_FILE_WATCHER | If set to "1" or "true", `dotnet watch` will use a polling file watcher instead of CoreFx's `FileSystemWatcher`. Used when watching files on network shares or Docker mounted volumes. |
+| DOTNET_WATCH_LOG_LEVEL | Used to set the logging level for messages coming from `dotnet watch`. Accepted values `None`, `Trace`, `Debug`, `Information`, `Warning`, `Error`, `Critical`. Default: `Information`. |
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/CommandOutputLogger.cs b/src/Microsoft.Extensions.SecretManager.Tools/CommandOutputLogger.cs
new file mode 100644
index 0000000000..f648400f65
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/CommandOutputLogger.cs
@@ -0,0 +1,62 @@
+// 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.Extensions.CommandLineUtils;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ ///
+ /// Logger to print formatted command output.
+ ///
+ public class CommandOutputLogger : ILogger
+ {
+ private readonly CommandOutputProvider _provider;
+ private readonly AnsiConsole _outConsole;
+
+ public CommandOutputLogger(CommandOutputProvider commandOutputProvider, bool useConsoleColor)
+ {
+ _provider = commandOutputProvider;
+ _outConsole = AnsiConsole.GetOutput(useConsoleColor);
+ }
+
+ public IDisposable BeginScope(TState state)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ if (logLevel < _provider.LogLevel)
+ {
+ return false;
+ }
+
+ return true;
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (IsEnabled(logLevel))
+ {
+ _outConsole.WriteLine(string.Format("{0}: {1}", Caption(logLevel), formatter(state, exception)));
+ }
+ }
+
+ private string Caption(LogLevel logLevel)
+ {
+ switch (logLevel)
+ {
+ case LogLevel.Trace: return "\x1b[35mtrace\x1b[39m";
+ case LogLevel.Debug: return "\x1b[35mdebug\x1b[39m";
+ case LogLevel.Information: return "\x1b[32minfo\x1b[39m";
+ case LogLevel.Warning: return "\x1b[33mwarn\x1b[39m";
+ case LogLevel.Error: return "\x1b[31mfail\x1b[39m";
+ case LogLevel.Critical: return "\x1b[31mcritical\x1b[39m";
+ }
+
+ throw new Exception("Unknown LogLevel");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/CommandOutputProvider.cs b/src/Microsoft.Extensions.SecretManager.Tools/CommandOutputProvider.cs
new file mode 100644
index 0000000000..0791830a69
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/CommandOutputProvider.cs
@@ -0,0 +1,23 @@
+// 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.InteropServices;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ public class CommandOutputProvider : ILoggerProvider
+ {
+ public ILogger CreateLogger(string name)
+ {
+ var useConsoleColor = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
+ return new CommandOutputLogger(this, useConsoleColor);
+ }
+
+ public void Dispose()
+ {
+ }
+
+ public LogLevel LogLevel { get; set; } = LogLevel.Information;
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs
new file mode 100644
index 0000000000..41c6634364
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ClearCommand.cs
@@ -0,0 +1,28 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class ClearCommand : ICommand
+ {
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Deletes all the application secrets";
+ command.HelpOption();
+
+ command.OnExecute(() =>
+ {
+ options.Command = new ClearCommand();
+ });
+ }
+
+ public void Execute(SecretsStore store, ILogger logger)
+ {
+ store.Clear();
+ store.Save();
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineApplicationExtensions.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineApplicationExtensions.cs
new file mode 100644
index 0000000000..386a5ddf30
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineApplicationExtensions.cs
@@ -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 System;
+
+namespace Microsoft.Extensions.CommandLineUtils
+{
+ public static class UserSecretsCommandLineExtensions
+ {
+ public static CommandOption HelpOption(this CommandLineApplication app)
+ {
+ return app.HelpOption("-?|-h|--help");
+ }
+
+ public static void OnExecute(this CommandLineApplication app, Action action)
+ {
+ app.OnExecute(() =>
+ {
+ action();
+ return 0;
+ });
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs
new file mode 100644
index 0000000000..1b93392b4d
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.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 System.IO;
+using System.Reflection;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ public class CommandLineOptions
+ {
+ public bool IsVerbose { get; set; }
+ public bool IsHelp { get; set; }
+ public string Project { get; set; }
+ internal ICommand Command { get; set; }
+
+ public static CommandLineOptions Parse(string[] args, TextWriter output)
+ {
+ var app = new CommandLineApplication()
+ {
+ Out = output,
+ Name = "dotnet user-secrets",
+ FullName = "User Secrets Manager",
+ Description = "Manages user secrets"
+ };
+
+ app.HelpOption();
+ app.VersionOption("--version", GetInformationalVersion());
+
+ var optionVerbose = app.Option("-v|--verbose", "Verbose output",
+ CommandOptionType.NoValue, inherited: true);
+
+ var optionProject = app.Option("-p|--project ", "Path to project, default is current directory",
+ CommandOptionType.SingleValue, inherited: true);
+
+ var options = new CommandLineOptions();
+ app.Command("set", c => SetCommand.Configure(c, options));
+ app.Command("remove", c => RemoveCommand.Configure(c, options));
+ app.Command("list", c => ListCommand.Configure(c, options));
+ app.Command("clear", c => ClearCommand.Configure(c, options));
+
+ // Show help information if no subcommand/option was specified.
+ app.OnExecute(() => app.ShowHelp());
+
+ if (app.Execute(args) != 0)
+ {
+ // when command line parsing error in subcommand
+ return null;
+ }
+
+ options.IsHelp = app.IsShowingInformation;
+ options.IsVerbose = optionVerbose.HasValue();
+ options.Project = optionProject.Value();
+
+ return options;
+ }
+
+ private static string GetInformationalVersion()
+ {
+ var assembly = typeof(Program).GetTypeInfo().Assembly;
+ var attribute = assembly.GetCustomAttribute();
+
+ var versionAttribute = attribute == null ?
+ assembly.GetName().Version.ToString() :
+ attribute.InformationalVersion;
+
+ return versionAttribute;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs
new file mode 100644
index 0000000000..7e54c8ba2f
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs
@@ -0,0 +1,25 @@
+// 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;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ ///
+ /// An exception whose stack trace should be suppressed in console output
+ ///
+ public class GracefulException : Exception
+ {
+ public GracefulException()
+ {
+ }
+
+ public GracefulException(string message) : base(message)
+ {
+ }
+
+ public GracefulException(string message, Exception innerException) : base(message, innerException)
+ {
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs
new file mode 100644
index 0000000000..3d6035d083
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ICommand.cs
@@ -0,0 +1,12 @@
+// 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
+{
+ internal interface ICommand
+ {
+ void Execute(SecretsStore store, ILogger logger);
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs
new file mode 100644
index 0000000000..efc012cd5a
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ListCommand.cs
@@ -0,0 +1,37 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class ListCommand : ICommand
+ {
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Lists all the application secrets";
+ command.HelpOption();
+
+ command.OnExecute(() =>
+ {
+ options.Command = new ListCommand();
+ });
+ }
+
+ public void Execute(SecretsStore store, ILogger logger)
+ {
+ if (store.Count == 0)
+ {
+ logger.LogInformation(Resources.Error_No_Secrets_Found);
+ }
+ else
+ {
+ foreach (var secret in store.AsEnumerable())
+ {
+ logger.LogInformation(Resources.FormatMessage_Secret_Value_Format(secret.Key, secret.Value));
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs
new file mode 100644
index 0000000000..ff48c253cc
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs
@@ -0,0 +1,49 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class RemoveCommand : ICommand
+ {
+ private readonly string _keyName;
+
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Removes the specified user secret";
+ command.HelpOption();
+
+ var keyArg = command.Argument("[name]", "Name of the secret");
+ command.OnExecute(() =>
+ {
+ if (keyArg.Value == null)
+ {
+ throw new GracefulException("Missing parameter value for 'name'.\nUse the '--help' flag to see info.");
+ }
+
+ options.Command = new RemoveCommand(keyArg.Value);
+ });
+ }
+
+
+ public RemoveCommand(string keyName)
+ {
+ _keyName = keyName;
+ }
+
+ public void Execute(SecretsStore store, ILogger logger)
+ {
+ if (!store.ContainsKey(_keyName))
+ {
+ logger.LogWarning(Resources.Error_Missing_Secret, _keyName);
+ }
+ else
+ {
+ store.Remove(_keyName);
+ store.Save();
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs
new file mode 100644
index 0000000000..7863fb725a
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs
@@ -0,0 +1,78 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Text;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Configuration.UserSecrets;
+using Microsoft.Extensions.Logging;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class SecretsStore
+ {
+ private readonly string _secretsFilePath;
+ private IDictionary _secrets;
+
+ public SecretsStore(string userSecretsId, ILogger logger)
+ {
+ if (userSecretsId == null)
+ {
+ throw new ArgumentNullException(nameof(userSecretsId));
+ }
+
+ _secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
+ logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);
+
+ // workaround https://github.com/aspnet/Configuration/issues/478
+ // TODO remove when tool upgrades to use 1.1.0
+ Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));
+ //end workaround
+
+ _secrets = new ConfigurationBuilder()
+ .AddJsonFile(_secretsFilePath, optional: true)
+ .Build()
+ .AsEnumerable()
+ .Where(i => i.Value != null)
+ .ToDictionary(i => i.Key, i => i.Value, StringComparer.OrdinalIgnoreCase);
+ }
+
+ public int Count => _secrets.Count;
+
+ public bool ContainsKey(string key) => _secrets.ContainsKey(key);
+
+ public IEnumerable> AsEnumerable() => _secrets;
+
+ public void Clear() => _secrets.Clear();
+
+ public void Set(string key, string value) => _secrets[key] = value;
+
+ public void Remove(string key)
+ {
+ if (_secrets.ContainsKey(key))
+ {
+ _secrets.Remove(key);
+ }
+ }
+
+ public void Save()
+ {
+ Directory.CreateDirectory(Path.GetDirectoryName(_secretsFilePath));
+
+ var contents = new JObject();
+ if (_secrets != null)
+ {
+ foreach (var secret in _secrets.AsEnumerable())
+ {
+ contents[secret.Key] = secret.Value;
+ }
+ }
+
+ File.WriteAllText(_secretsFilePath, contents.ToString(), Encoding.UTF8);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs
new file mode 100644
index 0000000000..15852cef64
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs
@@ -0,0 +1,51 @@
+// 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.CommandLineUtils;
+using Microsoft.Extensions.Logging;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ internal class SetCommand : ICommand
+ {
+ private readonly string _keyName;
+ private readonly string _keyValue;
+
+ public static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Sets the user secret to the specified value";
+ command.HelpOption();
+
+ var keyArg = command.Argument("[name]", "Name of the secret");
+ var valueArg = command.Argument("[value]", "Value of the secret");
+
+ command.OnExecute(() =>
+ {
+ if (keyArg.Value == null)
+ {
+ 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)
+ {
+ _keyName = keyName;
+ _keyValue = keyValue;
+ }
+
+ public void Execute(SecretsStore store, ILogger logger)
+ {
+ store.Set(_keyName, _keyValue);
+ store.Save();
+ logger.LogInformation(Resources.Message_Saved_Secret, _keyName, _keyValue);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.xproj b/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.xproj
new file mode 100644
index 0000000000..aad10398ce
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.xproj
@@ -0,0 +1,17 @@
+
+
+
+ 14.0
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 8730e848-ca0f-4e0a-9a2f-bc22ad0b2c4e
+ .\obj
+ .\bin\
+
+
+ 2.0
+
+
+
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Program.cs b/src/Microsoft.Extensions.SecretManager.Tools/Program.cs
new file mode 100644
index 0000000000..0da29bd567
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Program.cs
@@ -0,0 +1,183 @@
+// 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.Diagnostics;
+using System.IO;
+using System.Linq;
+using Microsoft.Extensions.Logging;
+using Microsoft.Extensions.FileProviders;
+using Microsoft.Extensions.FileProviders.Physical;
+using Microsoft.Extensions.SecretManager.Tools.Internal;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ public class Program
+ {
+ private ILogger _logger;
+ private CommandOutputProvider _loggerProvider;
+ private readonly TextWriter _consoleOutput;
+ private readonly string _workingDirectory;
+
+ public Program()
+ : this(Console.Out, Directory.GetCurrentDirectory())
+ {
+ }
+
+ internal Program(TextWriter consoleOutput, string workingDirectory)
+ {
+ _consoleOutput = consoleOutput;
+ _workingDirectory = workingDirectory;
+
+ var loggerFactory = new LoggerFactory();
+ CommandOutputProvider = new CommandOutputProvider();
+ loggerFactory.AddProvider(CommandOutputProvider);
+ Logger = loggerFactory.CreateLogger();
+ }
+
+ public ILogger Logger
+ {
+ get { return _logger; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ _logger = value;
+ }
+ }
+
+ public CommandOutputProvider CommandOutputProvider
+ {
+ get { return _loggerProvider; }
+ set
+ {
+ if (value == null)
+ {
+ throw new ArgumentNullException("value");
+ }
+
+ _loggerProvider = value;
+ }
+ }
+
+ public static int Main(string[] args)
+ {
+ HandleDebugFlag(ref args);
+
+ int rc;
+ new Program().TryRun(args, out rc);
+ return rc;
+ }
+
+ [Conditional("DEBUG")]
+ private static void HandleDebugFlag(ref string[] args)
+ {
+ for (var i = 0; i < args.Length; ++i)
+ {
+ if (args[i] == "--debug")
+ {
+ Console.WriteLine("Process ID " + Process.GetCurrentProcess().Id);
+ Console.WriteLine("Paused for debugger. Press ENTER to continue");
+ Console.ReadLine();
+
+ args = args.Take(i).Concat(args.Skip(i + 1)).ToArray();
+
+ return;
+ }
+ }
+ }
+
+ public bool TryRun(string[] args, out int returnCode)
+ {
+ try
+ {
+ returnCode = RunInternal(args);
+ return true;
+ }
+ catch (Exception exception)
+ {
+ if (exception is GracefulException)
+ {
+ Logger.LogError(exception.Message);
+ }
+ else
+ {
+ Logger.LogDebug(exception.ToString());
+ Logger.LogCritical(Resources.Error_Command_Failed, exception.Message);
+ }
+ returnCode = 1;
+ return false;
+ }
+ }
+
+ internal int RunInternal(params string[] args)
+ {
+ var options = CommandLineOptions.Parse(args, _consoleOutput);
+
+ if (options == null)
+ {
+ return 1;
+ }
+
+ if (options.IsHelp)
+ {
+ return 2;
+ }
+
+ if (options.IsVerbose)
+ {
+ CommandOutputProvider.LogLevel = LogLevel.Debug;
+ }
+
+ var userSecretsId = ResolveUserSecretsId(options);
+ var store = new SecretsStore(userSecretsId, Logger);
+ options.Command.Execute(store, Logger);
+ return 0;
+ }
+
+ private string ResolveUserSecretsId(CommandLineOptions options)
+ {
+ var projectPath = options.Project ?? _workingDirectory;
+
+ if (!projectPath.EndsWith("project.json", StringComparison.OrdinalIgnoreCase))
+ {
+ projectPath = Path.Combine(projectPath, "project.json");
+ }
+
+ var fileInfo = new PhysicalFileInfo(new FileInfo(projectPath));
+
+ Logger.LogDebug(Resources.Message_Project_File_Path, fileInfo.PhysicalPath);
+ return ReadUserSecretsId(fileInfo);
+ }
+
+ // TODO can use runtime API when upgrading to 1.1
+ private string ReadUserSecretsId(IFileInfo fileInfo)
+ {
+ if (fileInfo == null || !fileInfo.Exists)
+ {
+ throw new GracefulException($"Could not find file '{fileInfo.PhysicalPath}'");
+ }
+
+ using (var stream = fileInfo.CreateReadStream())
+ using (var streamReader = new StreamReader(stream))
+ using (var jsonReader = new JsonTextReader(streamReader))
+ {
+ var obj = JObject.Load(jsonReader);
+
+ var userSecretsId = obj.Value("userSecretsId");
+
+ if (string.IsNullOrEmpty(userSecretsId))
+ {
+ throw new GracefulException($"Could not find 'userSecretsId' in json file '{fileInfo.PhysicalPath}'");
+ }
+
+ return userSecretsId;
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Properties/AssemblyInfo.cs b/src/Microsoft.Extensions.SecretManager.Tools/Properties/AssemblyInfo.cs
new file mode 100644
index 0000000000..8d8d88195c
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Properties/AssemblyInfo.cs
@@ -0,0 +1,11 @@
+// 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.Reflection;
+using System.Resources;
+
+[assembly: AssemblyMetadata("Serviceable", "True")]
+[assembly: NeutralResourcesLanguage("en-us")]
+[assembly: AssemblyCompany("Microsoft Corporation.")]
+[assembly: AssemblyCopyright("© Microsoft Corporation. All rights reserved.")]
+[assembly: AssemblyProduct("Microsoft .NET Extensions")]
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Properties/InternalsVisibleTo.cs b/src/Microsoft.Extensions.SecretManager.Tools/Properties/InternalsVisibleTo.cs
new file mode 100644
index 0000000000..8057e7a4d5
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Properties/InternalsVisibleTo.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.Extensions.SecretManager.Tools.Tests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100f33a29044fa9d740c9b3213a93e57c84b472c84e0b8a0e1ae48e67a9f8f6de9d5f7f3d52ac23e48ac51801f1dc950abe901da34d2a9e3baadb141a17c77ef3c565dd5ee5054b91cf63bb3c6ab83f72ab3aafe93d0fc3c2348b764fafb0b1c0733de51459aeab46580384bf9d74c4e28164b7cde247f891ba07891c9d872ad2bb")]
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs b/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs
new file mode 100644
index 0000000000..c79e6e1fa2
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs
@@ -0,0 +1,142 @@
+//
+namespace Microsoft.Extensions.SecretManager.Tools
+{
+ using System.Globalization;
+ using System.Reflection;
+ using System.Resources;
+
+ internal static class Resources
+ {
+ private static readonly ResourceManager _resourceManager
+ = new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly);
+
+ ///
+ /// Command failed : {message}
+ ///
+ internal static string Error_Command_Failed
+ {
+ get { return GetString("Error_Command_Failed"); }
+ }
+
+ ///
+ /// Command failed : {message}
+ ///
+ internal static string FormatError_Command_Failed(object message)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
+ }
+
+ ///
+ /// Cannot find '{key}' in the secret store.
+ ///
+ internal static string Error_Missing_Secret
+ {
+ get { return GetString("Error_Missing_Secret"); }
+ }
+
+ ///
+ /// Cannot find '{key}' in the secret store.
+ ///
+ internal static string FormatError_Missing_Secret(object key)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key);
+ }
+
+ ///
+ /// No secrets configured for this application.
+ ///
+ internal static string Error_No_Secrets_Found
+ {
+ get { return GetString("Error_No_Secrets_Found"); }
+ }
+
+ ///
+ /// No secrets configured for this application.
+ ///
+ internal static string FormatError_No_Secrets_Found()
+ {
+ return GetString("Error_No_Secrets_Found");
+ }
+
+ ///
+ /// Project file path {project}.
+ ///
+ internal static string Message_Project_File_Path
+ {
+ get { return GetString("Message_Project_File_Path"); }
+ }
+
+ ///
+ /// Project file path {project}.
+ ///
+ internal static string FormatMessage_Project_File_Path(object project)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Project_File_Path", "project"), project);
+ }
+
+ ///
+ /// Successfully saved {key} = {value} to the secret store.
+ ///
+ internal static string Message_Saved_Secret
+ {
+ get { return GetString("Message_Saved_Secret"); }
+ }
+
+ ///
+ /// Successfully saved {key} = {value} to the secret store.
+ ///
+ internal static string FormatMessage_Saved_Secret(object key, object value)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
+ }
+
+ ///
+ /// Secrets file path {secretsFilePath}.
+ ///
+ internal static string Message_Secret_File_Path
+ {
+ get { return GetString("Message_Secret_File_Path"); }
+ }
+
+ ///
+ /// Secrets file path {secretsFilePath}.
+ ///
+ internal static string FormatMessage_Secret_File_Path(object secretsFilePath)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_File_Path", "secretsFilePath"), secretsFilePath);
+ }
+
+ ///
+ /// {key} = {value}
+ ///
+ internal static string Message_Secret_Value_Format
+ {
+ get { return GetString("Message_Secret_Value_Format"); }
+ }
+
+ ///
+ /// {key} = {value}
+ ///
+ internal static string FormatMessage_Secret_Value_Format(object key, object value)
+ {
+ return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_Value_Format", "key", "value"), key, value);
+ }
+
+ private static string GetString(string name, params string[] formatterNames)
+ {
+ var value = _resourceManager.GetString(name);
+
+ System.Diagnostics.Debug.Assert(value != null);
+
+ if (formatterNames != null)
+ {
+ for (var i = 0; i < formatterNames.Length; i++)
+ {
+ value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}");
+ }
+ }
+
+ return value;
+ }
+ }
+}
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx b/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx
new file mode 100644
index 0000000000..13c953c727
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx
@@ -0,0 +1,141 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ text/microsoft-resx
+
+
+ 2.0
+
+
+ System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089
+
+
+ Command failed : {message}
+
+
+ Cannot find '{key}' in the secret store.
+
+
+ No secrets configured for this application.
+
+
+ Project file path {project}.
+
+
+ Successfully saved {key} = {value} to the secret store.
+
+
+ Secrets file path {secretsFilePath}.
+
+
+ {key} = {value}
+
+
\ No newline at end of file
diff --git a/src/Microsoft.Extensions.SecretManager.Tools/project.json b/src/Microsoft.Extensions.SecretManager.Tools/project.json
new file mode 100644
index 0000000000..bd470ffdc8
--- /dev/null
+++ b/src/Microsoft.Extensions.SecretManager.Tools/project.json
@@ -0,0 +1,43 @@
+{
+ "version": "1.0.0-*",
+ "buildOptions": {
+ "outputName": "dotnet-user-secrets",
+ "emitEntryPoint": true,
+ "warningsAsErrors": true,
+ "keyFile": "../../tools/Key.snk",
+ "nowarn": [
+ "CS1591"
+ ],
+ "xmlDoc": true
+ },
+ "description": "Command line tool to manage user secrets for Microsoft.Extensions.Configuration.",
+ "packOptions": {
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/aspnet/DotNetTools"
+ },
+ "tags": [
+ "configuration",
+ "secrets",
+ "usersecrets"
+ ]
+ },
+ "dependencies": {
+ "Microsoft.Extensions.Configuration.UserSecrets": "1.1.0-*",
+ "Microsoft.Extensions.CommandLineUtils": "1.1.0-*",
+ "Microsoft.Extensions.Logging": "1.1.0-*",
+ "Newtonsoft.Json": "9.0.1",
+ "System.Runtime.InteropServices.RuntimeInformation": "4.0.0",
+ "System.Runtime.Serialization.Primitives": "4.1.1"
+ },
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0",
+ "type": "platform"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/Microsoft.Extensions.SecretManager.Tools.Tests.xproj b/test/Microsoft.Extensions.SecretManager.Tools.Tests/Microsoft.Extensions.SecretManager.Tools.Tests.xproj
new file mode 100644
index 0000000000..2f0849b6ac
--- /dev/null
+++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/Microsoft.Extensions.SecretManager.Tools.Tests.xproj
@@ -0,0 +1,21 @@
+
+
+
+ 14.0.25420
+ $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)
+
+
+
+ 7b331122-83b1-4f08-a119-dc846959844c
+ Microsoft.Extensions.SecretManager.Tools.Tests
+ .\obj
+ .\bin\
+
+
+ 2.0
+
+
+
+
+
+
\ No newline at end of file
diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs
new file mode 100644
index 0000000000..4618b5a92b
--- /dev/null
+++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs
@@ -0,0 +1,284 @@
+// 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.Collections.Generic;
+using System.IO;
+using System.Text;
+using Microsoft.Extensions.Configuration.UserSecrets;
+using Microsoft.Extensions.Configuration.UserSecrets.Tests;
+using Microsoft.Extensions.Logging;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+ public class SecretManagerTests
+ {
+ private TestLogger _logger;
+
+ public SecretManagerTests(ITestOutputHelper output)
+ {
+ _logger = new TestLogger(output);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void SetSecrets(bool fromCurrentDirectory)
+ {
+ var secrets = new KeyValuePair[]
+ {
+ new KeyValuePair("key1", Guid.NewGuid().ToString()),
+ new KeyValuePair("Facebook:AppId", Guid.NewGuid().ToString()),
+ new KeyValuePair(@"key-@\/.~123!#$%^&*())-+==", @"key-@\/.~123!#$%^&*())-+=="),
+ new KeyValuePair("key2", string.Empty)
+ };
+
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+
+ var dir = fromCurrentDirectory
+ ? projectPath
+ : Path.GetTempPath();
+ var secretManager = new Program(Console.Out, dir) { Logger = _logger };
+
+ foreach (var secret in secrets)
+ {
+ var parameters = fromCurrentDirectory ?
+ new string[] { "set", secret.Key, secret.Value } :
+ new string[] { "set", secret.Key, secret.Value, "-p", projectPath };
+ secretManager.RunInternal(parameters);
+ }
+
+ Assert.Equal(4, _logger.Messages.Count);
+
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
+ _logger.Messages);
+ }
+
+ _logger.Messages.Clear();
+ var args = fromCurrentDirectory
+ ? new string[] { "list" }
+ : new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Equal(4, _logger.Messages.Count);
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
+ _logger.Messages);
+ }
+
+ // Remove secrets.
+ _logger.Messages.Clear();
+ foreach (var secret in secrets)
+ {
+ var parameters = fromCurrentDirectory ?
+ new string[] { "remove", secret.Key } :
+ new string[] { "remove", secret.Key, "-p", projectPath };
+ secretManager.RunInternal(parameters);
+ }
+
+ // Verify secrets are removed.
+ _logger.Messages.Clear();
+ args = fromCurrentDirectory
+ ? new string[] { "list" }
+ : new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
+
+ UserSecretHelper.DeleteTempSecretProject(projectPath);
+ }
+
+ [Fact]
+ public void SetSecret_Update_Existing_Secret()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ var secretManager = new Program() { Logger = _logger };
+
+ secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
+ secretManager.RunInternal("set", "secret1", "value2", "-p", projectPath);
+ Assert.Equal(2, _logger.Messages.Count);
+ Assert.Contains("Successfully saved secret1 = value2 to the secret store.", _logger.Messages);
+
+ _logger.Messages.Clear();
+
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains("secret1 = value2", _logger.Messages);
+
+ UserSecretHelper.DeleteTempSecretProject(projectPath);
+ }
+
+ [Fact]
+ public void SetSecret_With_Verbose_Flag()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ _logger.SetLevel(LogLevel.Debug);
+ var secretManager = new Program() { Logger = _logger };
+
+ secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
+ Assert.Equal(3, _logger.Messages.Count);
+ Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "project.json")), _logger.Messages);
+ Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPath(projectPath)), _logger.Messages);
+ Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
+ _logger.Messages.Clear();
+
+ secretManager.RunInternal("-v", "list", "-p", projectPath);
+
+ Assert.Equal(3, _logger.Messages.Count);
+ Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "project.json")), _logger.Messages);
+ Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPath(projectPath)), _logger.Messages);
+ Assert.Contains("secret1 = value1", _logger.Messages);
+
+ UserSecretHelper.DeleteTempSecretProject(projectPath);
+ }
+
+ [Fact]
+ public void Remove_Non_Existing_Secret()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ var secretManager = new Program() { Logger = _logger };
+ secretManager.RunInternal("remove", "secret1", "-p", projectPath);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages);
+ }
+
+ [Fact]
+ public void Remove_Is_Case_Insensitive()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ var secretManager = new Program() { Logger = _logger };
+ secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Contains("SeCreT1 = value", _logger.Messages);
+ secretManager.RunInternal("remove", "secret1", "-p", projectPath);
+
+ Assert.Equal(2, _logger.Messages.Count);
+ _logger.Messages.Clear();
+ secretManager.RunInternal("list", "-p", projectPath);
+
+ Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
+
+ UserSecretHelper.DeleteTempSecretProject(projectPath);
+ }
+
+ [Fact]
+ public void List_Flattens_Nested_Objects()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ var secretsFile = PathHelper.GetSecretsPath(projectPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
+ File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
+ var secretManager = new Program() { Logger = _logger };
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
+
+ UserSecretHelper.DeleteTempSecretProject(projectPath);
+ }
+
+ [Fact]
+ public void Set_Flattens_Nested_Objects()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ var secretsFile = PathHelper.GetSecretsPath(projectPath);
+ Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
+ File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
+ var secretManager = new Program() { Logger = _logger };
+ secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
+ Assert.Equal(1, _logger.Messages.Count);
+ secretManager.RunInternal("list", "-p", projectPath);
+
+ Assert.Equal(2, _logger.Messages.Count);
+ Assert.Contains("AzureAd:ClientSecret = ¡™£¢∞", _logger.Messages);
+ var fileContents = File.ReadAllText(secretsFile, Encoding.UTF8);
+ Assert.Equal(@"{
+ ""AzureAd:ClientSecret"": ""¡™£¢∞""
+}",
+ fileContents, ignoreLineEndingDifferences: true, ignoreWhiteSpaceDifferences: true);
+
+ UserSecretHelper.DeleteTempSecretProject(projectPath);
+ }
+
+ [Fact]
+ public void List_Empty_Secrets_File()
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+ var secretManager = new Program() { Logger = _logger };
+ secretManager.RunInternal("list", "-p", projectPath);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
+ }
+
+ [Theory]
+ [InlineData(true)]
+ [InlineData(false)]
+ public void Clear_Secrets(bool fromCurrentDirectory)
+ {
+ var projectPath = UserSecretHelper.GetTempSecretProject();
+
+ var dir = fromCurrentDirectory
+ ? projectPath
+ : Path.GetTempPath();
+
+ var secretManager = new Program(Console.Out, dir) { Logger = _logger };
+
+ var secrets = new KeyValuePair[]
+ {
+ new KeyValuePair("key1", Guid.NewGuid().ToString()),
+ new KeyValuePair("Facebook:AppId", Guid.NewGuid().ToString()),
+ new KeyValuePair(@"key-@\/.~123!#$%^&*())-+==", @"key-@\/.~123!#$%^&*())-+=="),
+ new KeyValuePair("key2", string.Empty)
+ };
+
+ foreach (var secret in secrets)
+ {
+ var parameters = fromCurrentDirectory ?
+ new string[] { "set", secret.Key, secret.Value } :
+ new string[] { "set", secret.Key, secret.Value, "-p", projectPath };
+ secretManager.RunInternal(parameters);
+ }
+
+ Assert.Equal(4, _logger.Messages.Count);
+
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("Successfully saved {0} = {1} to the secret store.", keyValue.Key, keyValue.Value),
+ _logger.Messages);
+ }
+
+ // Verify secrets are persisted.
+ _logger.Messages.Clear();
+ var args = fromCurrentDirectory ?
+ new string[] { "list" } :
+ new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Equal(4, _logger.Messages.Count);
+ foreach (var keyValue in secrets)
+ {
+ Assert.Contains(
+ string.Format("{0} = {1}", keyValue.Key, keyValue.Value),
+ _logger.Messages);
+ }
+
+ // Clear secrets.
+ _logger.Messages.Clear();
+ args = fromCurrentDirectory ? new string[] { "clear" } : new string[] { "clear", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Equal(0, _logger.Messages.Count);
+
+ args = fromCurrentDirectory ? new string[] { "list" } : new string[] { "list", "-p", projectPath };
+ secretManager.RunInternal(args);
+ Assert.Equal(1, _logger.Messages.Count);
+ Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/TestLogger.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/TestLogger.cs
new file mode 100644
index 0000000000..1176d33218
--- /dev/null
+++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/TestLogger.cs
@@ -0,0 +1,50 @@
+// 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.Collections.Generic;
+using Microsoft.Extensions.Logging;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+ public class TestLogger : ILogger
+ {
+ private CommandOutputProvider _commandOutputProvider;
+ private readonly ILogger _wrapped;
+ private readonly ITestOutputHelper _output;
+
+ public TestLogger(ITestOutputHelper output = null)
+ {
+ _commandOutputProvider = new CommandOutputProvider();
+ _wrapped = _commandOutputProvider.CreateLogger("");
+ _output = output;
+ }
+
+ public void SetLevel(LogLevel level)
+ {
+ _commandOutputProvider.LogLevel = LogLevel.Debug;
+ }
+
+ public List Messages { get; set; } = new List();
+
+ public IDisposable BeginScope(TState state)
+ {
+ throw new NotImplementedException();
+ }
+
+ public bool IsEnabled(LogLevel logLevel)
+ {
+ return _wrapped.IsEnabled(logLevel);
+ }
+
+ public void Log(LogLevel logLevel, EventId eventId, TState state, Exception exception, Func formatter)
+ {
+ if (IsEnabled(logLevel))
+ {
+ Messages.Add(formatter(state, exception));
+ _output?.WriteLine(formatter(state, exception));
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs
new file mode 100644
index 0000000000..87dbddc3e4
--- /dev/null
+++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs
@@ -0,0 +1,47 @@
+// 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 Newtonsoft.Json;
+
+namespace Microsoft.Extensions.Configuration.UserSecrets.Tests
+{
+ internal class UserSecretHelper
+ {
+ internal static string GetTempSecretProject()
+ {
+ string userSecretsId;
+ return GetTempSecretProject(out userSecretsId);
+ }
+
+ internal static string GetTempSecretProject(out string userSecretsId)
+ {
+ var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "usersecretstest", Guid.NewGuid().ToString()));
+ userSecretsId = Guid.NewGuid().ToString();
+ File.WriteAllText(
+ Path.Combine(projectPath.FullName, "project.json"),
+ JsonConvert.SerializeObject(new { userSecretsId }));
+ return projectPath.FullName;
+ }
+
+ internal static void SetTempSecretInProject(string projectPath, string userSecretsId)
+ {
+ File.WriteAllText(
+ Path.Combine(projectPath, "project.json"),
+ JsonConvert.SerializeObject(new { userSecretsId }));
+ }
+
+ internal static void DeleteTempSecretProject(string projectPath)
+ {
+ try
+ {
+ Directory.Delete(projectPath, true);
+ }
+ catch (Exception)
+ {
+ // Ignore failures.
+ }
+ }
+ }
+}
\ No newline at end of file
diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/project.json b/test/Microsoft.Extensions.SecretManager.Tools.Tests/project.json
new file mode 100644
index 0000000000..18868d2684
--- /dev/null
+++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/project.json
@@ -0,0 +1,22 @@
+{
+ "buildOptions": {
+ "warningsAsErrors": true,
+ "keyFile": "../../tools/Key.snk"
+ },
+ "dependencies": {
+ "dotnet-test-xunit": "2.2.0-*",
+ "Microsoft.Extensions.SecretManager.Tools": "1.0.0-*",
+ "xunit": "2.2.0-*"
+ },
+ "testRunner": "xunit",
+ "frameworks": {
+ "netcoreapp1.0": {
+ "dependencies": {
+ "Microsoft.NETCore.App": {
+ "version": "1.0.0",
+ "type": "platform"
+ }
+ }
+ }
+ }
+}
\ No newline at end of file