diff --git a/src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs b/src/Tools/Shared/SecretsHelpers/MsBuildProjectFinder.cs similarity index 75% rename from src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs rename to src/Tools/Shared/SecretsHelpers/MsBuildProjectFinder.cs index 9bb6bef60c5c..3133799e95d3 100644 --- a/src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs +++ b/src/Tools/Shared/SecretsHelpers/MsBuildProjectFinder.cs @@ -1,13 +1,10 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; using System.Linq; +using Microsoft.AspNetCore.Tools; using Microsoft.Extensions.Tools.Internal; -namespace Microsoft.Extensions.SecretManager.Tools.Internal; - internal sealed class MsBuildProjectFinder { private readonly string _directory; @@ -36,12 +33,12 @@ public string FindMsBuildProject(string project) if (projects.Count > 1) { - throw new FileNotFoundException(Resources.FormatError_MultipleProjectsFound(projectPath)); + throw new FileNotFoundException(SecretsHelpersResources.FormatError_MultipleProjectsFound(projectPath)); } if (projects.Count == 0) { - throw new FileNotFoundException(Resources.FormatError_NoProjectsFound(projectPath)); + throw new FileNotFoundException(SecretsHelpersResources.FormatError_NoProjectsFound(projectPath)); } return projects[0]; @@ -49,7 +46,7 @@ public string FindMsBuildProject(string project) if (!File.Exists(projectPath)) { - throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath)); + throw new FileNotFoundException(SecretsHelpersResources.FormatError_ProjectPath_NotFound(projectPath)); } return projectPath; diff --git a/src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs b/src/Tools/Shared/SecretsHelpers/ProjectIdResolver.cs similarity index 85% rename from src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs rename to src/Tools/Shared/SecretsHelpers/ProjectIdResolver.cs index 95657b8c0ba8..d9c794586ae4 100644 --- a/src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs +++ b/src/Tools/Shared/SecretsHelpers/ProjectIdResolver.cs @@ -6,16 +6,15 @@ using System.IO; using System.Linq; using System.Text; +using Microsoft.AspNetCore.Tools; using Microsoft.Extensions.CommandLineUtils; using Microsoft.Extensions.Tools.Internal; -namespace Microsoft.Extensions.SecretManager.Tools.Internal; - /// /// This API supports infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. /// -public class ProjectIdResolver +internal sealed class ProjectIdResolver { private const string DefaultConfig = "Debug"; private readonly IReporter _reporter; @@ -32,9 +31,18 @@ public ProjectIdResolver(IReporter reporter, string workingDirectory) public string Resolve(string project, string configuration) { var finder = new MsBuildProjectFinder(_workingDirectory); - var projectFile = finder.FindMsBuildProject(project); + string projectFile; + try + { + projectFile = finder.FindMsBuildProject(project); + } + catch (Exception ex) + { + _reporter.Error(ex.Message); + return null; + } - _reporter.Verbose(Resources.FormatMessage_Project_File_Path(projectFile)); + _reporter.Verbose(SecretsHelpersResources.FormatMessage_Project_File_Path(projectFile)); configuration = !string.IsNullOrEmpty(configuration) ? configuration @@ -98,18 +106,20 @@ public string Resolve(string project, string configuration) _reporter.Verbose(outputBuilder.ToString()); _reporter.Verbose(errorBuilder.ToString()); _reporter.Error($"Exit code: {process.ExitCode}"); - throw new InvalidOperationException(Resources.FormatError_ProjectFailedToLoad(projectFile)); + _reporter.Error(SecretsHelpersResources.FormatError_ProjectFailedToLoad(projectFile)); + return null; } if (!File.Exists(outputFile)) { - throw new InvalidOperationException(Resources.FormatError_ProjectMissingId(projectFile)); + _reporter.Error(SecretsHelpersResources.FormatError_ProjectMissingId(projectFile)); + return null; } var id = File.ReadAllText(outputFile)?.Trim(); if (string.IsNullOrEmpty(id)) { - throw new InvalidOperationException(Resources.FormatError_ProjectMissingId(projectFile)); + _reporter.Error(SecretsHelpersResources.FormatError_ProjectMissingId(projectFile)); } return id; diff --git a/src/Tools/Shared/SecretsHelpers/SecretsHelpersResources.resx b/src/Tools/Shared/SecretsHelpers/SecretsHelpersResources.resx new file mode 100644 index 000000000000..bdc7cd88d5e9 --- /dev/null +++ b/src/Tools/Shared/SecretsHelpers/SecretsHelpersResources.resx @@ -0,0 +1,147 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + The UserSecretsId '{userSecretsId}' cannot contain any characters that cannot be used in a file path. + + + Multiple MSBuild project files found in '{projectPath}'. Specify which to use with the --project option. + + + Could not find a MSBuild project file in '{projectPath}'. Specify which project to use with the --project option. + + + Could not load the MSBuild project '{project}'. + + + 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 '{0}' does not exist. + + + The MSBuild project '{project}' has already been initialized with a UserSecretsId. + + + Project file path {project}. + + + Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'. + + \ No newline at end of file diff --git a/src/Tools/Shared/SecretsHelpers/UserSecretsCreator.cs b/src/Tools/Shared/SecretsHelpers/UserSecretsCreator.cs new file mode 100644 index 000000000000..19c2a6e5c1ee --- /dev/null +++ b/src/Tools/Shared/SecretsHelpers/UserSecretsCreator.cs @@ -0,0 +1,84 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using System.Linq; +using System.Xml; +using System.Xml.Linq; +using System.Xml.XPath; +using Microsoft.AspNetCore.Tools; +using Microsoft.Extensions.Tools.Internal; + +internal static class UserSecretsCreator +{ + public static string CreateUserSecretsId(IReporter reporter, string project, string workingDirectory, string overrideId = null) + { + var projectPath = ResolveProjectPath(project, workingDirectory); + + // Load the project file as XML + var projectDocument = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); + + // 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(newSecretsId.Contains)) + { + throw new ArgumentException(SecretsHelpersResources.FormatError_InvalidSecretsId(newSecretsId)); + } + + var existingUserSecretsId = projectDocument.XPathSelectElements("//UserSecretsId").FirstOrDefault(); + + // Check if a UserSecretsId is already set + if (existingUserSecretsId is not null) + { + // Only set the UserSecretsId if the user specified an explicit value + if (string.IsNullOrWhiteSpace(overrideId)) + { + reporter.Output(SecretsHelpersResources.FormatMessage_ProjectAlreadyInitialized(projectPath)); + return existingUserSecretsId.Value; + } + + 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(" "); + propertyGroup.Add(new XElement("UserSecretsId", newSecretsId)); + propertyGroup.Add($"{Environment.NewLine} "); + } + + var settings = new XmlWriterSettings + { + OmitXmlDeclaration = true, + }; + + using var xw = XmlWriter.Create(projectPath, settings); + projectDocument.Save(xw); + + reporter.Output(SecretsHelpersResources.FormatMessage_SetUserSecretsIdForProject(newSecretsId, projectPath)); + return newSecretsId; + } + + private static string ResolveProjectPath(string name, string path) + { + var finder = new MsBuildProjectFinder(path); + return finder.FindMsBuildProject(name); + } +} diff --git a/src/Tools/dotnet-user-secrets/src/assets/SecretManager.targets b/src/Tools/Shared/SecretsHelpers/assets/SecretManager.targets similarity index 100% rename from src/Tools/dotnet-user-secrets/src/assets/SecretManager.targets rename to src/Tools/Shared/SecretsHelpers/assets/SecretManager.targets diff --git a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs index e34a017553c3..155c581cbf6a 100644 --- a/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs +++ b/src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs @@ -4,8 +4,6 @@ using System.IdentityModel.Tokens.Jwt; using System.Linq; using System.Text.Json; -using System.Xml.Linq; -using System.Xml.XPath; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.UserSecrets; using Microsoft.Extensions.Tools.Internal; @@ -14,17 +12,15 @@ namespace Microsoft.AspNetCore.Authentication.JwtBearer.Tools; internal static class DevJwtCliHelpers { - public static string GetUserSecretsId(string projectFilePath) + public static string GetOrSetUserSecretsId(IReporter reporter, string projectFilePath) { - var projectDocument = XDocument.Load(projectFilePath, LoadOptions.PreserveWhitespace); - var existingUserSecretsId = projectDocument.XPathSelectElements("//UserSecretsId").FirstOrDefault(); - - if (existingUserSecretsId == null) + var resolver = new ProjectIdResolver(reporter, projectFilePath); + var id = resolver.Resolve(projectFilePath, configuration: null); + if (string.IsNullOrEmpty(id)) { - return null; + return UserSecretsCreator.CreateUserSecretsId(reporter, projectFilePath, projectFilePath); } - - return existingUserSecretsId.Value; + return id; } public static string GetProject(string projectPath = null) @@ -54,7 +50,7 @@ public static bool GetProjectAndSecretsId(string projectPath, IReporter reporter return false; } - userSecretsId = GetUserSecretsId(project); + userSecretsId = GetOrSetUserSecretsId(reporter, project); if (userSecretsId == null) { reporter.Error($"Project does not contain a user secrets ID."); @@ -85,6 +81,7 @@ public static byte[] CreateSigningKeyMaterial(string userSecretsId, bool reset = // Create signing material and save to user secrets var newKeyMaterial = System.Security.Cryptography.RandomNumberGenerator.GetBytes(DevJwtsDefaults.SigningKeyLength); var secretsFilePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); + Directory.CreateDirectory(Path.GetDirectoryName(secretsFilePath)); IDictionary secrets = null; if (File.Exists(secretsFilePath)) diff --git a/src/Tools/dotnet-user-jwts/src/Program.cs b/src/Tools/dotnet-user-jwts/src/Program.cs index 5d64717ccdb6..8967727ac24e 100644 --- a/src/Tools/dotnet-user-jwts/src/Program.cs +++ b/src/Tools/dotnet-user-jwts/src/Program.cs @@ -47,6 +47,13 @@ public void Run(string[] args) // Show help information if no subcommand/option was specified. userJwts.OnExecute(() => userJwts.ShowHelp()); - userJwts.Execute(args); + try + { + userJwts.Execute(args); + } + catch (Exception ex) + { + _reporter.Error(ex.Message); + } } } diff --git a/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj b/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj index 93b34c52c64b..53cc0514d7e4 100644 --- a/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj +++ b/src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj @@ -14,6 +14,15 @@ + + + + + + + Microsoft.AspNetCore.Tools.SecretsHelpersResources + + diff --git a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs index 413b8503987e..e0db638ae63d 100644 --- a/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs +++ b/src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs @@ -45,17 +45,21 @@ public void List_HandlesNoSecretsInProject() var app = new Program(_console); app.Run(new[] { "list", "--project", project }); - Assert.Contains("Project does not contain a user secrets ID.", _console.GetOutput()); + Assert.Contains("Set UserSecretsId to ", _console.GetOutput()); + Assert.Contains("No JWTs created yet!", _console.GetOutput()); } [Fact] - public void Create_WarnsOnNoSecretInproject() + public void Create_CreatesSecretOnNoSecretInproject() { var project = Path.Combine(_fixture.CreateProject(false), "TestProject.csproj"); var app = new Program(_console); app.Run(new[] { "create", "--project", project }); - Assert.Contains("Project does not contain a user secrets ID.", _console.GetOutput()); + var output = _console.GetOutput(); + Assert.DoesNotContain("could not find SecretManager.targets", output); + Assert.Contains("Set UserSecretsId to ", output); + Assert.Contains("New JWT saved", output); } [Fact] diff --git a/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj b/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj index 84d7ec58c998..5ad17868a98a 100644 --- a/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj +++ b/src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj @@ -7,10 +7,11 @@ + - \ No newline at end of file + diff --git a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs index 166847392504..d6aa7edd15ab 100644 --- a/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs +++ b/src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs @@ -1,12 +1,6 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. -using System; -using System.IO; -using System.Linq; -using System.Xml; -using System.Xml.Linq; -using System.Xml.XPath; using Microsoft.Extensions.CommandLineUtils; namespace Microsoft.Extensions.SecretManager.Tools.Internal; @@ -73,72 +67,6 @@ public void Execute(CommandContext context, string workingDirectory) public void Execute(CommandContext context) { - var projectPath = ResolveProjectPath(ProjectPath, WorkingDirectory); - - // Load the project file as XML - var projectDocument = XDocument.Load(projectPath, LoadOptions.PreserveWhitespace); - - // 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 is object) - { - // 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(" "); - propertyGroup.Add(new XElement("UserSecretsId", newSecretsId)); - propertyGroup.Add($"{Environment.NewLine} "); - } - - var settings = new XmlWriterSettings - { - OmitXmlDeclaration = true, - }; - - using var xw = XmlWriter.Create(projectPath, settings); - projectDocument.Save(xw); - - 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); + UserSecretsCreator.CreateUserSecretsId(context.Reporter, ProjectPath, WorkingDirectory, OverrideId); } } diff --git a/src/Tools/dotnet-user-secrets/src/Program.cs b/src/Tools/dotnet-user-secrets/src/Program.cs index 362a0701aef4..d6102f51631d 100644 --- a/src/Tools/dotnet-user-secrets/src/Program.cs +++ b/src/Tools/dotnet-user-secrets/src/Program.cs @@ -75,14 +75,10 @@ internal int RunInternal(params string[] args) return 0; } - string userSecretsId; - try - { - userSecretsId = ResolveId(options, reporter); - } - catch (Exception ex) when (ex is InvalidOperationException || ex is FileNotFoundException) + var userSecretsId = ResolveId(options, reporter); + + if (string.IsNullOrEmpty(userSecretsId)) { - reporter.Error(ex.Message); return 1; } diff --git a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj index 1ef774e1c5e7..61586af02eff 100644 --- a/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj +++ b/src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -16,7 +16,15 @@ - + + + + + + + Microsoft.AspNetCore.Tools.SecretsHelpersResources + + diff --git a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj index 94aa9103bdbc..87d33c99a122 100644 --- a/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj +++ b/src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj @@ -1,4 +1,4 @@ - + $(DefaultNetCoreTargetFramework) @@ -7,7 +7,7 @@ - +