Browse Source

[user-jwts] Read and generate secrets ID with SecretsManager (#42006)

* [user-jwts] Read secrets ID from SecretsManager and generate missing secrets
* Address feedback from review
* Add shared localizations strings for secrets
* Update localization and address feedback
Safia Abdalla 3 years ago
parent
commit
9ad5dc9dcc

+ 4 - 7
src/Tools/dotnet-user-secrets/src/Internal/MsBuildProjectFinder.cs → 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 @@ internal sealed class MsBuildProjectFinder
 
             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 @@ internal sealed class MsBuildProjectFinder
 
         if (!File.Exists(projectPath))
         {
-            throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath));
+            throw new FileNotFoundException(SecretsHelpersResources.FormatError_ProjectPath_NotFound(projectPath));
         }
 
         return projectPath;

+ 18 - 8
src/Tools/dotnet-user-secrets/src/Internal/ProjectIdResolver.cs → src/Tools/Shared/SecretsHelpers/ProjectIdResolver.cs

@@ -6,16 +6,15 @@ using System.Diagnostics;
 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;
-
 /// <summary>
 /// 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.
 /// </summary>
-public class ProjectIdResolver
+internal sealed class ProjectIdResolver
 {
     private const string DefaultConfig = "Debug";
     private readonly IReporter _reporter;
@@ -32,9 +31,18 @@ public class ProjectIdResolver
     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 class ProjectIdResolver
                 _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;
 

+ 147 - 0
src/Tools/Shared/SecretsHelpers/SecretsHelpersResources.resx

@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="utf-8"?>
+<root>
+  <!-- 
+    Microsoft ResX Schema 
+    
+    Version 2.0
+    
+    The primary goals of this format is to allow a simple XML format 
+    that is mostly human readable. The generation and parsing of the 
+    various data types are done through the TypeConverter classes 
+    associated with the data types.
+    
+    Example:
+    
+    ... ado.net/XML headers & schema ...
+    <resheader name="resmimetype">text/microsoft-resx</resheader>
+    <resheader name="version">2.0</resheader>
+    <resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
+    <resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
+    <data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
+    <data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
+    <data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
+        <value>[base64 mime encoded serialized .NET Framework object]</value>
+    </data>
+    <data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
+        <value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
+        <comment>This is a comment</comment>
+    </data>
+                
+    There are any number of "resheader" rows that contain simple 
+    name/value pairs.
+    
+    Each data row contains a name, and value. The row also contains a 
+    type or mimetype. Type corresponds to a .NET class that support 
+    text/value conversion through the TypeConverter architecture. 
+    Classes that don't support this are serialized and stored with the 
+    mimetype set.
+    
+    The mimetype is used for serialized objects, and tells the 
+    ResXResourceReader how to depersist the object. This is currently not 
+    extensible. For a given mimetype the value must be set accordingly:
+    
+    Note - application/x-microsoft.net.object.binary.base64 is the format 
+    that the ResXResourceWriter will generate, however the reader can 
+    read any of the formats listed below.
+    
+    mimetype: application/x-microsoft.net.object.binary.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
+            : and then encoded with base64 encoding.
+    
+    mimetype: application/x-microsoft.net.object.soap.base64
+    value   : The object must be serialized with 
+            : System.Runtime.Serialization.Formatters.Soap.SoapFormatter
+            : and then encoded with base64 encoding.
+
+    mimetype: application/x-microsoft.net.object.bytearray.base64
+    value   : The object must be serialized into a byte array 
+            : using a System.ComponentModel.TypeConverter
+            : and then encoded with base64 encoding.
+    -->
+  <xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
+    <xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
+    <xsd:element name="root" msdata:IsDataSet="true">
+      <xsd:complexType>
+        <xsd:choice maxOccurs="unbounded">
+          <xsd:element name="metadata">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" />
+              </xsd:sequence>
+              <xsd:attribute name="name" use="required" type="xsd:string" />
+              <xsd:attribute name="type" type="xsd:string" />
+              <xsd:attribute name="mimetype" type="xsd:string" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="assembly">
+            <xsd:complexType>
+              <xsd:attribute name="alias" type="xsd:string" />
+              <xsd:attribute name="name" type="xsd:string" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="data">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+                <xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
+              <xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
+              <xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
+              <xsd:attribute ref="xml:space" />
+            </xsd:complexType>
+          </xsd:element>
+          <xsd:element name="resheader">
+            <xsd:complexType>
+              <xsd:sequence>
+                <xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
+              </xsd:sequence>
+              <xsd:attribute name="name" type="xsd:string" use="required" />
+            </xsd:complexType>
+          </xsd:element>
+        </xsd:choice>
+      </xsd:complexType>
+    </xsd:element>
+  </xsd:schema>
+  <resheader name="resmimetype">
+    <value>text/microsoft-resx</value>
+  </resheader>
+  <resheader name="version">
+    <value>2.0</value>
+  </resheader>
+  <resheader name="reader">
+    <value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <resheader name="writer">
+    <value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
+  </resheader>
+  <data name="Error_InvalidSecretsId" xml:space="preserve">
+    <value>The UserSecretsId '{userSecretsId}' cannot contain any characters that cannot be used in a file path.</value>
+  </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_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_ProjectFailedToLoad" xml:space="preserve">
+    <value>Could not load the MSBuild project '{project}'.</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">
+    <value>The project file '{0}' does not exist.</value>
+  </data>
+  <data name="Message_ProjectAlreadyInitialized" xml:space="preserve">
+    <value>The MSBuild project '{project}' has already been initialized with a UserSecretsId.</value>
+  </data>
+  <data name="Message_Project_File_Path" xml:space="preserve">
+    <value>Project file path {project}.</value>
+  </data>
+  <data name="Message_SetUserSecretsIdForProject" xml:space="preserve">
+    <value>Set UserSecretsId to '{userSecretsId}' for MSBuild project '{project}'.</value>
+  </data>
+</root>

+ 84 - 0
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);
+    }
+}

