From 29df59b89cbe4021bf8b1f19f3e5f447c232ede4 Mon Sep 17 00:00:00 2001 From: Nate McMaster Date: Fri, 7 Oct 2016 09:56:08 -0700 Subject: [PATCH] Create initial prototype of dotnet-user-secrets with MSBuild support --- .gitignore | 1 + .../FindUserSecretsProperty.targets | 6 + .../Internal/CommandLineOptions.cs | 6 + .../Internal/GracefulException.cs | 25 ---- .../Internal/MsBuildProjectFinder.cs | 61 ++++++++ .../Internal/ProjectIdResolver.cs | 130 ++++++++++++++++++ .../Internal/RemoveCommand.cs | 1 + .../Internal/SecretsStore.cs | 1 + .../Internal/SetCommand.cs | 1 + ...soft.Extensions.SecretManager.Tools.nuspec | 10 +- .../Program.cs | 101 +++----------- .../Properties/Resources.Designer.cs | 80 +++++++++++ .../Resources.resx | 69 ++++++---- .../project.json | 20 ++- .../MsBuildProjectFinderTest.cs | 86 ++++++++++++ .../SecretManagerTests.cs | 115 +++++++++------- .../SetCommandTest.cs | 2 +- .../TemporaryFileProvider.cs | 29 ++++ .../UserSecretHelper.cs | 47 ------- .../UserSecretsTestFixture.cs | 103 ++++++++++++++ 20 files changed, 649 insertions(+), 245 deletions(-) create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/FindUserSecretsProperty.targets delete mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/MsBuildProjectFinder.cs create mode 100644 src/Microsoft.Extensions.SecretManager.Tools/Internal/ProjectIdResolver.cs create mode 100644 test/Microsoft.Extensions.SecretManager.Tools.Tests/MsBuildProjectFinderTest.cs create mode 100644 test/Microsoft.Extensions.SecretManager.Tools.Tests/TemporaryFileProvider.cs delete mode 100644 test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs create mode 100644 test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretsTestFixture.cs diff --git a/.gitignore b/.gitignore index 33889157be..29b5f79c9b 100644 --- a/.gitignore +++ b/.gitignore @@ -28,6 +28,7 @@ project.lock.json .testPublish/ .build/ /.vs/ +.vscode/ testWorkDir/ *.nuget.props *.nuget.targets \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/FindUserSecretsProperty.targets b/src/Microsoft.Extensions.SecretManager.Tools/FindUserSecretsProperty.targets new file mode 100644 index 0000000000..694dc25008 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/FindUserSecretsProperty.targets @@ -0,0 +1,6 @@ + + + + + + \ 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 index 37705429a5..42d826e1e0 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/CommandLineOptions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Reflection; +using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.Extensions.SecretManager.Tools.Internal @@ -13,6 +14,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal public bool IsHelp { get; set; } public string Project { get; set; } public ICommand Command { get; set; } + public string Configuration { get; set; } public static CommandLineOptions Parse(string[] args, IConsole console) { @@ -34,6 +36,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal var optionProject = app.Option("-p|--project ", "Path to project, default is current directory", CommandOptionType.SingleValue, inherited: true); + var optionConfig = app.Option("-c|--configuration ", $"The project configuration to use. Defaults to {Constants.DefaultConfiguration}", + CommandOptionType.SingleValue, inherited: true); + // the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one // in the current project var optionId = app.Option("--id", "The user secret id to use.", @@ -55,6 +60,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal return null; } + options.Configuration = optionConfig.Value(); options.Id = optionId.Value(); options.IsHelp = app.IsShowingInformation; options.IsVerbose = optionVerbose.HasValue(); diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs deleted file mode 100644 index 7e54c8ba2f..0000000000 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/GracefulException.cs +++ /dev/null @@ -1,25 +0,0 @@ -// 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/MsBuildProjectFinder.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/MsBuildProjectFinder.cs new file mode 100644 index 0000000000..e080b4cb83 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/MsBuildProjectFinder.cs @@ -0,0 +1,61 @@ +// 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 System.Linq; +using Microsoft.DotNet.Cli.Utils; + +namespace Microsoft.Extensions.SecretManager.Tools.Internal +{ + internal class MsBuildProjectFinder + { + private readonly string _directory; + + public MsBuildProjectFinder(string directory) + { + if (string.IsNullOrEmpty(directory)) + { + throw new ArgumentException(Resources.Common_StringNullOrEmpty, nameof(directory)); + } + + _directory = directory; + } + + public string FindMsBuildProject(string project) + { + var projectPath = project ?? _directory; + + if (!Path.IsPathRooted(projectPath)) + { + projectPath = Path.Combine(_directory, projectPath); + } + + if (Directory.Exists(projectPath)) + { + var projects = Directory.EnumerateFileSystemEntries(projectPath, "*.*proj", SearchOption.TopDirectoryOnly) + .Where(f => !".xproj".Equals(Path.GetExtension(f), StringComparison.OrdinalIgnoreCase)) + .ToList(); + + if (projects.Count > 1) + { + throw new GracefulException(Resources.FormatError_MultipleProjectsFound(projectPath)); + } + + if (projects.Count == 0) + { + throw new GracefulException(Resources.FormatError_NoProjectsFound(projectPath)); + } + + return projects[0]; + } + + if (!File.Exists(projectPath)) + { + throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(projectPath)); + } + + return projectPath; + } + } +} \ No newline at end of file diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/ProjectIdResolver.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ProjectIdResolver.cs new file mode 100644 index 0000000000..683f412c83 --- /dev/null +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/ProjectIdResolver.cs @@ -0,0 +1,130 @@ +// 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.Reflection; +using Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.Logging; + +namespace Microsoft.Extensions.SecretManager.Tools.Internal +{ + public class ProjectIdResolver : IDisposable + { + private const string TargetsFileName = "FindUserSecretsProperty.targets"; + private readonly ILogger _logger; + private readonly string _workingDirectory; + private readonly List _tempFiles = new List(); + + public ProjectIdResolver(ILogger logger, string workingDirectory) + { + _workingDirectory = workingDirectory; + _logger = logger; + } + + public string Resolve(string project, string configuration = Constants.DefaultConfiguration) + { + var finder = new MsBuildProjectFinder(_workingDirectory); + var projectFile = finder.FindMsBuildProject(project); + + _logger.LogDebug(Resources.Message_Project_File_Path, projectFile); + + var targetFile = GetTargetFile(); + var outputFile = Path.GetTempFileName(); + _tempFiles.Add(outputFile); + + var commandOutput = new List(); + var commandResult = Command.CreateDotNet("msbuild", + new[] { + targetFile, + "/nologo", + "/t:_FindUserSecretsProperty", + $"/p:Project={projectFile}", + $"/p:OutputFile={outputFile}", + $"/p:Configuration={configuration}" + }) + .CaptureStdErr() + .CaptureStdOut() + .OnErrorLine(l => commandOutput.Add(l)) + .OnOutputLine(l => commandOutput.Add(l)) + .Execute(); + + if (commandResult.ExitCode != 0) + { + _logger.LogDebug(string.Join(Environment.NewLine, commandOutput)); + throw new GracefulException(Resources.FormatError_ProjectFailedToLoad(projectFile)); + } + + var id = File.ReadAllText(outputFile)?.Trim(); + if (string.IsNullOrEmpty(id)) + { + throw new GracefulException(Resources.FormatError_ProjectMissingId(projectFile)); + } + + return id; + } + + public void Dispose() + { + foreach (var file in _tempFiles) + { + TryDelete(file); + } + } + + private string GetTargetFile() + { + var assemblyDir = Path.GetDirectoryName(GetType().GetTypeInfo().Assembly.Location); + + // targets should be in one of these locations, depending on test setup and tools installation + var searchPaths = new[] + { + AppContext.BaseDirectory, + assemblyDir, // next to assembly + Path.Combine(assemblyDir, "../../tools"), // inside the nupkg + }; + + var foundFile = searchPaths + .Select(dir => Path.Combine(dir, TargetsFileName)) + .Where(File.Exists) + .FirstOrDefault(); + + if (foundFile != null) + { + return foundFile; + } + + // This should only really happen during testing. Current build system doesn't give us a good way to ensure the + // test project has an always-up to date version of the targets file. + // TODO cleanup after we switch to an MSBuild system in which can specify "CopyToOutputDirectory: Always" to resolve this issue + var outputPath = Path.GetTempFileName(); + using (var resource = GetType().GetTypeInfo().Assembly.GetManifestResourceStream(TargetsFileName)) + using (var stream = new FileStream(outputPath, FileMode.Create)) + { + resource.CopyTo(stream); + } + + // cleanup + _tempFiles.Add(outputPath); + + return outputPath; + } + + private static void TryDelete(string file) + { + try + { + if (File.Exists(file)) + { + File.Delete(file); + } + } + catch + { + // whatever + } + } + } +} \ 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 index 84910b9a99..6e6e5af027 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/RemoveCommand.cs @@ -1,6 +1,7 @@ // 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.DotNet.Cli.Utils; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs index 67461d732d..588e66dcba 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SecretsStore.cs @@ -26,6 +26,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal } _secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); + logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath); _secrets = Load(userSecretsId); } diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs index 4799d9ff88..e8c3855961 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Internal/SetCommand.cs @@ -4,6 +4,7 @@ using System.Diagnostics; using System.IO; using System.Text; +using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Logging; diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.nuspec b/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.nuspec index 2ddab547bf..db6cb0bf93 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.nuspec +++ b/src/Microsoft.Extensions.SecretManager.Tools/Microsoft.Extensions.SecretManager.Tools.nuspec @@ -16,16 +16,16 @@ - + - - - - + + + + diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Program.cs b/src/Microsoft.Extensions.SecretManager.Tools/Program.cs index cef3cad322..3ef60eb563 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Program.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Program.cs @@ -2,15 +2,10 @@ // 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.DotNet.Cli.Utils; 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 { @@ -21,12 +16,16 @@ namespace Microsoft.Extensions.SecretManager.Tools private readonly IConsole _console; private readonly string _workingDirectory; - public Program() - : this(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()) + public static int Main(string[] args) { + DebugHelper.HandleDebugSwitch(ref args); + + int rc; + new Program(PhysicalConsole.Singleton, Directory.GetCurrentDirectory()).TryRun(args, out rc); + return rc; } - internal Program(IConsole console, string workingDirectory) + public Program(IConsole console, string workingDirectory) { _console = console; _workingDirectory = workingDirectory; @@ -65,33 +64,6 @@ namespace Microsoft.Extensions.SecretManager.Tools } } - 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 @@ -103,6 +75,11 @@ namespace Microsoft.Extensions.SecretManager.Tools { if (exception is GracefulException) { + if (exception.InnerException != null) + { + Logger.LogInformation(exception.InnerException.Message); + } + Logger.LogError(exception.Message); } else @@ -134,65 +111,23 @@ namespace Microsoft.Extensions.SecretManager.Tools CommandOutputProvider.LogLevel = LogLevel.Debug; } - var userSecretsId = ResolveUserSecretsId(options); + var userSecretsId = ResolveId(options); var store = new SecretsStore(userSecretsId, Logger); - var context = new CommandContext(store, Logger, _console); + var context = new Internal.CommandContext(store, Logger, _console); options.Command.Execute(context); return 0; } - private string ResolveUserSecretsId(CommandLineOptions options) + internal string ResolveId(CommandLineOptions options) { if (!string.IsNullOrEmpty(options.Id)) { return options.Id; } - var projectPath = options.Project ?? _workingDirectory; - - if (!Path.IsPathRooted(projectPath)) + using (var resolver = new ProjectIdResolver(Logger, _workingDirectory)) { - projectPath = Path.Combine(_workingDirectory, projectPath); - } - - if (!projectPath.EndsWith("project.json", StringComparison.OrdinalIgnoreCase)) - { - projectPath = Path.Combine(projectPath, "project.json"); - } - - var fileInfo = new PhysicalFileInfo(new FileInfo(projectPath)); - - if (!fileInfo.Exists) - { - throw new GracefulException(Resources.FormatError_ProjectPath_NotFound(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; + return resolver.Resolve(options.Project, options.Configuration); } } } diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs b/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs index f8af024110..a74bdc6798 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs +++ b/src/Microsoft.Extensions.SecretManager.Tools/Properties/Resources.Designer.cs @@ -10,6 +10,22 @@ namespace Microsoft.Extensions.SecretManager.Tools private static readonly ResourceManager _resourceManager = new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly); + /// + /// Value cannot be null or an empty string. + /// + internal static string Common_StringNullOrEmpty + { + get { return GetString("Common_StringNullOrEmpty"); } + } + + /// + /// Value cannot be null or an empty string. + /// + internal static string FormatCommon_StringNullOrEmpty() + { + return GetString("Common_StringNullOrEmpty"); + } + /// /// Command failed : {message} /// @@ -60,6 +76,22 @@ namespace Microsoft.Extensions.SecretManager.Tools return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key); } + /// + /// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + /// + internal static string Error_MultipleProjectsFound + { + get { return GetString("Error_MultipleProjectsFound"); } + } + + /// + /// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + /// + internal static string FormatError_MultipleProjectsFound(object projectPath) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath); + } + /// /// No secrets configured for this application. /// @@ -76,6 +108,38 @@ namespace Microsoft.Extensions.SecretManager.Tools return GetString("Error_No_Secrets_Found"); } + /// + /// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + /// + internal static string Error_NoProjectsFound + { + get { return GetString("Error_NoProjectsFound"); } + } + + /// + /// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + /// + internal static string FormatError_NoProjectsFound(object projectPath) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath); + } + + /// + /// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option. + /// + internal static string Error_ProjectMissingId + { + get { return GetString("Error_ProjectMissingId"); } + } + + /// + /// Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option. + /// + internal static string FormatError_ProjectMissingId(object project) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectMissingId", "project"), project); + } + /// /// The project file '{path}' does not exist. /// @@ -92,6 +156,22 @@ namespace Microsoft.Extensions.SecretManager.Tools return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path); } + /// + /// Could not load the MSBuild project '{project}'. + /// + internal static string Error_ProjectFailedToLoad + { + get { return GetString("Error_ProjectFailedToLoad"); } + } + + /// + /// Could not load the MSBuild project '{project}'. + /// + internal static string FormatError_ProjectFailedToLoad(object project) + { + return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectFailedToLoad", "project"), project); + } + /// /// Project file path {project}. /// diff --git a/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx b/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx index 63d65502dc..3ee74a41e9 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx +++ b/src/Microsoft.Extensions.SecretManager.Tools/Resources.resx @@ -1,17 +1,17 @@  - @@ -117,6 +117,9 @@ System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + Value cannot be null or an empty string. + Command failed : {message} @@ -127,12 +130,24 @@ Use the '--help' flag to see info. Cannot find '{key}' in the secret store. + + Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + No secrets configured for this application. + + Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + + + Could not find the global property 'UserSecretsId' in MSBuild project '{project}'. Ensure this property is set in the project or use the '--id' command line option. + The project file '{path}' does not exist. + + Could not load the MSBuild project '{project}'. + Project file path {project}. diff --git a/src/Microsoft.Extensions.SecretManager.Tools/project.json b/src/Microsoft.Extensions.SecretManager.Tools/project.json index 77d376be2d..1f2aaa7e15 100644 --- a/src/Microsoft.Extensions.SecretManager.Tools/project.json +++ b/src/Microsoft.Extensions.SecretManager.Tools/project.json @@ -4,7 +4,13 @@ "outputName": "dotnet-user-secrets", "emitEntryPoint": true, "warningsAsErrors": true, - "keyFile": "../../tools/Key.snk" + "keyFile": "../../tools/Key.snk", + "copyToOutput": "*.targets", + "embed": { + "mappings": { + "FindUserSecretsProperty.targets": "./FindUserSecretsProperty.targets" + } + } }, "description": "Command line tool to manage user secrets for Microsoft.Extensions.Configuration.", "packOptions": { @@ -16,18 +22,22 @@ "configuration", "secrets", "usersecrets" - ] + ], + "files": { + "mappings": { + "tools/FindUserSecretsProperty.targets": "FindUserSecretsProperty.targets" + } + } }, "dependencies": { + "Microsoft.DotNet.Cli.Utils": "1.0.0-*", "Microsoft.Extensions.CommandLineUtils": "1.1.0-*", "Microsoft.Extensions.Configuration.UserSecrets": "1.1.0-*", "Microsoft.Extensions.Logging": "1.1.0-*", "Microsoft.NETCore.App": { "version": "1.1.0-*", "type": "platform" - }, - "Newtonsoft.Json": "9.0.1", - "System.Runtime.Serialization.Primitives": "4.3.0-*" + } }, "frameworks": { "netcoreapp1.0": {} diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/MsBuildProjectFinderTest.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/MsBuildProjectFinderTest.cs new file mode 100644 index 0000000000..fad1808677 --- /dev/null +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/MsBuildProjectFinderTest.cs @@ -0,0 +1,86 @@ +// 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 Microsoft.DotNet.Cli.Utils; +using Microsoft.Extensions.SecretManager.Tools.Internal; +using Xunit; + +namespace Microsoft.Extensions.SecretsManager.Tools.Tests +{ + public class MsBuildProjectFinderTest + { + [Theory] + [InlineData(".csproj")] + [InlineData(".vbproj")] + [InlineData(".fsproj")] + public void FindsSingleProject(string extension) + { + using (var files = new TemporaryFileProvider()) + { + var filename = "TestProject" + extension; + files.Add(filename, ""); + + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Equal(Path.Combine(files.Root, filename), finder.FindMsBuildProject(null)); + } + } + + [Fact] + public void ThrowsWhenNoFile() + { + using (var files = new TemporaryFileProvider()) + { + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Throws(() => finder.FindMsBuildProject(null)); + } + } + + [Fact] + public void DoesNotMatchXproj() + { + using (var files = new TemporaryFileProvider()) + { + var finder = new MsBuildProjectFinder(files.Root); + files.Add("test.xproj", ""); + + Assert.Throws(() => finder.FindMsBuildProject(null)); + } + } + + [Fact] + public void ThrowsWhenMultipleFile() + { + using (var files = new TemporaryFileProvider()) + { + files.Add("Test1.csproj", ""); + files.Add("Test2.csproj", ""); + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Throws(() => finder.FindMsBuildProject(null)); + } + } + + [Fact] + public void ThrowsWhenFileDoesNotExist() + { + using (var files = new TemporaryFileProvider()) + { + var finder = new MsBuildProjectFinder(files.Root); + + Assert.Throws(() => finder.FindMsBuildProject("test.csproj")); + } + } + + [Fact] + public void ThrowsWhenRootDoesNotExist() + { + var files = new TemporaryFileProvider(); + var finder = new MsBuildProjectFinder(files.Root); + files.Dispose(); + Assert.Throws(() => finder.FindMsBuildProject(null)); + } + } +} diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs index 082b53c3dd..9420fc2110 100644 --- a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SecretManagerTests.cs @@ -5,68 +5,79 @@ using System; using System.Collections.Generic; using System.IO; using System.Text; +using Microsoft.DotNet.Cli.Utils; using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Configuration.UserSecrets.Tests; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.SecretManager.Tools.Internal; using Xunit; using Xunit.Abstractions; namespace Microsoft.Extensions.SecretManager.Tools.Tests { - public class SecretManagerTests : IDisposable + public class SecretManagerTests : IClassFixture { private TestLogger _logger; - private Stack _disposables = new Stack(); + private readonly UserSecretsTestFixture _fixture; - public SecretManagerTests(ITestOutputHelper output) + public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output) { + _fixture = fixture; _logger = new TestLogger(output); + } - private string GetTempSecretProject() + private Program CreateProgram() { - string id; - return GetTempSecretProject(out id); - } - - private string GetTempSecretProject(out string userSecretsId) - { - var projectPath = UserSecretHelper.GetTempSecretProject(out userSecretsId); - _disposables.Push(() => UserSecretHelper.DeleteTempSecretProject(projectPath)); - return projectPath; - } - public void Dispose() - { - while (_disposables.Count > 0) + return new Program(new TestConsole(), Directory.GetCurrentDirectory()) { - _disposables.Pop().Invoke(); - } + Logger = _logger + }; + } + + [Theory] + [InlineData(null)] + [InlineData("")] + public void Error_MissingId(string id) + { + var project = Path.Combine(_fixture.CreateProject(id), "TestProject.csproj"); + var secretManager = CreateProgram(); + + var ex = Assert.Throws(() => secretManager.RunInternal("list", "-p", project)); + Assert.Equal(Resources.FormatError_ProjectMissingId(project), ex.Message); + } + + [Fact] + public void Error_InvalidProjectFormat() + { + var project = Path.Combine(_fixture.CreateProject("<"), "TestProject.csproj"); + var secretManager = CreateProgram(); + + var ex = Assert.Throws(() => secretManager.RunInternal("list", "-p", project)); + Assert.Equal(Resources.FormatError_ProjectFailedToLoad(project), ex.Message); } [Fact] public void Error_Project_DoesNotExist() { - var projectPath = Path.Combine(GetTempSecretProject(), "does_not_exist", "project.json"); - var secretManager = new Program(new TestConsole(), Directory.GetCurrentDirectory()) { Logger = _logger }; + var projectPath = Path.Combine(_fixture.GetTempSecretProject(), "does_not_exist", "TestProject.csproj"); + var secretManager = CreateProgram(); var ex = Assert.Throws(() => secretManager.RunInternal("list", "--project", projectPath)); - Assert.Equal(Resources.FormatError_ProjectPath_NotFound(projectPath), ex.Message); } [Fact] public void SupportsRelativePaths() { - var projectPath = GetTempSecretProject(); + var projectPath = _fixture.GetTempSecretProject(); var cwd = Path.Combine(projectPath, "nested1"); Directory.CreateDirectory(cwd); var secretManager = new Program(new TestConsole(), cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider }; secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug; - secretManager.RunInternal("list", "-p", "../", "--verbose"); + secretManager.RunInternal("list", "-p", ".." + Path.DirectorySeparatorChar, "--verbose"); - Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(projectPath, "project.json")), _logger.Messages); + Assert.Contains(Resources.FormatMessage_Project_File_Path(Path.Combine(cwd, "..", "TestProject.csproj")), _logger.Messages); } [Theory] @@ -82,7 +93,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests new KeyValuePair("key2", string.Empty) }; - var projectPath = GetTempSecretProject(); + var projectPath = _fixture.GetTempSecretProject(); var dir = fromCurrentDirectory ? projectPath : Path.GetTempPath(); @@ -141,8 +152,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [Fact] public void SetSecret_Update_Existing_Secret() { - var projectPath = GetTempSecretProject(); - var secretManager = new Program() { Logger = _logger }; + var projectPath = _fixture.GetTempSecretProject(); + var secretManager = CreateProgram(); secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath); Assert.Equal(1, _logger.Messages.Count); @@ -161,31 +172,31 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [Fact] public void SetSecret_With_Verbose_Flag() { - string id; - var projectPath = GetTempSecretProject(out id); + string secretId; + var projectPath = _fixture.GetTempSecretProject(out secretId); _logger.SetLevel(LogLevel.Debug); - var secretManager = new Program() { Logger = _logger }; + var secretManager = CreateProgram(); 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.GetSecretsPathFromSecretsId(id)), _logger.Messages); + Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages); + Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _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.GetSecretsPathFromSecretsId(id)), _logger.Messages); + Assert.Contains(string.Format("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages); + Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages); Assert.Contains("secret1 = value1", _logger.Messages); } [Fact] public void Remove_Non_Existing_Secret() { - var projectPath = GetTempSecretProject(); - var secretManager = new Program() { Logger = _logger }; + var projectPath = _fixture.GetTempSecretProject(); + var secretManager = CreateProgram(); secretManager.RunInternal("remove", "secret1", "-p", projectPath); Assert.Equal(1, _logger.Messages.Count); Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages); @@ -194,8 +205,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [Fact] public void Remove_Is_Case_Insensitive() { - var projectPath = GetTempSecretProject(); - var secretManager = new Program() { Logger = _logger }; + var projectPath = _fixture.GetTempSecretProject(); + var secretManager = CreateProgram(); secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath); Assert.Contains("SeCreT1 = value", _logger.Messages); @@ -211,12 +222,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [Fact] public void List_Flattens_Nested_Objects() { - string id; - var projectPath = GetTempSecretProject(out id); - var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); + string secretId; + var projectPath = _fixture.GetTempSecretProject(out secretId); + var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); - var secretManager = new Program() { Logger = _logger }; + var secretManager = CreateProgram(); secretManager.RunInternal("list", "-p", projectPath); Assert.Equal(1, _logger.Messages.Count); Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages); @@ -231,7 +242,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests Out = new StringWriter(output) }; string id; - var projectPath = GetTempSecretProject(out id); + var projectPath = _fixture.GetTempSecretProject(out id); var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); @@ -246,12 +257,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [Fact] public void Set_Flattens_Nested_Objects() { - string id; - var projectPath = GetTempSecretProject(out id); - var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); + string secretId; + var projectPath = _fixture.GetTempSecretProject(out secretId); + var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); - var secretManager = new Program() { Logger = _logger }; + var secretManager = CreateProgram(); secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath); Assert.Equal(1, _logger.Messages.Count); secretManager.RunInternal("list", "-p", projectPath); @@ -268,8 +279,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [Fact] public void List_Empty_Secrets_File() { - var projectPath = GetTempSecretProject(); - var secretManager = new Program() { Logger = _logger }; + var projectPath = _fixture.GetTempSecretProject(); + var secretManager = CreateProgram(); secretManager.RunInternal("list", "-p", projectPath); Assert.Equal(1, _logger.Messages.Count); Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages); @@ -280,7 +291,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests [InlineData(false)] public void Clear_Secrets(bool fromCurrentDirectory) { - var projectPath = GetTempSecretProject(); + var projectPath = _fixture.GetTempSecretProject(); var dir = fromCurrentDirectory ? projectPath diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs index 7fdd43b009..b704a87c36 100644 --- a/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/SetCommandTest.cs @@ -75,7 +75,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests var secretStore = new TestSecretsStore(); var command = new SetCommand("key", null); - var ex = Assert.Throws( + var ex = Assert.Throws< Microsoft.DotNet.Cli.Utils.GracefulException>( () => command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole))); Assert.Equal(Resources.FormatError_MissingArgument("value"), ex.Message); } diff --git a/test/Microsoft.Extensions.SecretManager.Tools.Tests/TemporaryFileProvider.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/TemporaryFileProvider.cs new file mode 100644 index 0000000000..08e4449d0c --- /dev/null +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/TemporaryFileProvider.cs @@ -0,0 +1,29 @@ +// 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 System.Text; + +namespace Microsoft.Extensions.SecretsManager.Tools.Tests +{ + internal class TemporaryFileProvider : IDisposable + { + public TemporaryFileProvider() + { + Root = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "tmpfiles", Guid.NewGuid().ToString())).FullName; + } + + public string Root { get; } + + public void Add(string filename, string contents) + { + File.WriteAllText(Path.Combine(Root, filename), contents, Encoding.UTF8); + } + + public void Dispose() + { + Directory.Delete(Root, recursive: true); + } + } +} \ 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 deleted file mode 100644 index 00932db0f3..0000000000 --- a/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretHelper.cs +++ /dev/null @@ -1,47 +0,0 @@ -// 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 -{ - public 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/UserSecretsTestFixture.cs b/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretsTestFixture.cs new file mode 100644 index 0000000000..cf92e9b040 --- /dev/null +++ b/test/Microsoft.Extensions.SecretManager.Tools.Tests/UserSecretsTestFixture.cs @@ -0,0 +1,103 @@ +// 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; + +namespace Microsoft.Extensions.Configuration.UserSecrets.Tests +{ + public class UserSecretsTestFixture : IDisposable + { + private Stack _disposables = new Stack(); + + public const string TestSecretsId = "b918174fa80346bbb7f4a386729c0eff"; + + public UserSecretsTestFixture() + { + _disposables.Push(() => TryDelete(Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(TestSecretsId)))); + } + + public void Dispose() + { + while (_disposables.Count > 0) + { + _disposables.Pop()?.Invoke(); + } + } + + public string GetTempSecretProject() + { + string userSecretsId; + return GetTempSecretProject(out userSecretsId); + } + + private const string ProjectTemplate = @" + + + + Exe + netcoreapp1.0 + {0} + + + + + + + + + + +"; + + public string GetTempSecretProject(out string userSecretsId) + { + userSecretsId = Guid.NewGuid().ToString(); + return CreateProject(userSecretsId); + } + + public string CreateProject(string userSecretsId) + { + var projectPath = Directory.CreateDirectory(Path.Combine(Path.GetTempPath(), "usersecretstest", Guid.NewGuid().ToString())); + var prop = string.IsNullOrEmpty(userSecretsId) + ? string.Empty + : $"{userSecretsId}"; + + File.WriteAllText( + Path.Combine(projectPath.FullName, "TestProject.csproj"), + string.Format(ProjectTemplate, prop)); + + var id = userSecretsId; + _disposables.Push(() => + { + try + { + // may throw if id is bad + var secretsDir = Path.GetDirectoryName(PathHelper.GetSecretsPathFromSecretsId(id)); + TryDelete(secretsDir); + } + catch { } + }); + _disposables.Push(() => TryDelete(projectPath.FullName)); + + return projectPath.FullName; + } + + private static void TryDelete(string directory) + { + try + { + if (Directory.Exists(directory)) + { + Directory.Delete(directory, true); + } + } + catch (Exception) + { + // Ignore failures. + Console.WriteLine("Failed to delete " + directory); + } + } + } +} \ No newline at end of file