Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 9, 2022
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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,20 +33,20 @@ 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];
}

if (!File.Exists(projectPath))
{
throw new FileNotFoundException(Resources.FormatError_ProjectPath_NotFound(projectPath));
throw new FileNotFoundException(SecretsHelpersResources.FormatError_ProjectPath_NotFound(projectPath));
}

return projectPath;
Original file line number Diff line number Diff line change
@@ -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;

/// <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 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;

147 changes: 147 additions & 0 deletions src/Tools/Shared/SecretsHelpers/SecretsHelpersResources.resx
Original file line number Diff line number Diff line change
@@ -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 changes: 84 additions & 0 deletions src/Tools/Shared/SecretsHelpers/UserSecretsCreator.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
19 changes: 8 additions & 11 deletions src/Tools/dotnet-user-jwts/src/Helpers/DevJwtCliHelpers.cs
Original file line number Diff line number Diff line change
@@ -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<string, string> secrets = null;
if (File.Exists(secretsFilePath))
9 changes: 8 additions & 1 deletion src/Tools/dotnet-user-jwts/src/Program.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
}
9 changes: 9 additions & 0 deletions src/Tools/dotnet-user-jwts/src/dotnet-user-jwts.csproj
Original file line number Diff line number Diff line change
@@ -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>
10 changes: 7 additions & 3 deletions src/Tools/dotnet-user-jwts/test/UserJwtsTests.cs
Original file line number Diff line number Diff line change
@@ -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]
Original file line number Diff line number Diff line change
@@ -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>
74 changes: 1 addition & 73 deletions src/Tools/dotnet-user-secrets/src/Internal/InitCommand.cs
Original file line number Diff line number Diff line change
@@ -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);
}
}
10 changes: 3 additions & 7 deletions src/Tools/dotnet-user-secrets/src/Program.cs
Original file line number Diff line number Diff line change
@@ -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;
}

12 changes: 10 additions & 2 deletions src/Tools/dotnet-user-secrets/src/dotnet-user-secrets.csproj
Original file line number Diff line number Diff line change
@@ -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>
Original file line number Diff line number Diff line change
@@ -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>