Create initial prototype of dotnet-user-secrets with MSBuild support

This commit is contained in:
Nate McMaster 2016-10-07 09:56:08 -07:00 committed by Nate McMaster
parent 9744f94b39
commit 29df59b89c
20 changed files with 649 additions and 245 deletions

1
.gitignore vendored
View File

@ -28,6 +28,7 @@ project.lock.json
.testPublish/ .testPublish/
.build/ .build/
/.vs/ /.vs/
.vscode/
testWorkDir/ testWorkDir/
*.nuget.props *.nuget.props
*.nuget.targets *.nuget.targets

View File

@ -0,0 +1,6 @@
<Project ToolsVersion="14.0" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<Import Project="$(Project)" />
<Target Name="_FindUserSecretsProperty">
<WriteLinesToFile File="$(OutputFile)" Lines="$(UserSecretsId)" />
</Target>
</Project>

View File

@ -2,6 +2,7 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System.Reflection; using System.Reflection;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
namespace Microsoft.Extensions.SecretManager.Tools.Internal namespace Microsoft.Extensions.SecretManager.Tools.Internal
@ -13,6 +14,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
public bool IsHelp { get; set; } public bool IsHelp { get; set; }
public string Project { get; set; } public string Project { get; set; }
public ICommand Command { get; set; } public ICommand Command { get; set; }
public string Configuration { get; set; }
public static CommandLineOptions Parse(string[] args, IConsole console) public static CommandLineOptions Parse(string[] args, IConsole console)
{ {
@ -34,6 +36,9 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory", var optionProject = app.Option("-p|--project <PROJECT>", "Path to project, default is current directory",
CommandOptionType.SingleValue, inherited: true); CommandOptionType.SingleValue, inherited: true);
var optionConfig = app.Option("-c|--configuration <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 // the escape hatch if project evaluation fails, or if users want to alter a secret store other than the one
// in the current project // in the current project
var optionId = app.Option("--id", "The user secret id to use.", var optionId = app.Option("--id", "The user secret id to use.",
@ -55,6 +60,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
return null; return null;
} }
options.Configuration = optionConfig.Value();
options.Id = optionId.Value(); options.Id = optionId.Value();
options.IsHelp = app.IsShowingInformation; options.IsHelp = app.IsShowingInformation;
options.IsVerbose = optionVerbose.HasValue(); options.IsVerbose = optionVerbose.HasValue();

View File

@ -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
{
/// <summary>
/// An exception whose stack trace should be suppressed in console output
/// </summary>
public class GracefulException : Exception
{
public GracefulException()
{
}
public GracefulException(string message) : base(message)
{
}
public GracefulException(string message, Exception innerException) : base(message, innerException)
{
}
}
}

View File

@ -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;
}
}
}

View File

@ -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<string> _tempFiles = new List<string>();
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<string>();
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
}
}
}
}

View File

@ -1,6 +1,7 @@
// Copyright (c) .NET Foundation. All rights reserved. // Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@ -26,6 +26,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Internal
} }
_secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); _secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId);
logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath); logger.LogDebug(Resources.Message_Secret_File_Path, _secretsFilePath);
_secrets = Load(userSecretsId); _secrets = Load(userSecretsId);
} }

View File

@ -4,6 +4,7 @@
using System.Diagnostics; using System.Diagnostics;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.CommandLineUtils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;

View File

