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