Skip to content

Commit 3dbb6c7

Browse files
committed
Add azartifacts credential provider integration
1 parent 232ae94 commit 3dbb6c7

File tree

9 files changed

+504
-139
lines changed

9 files changed

+504
-139
lines changed

src/code/CredentialProvider.cs

Lines changed: 273 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,273 @@
1+
using System;
2+
using System.Diagnostics;
3+
using System.IO;
4+
using System.Security;
5+
using System.Management.Automation;
6+
using System.Text.Json;
7+
8+
namespace Microsoft.PowerShell.PSResourceGet.UtilClasses
9+
{
10+
internal static class CredentialProvider
11+
{
12+
private static string FindCredProviderFromPluginsPath()
13+
{
14+
// Get environment variable "NUGET_PLUGIN_PATHS"
15+
// The environment variable NUGET_PLUGIN_PATHS should have the value of the .exe or .dll of the credential provider found in plugins\netfx\CredentialProvider.Microsoft\
16+
// For example, $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.exe".
17+
// OR $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.dll"
18+
19+
return Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.Machine);
20+
}
21+
22+
private static string FindCredProviderFromDefaultLocation()
23+
{
24+
// Default locations are either:
25+
// $env:UserProfile\.nuget\plugins\netfx\CredentialProvider\CredentialProvider.Microsoft.exe
26+
// OR $env:UserProfile\.nuget\plugins\netcore\CredentialProvider\CredentialProvider.Microsoft.exe (or) CredentialProvider.Microsoft.dll
27+
var credProviderDefaultLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "plugins");
28+
29+
var netCorePath = Path.Combine(credProviderDefaultLocation, "netcore", "CredentialProvider.Microsoft");
30+
var netFxPath = Path.Combine(credProviderDefaultLocation, "netfx", "CredentialProvider.Microsoft");
31+
var credProviderPath = string.Empty;
32+
if (Directory.Exists(netCorePath))
33+
{
34+
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
35+
{
36+
credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.exe");
37+
}
38+
else
39+
{
40+
credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll");
41+
}
42+
}
43+
else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT)
44+
{
45+
credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe");
46+
}
47+
48+
return credProviderPath;
49+
}
50+
51+
private static string FindCredProviderFromVSLocation(out ErrorRecord error)
52+
{
53+
error = null;
54+
55+
// C:\Program Files\Microsoft Visual Studio\
56+
var visualStudioPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Visual Studio");
57+
// "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe"
58+
// "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.dll"
59+
60+
var credProviderPath = string.Empty;
61+
if (Directory.Exists(visualStudioPath))
62+
{
63+
if (Environment.OSVersion.Platform == PlatformID.Win32NT)
64+
{
65+
credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.exe", out error);
66+
}
67+
else if (string.IsNullOrEmpty(credProviderPath))
68+
{
69+
credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", out error);
70+
}
71+
}
72+
73+
return credProviderPath;
74+
}
75+
76+
private static string VSCredentialProviderFile(string visualStudioPath, string credProviderFile, out ErrorRecord error)
77+
{
78+
error = null;
79+
try
80+
{
81+
// Search for the file in the directory and subdirectories
82+
string[] exeFile = Directory.GetFiles(visualStudioPath, credProviderFile, SearchOption.AllDirectories);
83+
84+
if (exeFile.Length > 0)
85+
{
86+
return exeFile[0];
87+
}
88+
}
89+
catch (UnauthorizedAccessException e)
90+
{
91+
error = new ErrorRecord(
92+
e,
93+
"AccessToCredentialProviderFileDenied",
94+
ErrorCategory.PermissionDenied,
95+
null);
96+
}
97+
catch (Exception ex)
98+
{
99+
error = new ErrorRecord(
100+
ex,
101+
"ErrorRetrievingCredentialProvider",
102+
ErrorCategory.NotSpecified,
103+
null);
104+
}
105+
106+
return string.Empty;
107+
}
108+
109+
internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn)
110+
{
111+
string credProviderPath = string.Empty;
112+
113+
// Find credential provider
114+
// Option 1. Use env var 'NUGET_PLUGIN_PATHS' to find credential provider.
115+
// See: https://docs.microsoft.com/en-us/nuget/reference/extensibility/nuget-cross-platform-plugins#plugin-installation-and-discovery
116+
// Nuget prioritizes credential providers stored in the NUGET_PLUGIN_PATHS env var
117+
credProviderPath = FindCredProviderFromPluginsPath();
118+
119+
// Option 2. Check default locations ($env:UserProfile\.nuget\plugins)
120+
// .NET Core based plugins should be installed in:
121+
// %UserProfile%/.nuget/plugins/netcore
122+
// .NET Framework based plugins should be installed in:
123+
// %UserProfile%/.nuget/plugins/netfx
124+
if (String.IsNullOrEmpty(credProviderPath))
125+
{
126+
credProviderPath = FindCredProviderFromDefaultLocation();
127+
}
128+
129+
// Option 3. Check Visual Studio installation paths
130+
if (String.IsNullOrEmpty(credProviderPath))
131+
{
132+
credProviderPath = FindCredProviderFromVSLocation(out ErrorRecord error);
133+
if (error != null)
134+
{
135+
cmdletPassedIn.WriteError(error);
136+
return null;
137+
}
138+
}
139+
140+
if (string.IsNullOrEmpty(credProviderPath))
141+
{
142+
cmdletPassedIn.WriteError(new ErrorRecord(
143+
new ArgumentNullException("Path to the Azure Artifacts Credential Provider is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."),
144+
"CredentialProviderPathIsNullOrEmpty",
145+
ErrorCategory.InvalidArgument,
146+
null));
147+
return null;
148+
}
149+
150+
// Check case sensitivity here
151+
if (!File.Exists(credProviderPath))
152+
{
153+
cmdletPassedIn.WriteError(new ErrorRecord(
154+
new FileNotFoundException($"Path found '{credProviderPath}' is not a valid Azure Artifact Credential Provider executable. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery to set up the Credential Provider."),
155+
"CredentialProviderFileNotFound",
156+
ErrorCategory.ObjectNotFound,
157+
null));
158+
return null;
159+
}
160+
161+
cmdletPassedIn.WriteVerbose($"Credential Provider path found at: '{credProviderPath}'");
162+
163+
string fileName = credProviderPath;
164+
// If running on unix machines, the Credential Provider needs to be called with dotnet cli.
165+
if (credProviderPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase))
166+
{
167+
fileName = "dotnet";
168+
}
169+
170+
string arguments = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ?
171+
$"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" :
172+
$"-Uri {uri} -NonInteractive -IsRetry -F Json";
173+
string fullCallingCmd = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ?
174+
$"dotnet {credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" :
175+
$"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json";
176+
cmdletPassedIn.WriteVerbose($"Calling Credential Provider with the following: '{fullCallingCmd}'");
177+
using (Process process = new Process())
178+
{
179+
// Windows call should look like: "CredentialProvider.Microsoft.exe -Uri <uri> -NonInteractive -IsRetry -F Json"
180+
// Unix call should look like: "dotnet CredentialProvider.Microsoft.dll -Uri <uri> -NonInteractive -IsRetry -F Json"
181+
process.StartInfo.FileName = fileName;
182+
process.StartInfo.Arguments = arguments;
183+
process.StartInfo.UseShellExecute = false;
184+
process.StartInfo.RedirectStandardOutput = true;
185+
process.StartInfo.RedirectStandardError = true;
186+
process.Start();
187+
var output = process.StandardOutput.ReadToEnd();
188+
var stdError = process.StandardError.ReadToEnd();
189+
190+
// Timeout in milliseconds (e.g., 5000 ms = 5 seconds)
191+
process.WaitForExit(5000);
192+
193+
if (process.ExitCode != 0)
194+
{
195+
if (!string.IsNullOrEmpty(stdError))
196+
{
197+
cmdletPassedIn.WriteError(new ErrorRecord(
198+
new ArgumentException($"Standard error: {stdError}"),
199+
"ProcessStandardError",
200+
ErrorCategory.InvalidResult,
201+
credProviderPath));
202+
}
203+
204+
cmdletPassedIn.WriteError(new ErrorRecord(
205+
new Exception($"Process exited with code {process.ExitCode}"),
206+
"ProcessExitCodeError",
207+
ErrorCategory.InvalidResult,
208+
credProviderPath));
209+
}
210+
else if (string.IsNullOrEmpty(output))
211+
{
212+
cmdletPassedIn.WriteError(new ErrorRecord(
213+
new ArgumentException($"Standard output is empty."),
214+
"ProcessStandardOutputError",
215+
ErrorCategory.InvalidResult,
216+
credProviderPath));
217+
}
218+
219+
string username = string.Empty;
220+
SecureString passwordSecure = new SecureString();
221+
try
222+
{
223+
using (JsonDocument doc = JsonDocument.Parse(output))
224+
{
225+
JsonElement root = doc.RootElement;
226+
if (root.TryGetProperty("Username", out JsonElement usernameToken))
227+
{
228+
username = usernameToken.GetString();
229+
cmdletPassedIn.WriteVerbose("Username retrieved from Credential Provider.");
230+
}
231+
if (String.IsNullOrEmpty(username))
232+
{
233+
cmdletPassedIn.WriteError(new ErrorRecord(
234+
new ArgumentNullException("Credential Provider username is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."),
235+
"CredentialProviderUserNameIsNullOrEmpty",
236+
ErrorCategory.InvalidArgument,
237+
null));
238+
return null;
239+
}
240+
241+
if (root.TryGetProperty("Password", out JsonElement passwordToken))
242+
{
243+
string password = passwordToken.GetString();
244+
if (String.IsNullOrEmpty(password))
245+
{
246+
cmdletPassedIn.WriteError(new ErrorRecord(
247+
new ArgumentNullException("Credential Provider password is null or empty. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info."),
248+
"CredentialProviderUserNameIsNullOrEmpty",
249+
ErrorCategory.InvalidArgument,
250+
null));
251+
return null;
252+
}
253+
254+
passwordSecure = Utils.ConvertToSecureString(password);
255+
cmdletPassedIn.WriteVerbose("Password retrieved from Credential Provider.");
256+
}
257+
}
258+
}
259+
catch (Exception e)
260+
{
261+
cmdletPassedIn.WriteError(new ErrorRecord(
262+
new Exception("Error retrieving credentials from Credential Provider. See https://github.com/NuGet/Home/wiki/NuGet-cross-plat-authentication-plugin#plugin-installation-and-discovery for more info.", e),
263+
"InvalidCredentialProviderResponse",
264+
ErrorCategory.InvalidResult,
265+
null));
266+
return null;
267+
}
268+
269+
return new PSCredential(username, passwordSecure);
270+
}
271+
}
272+
}
273+
}