@ -16,16 +16,16 @@
<dependencies> <dependencies>
<group targetFramework=".NETCoreApp1.0"> <group targetFramework=".NETCoreApp1.0">
<!-- MUST BE alphabetical --> <!-- MUST BE alphabetical -->
<dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_1$" /> <dependency id="Microsoft.DotNet.Cli.Utils" version="$dep_1$" />
<dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_2$" /> <dependency id="Microsoft.Extensions.CommandLineUtils" version="$dep_2$" />
<dependency id="Microsoft.Extensions.Logging" version="$dep_3$" /> <dependency id="Microsoft.Extensions.Configuration.UserSecrets" version="$dep_3$" />
<dependency id="Microsoft.NETCore.App" version="$dep_4$" /> <dependency id="Microsoft.Extensions.Logging" version="$dep_4$" />
<dependency id="Newtonsoft.Json" version="$dep_5$" /> <dependency id="Microsoft.NETCore.App" version="$dep_5$" />
<dependency id="System.Runtime.Serialization.Primitives" version="$dep_6$" />
</group> </group>
</dependencies> </dependencies>
</metadata> </metadata>
<files> <files>
<file src="FindUserSecretsProperty.targets" target="tools\" />
<file src="bin/$configuration$/netcoreapp1.0/dotnet-user-secrets.dll" target="lib\netcoreapp1.0\" /> <file src="bin/$configuration$/netcoreapp1.0/dotnet-user-secrets.dll" target="lib\netcoreapp1.0\" />
<file src="bin/$configuration$/netcoreapp1.0/dotnet-user-secrets.runtimeconfig.json" target="lib/netcoreapp1.0\" /> <file src="bin/$configuration$/netcoreapp1.0/dotnet-user-secrets.runtimeconfig.json" target="lib/netcoreapp1.0\" />
</files> </files>

View File

@ -2,15 +2,10 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System; using System;
using System.Diagnostics;
using System.IO; using System.IO;
using System.Linq; using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.FileProviders;
using Microsoft.Extensions.FileProviders.Physical;
using Microsoft.Extensions.SecretManager.Tools.Internal; using Microsoft.Extensions.SecretManager.Tools.Internal;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Microsoft.Extensions.SecretManager.Tools namespace Microsoft.Extensions.SecretManager.Tools
{ {
@ -21,12 +16,16 @@ namespace Microsoft.Extensions.SecretManager.Tools
private readonly IConsole _console; private readonly IConsole _console;
private readonly string _workingDirectory; private readonly string _workingDirectory;
public Program() public static int Main(string[] args)
: this(PhysicalConsole.Singleton, Directory.GetCurrentDirectory())
{ {
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; _console = console;
_workingDirectory = workingDirectory; _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) public bool TryRun(string[] args, out int returnCode)
{ {
try try
@ -103,6 +75,11 @@ namespace Microsoft.Extensions.SecretManager.Tools
{ {
if (exception is GracefulException) if (exception is GracefulException)
{ {
if (exception.InnerException != null)
{
Logger.LogInformation(exception.InnerException.Message);
}
Logger.LogError(exception.Message); Logger.LogError(exception.Message);
} }
else else
@ -134,65 +111,23 @@ namespace Microsoft.Extensions.SecretManager.Tools
CommandOutputProvider.LogLevel = LogLevel.Debug; CommandOutputProvider.LogLevel = LogLevel.Debug;
} }
var userSecretsId = ResolveUserSecretsId(options); var userSecretsId = ResolveId(options);
var store = new SecretsStore(userSecretsId, Logger); 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); options.Command.Execute(context);
return 0; return 0;
} }
private string ResolveUserSecretsId(CommandLineOptions options) internal string ResolveId(CommandLineOptions options)
{ {
if (!string.IsNullOrEmpty(options.Id)) if (!string.IsNullOrEmpty(options.Id))
{ {
return options.Id; return options.Id;
} }
var projectPath = options.Project ?? _workingDirectory; using (var resolver = new ProjectIdResolver(Logger, _workingDirectory))
if (!Path.IsPathRooted(projectPath))
{ {
projectPath = Path.Combine(_workingDirectory, projectPath); return resolver.Resolve(options.Project, options.Configuration);
}
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<string>("userSecretsId");
if (string.IsNullOrEmpty(userSecretsId))
{
throw new GracefulException($"Could not find 'userSecretsId' in json file '{fileInfo.PhysicalPath}'");
}
return userSecretsId;
} }
} }
} }

View File

