diff --git a/DotNetTools.sln b/DotNetTools.sln index 95b7b67009..302f0e2f5d 100644 --- a/DotNetTools.sln +++ b/DotNetTools.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.27110.0 +VisualStudioVersion = 15.0.27120.0 MinimumVisualStudioVersion = 15.0.26730.03 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{66517987-2A5A-4330-B130-207039378FD4}" ProjectSection(SolutionItems) = preProject @@ -20,12 +20,14 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution build.ps1 = build.ps1 build.sh = build.sh CONTRIBUTING.md = CONTRIBUTING.md + build\dependencies.props = build\dependencies.props Directory.Build.props = Directory.Build.props Directory.Build.targets = Directory.Build.targets LICENSE.txt = LICENSE.txt NuGet.config = NuGet.config NuGetPackageVerifier.json = NuGetPackageVerifier.json README.md = README.md + build\sources.props = build\sources.props version.props = version.props EndProjectSection EndProject @@ -46,46 +48,98 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Extensions.Cachin EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.Tools", "src\Microsoft.AspNetCore.DeveloperCertificates.Tools\Microsoft.AspNetCore.DeveloperCertificates.Tools.csproj", "{4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "src\Microsoft.AspNetCore.DeveloperCertificates.XPlat\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{96E71881-1465-44F5-B4B7-DF9B370FFD02}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.SecretManager", "tooling\Microsoft.VisualStudio.SecretManager\Microsoft.VisualStudio.SecretManager.csproj", "{5E117F2E-7152-447F-BF47-59F759EEF3A7}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "tooling", "tooling", "{62826851-7D74-4F1E-B7D1-12553B789CD8}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Microsoft.VisualStudio.SecretManager.TestExtension", "tooling\Microsoft.VisualStudio.SecretManager.TestExtension\Microsoft.VisualStudio.SecretManager.TestExtension.csproj", "{965F8820-F809-4081-9090-1AEC903F291B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.AspNetCore.DeveloperCertificates.XPlat", "src\Microsoft.AspNetCore.DeveloperCertificates.XPlat\Microsoft.AspNetCore.DeveloperCertificates.XPlat.csproj", "{96E71881-1465-44F5-B4B7-DF9B370FFD02}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU + DebugNoVSIX|Any CPU = DebugNoVSIX|Any CPU Release|Any CPU = Release|Any CPU + ReleaseNoVSIX|Any CPU = ReleaseNoVSIX|Any CPU EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.Release|Any CPU.Build.0 = Release|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {8A8CEABC-AC47-43FF-A5DF-69224F7E1F46}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {16BADE2F-1184-4518-8A70-B68A19D0805B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {16BADE2F-1184-4518-8A70-B68A19D0805B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {16BADE2F-1184-4518-8A70-B68A19D0805B}.Release|Any CPU.ActiveCfg = Release|Any CPU {16BADE2F-1184-4518-8A70-B68A19D0805B}.Release|Any CPU.Build.0 = Release|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {16BADE2F-1184-4518-8A70-B68A19D0805B}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Release|Any CPU.ActiveCfg = Release|Any CPU {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.Release|Any CPU.Build.0 = Release|Any CPU + {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {8730E848-CA0F-4E0A-9A2F-BC22AD0B2C4E}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {7B331122-83B1-4F08-A119-DC846959844C}.Debug|Any CPU.Build.0 = Debug|Any CPU + {7B331122-83B1-4F08-A119-DC846959844C}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {7B331122-83B1-4F08-A119-DC846959844C}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.ActiveCfg = Release|Any CPU {7B331122-83B1-4F08-A119-DC846959844C}.Release|Any CPU.Build.0 = Release|Any CPU + {7B331122-83B1-4F08-A119-DC846959844C}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {7B331122-83B1-4F08-A119-DC846959844C}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Release|Any CPU.ActiveCfg = Release|Any CPU {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.Release|Any CPU.Build.0 = Release|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {8A2E6961-6B12-4A8E-8215-3E7301D52EAC}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Debug|Any CPU.Build.0 = Debug|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.ActiveCfg = Release|Any CPU {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.Release|Any CPU.Build.0 = Release|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {53F3B53D-303A-4DAA-9C38-4F55195FA5B9}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Release|Any CPU.ActiveCfg = Release|Any CPU {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.Release|Any CPU.Build.0 = Release|Any CPU + {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU + {5E117F2E-7152-447F-BF47-59F759EEF3A7}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {5E117F2E-7152-447F-BF47-59F759EEF3A7}.Debug|Any CPU.Build.0 = Debug|Any CPU + {5E117F2E-7152-447F-BF47-59F759EEF3A7}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {5E117F2E-7152-447F-BF47-59F759EEF3A7}.Release|Any CPU.ActiveCfg = Release|Any CPU + {5E117F2E-7152-447F-BF47-59F759EEF3A7}.Release|Any CPU.Build.0 = Release|Any CPU + {5E117F2E-7152-447F-BF47-59F759EEF3A7}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {965F8820-F809-4081-9090-1AEC903F291B}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {965F8820-F809-4081-9090-1AEC903F291B}.Debug|Any CPU.Build.0 = Debug|Any CPU + {965F8820-F809-4081-9090-1AEC903F291B}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {965F8820-F809-4081-9090-1AEC903F291B}.Release|Any CPU.ActiveCfg = Release|Any CPU + {965F8820-F809-4081-9090-1AEC903F291B}.Release|Any CPU.Build.0 = Release|Any CPU + {965F8820-F809-4081-9090-1AEC903F291B}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU {96E71881-1465-44F5-B4B7-DF9B370FFD02}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {96E71881-1465-44F5-B4B7-DF9B370FFD02}.Debug|Any CPU.Build.0 = Debug|Any CPU + {96E71881-1465-44F5-B4B7-DF9B370FFD02}.DebugNoVSIX|Any CPU.ActiveCfg = Debug|Any CPU + {96E71881-1465-44F5-B4B7-DF9B370FFD02}.DebugNoVSIX|Any CPU.Build.0 = Debug|Any CPU {96E71881-1465-44F5-B4B7-DF9B370FFD02}.Release|Any CPU.ActiveCfg = Release|Any CPU {96E71881-1465-44F5-B4B7-DF9B370FFD02}.Release|Any CPU.Build.0 = Release|Any CPU + {96E71881-1465-44F5-B4B7-DF9B370FFD02}.ReleaseNoVSIX|Any CPU.ActiveCfg = Release|Any CPU + {96E71881-1465-44F5-B4B7-DF9B370FFD02}.ReleaseNoVSIX|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -98,6 +152,8 @@ Global {8A2E6961-6B12-4A8E-8215-3E7301D52EAC} = {F5B382BC-258F-46E1-AC3D-10E5CCD55134} {53F3B53D-303A-4DAA-9C38-4F55195FA5B9} = {66517987-2A5A-4330-B130-207039378FD4} {4FED5119-EE5C-4753-88A4-D61BDEB4D6C8} = {66517987-2A5A-4330-B130-207039378FD4} + {5E117F2E-7152-447F-BF47-59F759EEF3A7} = {62826851-7D74-4F1E-B7D1-12553B789CD8} + {965F8820-F809-4081-9090-1AEC903F291B} = {62826851-7D74-4F1E-B7D1-12553B789CD8} {96E71881-1465-44F5-B4B7-DF9B370FFD02} = {66517987-2A5A-4330-B130-207039378FD4} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution diff --git a/build/VSIX.props b/build/VSIX.props new file mode 100644 index 0000000000..9640955a99 --- /dev/null +++ b/build/VSIX.props @@ -0,0 +1,9 @@ + + + + + + diff --git a/build/VSIX.targets b/build/VSIX.targets new file mode 100644 index 0000000000..e679e98e42 --- /dev/null +++ b/build/VSIX.targets @@ -0,0 +1,86 @@ + + + true + $(RestoreDependsOn);RestoreVSIX + $(PackageDependsOn);PackageVSIX + Microsoft.VisualStudio.SecretManager + $(RepositoryRoot)tooling\$(VSIXName)\$(VSIXName).csproj + $(BuildDir)$(VSIXName).vsix + $(ArtifactsDir)msbuild\ + + + + + + + + + $(MSBuildArtifactsDir)vsix-restore.rsp + + + + + + + + + + + + + + + + + + + + + + $(MSBuildArtifactsDir)vsix.log + $(MSBuildArtifactsDir)vsix-build.rsp + + + + + + + + + + + + + + + diff --git a/build/dependencies.props b/build/dependencies.props index 72cc8468e4..87f9d17fce 100644 --- a/build/dependencies.props +++ b/build/dependencies.props @@ -3,17 +3,18 @@ $(MSBuildAllProjects);$(MSBuildThisFileFullPath) - 2.1.0-preview1-15549 - 2.1.0-preview1-27595 - 2.1.0-preview1-27595 - 2.1.0-preview1-27595 - 2.1.0-preview1-27595 - 2.1.0-preview1-27595 + 2.1.0-preview1-15573 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 + 2.1.0-preview1-27644 2.0.0 2.1.0-preview1-25907-02 15.3.0 4.4.0 4.4.0 + 9.0.1 2.3.0 2.3.0 diff --git a/build/repo.targets b/build/repo.targets new file mode 100644 index 0000000000..1183428580 --- /dev/null +++ b/build/repo.targets @@ -0,0 +1,10 @@ + + + + + + + Configuration=$(Configuration)NoVSIX + + + diff --git a/build/sources.props b/build/sources.props index c03f3ddb60..77203a6622 100644 --- a/build/sources.props +++ b/build/sources.props @@ -1,4 +1,4 @@ - + @@ -7,6 +7,7 @@ $(RestoreSources); https://dotnet.myget.org/F/aspnetcore-ci-dev/api/v3/index.json; https://dotnet.myget.org/F/aspnetcore-tools/api/v3/index.json; + https://vside.myget.org/F/vssdk/api/v3/index.json; $(RestoreSources); diff --git a/korebuild-lock.txt b/korebuild-lock.txt index 45463cc71e..e255811551 100644 --- a/korebuild-lock.txt +++ b/korebuild-lock.txt @@ -1,2 +1,2 @@ -version:2.1.0-preview1-15549 -commithash:f570e08585fec510dd60cd4bfe8795388b757a95 +version:2.1.0-preview1-15573 +commithash:82cb53a8578610baf96ef33142797c68bae68c81 diff --git a/korebuild.json b/korebuild.json index bd5d51a51b..0a2e713c08 100644 --- a/korebuild.json +++ b/korebuild.json @@ -1,4 +1,16 @@ { "$schema": "https://raw.githubusercontent.com/aspnet/BuildTools/dev/tools/korebuild.schema.json", - "channel": "dev" + "channel": "dev", + "toolsets": { + "visualstudio": { + "required": [ + "windows" + ], + "includePrerelease": true, + "minVersion": "15.4", + "requiredWorkloads": [ + "Microsoft.VisualStudio.Component.VSSDK" + ] + } + } } diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Key.snk b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Key.snk new file mode 100644 index 0000000000..2181a1627b Binary files /dev/null and b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Key.snk differ diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Microsoft.VisualStudio.SecretManager.TestExtension.csproj b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Microsoft.VisualStudio.SecretManager.TestExtension.csproj new file mode 100644 index 0000000000..c51b275dfe --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Microsoft.VisualStudio.SecretManager.TestExtension.csproj @@ -0,0 +1,149 @@ + + + + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + False + False + False + True + True + ..\..\build\Key.snk + True + + + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {965F8820-F809-4081-9090-1AEC903F291B} + Library + Properties + Microsoft.VisualStudio.SecretManager.TestExtension + Microsoft.VisualStudio.SecretManager.TestExtension + v4.6.1 + true + true + true + true + true + false + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + true + full + false + bin\Debug\ + DEBUG;TRACE + prompt + 4 + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + + + + + + + + + + SecretManagerTestControl.xaml + + + + + + + Designer + + + + + + + + + Designer + MSBuild:Compile + + + + + + + + False + + + + + + + + + + + + + + + + + + + 7.10.6071 + + + + + + 11.0.61030 + + + 12.0.30110 + + + 15.0.26606 + + + 8.0.50727 + + + 9.0.30729 + + + + 4.4.0 + + + + + + + Menus.ctmenu + + + + + true + VSPackage + + + + + + diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/NotifyPropertyChanged.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/NotifyPropertyChanged.cs new file mode 100644 index 0000000000..720593d469 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/NotifyPropertyChanged.cs @@ -0,0 +1,18 @@ +// 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.ComponentModel; +using System.Runtime.CompilerServices; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + public abstract class NotifyPropertyChanged : INotifyPropertyChanged + { + public event PropertyChangedEventHandler PropertyChanged; + + protected void OnPropertyChanged([CallerMemberName] string propertyName = null) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName)); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/ProjectViewModel.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/ProjectViewModel.cs new file mode 100644 index 0000000000..f667db5267 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/ProjectViewModel.cs @@ -0,0 +1,20 @@ +// 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.VisualStudio.ProjectSystem; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + public class ProjectViewModel : NotifyPropertyChanged + { + public ProjectViewModel(UnconfiguredProject project) + { + Project = project; + } + + internal UnconfiguredProject Project { get; } + + public string ProjectName => Path.GetFileNameWithoutExtension(Project.FullPath); + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Properties/AssemblyInfo.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..9999a9ba7e --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Properties/AssemblyInfo.cs @@ -0,0 +1,33 @@ +using System.Reflection; +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; + +// General Information about an assembly is controlled through the following +// set of attributes. Change these attribute values to modify the information +// associated with an assembly. +[assembly: AssemblyTitle("Microsoft.VisualStudio.SecretManager.TestExtension")] +[assembly: AssemblyDescription("")] +[assembly: AssemblyConfiguration("")] +[assembly: AssemblyCompany("")] +[assembly: AssemblyProduct("Microsoft.VisualStudio.SecretManager.TestExtension")] +[assembly: AssemblyCopyright("")] +[assembly: AssemblyTrademark("")] +[assembly: AssemblyCulture("")] + +// Setting ComVisible to false makes the types in this assembly not visible +// to COM components. If you need to access a type in this assembly from +// COM, set the ComVisible attribute to true on that type. +[assembly: ComVisible(false)] + +// Version information for an assembly consists of the following four values: +// +// Major Version +// Minor Version +// Build Number +// Revision +// +// You can specify all the values or you can default the Build and Revision Numbers +// by using the '*' as shown below: +// [assembly: AssemblyVersion("1.0.*")] +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: AssemblyFileVersion("1.0.0.0")] diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/RelayCommand.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/RelayCommand.cs new file mode 100644 index 0000000000..e12b2bd622 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/RelayCommand.cs @@ -0,0 +1,44 @@ +// 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.Windows.Input; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + public class RelayCommand : ICommand + { + readonly Action _execute = null; + readonly Predicate _canExecute = null; + + public RelayCommand(Action execute) + : this(execute, null) + { + } + + public RelayCommand(Action execute, Predicate canExecute) + { + if (execute == null) + throw new ArgumentNullException("execute"); + + _execute = execute; + _canExecute = canExecute; + } + + public bool CanExecute(object parameter) + { + return _canExecute == null ? true : _canExecute((T)parameter); + } + + public event EventHandler CanExecuteChanged + { + add { CommandManager.RequerySuggested += value; } + remove { CommandManager.RequerySuggested -= value; } + } + + public void Execute(object parameter) + { + _execute((T)parameter); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Resources/SecretManagerTestCommand.png b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Resources/SecretManagerTestCommand.png new file mode 100644 index 0000000000..b22d975cbf Binary files /dev/null and b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Resources/SecretManagerTestCommand.png differ diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Resources/SecretManagerTestPackage.ico b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Resources/SecretManagerTestPackage.ico new file mode 100644 index 0000000000..d323b07fb8 Binary files /dev/null and b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/Resources/SecretManagerTestPackage.ico differ diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestCommand.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestCommand.cs new file mode 100644 index 0000000000..e2729a214a --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestCommand.cs @@ -0,0 +1,100 @@ +// 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.ComponentModel.Design; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + internal sealed class SecretManagerTestCommand + { + /// + /// Command ID. + /// + public const int CommandId = 0x0100; + + /// + /// Command menu group (command set GUID). + /// + public static readonly Guid CommandSet = new Guid("e415a3f4-f2a8-4834-b7f7-f89844b2505c"); + + /// + /// VS Package that provides this command, not null. + /// + private readonly Package package; + + /// + /// Initializes a new instance of the class. + /// Adds our command handlers for menu (commands must exist in the command table file) + /// + /// Owner package, not null. + private SecretManagerTestCommand(Package package) + { + if (package == null) + { + throw new ArgumentNullException("package"); + } + + this.package = package; + + OleMenuCommandService commandService = this.ServiceProvider.GetService(typeof(IMenuCommandService)) as OleMenuCommandService; + if (commandService != null) + { + var menuCommandID = new CommandID(CommandSet, CommandId); + var menuItem = new MenuCommand(this.ShowToolWindow, menuCommandID); + commandService.AddCommand(menuItem); + } + } + + /// + /// Gets the instance of the command. + /// + public static SecretManagerTestCommand Instance + { + get; + private set; + } + + /// + /// Gets the service provider from the owner package. + /// + private IServiceProvider ServiceProvider + { + get + { + return this.package; + } + } + + /// + /// Initializes the singleton instance of the command. + /// + /// Owner package, not null. + public static void Initialize(Package package) + { + Instance = new SecretManagerTestCommand(package); + } + + /// + /// Shows the tool window when the menu item is clicked. + /// + /// The event sender. + /// The event args. + private void ShowToolWindow(object sender, EventArgs e) + { + // Get the instance number 0 of this tool window. This window is single instance so this instance + // is actually the only one. + // The last flag is set to true so that if the tool window does not exists it will be created. + ToolWindowPane window = this.package.FindToolWindow(typeof(SecretManagerTestWindow), 0, true); + if ((null == window) || (null == window.Frame)) + { + throw new NotSupportedException("Cannot create tool window"); + } + + IVsWindowFrame windowFrame = (IVsWindowFrame)window.Frame; + Microsoft.VisualStudio.ErrorHandler.ThrowOnFailure(windowFrame.Show()); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestControl.xaml b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestControl.xaml new file mode 100644 index 0000000000..201144b7e1 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestControl.xaml @@ -0,0 +1,87 @@ + + + + + + + + + + + + + + + + + + + diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestControl.xaml.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestControl.xaml.cs new file mode 100644 index 0000000000..b39c208ddf --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestControl.xaml.cs @@ -0,0 +1,37 @@ +// 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. + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + using System.Diagnostics.CodeAnalysis; + using System.Windows; + using System.Windows.Controls; + + /// + /// Interaction logic for SecretManagerTestControl. + /// + public partial class SecretManagerTestControl : UserControl + { + /// + /// Initializes a new instance of the class. + /// + public SecretManagerTestControl() + { + this.InitializeComponent(); + } + + /// + /// Handles click on the button by displaying a message box. + /// + /// The event sender. + /// The event args. + [SuppressMessage("Microsoft.Globalization", "CA1300:SpecifyMessageBoxOptions", Justification = "Sample code")] + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1300:ElementMustBeginWithUpperCaseLetter", Justification = "Default event handler naming pattern")] + private void button1_Click(object sender, RoutedEventArgs e) + { + MessageBox.Show( + string.Format(System.Globalization.CultureInfo.CurrentUICulture, "Invoked '{0}'", this.ToString()), + "SecretManagerTest"); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestPackage.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestPackage.cs new file mode 100644 index 0000000000..ada842d9cb --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestPackage.cs @@ -0,0 +1,27 @@ +// 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.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + [PackageRegistration(UseManagedResourcesOnly = true)] + [InstalledProductRegistration("#110", "#112", "1.0", IconResourceID = 400)] // Info on this package for Help/About + [ProvideMenuResource("Menus.ctmenu", 1)] + [ProvideToolWindow(typeof(SecretManagerTestWindow))] + [Guid(SecretManagerTestPackage.PackageGuidString)] + [SuppressMessage("StyleCop.CSharp.DocumentationRules", "SA1650:ElementDocumentationMustBeSpelledCorrectly", Justification = "pkgdef, VS and vsixmanifest are valid VS terms")] + public sealed class SecretManagerTestPackage : Package + { + public const string PackageGuidString = "7b771e3e-f599-4fde-95a9-e35019e705f7"; + + protected override void Initialize() + { + SecretManagerTestCommand.Initialize(this); + base.Initialize(); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestPackage.vsct b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestPackage.vsct new file mode 100644 index 0000000000..554ae60164 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestPackage.vsct @@ -0,0 +1,83 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestWindow.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestWindow.cs new file mode 100644 index 0000000000..240a8fb935 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerTestWindow.cs @@ -0,0 +1,31 @@ +// 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.Runtime.InteropServices; +using Microsoft.VisualStudio.ComponentModelHost; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + [Guid("6afffd63-17b6-4ef2-b515-fee22d767631")] + public class SecretManagerTestWindow : ToolWindowPane + { + public SecretManagerTestWindow() + : base(null) + { + this.Caption = "SecretManager Test Window"; + this.Content = new SecretManagerTestControl(); + } + + protected override void Initialize() + { + base.Initialize(); + + var component = (IComponentModel)GetService(typeof(SComponentModel)); + var projectService = component.GetService().GetProjectService(); + ((SecretManagerTestControl)Content).DataContext = new SecretManagerViewModel(projectService); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerViewModel.cs b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerViewModel.cs new file mode 100644 index 0000000000..3a4d5cbf61 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/SecretManagerViewModel.cs @@ -0,0 +1,214 @@ +// 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.Collections.ObjectModel; +using System.Linq; +using System.Runtime.InteropServices; +using System.Threading.Tasks; +using System.Windows; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell; +using Microsoft.VisualStudio.Shell.Interop; +using Microsoft.VisualStudio.Threading; +using IOleServiceProvider = Microsoft.VisualStudio.OLE.Interop.IServiceProvider; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.VisualStudio.SecretManager.TestExtension +{ + public class SecretManagerViewModel : NotifyPropertyChanged + { + private readonly IProjectService _projectService; + private readonly Random _rand; + private string _error; + private bool _isLoaded; + private ProjectViewModel _selectedProject; + + public SecretManagerViewModel(IProjectService projectService) + { + _projectService = projectService; + + RefreshCommand = new RelayCommand(Refresh, RefreshIsEnabled); + AddCommand = new RelayCommand(Add, IsProjectLoaded); + SaveCommand = new RelayCommand(Save, IsProjectLoaded); + Refresh(null); + _rand = new Random(); + } + + public RelayCommand RefreshCommand { get; } + + public RelayCommand AddCommand { get; } + public RelayCommand SaveCommand { get; } + + public ObservableCollection Projects { get; } = new ObservableCollection(); + + public ProjectViewModel SelectedProject + { + get => _selectedProject; + set + { + if (value == _selectedProject) + { + return; + } + + _selectedProject = value; + OnSelectedProjectChanged(); + OnPropertyChanged(); + } + } + + public bool IsLoaded + { + get => _isLoaded; + set + { + if (value == _isLoaded) + { + return; + } + + _isLoaded = value; + OnPropertyChanged(); + } + } + + public string Error + { + get => _error; + set + { + if (value == _error) + { + return; + } + + _error = value; + OnPropertyChanged(); + OnPropertyChanged(nameof(ErrorVisibility)); + } + } + + public Visibility ErrorVisibility => Error == null ? Visibility.Collapsed : Visibility.Visible; + + public ObservableCollection> Secrets { get; } = new ObservableCollection>(); + + private bool RefreshIsEnabled(object obj) => IsLoaded || SelectedProject == null; + + private void Refresh(object obj) + { + Projects.Clear(); + + foreach (var project in _projectService.LoadedUnconfiguredProjects) + { + Projects.Add(new ProjectViewModel(project)); + } + } + + private bool IsProjectLoaded(object obj) => IsLoaded && SelectedProject != null; + + private void Add(object obj) + { + Secrets.Add(new KeyValuePair("NewKey" + _rand.Next(10_000), "My new totally random and secret test value")); + } + + private async void Save(object obj) + { + Exception exception; + + try + { + IOleServiceProvider oleServices; + var project = (IVsProject)_selectedProject.Project.Services.HostObject; + Marshal.ThrowExceptionForHR(project.GetItemContext((uint)VSConstants.VSITEMID.Root, out oleServices)); + var services = new ServiceProvider(oleServices); + + var projectSecrets = (IVsProjectSecrets)services.GetService(typeof(SVsProjectLocalSecrets)); + await TaskScheduler.Default; + + if (projectSecrets == null) + { + exception = null; + } + else + { + foreach (var secret in Secrets) + { + await projectSecrets.SetSecretAsync(secret.Key, secret.Value).ConfigureAwait(false); + } + + exception = null; + } + } + catch (Exception ex) + { + exception = ex; + } + + if (exception != null) + { + Error = exception.ToString(); + } + } + + private async void OnSelectedProjectChanged() + { + Secrets.Clear(); + IsLoaded = false; + + if (_selectedProject == null) + { + return; + } + + KeyValuePair[] results; + Exception exception; + + try + { + IOleServiceProvider oleServices; + var project = (IVsProject)_selectedProject.Project.Services.HostObject; + Marshal.ThrowExceptionForHR(project.GetItemContext((uint)VSConstants.VSITEMID.Root, out oleServices)); + var services = new ServiceProvider(oleServices); + + var projectSecrets = (IVsProjectSecrets)services.GetService(typeof(SVsProjectLocalSecrets)); + await TaskScheduler.Default; + + if (projectSecrets == null) + { + results = null; + exception = null; + } + else + { + var secrets = await projectSecrets.GetSecretsAsync().ConfigureAwait(false); + + results = secrets.ToArray(); + exception = null; + } + } + catch (Exception ex) + { + results = null; + exception = ex; + } + + await ThreadHelper.JoinableTaskFactory.SwitchToMainThreadAsync(); + + if (exception != null) + { + Error = exception.ToString(); + } + else if (results != null) + { + for (var i = 0; i < results.Length; i++) + { + Secrets.Add(results[i]); + } + } + + IsLoaded = true; + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/VSPackage.resx b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/VSPackage.resx new file mode 100644 index 0000000000..ca1c752309 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/VSPackage.resx @@ -0,0 +1,140 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + + SecretManagerTest Extension + + + SecretManagerTest Visual Studio Extension Detailed Info + + + Resources\SecretManagerTestPackage.ico;System.Drawing.Icon, System.Drawing, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a + + \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/app.config b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/app.config new file mode 100644 index 0000000000..4bac29887b --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/app.config @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/source.extension.vsixmanifest b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/source.extension.vsixmanifest new file mode 100644 index 0000000000..3b8e971651 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager.TestExtension/source.extension.vsixmanifest @@ -0,0 +1,21 @@ + + + + + Microsoft.VisualStudio.SecretManager.TestExtension + A test extension for Microsoft.VisualStudio.TestExtension + + + + + + + + + + + + + + + diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Microsoft.VisualStudio.SecretManager.csproj b/tooling/Microsoft.VisualStudio.SecretManager/Microsoft.VisualStudio.SecretManager.csproj new file mode 100644 index 0000000000..bfd99e46b7 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Microsoft.VisualStudio.SecretManager.csproj @@ -0,0 +1,106 @@ + + + + 15.0 + $(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion) + False + False + False + True + True + ..\..\build\Key.snk + True + + + + + + + Debug + AnyCPU + 2.0 + {82b43b9b-a64c-4715-b499-d71e9ca2bd60};{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC} + {5E117F2E-7152-447F-BF47-59F759EEF3A7} + Library + Properties + Microsoft.VisualStudio.SecretManager + Microsoft.VisualStudio.SecretManager + v4.6.1 + true + true + true + true + true + false + Program + $(DevEnvDir)devenv.exe + /rootsuffix Exp + + + true + full + false + bin\Debug\ + TRACE;DEBUG;EXTENSION_DEVELOPER_MODE + prompt + 4 + latest + + + pdbonly + true + bin\Release\ + TRACE + prompt + 4 + latest + + + + + + + + + + + + + + Designer + + + + + + + + + + + + + + + + + + + + + + + + + + + Designer + + + + + + diff --git a/tooling/Microsoft.VisualStudio.SecretManager/ProjectLocalSecretsManager.cs b/tooling/Microsoft.VisualStudio.SecretManager/ProjectLocalSecretsManager.cs new file mode 100644 index 0000000000..e516a85ffb --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/ProjectLocalSecretsManager.cs @@ -0,0 +1,148 @@ +// 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.Threading; +using System.Threading.Tasks; +using Microsoft.VisualStudio.ProjectSystem.Properties; +using Microsoft.VisualStudio.Threading; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.VisualStudio.SecretManager +{ + /// + /// Provides an thread-safe access the secrets.json file based on the UserSecretsId property in a configured project. + /// + internal class ProjectLocalSecretsManager : Shell.IVsProjectSecrets, Shell.SVsProjectLocalSecrets + { + private const string UserSecretsPropertyName = "UserSecretsId"; + + private readonly AsyncSemaphore _semaphore; + private readonly IProjectPropertiesProvider _propertiesProvider; + private readonly Lazy _services; + + public ProjectLocalSecretsManager(IProjectPropertiesProvider propertiesProvider, Lazy serviceProvider) + { + _propertiesProvider = propertiesProvider ?? throw new ArgumentNullException(nameof(propertiesProvider)); + _services = serviceProvider ?? throw new ArgumentNullException(nameof(serviceProvider)); + _semaphore = new AsyncSemaphore(1); + } + + public string SanitizeName(string name) => name; + + public IReadOnlyCollection GetInvalidCharactersFrom(string name) => Array.Empty(); + + public async Task AddSecretAsync(string name, string value, CancellationToken cancellationToken = default) + { + EnsureKeyNameIsValid(name); + await TaskScheduler.Default; + + using (await _semaphore.EnterAsync(cancellationToken)) + using (var store = await GetOrCreateStoreAsync(cancellationToken)) + { + if (store.ContainsKey(name)) + { + throw new ArgumentException(Resources.Error_SecretAlreadyExists, nameof(name)); + } + + store.Set(name, value); + await store.SaveAsync(cancellationToken); + } + } + + public async Task SetSecretAsync(string name, string value, CancellationToken cancellationToken = default) + { + EnsureKeyNameIsValid(name); + await TaskScheduler.Default; + + using (await _semaphore.EnterAsync(cancellationToken)) + using (var store = await GetOrCreateStoreAsync(cancellationToken)) + { + store.Set(name, value); + await store.SaveAsync(cancellationToken); + } + } + + public async Task GetSecretAsync(string name, CancellationToken cancellationToken = default) + { + EnsureKeyNameIsValid(name); + await TaskScheduler.Default; + + using (await _semaphore.EnterAsync(cancellationToken)) + using (var store = await GetOrCreateStoreAsync(cancellationToken)) + { + return store.Get(name); + } + } + + public async Task> GetSecretNamesAsync(CancellationToken cancellationToken = default) + { + await TaskScheduler.Default; + + using (await _semaphore.EnterAsync(cancellationToken)) + using (var store = await GetOrCreateStoreAsync(cancellationToken)) + { + return store.ReadOnlyKeys; + } + } + + + public async Task> GetSecretsAsync(CancellationToken cancellationToken = default) + { + await TaskScheduler.Default; + + using (await _semaphore.EnterAsync(cancellationToken)) + using (var store = await GetOrCreateStoreAsync(cancellationToken)) + { + return store.Values; + } + } + + public async Task RemoveSecretAsync(string name, CancellationToken cancellationToken = default) + { + EnsureKeyNameIsValid(name); + await TaskScheduler.Default; + + using (await _semaphore.EnterAsync(cancellationToken)) + using (var store = await GetOrCreateStoreAsync(cancellationToken)) + { + if (store.Remove(name)) + { + await store.SaveAsync(cancellationToken); + return true; + } + + return false; + } + } + + private void EnsureKeyNameIsValid(string name) + { + if (name == null) + { + throw new ArgumentNullException(nameof(name)); + } + + if (name.Length == 0) + { + throw new ArgumentException(nameof(name)); + } + } + + private async Task GetOrCreateStoreAsync(CancellationToken cancellationToken) + { + var userSecretsId = await _propertiesProvider.GetCommonProperties().GetEvaluatedPropertyValueAsync(UserSecretsPropertyName); + + if (string.IsNullOrEmpty(userSecretsId)) + { + userSecretsId = Guid.NewGuid().ToString(); + await _propertiesProvider.GetCommonProperties().SetPropertyValueAsync(UserSecretsPropertyName, userSecretsId); + } + + var store = new SecretStore(userSecretsId); + await store.LoadAsync(cancellationToken); + return store; + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Properties/AssemblyInfo.cs b/tooling/Microsoft.VisualStudio.SecretManager/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..56a9841165 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Properties/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// 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.Reflection; +using Microsoft.VisualStudio.Shell; + +// required for VS to generate the pkgdef +[assembly: AssemblyVersion("1.0.0.0")] +[assembly: ProvideCodeBase(CodeBase = @"$PackageFolder$\Microsoft.VisualStudio.SecretManager.dll")] +[assembly: ProvideBindingRedirection( + AssemblyName = "Microsoft.VisualStudio.SecretManager", + GenerateCodeBase = true, + PublicKeyToken = "adb9793829ddae60", + OldVersionLowerBound = "0.0.0.0", + OldVersionUpperBound = "1.0.0.0", + NewVersion = "1.0.0.0")] diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Resources.Designer.cs b/tooling/Microsoft.VisualStudio.SecretManager/Resources.Designer.cs new file mode 100644 index 0000000000..bd79af4b8c --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Resources.Designer.cs @@ -0,0 +1,140 @@ +// +namespace Microsoft.VisualStudio.SecretManager +{ + using System.Globalization; + using System.Reflection; + using System.Resources; + + internal static class Resources + { + private static readonly ResourceManager _resourceManager + = new ResourceManager("Microsoft.VisualStudio.SecretManager.Resources", typeof(Resources).GetTypeInfo().Assembly); + + /// + /// A secret with this name already exists. + /// + internal static string Error_SecretAlreadyExists + { + get => GetString("Error_SecretAlreadyExists"); + } + + /// + /// Value cannot be null or an empty string. + /// + internal static string Common_StringNullOrEmpty + { + get => GetString("Common_StringNullOrEmpty"); + } + + /// + /// Value cannot be null or an empty string. + /// + internal static string FormatCommon_StringNullOrEmpty() + => GetString("Common_StringNullOrEmpty"); + + /// + /// Invalid character '{0}' found in the user secrets ID at index '{1}'. + /// + internal static string Error_Invalid_Character_In_UserSecrets_Id + { + get => GetString("Error_Invalid_Character_In_UserSecrets_Id"); + } + + /// + /// Invalid character '{0}' found in the user secrets ID at index '{1}'. + /// + internal static string FormatError_Invalid_Character_In_UserSecrets_Id(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Error_Invalid_Character_In_UserSecrets_Id"), p0, p1); + + /// + /// Could not find 'UserSecretsIdAttribute' on assembly '{0}'. + /// Check that the project for '{0}' has set the 'UserSecretsId' build property. + /// If the 'UserSecretsId' property is already set then add a reference to the Microsoft.Extensions.Configuration.UserSecrets package. + /// + internal static string Error_Missing_UserSecretsIdAttribute + { + get => GetString("Error_Missing_UserSecretsIdAttribute"); + } + + /// + /// Could not find 'UserSecretsIdAttribute' on assembly '{0}'. + /// Check that the project for '{0}' has set the 'UserSecretsId' build property. + /// If the 'UserSecretsId' property is already set then add a reference to the Microsoft.Extensions.Configuration.UserSecrets package. + /// + internal static string FormatError_Missing_UserSecretsIdAttribute(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Error_Missing_UserSecretsIdAttribute"), p0); + + /// + /// File path must be a non-empty string. + /// + internal static string Error_InvalidFilePath + { + get => GetString("Error_InvalidFilePath"); + } + + /// + /// File path must be a non-empty string. + /// + internal static string FormatError_InvalidFilePath() + => GetString("Error_InvalidFilePath"); + + /// + /// Could not parse the JSON file. Error on line number '{0}': '{1}'. + /// + internal static string Error_JSONParseError + { + get => GetString("Error_JSONParseError"); + } + + /// + /// Could not parse the JSON file. Error on line number '{0}': '{1}'. + /// + internal static string FormatError_JSONParseError(object p0, object p1) + => string.Format(CultureInfo.CurrentCulture, GetString("Error_JSONParseError"), p0, p1); + + /// + /// A duplicate key '{0}' was found. + /// + internal static string Error_KeyIsDuplicated + { + get => GetString("Error_KeyIsDuplicated"); + } + + /// + /// A duplicate key '{0}' was found. + /// + internal static string FormatError_KeyIsDuplicated(object p0) + => string.Format(CultureInfo.CurrentCulture, GetString("Error_KeyIsDuplicated"), p0); + + /// + /// Unsupported JSON token '{0}' was found. Path '{1}', line {2} position {3}. + /// + internal static string Error_UnsupportedJSONToken + { + get => GetString("Error_UnsupportedJSONToken"); + } + + /// + /// Unsupported JSON token '{0}' was found. Path '{1}', line {2} position {3}. + /// + internal static string FormatError_UnsupportedJSONToken(object p0, object p1, object p2, object p3) + => string.Format(CultureInfo.CurrentCulture, GetString("Error_UnsupportedJSONToken"), p0, p1, p2, p3); + + private static string GetString(string name, params string[] formatterNames) + { + var value = _resourceManager.GetString(name); + + System.Diagnostics.Debug.Assert(value != null); + + if (formatterNames != null) + { + for (var i = 0; i < formatterNames.Length; i++) + { + value = value.Replace("{" + formatterNames[i] + "}", "{" + i + "}"); + } + } + + return value; + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Resources.resx b/tooling/Microsoft.VisualStudio.SecretManager/Resources.resx new file mode 100644 index 0000000000..1057cd7926 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Resources.resx @@ -0,0 +1,146 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 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 + + + File path must be a non-empty string. + + + Could not parse the JSON file. Error on line number '{0}': '{1}'. + + + A duplicate key '{0}' was found. + + + Unsupported JSON token '{0}' was found. Path '{1}', line {2} position {3}. + + + Value cannot be null or an empty string. + + + Invalid character '{0}' found in the user secrets ID at index '{1}'. + + + Could not find 'UserSecretsIdAttribute' on assembly '{0}'. +Check that the project for '{0}' has set the 'UserSecretsId' build property. +If the 'UserSecretsId' property is already set then add a reference to the Microsoft.Extensions.Configuration.UserSecrets package. + + + A secret with this name already exists. + + \ No newline at end of file diff --git a/tooling/Microsoft.VisualStudio.SecretManager/SecretManagerFactory.cs b/tooling/Microsoft.VisualStudio.SecretManager/SecretManagerFactory.cs new file mode 100644 index 0000000000..b37212e23b --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/SecretManagerFactory.cs @@ -0,0 +1,37 @@ +// 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.ComponentModel.Composition; +using Microsoft.VisualStudio.ProjectSystem; +using Microsoft.VisualStudio.Shell; + +namespace Microsoft.VisualStudio.SecretManager +{ + internal class SecretManagerFactory + { + // This is capability is set in Microsoft.Extensions.Configuration.UserSecrets + private const string CapabilityName = "LocalUserSecrets"; + + private readonly Lazy _secretManager; + private readonly UnconfiguredProject _project; + + [ImportingConstructor] + public SecretManagerFactory(UnconfiguredProject project, SVsServiceProvider vsServiceProvider) + { + _project = project; + + var serviceProvider = new Lazy(() => vsServiceProvider); + + _secretManager = new Lazy(() => + { + var propertiesProvider = _project.Services.ActiveConfiguredProjectProvider.ActiveConfiguredProject.Services.ProjectPropertiesProvider; + return new ProjectLocalSecretsManager(propertiesProvider, serviceProvider); + }); + } + + [ExportVsProfferedProjectService(typeof(SVsProjectLocalSecrets))] + [AppliesTo(CapabilityName)] + public SVsProjectLocalSecrets ProjectLocalSecretsManager => _secretManager.Value; + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/SecretStore.cs b/tooling/Microsoft.VisualStudio.SecretManager/SecretStore.cs new file mode 100644 index 0000000000..ee10d5069c --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/SecretStore.cs @@ -0,0 +1,169 @@ +// 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.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration.Json; +using Microsoft.Extensions.Configuration.UserSecrets; +using Microsoft.VisualStudio.Threading; +using Newtonsoft.Json.Linq; +using Task = System.Threading.Tasks.Task; + +namespace Microsoft.VisualStudio.SecretManager +{ + /// + /// Provides read and write access to the secrets.json file for local user secrets. + /// This is not thread-safe. + /// This object is meant to have a short lifetime. + /// When calling , this will overwrite the secrets.json file. It does not check for concurrency issues if another process has edited this file. + /// + internal class SecretStore : IDisposable + { + private Dictionary _secrets; + private string _fileDir; + private string _filePath; + private bool _isDirty; + private volatile bool _disposed; + + public SecretStore(string userSecretsId) + { + _filePath = PathHelper.GetSecretsPathFromSecretsId(userSecretsId); + _fileDir = Path.GetDirectoryName(_filePath); + } + + public IReadOnlyCollection ReadOnlyKeys + { + get + { + EnsureNotDisposed(); + return _secrets.Keys; + } + } + + public IReadOnlyDictionary Values + { + get + { + EnsureNotDisposed(); + + return _secrets; + } + } + + public bool ContainsKey(string key) + { + EnsureNotDisposed(); + + return _secrets.ContainsKey(key); + } + + public string Get(string name) + { + EnsureNotDisposed(); + + return _secrets[name]; + } + + public void Set(string key, string value) + { + EnsureNotDisposed(); + + _isDirty = true; + _secrets[key] = value; + } + + public bool Remove(string key) + { + EnsureNotDisposed(); + _isDirty = true; + return _secrets.Remove(key); + } + + public async Task LoadAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await TaskScheduler.Default; + + EnsureNotDisposed(); + + string text = null; + + if (File.Exists(_filePath)) + { + text = File.ReadAllText(_filePath); + } + + _secrets = DeserializeJson(text); + } + + public async Task SaveAsync(CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + await TaskScheduler.Default; + + EnsureNotDisposed(); + + if (!_isDirty) + { + return; + } + + Directory.CreateDirectory(_fileDir); + File.WriteAllText(_filePath, Stringify(_secrets), Encoding.UTF8); + + _isDirty = false; + } + + private void EnsureNotDisposed() + { + if (_disposed) + { + throw new ObjectDisposedException(nameof(SecretStore)); + } + } + + private static string Stringify(Dictionary secrets) + { + var contents = new JObject(); + if (secrets != null) + { + foreach (var secret in secrets) + { + contents[secret.Key] = secret.Value; + } + } + + return contents.ToString(); + } + + private static Dictionary DeserializeJson(string text) + { + if (string.IsNullOrEmpty(text)) + { + return new Dictionary(StringComparer.OrdinalIgnoreCase); + } + + using (var stream = new MemoryStream()) + { + var bytes = Encoding.UTF8.GetBytes(text); + stream.Write(bytes, 0, bytes.Length); + stream.Position = 0; + + // might throw FormatException if JSON is malformed. + var data = JsonConfigurationFileParser.Parse(stream); + + return new Dictionary(data, StringComparer.OrdinalIgnoreCase); + } + } + + public void Dispose() + { + if (_disposed) return; + _disposed = true; + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Sources/ConfigurationPath.cs b/tooling/Microsoft.VisualStudio.SecretManager/Sources/ConfigurationPath.cs new file mode 100644 index 0000000000..d4f277e0d1 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Sources/ConfigurationPath.cs @@ -0,0 +1,79 @@ +// 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; + +namespace Microsoft.Extensions.Configuration +{ + /// + /// Utility methods and constants for manipulating Configuration paths + /// + internal static class ConfigurationPath + { + /// + /// The delimiter ":" used to separate individual keys in a path. + /// + public static readonly string KeyDelimiter = ":"; + + /// + /// Combines path segments into one path. + /// + /// The path segments to combine. + /// The combined path. + public static string Combine(params string[] pathSegments) + { + if (pathSegments == null) + { + throw new ArgumentNullException(nameof(pathSegments)); + } + return string.Join(KeyDelimiter, pathSegments); + } + + /// + /// Combines path segments into one path. + /// + /// The path segments to combine. + /// The combined path. + public static string Combine(IEnumerable pathSegments) + { + if (pathSegments == null) + { + throw new ArgumentNullException(nameof(pathSegments)); + } + return string.Join(KeyDelimiter, pathSegments); + } + + /// + /// Extracts the last path segment from the path. + /// + /// The path. + /// The last path segment of the path. + public static string GetSectionKey(string path) + { + if (string.IsNullOrEmpty(path)) + { + return path; + } + + var lastDelimiterIndex = path.LastIndexOf(KeyDelimiter, StringComparison.OrdinalIgnoreCase); + return lastDelimiterIndex == -1 ? path : path.Substring(lastDelimiterIndex + 1); + } + + /// + /// Extracts the path corresponding to the parent node for a given path. + /// + /// The path. + /// The original path minus the last individual segment found in it. Null if the original path corresponds to a top level node. + public static string GetParentPath(string path) + { + if (string.IsNullOrEmpty(path)) + { + return null; + } + + var lastDelimiterIndex = path.LastIndexOf(KeyDelimiter, StringComparison.OrdinalIgnoreCase); + return lastDelimiterIndex == -1 ? null : path.Substring(0, lastDelimiterIndex); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Sources/JsonConfigurationFileParser.cs b/tooling/Microsoft.VisualStudio.SecretManager/Sources/JsonConfigurationFileParser.cs new file mode 100644 index 0000000000..1cc65407c5 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Sources/JsonConfigurationFileParser.cs @@ -0,0 +1,120 @@ +// 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.Globalization; +using System.IO; +using System.Linq; +using Microsoft.VisualStudio.SecretManager; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Extensions.Configuration.Json +{ + internal class JsonConfigurationFileParser + { + private JsonConfigurationFileParser() { } + + private readonly IDictionary _data = new SortedDictionary(StringComparer.OrdinalIgnoreCase); + private readonly Stack _context = new Stack(); + private string _currentPath; + + private JsonTextReader _reader; + + public static IDictionary Parse(Stream input) + => new JsonConfigurationFileParser().ParseStream(input); + + private IDictionary ParseStream(Stream input) + { + _data.Clear(); + _reader = new JsonTextReader(new StreamReader(input)); + _reader.DateParseHandling = DateParseHandling.None; + + var jsonConfig = JObject.Load(_reader); + + VisitJObject(jsonConfig); + + return _data; + } + + private void VisitJObject(JObject jObject) + { + foreach (var property in jObject.Properties()) + { + EnterContext(property.Name); + VisitProperty(property); + ExitContext(); + } + } + + private void VisitProperty(JProperty property) + { + VisitToken(property.Value); + } + + private void VisitToken(JToken token) + { + switch (token.Type) + { + case JTokenType.Object: + VisitJObject(token.Value()); + break; + + case JTokenType.Array: + VisitArray(token.Value()); + break; + + case JTokenType.Integer: + case JTokenType.Float: + case JTokenType.String: + case JTokenType.Boolean: + case JTokenType.Bytes: + case JTokenType.Raw: + case JTokenType.Null: + VisitPrimitive(token.Value()); + break; + + default: + throw new FormatException(Resources.FormatError_UnsupportedJSONToken( + _reader.TokenType, + _reader.Path, + _reader.LineNumber, + _reader.LinePosition)); + } + } + + private void VisitArray(JArray array) + { + for (int index = 0; index < array.Count; index++) + { + EnterContext(index.ToString()); + VisitToken(array[index]); + ExitContext(); + } + } + + private void VisitPrimitive(JValue data) + { + var key = _currentPath; + + if (_data.ContainsKey(key)) + { + throw new FormatException(Resources.FormatError_KeyIsDuplicated(key)); + } + _data[key] = data.ToString(CultureInfo.InvariantCulture); + } + + private void EnterContext(string context) + { + _context.Push(context); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + + private void ExitContext() + { + _context.Pop(); + _currentPath = ConfigurationPath.Combine(_context.Reverse()); + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/Sources/PathHelper.cs b/tooling/Microsoft.VisualStudio.SecretManager/Sources/PathHelper.cs new file mode 100644 index 0000000000..ae135cb5c4 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/Sources/PathHelper.cs @@ -0,0 +1,57 @@ +// 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 Microsoft.VisualStudio.SecretManager; + +namespace Microsoft.Extensions.Configuration.UserSecrets +{ + /// + /// Provides paths for user secrets configuration files. + /// + internal class PathHelper + { + internal const string SecretsFileName = "secrets.json"; + + /// + /// + /// Returns the path to the JSON file that stores user secrets. + /// + /// + /// This uses the current user profile to locate the secrets file on disk in a location outside of source control. + /// + /// + /// The user secret ID. + /// The full path to the secret file. + public static string GetSecretsPathFromSecretsId(string userSecretsId) + { + if (string.IsNullOrEmpty(userSecretsId)) + { + throw new ArgumentException(Resources.Common_StringNullOrEmpty, nameof(userSecretsId)); + } + + var badCharIndex = userSecretsId.IndexOfAny(Path.GetInvalidFileNameChars()); + if (badCharIndex != -1) + { + throw new InvalidOperationException( + string.Format( + Resources.Error_Invalid_Character_In_UserSecrets_Id, + userSecretsId[badCharIndex], + badCharIndex)); + } + + var root = Environment.GetEnvironmentVariable("APPDATA") ?? // On Windows it goes to %APPDATA%\Microsoft\UserSecrets\ + Environment.GetEnvironmentVariable("HOME"); // On Mac/Linux it goes to ~/.microsoft/usersecrets/ + + if (!string.IsNullOrEmpty(Environment.GetEnvironmentVariable("APPDATA"))) + { + return Path.Combine(root, "Microsoft", "UserSecrets", userSecretsId, SecretsFileName); + } + else + { + return Path.Combine(root, ".microsoft", "usersecrets", userSecretsId, SecretsFileName); + } + } + } +} diff --git a/tooling/Microsoft.VisualStudio.SecretManager/source.extension.vsixmanifest b/tooling/Microsoft.VisualStudio.SecretManager/source.extension.vsixmanifest new file mode 100644 index 0000000000..643914b606 --- /dev/null +++ b/tooling/Microsoft.VisualStudio.SecretManager/source.extension.vsixmanifest @@ -0,0 +1,21 @@ + + + + + Microsoft.VisualStudio.SecretManager + Enables IVsProjectSecrets for ASP.NET Core projects that use Microsoft.Extensions.Configuration.UserSecrets. + + + + + + + + + + + + + + + diff --git a/version.props b/version.props index 5c4a7c32d1..4884e87b9e 100644 --- a/version.props +++ b/version.props @@ -1,9 +1,12 @@ 2.1.0 + 15.6 preview1 $(VersionPrefix) $(VersionPrefix)-$(VersionSuffix)-final + $(VsixVersion).$(BuildNumber) + $(VsixVersion).999999 t000 $(VersionSuffix)-$(BuildNumber)