diff --git a/src/dotnet-user-secrets/CommandLineOptions.cs b/src/dotnet-user-secrets/CommandLineOptions.cs
index 6ce543cc00..8495b6de9d 100644
--- a/src/dotnet-user-secrets/CommandLineOptions.cs
+++ b/src/dotnet-user-secrets/CommandLineOptions.cs
@@ -50,6 +50,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
app.Command("remove", c => RemoveCommand.Configure(c, options));
app.Command("list", c => ListCommand.Configure(c, options));
app.Command("clear", c => ClearCommand.Configure(c, options));
+ app.Command("init", c => InitCommandFactory.Configure(c, options));
// Show help information if no subcommand/option was specified.
app.OnExecute(() => app.ShowHelp());
diff --git a/src/dotnet-user-secrets/Internal/InitCommand.cs b/src/dotnet-user-secrets/Internal/InitCommand.cs
new file mode 100644
index 0000000000..670da42c4e
--- /dev/null
+++ b/src/dotnet-user-secrets/Internal/InitCommand.cs
@@ -0,0 +1,126 @@
+// 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 System.Xml.Linq;
+using System.Xml.XPath;
+using Microsoft.Extensions.CommandLineUtils;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Internal
+{
+ // Workaround used to handle the fact that the options have not been parsed at configuration time
+ public class InitCommandFactory : ICommand
+ {
+ public CommandLineOptions Options { get; }
+
+ internal static void Configure(CommandLineApplication command, CommandLineOptions options)
+ {
+ command.Description = "Set a user secrets ID to enable secret storage";
+ command.HelpOption();
+
+ command.OnExecute(() =>
+ {
+ options.Command = new InitCommandFactory(options);
+ });
+ }
+
+ public InitCommandFactory(CommandLineOptions options)
+ {
+ Options = options;
+ }
+
+ public void Execute(CommandContext context)
+ {
+ new InitCommand(Options.Id, Options.Project).Execute(context);
+ }
+
+ public void Execute(CommandContext context, string workingDirectory)
+ {
+ new InitCommand(Options.Id, Options.Project).Execute(context, workingDirectory);
+ }
+ }
+
+ public class InitCommand : ICommand
+ {
+ public string OverrideId { get; }
+ public string ProjectPath { get; }
+ public string WorkingDirectory { get; private set; } = Directory.GetCurrentDirectory();
+
+ public InitCommand(string id, string project)
+ {
+ OverrideId = id;
+ ProjectPath = project;
+ }
+
+ public void Execute(CommandContext context, string workingDirectory)
+ {
+ WorkingDirectory = workingDirectory;
+ Execute(context);
+ }
+
+ public void Execute(CommandContext context)
+ {
+ var projectPath = ResolveProjectPath(ProjectPath, WorkingDirectory);
+
+ // Load the project file as XML
+ var projectDocument = XDocument.Load(projectPath);
+
+ // Accept the `--id` CLI option to the main app
+ string newSecretsId = string.IsNullOrWhiteSpace(OverrideId)
+ ? Guid.NewGuid().ToString()
+ : OverrideId;
+
+ // Confirm secret ID does not contain invalid characters
+ if (Path.GetInvalidPathChars().Any(invalidChar => newSecretsId.Contains(invalidChar)))
+ {
+ throw new ArgumentException(Resources.FormatError_InvalidSecretsId(newSecretsId));
+ }
+
+ var existingUserSecretsId = projectDocument.XPathSelectElements("//UserSecretsId").FirstOrDefault();
+
+ // Check if a UserSecretsId is already set
+ if (existingUserSecretsId != default)
+ {
+ // Only set the UserSecretsId if the user specified an explicit value
+ if (string.IsNullOrWhiteSpace(OverrideId))
+ {
+ context.Reporter.Output(Resources.FormatMessage_ProjectAlreadyInitialized(projectPath));
+ return;
+ }
+
+ existingUserSecretsId.SetValue(newSecretsId);
+ }
+ else
+ {
+ // Find the first non-conditional PropertyGroup
+ var propertyGroup = projectDocument.Root.DescendantNodes()
+ .FirstOrDefault(node => node is XElement el
+ && el.Name == "PropertyGroup"
+ && el.Attributes().All(attr =>
+ attr.Name != "Condition")) as XElement;
+
+ // No valid property group, create a new one
+ if (propertyGroup == null)
+ {
+ propertyGroup = new XElement("PropertyGroup");
+ projectDocument.Root.AddFirst(propertyGroup);
+ }
+
+ // Add UserSecretsId element
+ propertyGroup.Add(new XElement("UserSecretsId", newSecretsId));
+ }
+
+ projectDocument.Save(projectPath);
+
+ context.Reporter.Output(Resources.FormatMessage_SetUserSecretsIdForProject(newSecretsId, projectPath));
+ }
+
+ private static string ResolveProjectPath(string name, string path)
+ {
+ var finder = new MsBuildProjectFinder(path);
+ return finder.FindMsBuildProject(name);
+ }
+ }
+}
diff --git a/src/dotnet-user-secrets/Program.cs b/src/dotnet-user-secrets/Program.cs
index 187e40128f..710756eb88 100644
--- a/src/dotnet-user-secrets/Program.cs
+++ b/src/dotnet-user-secrets/Program.cs
@@ -71,6 +71,12 @@ namespace Microsoft.Extensions.SecretManager.Tools
var reporter = CreateReporter(options.IsVerbose);
+ if (options.Command is InitCommandFactory initCmd)
+ {
+ initCmd.Execute(new CommandContext(null, reporter, _console), _workingDirectory);
+ return 0;
+ }
+
string userSecretsId;
try
{
diff --git a/src/dotnet-user-secrets/Properties/Resources.Designer.cs b/src/dotnet-user-secrets/Properties/Resources.Designer.cs
index a75fc0108f..8ee2a8f38e 100644
--- a/src/dotnet-user-secrets/Properties/Resources.Designer.cs
+++ b/src/dotnet-user-secrets/Properties/Resources.Designer.cs
@@ -15,16 +15,14 @@ namespace Microsoft.Extensions.SecretManager.Tools
///
internal static string Error_Command_Failed
{
- get { return GetString("Error_Command_Failed"); }
+ get => GetString("Error_Command_Failed");
}
///
/// Command failed : {message}
///
internal static string FormatError_Command_Failed(object message)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Error_Command_Failed", "message"), message);
///
/// Missing parameter value for '{name}'.
@@ -32,7 +30,7 @@ namespace Microsoft.Extensions.SecretManager.Tools
///
internal static string Error_MissingArgument
{
- get { return GetString("Error_MissingArgument"); }
+ get => GetString("Error_MissingArgument");
}
///
@@ -40,202 +38,218 @@ namespace Microsoft.Extensions.SecretManager.Tools
/// Use the '--help' flag to see info.
///
internal static string FormatError_MissingArgument(object name)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Error_MissingArgument", "name"), name);
///
/// Cannot find '{key}' in the secret store.
///
internal static string Error_Missing_Secret
{
- get { return GetString("Error_Missing_Secret"); }
+ get => GetString("Error_Missing_Secret");
}
///
/// Cannot find '{key}' in the secret store.
///
internal static string FormatError_Missing_Secret(object key)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_Secret", "key"), key);
- }
+ => 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"); }
+ get => 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);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Error_MultipleProjectsFound", "projectPath"), projectPath);
///
/// No secrets configured for this application.
///
internal static string Error_No_Secrets_Found
{
- get { return GetString("Error_No_Secrets_Found"); }
+ get => GetString("Error_No_Secrets_Found");
}
///
/// No secrets configured for this application.
///
internal static string FormatError_No_Secrets_Found()
- {
- return GetString("Error_No_Secrets_Found");
- }
+ => 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"); }
+ get => 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);
- }
+ => 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"); }
+ get => 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);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectMissingId", "project"), project);
///
/// The project file '{path}' does not exist.
///
internal static string Error_ProjectPath_NotFound
{
- get { return GetString("Error_ProjectPath_NotFound"); }
+ get => GetString("Error_ProjectPath_NotFound");
}
///
/// The project file '{path}' does not exist.
///
internal static string FormatError_ProjectPath_NotFound(object path)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectPath_NotFound", "path"), path);
- }
+ => 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"); }
+ get => 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);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Error_ProjectFailedToLoad", "project"), project);
///
/// Project file path {project}.
///
internal static string Message_Project_File_Path
{
- get { return GetString("Message_Project_File_Path"); }
+ get => GetString("Message_Project_File_Path");
}
///
/// Project file path {project}.
///
internal static string FormatMessage_Project_File_Path(object project)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Message_Project_File_Path", "project"), project);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_Project_File_Path", "project"), project);
///
/// Successfully saved {key} = {value} to the secret store.
///
internal static string Message_Saved_Secret
{
- get { return GetString("Message_Saved_Secret"); }
+ get => GetString("Message_Saved_Secret");
}
///
/// Successfully saved {key} = {value} to the secret store.
///
internal static string FormatMessage_Saved_Secret(object key, object value)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secret", "key", "value"), key, value);
///
/// Successfully saved {number} secrets to the secret store.
///
internal static string Message_Saved_Secrets
{
- get { return GetString("Message_Saved_Secrets"); }
+ get => GetString("Message_Saved_Secrets");
}
///
/// Successfully saved {number} secrets to the secret store.
///
internal static string FormatMessage_Saved_Secrets(object number)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_Saved_Secrets", "number"), number);
///
/// Secrets file path {secretsFilePath}.
///
internal static string Message_Secret_File_Path
{
- get { return GetString("Message_Secret_File_Path"); }
+ get => GetString("Message_Secret_File_Path");
}
///
/// Secrets file path {secretsFilePath}.
///
internal static string FormatMessage_Secret_File_Path(object secretsFilePath)
- {
- return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_File_Path", "secretsFilePath"), secretsFilePath);
- }
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_File_Path", "secretsFilePath"), secretsFilePath);
///
/// {key} = {value}
///
internal static string Message_Secret_Value_Format
{
- get { return GetString("Message_Secret_Value_Format"); }
+ get => GetString("Message_Secret_Value_Format");
}
///
/// {key} = {value}
///
internal static string FormatMessage_Secret_Value_Format(object key, object value)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_Value_Format", "key", "value"), key, value);
+
+ ///
+ /// The UserSecretsId '{userSecretsId}' cannot contain any characters that cannot be used in a file path.
+ ///
+ internal static string Error_InvalidSecretsId
{
- return string.Format(CultureInfo.CurrentCulture, GetString("Message_Secret_Value_Format", "key", "value"), key, value);
+ get => GetString("Error_InvalidSecretsId");
}
+ ///
+ /// The UserSecretsId '{userSecretsId}' cannot contain any characters that cannot be used in a file path.
+ ///
+ internal static string FormatError_InvalidSecretsId(object userSecretsId)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Error_InvalidSecretsId", "userSecretsId"), userSecretsId);
+
+ ///
+ /// The MSBuild project '{project}' has already been initialized with a UserSecretsId.
+ ///
+ internal static string Message_ProjectAlreadyInitialized
+ {
+ get => GetString("Message_ProjectAlreadyInitialized");
+ }
+
+ ///
+ /// The MSBuild project '{project}' has already been initialized with a UserSecretsId.
+ ///
+ internal static string FormatMessage_ProjectAlreadyInitialized(object project)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_ProjectAlreadyInitialized", "project"), project);
+
+ ///
+ /// Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'.
+ ///
+ internal static string Message_SetUserSecretsIdForProject
+ {
+ get => GetString("Message_SetUserSecretsIdForProject");
+ }
+
+ ///
+ /// Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'.
+ ///
+ internal static string FormatMessage_SetUserSecretsIdForProject(object userSecretsId, object project)
+ => string.Format(CultureInfo.CurrentCulture, GetString("Message_SetUserSecretsIdForProject", "userSecretsId", "project"), userSecretsId, project);
+
private static string GetString(string name, params string[] formatterNames)
{
var value = _resourceManager.GetString(name);
diff --git a/src/dotnet-user-secrets/Resources.resx b/src/dotnet-user-secrets/Resources.resx
index c9222930fc..bfcdf99c2a 100644
--- a/src/dotnet-user-secrets/Resources.resx
+++ b/src/dotnet-user-secrets/Resources.resx
@@ -1,17 +1,17 @@
-
@@ -160,4 +160,13 @@ Use the '--help' flag to see info.
{key} = {value}
+
+ The UserSecretsId '{userSecretsId}' cannot contain any characters that cannot be used in a file path.
+
+
+ The MSBuild project '{project}' has already been initialized with a UserSecretsId.
+
+
+ Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'.
+
\ No newline at end of file
diff --git a/test/dotnet-user-secrets.Tests/InitCommandTest.cs b/test/dotnet-user-secrets.Tests/InitCommandTest.cs
new file mode 100644
index 0000000000..d1558e8811
--- /dev/null
+++ b/test/dotnet-user-secrets.Tests/InitCommandTest.cs
@@ -0,0 +1,119 @@
+// 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;
+using Microsoft.Extensions.Configuration.UserSecrets.Tests;
+using Microsoft.Extensions.SecretManager.Tools.Internal;
+using Microsoft.Extensions.Tools.Internal;
+using Xunit;
+using Xunit.Abstractions;
+
+namespace Microsoft.Extensions.SecretManager.Tools.Tests
+{
+ public class InitCommandTests : IClassFixture
+ {
+ private UserSecretsTestFixture _fixture;
+ private ITestOutputHelper _output;
+ private TestConsole _console;
+ private StringBuilder _textOutput;
+
+ public InitCommandTests(UserSecretsTestFixture fixture, ITestOutputHelper output)
+ {
+ _fixture = fixture;
+ _output = output;
+ _textOutput = new StringBuilder();
+
+ _console = new TestConsole(output)
+ {
+ Error = new StringWriter(_textOutput),
+ Out = new StringWriter(_textOutput),
+ };
+ }
+
+ private CommandContext MakeCommandContext() => new CommandContext(null, new TestReporter(_output), _console);
+
+ [Fact]
+ public void AddsSecretIdToProject()
+ {
+ var projectDir = _fixture.CreateProject(null);
+
+ new InitCommand(null, null).Execute(MakeCommandContext(), projectDir);
+
+ var idResolver = new ProjectIdResolver(MakeCommandContext().Reporter, projectDir);
+
+ Assert.False(string.IsNullOrWhiteSpace(idResolver.Resolve(null, null)));
+ }
+
+ [Fact]
+ public void AddsSpecificSecretIdToProject()
+ {
+ const string SecretId = "TestSecretId";
+
+ var projectDir = _fixture.CreateProject(null);
+
+ new InitCommand(SecretId, null).Execute(MakeCommandContext(), projectDir);
+
+ var idResolver = new ProjectIdResolver(MakeCommandContext().Reporter, projectDir);
+
+ Assert.Equal(SecretId, idResolver.Resolve(null, null));
+ }
+
+ [Fact]
+ public void AddsEscapedSpecificSecretIdToProject()
+ {
+ const string SecretId = @"&";
+
+ var projectDir = _fixture.CreateProject(null);
+
+ new InitCommand(SecretId, null).Execute(MakeCommandContext(), projectDir);
+
+ var idResolver = new ProjectIdResolver(MakeCommandContext().Reporter, projectDir);
+
+ Assert.Equal(SecretId, idResolver.Resolve(null, null));
+ }
+
+ [Fact]
+ public void DoesNotGenerateIdForProjectWithSecretId()
+ {
+ const string SecretId = "AlreadyExists";
+
+ var projectDir = _fixture.CreateProject(SecretId);
+
+ new InitCommand(null, null).Execute(MakeCommandContext(), projectDir);
+
+ var idResolver = new ProjectIdResolver(MakeCommandContext().Reporter, projectDir);
+
+ Assert.Equal(SecretId, idResolver.Resolve(null, null));
+ }
+
+ [Fact]
+ public void OverridesIdForProjectWithSecretId()
+ {
+ const string SecretId = "AlreadyExists";
+ const string NewId = "TestValue";
+
+ var projectDir = _fixture.CreateProject(SecretId);
+
+ new InitCommand(NewId, null).Execute(MakeCommandContext(), projectDir);
+
+ var idResolver = new ProjectIdResolver(MakeCommandContext().Reporter, projectDir);
+
+ Assert.Equal(NewId, idResolver.Resolve(null, null));
+ }
+
+ [Fact]
+ public void FailsForInvalidId()
+ {
+ string secretId = $"invalid{Path.GetInvalidPathChars()[0]}secret-id";
+
+ var projectDir = _fixture.CreateProject(null);
+
+ Assert.Throws(() =>
+ {
+ new InitCommand(secretId, null).Execute(MakeCommandContext(), projectDir);
+ });
+ }
+ }
+}
diff --git a/test/dotnet-user-secrets.Tests/SecretManagerTests.cs b/test/dotnet-user-secrets.Tests/SecretManagerTests.cs
index ee7ca55247..3a390d4439 100644
--- a/test/dotnet-user-secrets.Tests/SecretManagerTests.cs
+++ b/test/dotnet-user-secrets.Tests/SecretManagerTests.cs
@@ -323,5 +323,18 @@ namespace Microsoft.Extensions.SecretManager.Tools.Tests
secretManager.RunInternal(args);
Assert.Contains(Resources.Error_No_Secrets_Found, _output.ToString());
}
+
+ [Fact]
+ public void Init_When_Project_Has_No_Secrets_Id()
+ {
+ var projectPath = _fixture.CreateProject(null);
+ var project = Path.Combine(projectPath, "TestProject.csproj");
+ var secretManager = new Program(_console, projectPath);
+
+ secretManager.RunInternal("init", "-p", project);
+
+ Assert.DoesNotContain(Resources.FormatError_ProjectMissingId(project), _output.ToString());
+ Assert.DoesNotContain("--help", _output.ToString());
+ }
}
-}
\ No newline at end of file
+}