@ -10,6 +10,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
private static readonly ResourceManager _resourceManager private static readonly ResourceManager _resourceManager
= new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly); = new ResourceManager("Microsoft.Extensions.SecretManager.Tools.Resources", typeof(Resources).GetTypeInfo().Assembly);
/// <summary>
/// Value cannot be null or an empty string.
/// </summary>
internal static string Common_StringNullOrEmpty
{
get { return GetString("Common_StringNullOrEmpty"); }
}
/// <summary>
/// Value cannot be null or an empty string.
/// </summary>
internal static string FormatCommon_StringNullOrEmpty()
{
return GetString("Common_StringNullOrEmpty");
}
/// <summary> /// <summary>
/// Command failed : {message} /// Command failed : {message}
/// </summary> /// </summary>
@ -60,6 +76,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key); return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key);
} }
/// <summary>
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
/// </summary>
internal static string Error_MultipleProjectsFound
{
get { return GetString("Error_MultipleProjectsFound"); }
}
/// <summary>
/// Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.
/// </summary>
internal static string FormatError_MultipleProjectsFound(object projectPath)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
}
/// <summary> /// <summary>
/// No secrets configured for this application. /// No secrets configured for this application.
/// </summary> /// </summary>
@ -76,6 +108,38 @@ namespace Microsoft.Extensions.SecretManager.Tools
return GetString("Error_No_Secrets_Found"); return GetString("Error_No_Secrets_Found");
} }
/// <summary>
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
/// </summary>
internal static string Error_NoProjectsFound
{
get { return GetString("Error_NoProjectsFound"); }
}
/// <summary>
/// Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.
/// </summary>
internal static string FormatError_NoProjectsFound(object projectPath)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_NoProjectsFound", "projectPath"), projectPath);
}
/// <summary>
/// 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.
/// </summary>
internal static string Error_ProjectMissingId
{
get { return GetString("Error_ProjectMissingId"); }
}
/// <summary>
/// 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.
/// </summary>
internal static string FormatError_ProjectMissingId(object project)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectMissingId", "project"), project);
}
/// <summary> /// <summary>
/// The project file '{path}' does not exist. /// The project file '{path}' does not exist.
/// </summary> /// </summary>
@ -92,6 +156,22 @@ namespace Microsoft.Extensions.SecretManager.Tools
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path); return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
} }
/// <summary>
/// Could not load the MSBuild project '{project}'.
/// </summary>
internal static string Error_ProjectFailedToLoad
{
get { return GetString("Error_ProjectFailedToLoad"); }
}
/// <summary>
/// Could not load the MSBuild project '{project}'.
/// </summary>
internal static string FormatError_ProjectFailedToLoad(object project)
{
return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectFailedToLoad", "project"), project);
}
/// <summary> /// <summary>
/// Project file path {project}. /// Project file path {project}.
/// </summary> /// </summary>

View File

@ -117,6 +117,9 @@
<resheader name="writer"> <resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value> <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader> </resheader>
<data name="Common_StringNullOrEmpty" xml:space="preserve">
<value>Value cannot be null or an empty string.</value>
</data>
<data name="Error_Command_Failed" xml:space="preserve"> <data name="Error_Command_Failed" xml:space="preserve">
<value>Command failed : {message}</value> <value>Command failed : {message}</value>
</data> </data>
@ -127,12 +130,24 @@ Use the '--help' flag to see info.</value>
<data name="Error_Missing_Secret" xml:space="preserve"> <data name="Error_Missing_Secret" xml:space="preserve">
<value>Cannot find '{key}' in the secret store.</value> <value>Cannot find '{key}' in the secret store.</value>
</data> </data>
<data name="Error_MultipleProjectsFound" xml:space="preserve">
<value>Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option.</value>
</data>
<data name="Error_No_Secrets_Found" xml:space="preserve"> <data name="Error_No_Secrets_Found" xml:space="preserve">
<value>No secrets configured for this application.</value> <value>No secrets configured for this application.</value>
</data> </data>
<data name="Error_NoProjectsFound" xml:space="preserve">
<value>Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option.</value>
</data>
<data name="Error_ProjectMissingId" xml:space="preserve">
<value>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.</value>
</data>
<data name="Error_ProjectPath_NotFound" xml:space="preserve"> <data name="Error_ProjectPath_NotFound" xml:space="preserve">
<value>The project file '{path}' does not exist.</value> <value>The project file '{path}' does not exist.</value>
</data> </data>
<data name="Error_ProjectFailedToLoad" xml:space="preserve">
<value>Could not load the MSBuild project '{project}'.</value>
</data>
<data name="Message_Project_File_Path" xml:space="preserve"> <data name="Message_Project_File_Path" xml:space="preserve">
<value>Project file path {project}.</value> <value>Project file path {project}.</value>
</data> </data>