src/code/FindHelper.cs

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -202,7 +202,16 @@ public IEnumerable<PSResourceInfo> FindByResourceName(
202202
}
203203

204204
repositoryNamesToSearch.Add(currentRepository.Name);
205-
_networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
205+
206+
// Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement.
207+
if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts))
208+
{
209+
_networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
210+
}
211+
else {
212+
_networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
213+
}
214+
206215
ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential);
207216
if (currentServer == null)
208217
{
@@ -386,7 +395,17 @@ public IEnumerable<PSCommandResourceInfo> FindByCommandOrDscResource(
386395
}
387396

388397
repositoryNamesToSearch.Add(currentRepository.Name);
389-
_networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
398+
399+
// Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement.
400+
if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts))
401+
{
402+
_networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
403+
}
404+
else
405+
{
406+
_networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
407+
}
408+
390409
ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential);
391410
if (currentServer == null)
392411
{
@@ -590,7 +609,17 @@ public IEnumerable<PSResourceInfo> FindByTag(
590609
}
591610

592611
repositoryNamesToSearch.Add(currentRepository.Name);
593-
_networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
612+
613+
// Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement.
614+
if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts))
615+
{
616+
_networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
617+
}
618+
else
619+
{
620+
_networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn);
621+
}
622+
594623
ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential);
595624
if (currentServer == null)
596625
{

0 commit comments

Comments
 (0)