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 +[![Travis build status](https://img.shields.io/travis/aspnet/dotnet-watch.svg?label=travis-ci&branch=dev&style=flat-square)](https://travis-ci.org/aspnet/dotnet-watch/branches) +[![AppVeyor build status](https://img.shields.io/appveyor/ci/aspnetci/dnx-watch/dev.svg?label=appveyor&style=flat-square)](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: [![AppVeyor](https://ci.appveyor.com/api/projects/status/fxhto3omtehio3aj/branch/dev?svg=true)](https://ci.appveyor.com/project/aspnetci/dnx-watch/branch/dev) - -Travis: [![Travis](https://travis-ci.org/aspnet/dotnet-watch.svg?branch=dev)](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