View File

@ -4,7 +4,13 @@
"outputName": "dotnet-user-secrets", "outputName": "dotnet-user-secrets",
"emitEntryPoint": true, "emitEntryPoint": true,
"warningsAsErrors": 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.", "description": "Command line tool to manage user secrets for Microsoft.Extensions.Configuration.",
"packOptions": { "packOptions": {
@ -16,18 +22,22 @@
"configuration", "configuration",
"secrets", "secrets",
"usersecrets" "usersecrets"
] ],
"files": {
"mappings": {
"tools/FindUserSecretsProperty.targets": "FindUserSecretsProperty.targets"
}
}
}, },
"dependencies": { "dependencies": {
"Microsoft.DotNet.Cli.Utils": "1.0.0-*",
"Microsoft.Extensions.CommandLineUtils": "1.1.0-*", "Microsoft.Extensions.CommandLineUtils": "1.1.0-*",
"Microsoft.Extensions.Configuration.UserSecrets": "1.1.0-*", "Microsoft.Extensions.Configuration.UserSecrets": "1.1.0-*",
"Microsoft.Extensions.Logging": "1.1.0-*", "Microsoft.Extensions.Logging": "1.1.0-*",
"Microsoft.NETCore.App": { "Microsoft.NETCore.App": {
"version": "1.1.0-*", "version": "1.1.0-*",
"type": "platform" "type": "platform"
}, }
"Newtonsoft.Json": "9.0.1",
"System.Runtime.Serialization.Primitives": "4.3.0-*"
}, },
"frameworks": { "frameworks": {
"netcoreapp1.0": {} "netcoreapp1.0": {}

View File

@ -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<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
[Fact]
public void DoesNotMatchXproj()
{
using (var files = new TemporaryFileProvider())
{
var finder = new MsBuildProjectFinder(files.Root);
files.Add("test.xproj", "");
Assert.Throws<GracefulException>(() => 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<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
[Fact]
public void ThrowsWhenFileDoesNotExist()
{
using (var files = new TemporaryFileProvider())
{
var finder = new MsBuildProjectFinder(files.Root);
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject("test.csproj"));
}
}
[Fact]
public void ThrowsWhenRootDoesNotExist()
{
var files = new TemporaryFileProvider();
var finder = new MsBuildProjectFinder(files.Root);
files.Dispose();
Assert.Throws<GracefulException>(() => finder.FindMsBuildProject(null));
}
}
}

View File

@ -5,68 +5,79 @@ using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
using System.Text; using System.Text;
using Microsoft.DotNet.Cli.Utils;
using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Configuration.UserSecrets;
using Microsoft.Extensions.Configuration.UserSecrets.Tests; using Microsoft.Extensions.Configuration.UserSecrets.Tests;
using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging;
using Microsoft.Extensions.SecretManager.Tools.Internal;
using Xunit; using Xunit;
using Xunit.Abstractions; using Xunit.Abstractions;
namespace Microsoft.Extensions.SecretManager.Tools.Tests namespace Microsoft.Extensions.SecretManager.Tools.Tests
{ {
public class SecretManagerTests : IDisposable public class SecretManagerTests : IClassFixture<UserSecretsTestFixture>
{ {
private TestLogger _logger; private TestLogger _logger;
private Stack<Action> _disposables = new Stack<Action>(); private readonly UserSecretsTestFixture _fixture;
public SecretManagerTests(ITestOutputHelper output) public SecretManagerTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
{ {
_fixture = fixture;
_logger = new TestLogger(output); _logger = new TestLogger(output);
} }
private string GetTempSecretProject() private Program CreateProgram()
{ {
string id; return new Program(new TestConsole(), Directory.GetCurrentDirectory())
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)
{ {
_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<GracefulException>(() => 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<GracefulException>(() => secretManager.RunInternal("list", "-p", project));
Assert.Equal(Resources.FormatError_ProjectFailedToLoad(project), ex.Message);
} }
[Fact] [Fact]
public void Error_Project_DoesNotExist() public void Error_Project_DoesNotExist()
{ {
var projectPath = Path.Combine(GetTempSecretProject(), "does_not_exist", "project.json"); var projectPath = Path.Combine(_fixture.GetTempSecretProject(), "does_not_exist", "TestProject.csproj");
var secretManager = new Program(new TestConsole(), Directory.GetCurrentDirectory()) { Logger = _logger }; var secretManager = CreateProgram();
var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "--project", projectPath)); var ex = Assert.Throws<GracefulException>(() => secretManager.RunInternal("list", "--project", projectPath));
Assert.Equal(Resources.FormatError_ProjectPath_NotFound(projectPath), ex.Message); Assert.Equal(Resources.FormatError_ProjectPath_NotFound(projectPath), ex.Message);
} }
[Fact] [Fact]
public void SupportsRelativePaths() public void SupportsRelativePaths()
{ {
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var cwd = Path.Combine(projectPath, "nested1"); var cwd = Path.Combine(projectPath, "nested1");
Directory.CreateDirectory(cwd); Directory.CreateDirectory(cwd);
var secretManager = new Program(new TestConsole(), cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider }; var secretManager = new Program(new TestConsole(), cwd) { Logger = _logger, CommandOutputProvider = _logger.CommandOutputProvider };
secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug; secretManager.CommandOutputProvider.LogLevel = LogLevel.Debug;
secretManager.RunInternal("list", "-p", "../", "--verbose"); secretManager.RunInternal("list", "-p", ".." + 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] [Theory]
@ -82,7 +93,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
new KeyValuePair<string, string>("key2", string.Empty) new KeyValuePair<string, string>("key2", string.Empty)
}; };
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var dir = fromCurrentDirectory var dir = fromCurrentDirectory
? projectPath ? projectPath
: Path.GetTempPath(); : Path.GetTempPath();
@ -141,8 +152,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact] [Fact]
public void SetSecret_Update_Existing_Secret() public void SetSecret_Update_Existing_Secret()
{ {
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath); secretManager.RunInternal("set", "secret1", "value1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Equal(1, _logger.Messages.Count);
@ -161,31 +172,31 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact] [Fact]
public void SetSecret_With_Verbose_Flag() public void SetSecret_With_Verbose_Flag()
{ {
string id; string secretId;
var projectPath = GetTempSecretProject(out id); var projectPath = _fixture.GetTempSecretProject(out secretId);
_logger.SetLevel(LogLevel.Debug); _logger.SetLevel(LogLevel.Debug);
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath); secretManager.RunInternal("-v", "set", "secret1", "value1", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count); 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("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(id)), _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); Assert.Contains("Successfully saved secret1 = value1 to the secret store.", _logger.Messages);
_logger.Messages.Clear(); _logger.Messages.Clear();
secretManager.RunInternal("-v", "list", "-p", projectPath); secretManager.RunInternal("-v", "list", "-p", projectPath);
Assert.Equal(3, _logger.Messages.Count); 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("Project file path {0}.", Path.Combine(projectPath, "TestProject.csproj")), _logger.Messages);
Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(id)), _logger.Messages); Assert.Contains(string.Format("Secrets file path {0}.", PathHelper.GetSecretsPathFromSecretsId(secretId)), _logger.Messages);
Assert.Contains("secret1 = value1", _logger.Messages); Assert.Contains("secret1 = value1", _logger.Messages);
} }
[Fact] [Fact]
public void Remove_Non_Existing_Secret() public void Remove_Non_Existing_Secret()
{ {
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("remove", "secret1", "-p", projectPath); secretManager.RunInternal("remove", "secret1", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages); Assert.Contains("Cannot find 'secret1' in the secret store.", _logger.Messages);
@ -194,8 +205,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact] [Fact]
public void Remove_Is_Case_Insensitive() public void Remove_Is_Case_Insensitive()
{ {
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath); secretManager.RunInternal("set", "SeCreT1", "value", "-p", projectPath);
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Contains("SeCreT1 = value", _logger.Messages); Assert.Contains("SeCreT1 = value", _logger.Messages);
@ -211,12 +222,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact] [Fact]
public void List_Flattens_Nested_Objects() public void List_Flattens_Nested_Objects()
{ {
string id; string secretId;
var projectPath = GetTempSecretProject(out id); var projectPath = _fixture.GetTempSecretProject(out secretId);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Equal(1, _logger.Messages.Count);
Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages); Assert.Contains("AzureAd:ClientSecret = abcd郩˙î", _logger.Messages);
@ -231,7 +242,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
Out = new StringWriter(output) Out = new StringWriter(output)
}; };
string id; string id;
var projectPath = GetTempSecretProject(out id); var projectPath = _fixture.GetTempSecretProject(out id);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
@ -246,12 +257,12 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact] [Fact]
public void Set_Flattens_Nested_Objects() public void Set_Flattens_Nested_Objects()
{ {
string id; string secretId;
var projectPath = GetTempSecretProject(out id); var projectPath = _fixture.GetTempSecretProject(out secretId);
var secretsFile = PathHelper.GetSecretsPathFromSecretsId(id); var secretsFile = PathHelper.GetSecretsPathFromSecretsId(secretId);
Directory.CreateDirectory(Path.GetDirectoryName(secretsFile)); Directory.CreateDirectory(Path.GetDirectoryName(secretsFile));
File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8); File.WriteAllText(secretsFile, @"{ ""AzureAd"": { ""ClientSecret"": ""abcd郩˙î""} }", Encoding.UTF8);
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath); secretManager.RunInternal("set", "AzureAd:ClientSecret", "¡™£¢∞", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Equal(1, _logger.Messages.Count);
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
@ -268,8 +279,8 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[Fact] [Fact]
public void List_Empty_Secrets_File() public void List_Empty_Secrets_File()
{ {
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var secretManager = new Program() { Logger = _logger }; var secretManager = CreateProgram();
secretManager.RunInternal("list", "-p", projectPath); secretManager.RunInternal("list", "-p", projectPath);
Assert.Equal(1, _logger.Messages.Count); Assert.Equal(1, _logger.Messages.Count);
Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages); Assert.Contains(Resources.Error_No_Secrets_Found, _logger.Messages);
@ -280,7 +291,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
[InlineData(false)] [InlineData(false)]
public void Clear_Secrets(bool fromCurrentDirectory) public void Clear_Secrets(bool fromCurrentDirectory)
{ {
var projectPath = GetTempSecretProject(); var projectPath = _fixture.GetTempSecretProject();
var dir = fromCurrentDirectory var dir = fromCurrentDirectory
? projectPath ? projectPath

View File

@ -75,7 +75,7 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
var secretStore = new TestSecretsStore(); var secretStore = new TestSecretsStore();
var command = new SetCommand("key", null); var command = new SetCommand("key", null);
var ex = Assert.Throws<GracefulException>( var ex = Assert.Throws< Microsoft.DotNet.Cli.Utils.GracefulException>(
() => command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole))); () => command.Execute(new CommandContext(secretStore, NullLogger.Instance, testConsole)));
Assert.Equal(Resources.FormatError_MissingArgument("value"), ex.Message); Assert.Equal(Resources.FormatError_MissingArgument("value"), ex.Message);
} }

View File

@ -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);
}
}
}

View File

@ -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.
}
}
}
}

View File

@ -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<Action> _disposables = new Stack<Action>();
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 = @"<Project ToolsVersion=""14.0"" xmlns=""http://schemas.microsoft.com/developer/msbuild/2003"">
<Import Project=""$(MSBuildExtensionsPath)\$(MSBuildToolsVersion)\Microsoft.Common.props"" />
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp1.0</TargetFrameworks>
{0}
</PropertyGroup>
<ItemGroup>
<Compile Include=""**\*.cs"" Exclude=""Excluded.cs"" />
<PackageReference Include=""Microsoft.NET.Sdk"" Version=""1.0.0-*"" PrivateAssets=""All"" />
<PackageReference Include=""Microsoft.NETCore.App"" Version=""1.0.0"" />
</ItemGroup>
<Import Project=""$(MSBuildToolsPath)\Microsoft.CSharp.targets"" />
</Project>";
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>{userSecretsId}</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);
}
}
}
}