+ 0 - 0
src/Tools/dotnet-user-secrets/src/assets/SecretManager.targets → src/Tools/Shared/SecretsHelpers/assets/SecretManager.targets


+ 8 - 11
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 @@ internal static class DevJwtCliHelpers
             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 @@ internal static class DevJwtCliHelpers
         // 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<string, string> secrets = null;
         if (File.Exists(secretsFilePath))

+ 8 - 1
src/Tools/dotnet-user-jwts/src/Program.cs

@@ -47,6 +47,13 @@ public class Program
         // 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);
+        }
     }
 }

+ 9 - 0
src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj

@@ -14,6 +14,15 @@
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" LinkBase="Shared" />
     <Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" LinkBase="Shared" />
+    <Compile Include="$(ToolSharedSourceRoot)SecretsHelpers\*.cs" LinkBase="Shared" />
+    <None Include="$(ToolSharedSourceRoot)\SecretsHelpers\assets\SecretManager.targets" Link="assets\SecretManager.targets" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="$(ToolSharedSourceRoot)\SecretsHelpers\SecretsHelpersResources.resx">
+      <ManifestResourceName>Microsoft.AspNetCore.Tools.SecretsHelpersResources</ManifestResourceName>
+      <Generator></Generator>
+    </EmbeddedResource>
   </ItemGroup>
 
   <ItemGroup>

+ 7 - 3
src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs

@@ -45,17 +45,21 @@ public class UserJwtsTests : IClassFixture<UserJwtsTestFixture>
         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]

+ 2 - 1
src/Tools/dotnet-user-jwts/test/dotnet-user-jwts.Tests.csproj

@@ -7,10 +7,11 @@
 
   <ItemGroup>
     <Compile Include="$(ToolSharedSourceRoot)TestHelpers\**\*.cs" />
+    <Content Include="$(ToolSharedSourceRoot)\SecretsHelpers\assets\SecretManager.targets" Link="assets\SecretManager.targets" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
 
   <ItemGroup>
     <ProjectReference Include="..\src\dotnet-user-jwts.csproj" />
   </ItemGroup>
 
-</Project>
+</Project>

+ 1 - 73
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 class InitCommand : ICommand
 
     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);
     }
 }

+ 3 - 7
src/Tools/dotnet-user-secrets/src/Program.cs

@@ -75,14 +75,10 @@ public class Program
             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;
         }
 

+ 10 - 2
src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -16,7 +16,15 @@
   <ItemGroup>
     <Compile Include="$(SharedSourceRoot)CommandLineUtils\**\*.cs" />
     <Compile Include="$(ToolSharedSourceRoot)CommandLine\**\*.cs" />
-    <None Include="assets\**\*" CopyToOutputDirectory="PreserveNewest" CopyToPublishDirectory="PreserveNewest" />
+    <Compile Include="$(ToolSharedSourceRoot)SecretsHelpers\*.cs" />
+    <None Include="$(ToolSharedSourceRoot)\SecretsHelpers\assets\SecretManager.targets" Link="assets\SecretManager.targets" CopyToOutputDirectory="PreserveNewest" />
+  </ItemGroup>
+
+  <ItemGroup>
+    <EmbeddedResource Include="$(ToolSharedSourceRoot)\SecretsHelpers\SecretsHelpersResources.resx">
+      <ManifestResourceName>Microsoft.AspNetCore.Tools.SecretsHelpersResources</ManifestResourceName>
+      <Generator></Generator>
+    </EmbeddedResource>
   </ItemGroup>
 
   <ItemGroup>

+ 2 - 2
src/Tools/dotnet-user-secrets/test/dotnet-user-secrets.Tests.csproj

@@ -1,4 +1,4 @@
-<Project Sdk="Microsoft.NET.Sdk">
+<Project Sdk="Microsoft.NET.Sdk">
 
   <PropertyGroup>
     <TargetFramework>$(DefaultNetCoreTargetFramework)</TargetFramework>
@@ -7,7 +7,7 @@
 
   <ItemGroup>
     <Compile Include="$(ToolSharedSourceRoot)TestHelpers\**\*.cs" />
-    <Content Include="..\src\assets\SecretManager.targets" Link="assets\SecretManager.targets" CopyToOutputDirectory="PreserveNewest" />
+    <Content Include="$(ToolSharedSourceRoot)\SecretsHelpers\assets\SecretManager.targets" Link="assets\SecretManager.targets" CopyToOutputDirectory="PreserveNewest" />
   </ItemGroup>
 
   <ItemGroup>