diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs new file mode 100644 index 000000000..3cd1ee738 --- /dev/null +++ b/src/code/CredentialProvider.cs @@ -0,0 +1,301 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security; +using System.Management.Automation; +using System.Text.Json; +using System.Net.Http; +using System.Net; + +namespace Microsoft.PowerShell.PSResourceGet.UtilClasses +{ + internal static class CredentialProvider + { + private static readonly string _credProviderExe = "CredentialProvider.Microsoft.exe"; + private static readonly string _credProviderDll = "CredentialProvider.Microsoft.dll"; + + private static string FindCredProviderFromPluginsPath() + { + // Get environment variable "NUGET_PLUGIN_PATHS" + // 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\ + // For example, $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.exe". + // OR $env:NUGET_PLUGIN_PATHS="my-alternative-location\CredentialProvider.Microsoft.dll" + + return Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.User) ?? Environment.GetEnvironmentVariable("NUGET_PLUGIN_PATHS", EnvironmentVariableTarget.Machine); + } + + private static string FindCredProviderFromDefaultLocation() + { + // Default locations are either: + // $env:UserProfile\.nuget\plugins\netfx\CredentialProvider\CredentialProvider.Microsoft.exe + // OR $env:UserProfile\.nuget\plugins\netcore\CredentialProvider\CredentialProvider.Microsoft.exe (or) CredentialProvider.Microsoft.dll + var credProviderDefaultLocation = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".nuget", "plugins"); + + var netCorePath = Path.Combine(credProviderDefaultLocation, "netcore", "CredentialProvider.Microsoft"); + var netFxPath = Path.Combine(credProviderDefaultLocation, "netfx", "CredentialProvider.Microsoft"); + var credProviderPath = string.Empty; + if (Directory.Exists(netCorePath)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netCorePath, _credProviderExe); + } + else + { + credProviderPath = Path.Combine(netCorePath, _credProviderDll); + } + } + else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netFxPath, _credProviderExe); + } + + return credProviderPath; + } + + private static string FindCredProviderFromVSLocation(out ErrorRecord error) + { + error = null; + + // C:\Program Files\Microsoft Visual Studio\ + var visualStudioPath = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles), "Microsoft Visual Studio"); + // "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe" + // "C:\Program Files\Microsoft Visual Studio\2022\Enterprise\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.dll" + + var credProviderPath = string.Empty; + if (Directory.Exists(visualStudioPath)) + { + if (Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderExe, out error); + } + else if (string.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderDll, out error); + } + } + + return credProviderPath; + } + + private static string VSCredentialProviderFile(string visualStudioPath, string credProviderFile, out ErrorRecord error) + { + error = null; + try + { + // Search for the file in the directory and subdirectories + string[] exeFile = Directory.GetFiles(visualStudioPath, credProviderFile, SearchOption.AllDirectories); + + if (exeFile.Length > 0) + { + return exeFile[0]; + } + } + catch (UnauthorizedAccessException e) + { + error = new ErrorRecord( + e, + "AccessToCredentialProviderFileDenied", + ErrorCategory.PermissionDenied, + null); + } + catch (Exception ex) + { + error = new ErrorRecord( + ex, + "ErrorRetrievingCredentialProvider", + ErrorCategory.NotSpecified, + null); + } + + return string.Empty; + } + + internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) + { + cmdletPassedIn.WriteDebug("Enterting CredentialProvider::GetCredentialsFromProvider"); + string credProviderPath = string.Empty; + + // Find credential provider + // Option 1. Use env var 'NUGET_PLUGIN_PATHS' to find credential provider. + // See: https://docs.microsoft.com/en-us/nuget/reference/extensibility/nuget-cross-platform-plugins#plugin-installation-and-discovery + // Nuget prioritizes credential providers stored in the NUGET_PLUGIN_PATHS env var + credProviderPath = FindCredProviderFromPluginsPath(); + + // Option 2. Check default locations ($env:UserProfile\.nuget\plugins) + // .NET Core based plugins should be installed in: + // %UserProfile%/.nuget/plugins/netcore + // .NET Framework based plugins should be installed in: + // %UserProfile%/.nuget/plugins/netfx + if (String.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = FindCredProviderFromDefaultLocation(); + } + + // Option 3. Check Visual Studio installation paths + if (String.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = FindCredProviderFromVSLocation(out ErrorRecord error); + if (error != null) + { + cmdletPassedIn.WriteError(error); + return null; + } + } + + cmdletPassedIn.WriteDebug($"Credential provider path is '{credProviderPath}'"); + if (string.IsNullOrEmpty(credProviderPath)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + 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."), + "CredentialProviderPathIsNullOrEmpty", + ErrorCategory.InvalidArgument, + credProviderPath)); + return null; + } + + if (!File.Exists(credProviderPath)) + { + // If the Credential Provider is not found on a Unix machine, try looking for a case insensitive file. + if (Environment.OSVersion.Platform == PlatformID.Unix) + { + FileInfo fileInfo = new FileInfo(credProviderPath); + string resolvedFilePath = Utils.GetCaseInsensitiveFilePath(fileInfo.Directory.FullName, _credProviderDll); + if (resolvedFilePath != null) + { + credProviderPath = resolvedFilePath; + } + else + { + cmdletPassedIn.WriteError(new ErrorRecord( + 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."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + credProviderPath)); + } + } + else + { + cmdletPassedIn.WriteError(new ErrorRecord( + 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."), + "CredentialProviderFileNotFound", + ErrorCategory.ObjectNotFound, + credProviderPath)); + + return null; + } + } + + cmdletPassedIn.WriteVerbose($"Credential Provider path found at: '{credProviderPath}'"); + + string fileName = credProviderPath; + // If running on unix machines, the Credential Provider needs to be called with dotnet cli. + if (credProviderPath.EndsWith(".dll", StringComparison.OrdinalIgnoreCase)) + { + fileName = "dotnet"; + } + + string arguments = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ? + $"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" : + $"-Uri {uri} -NonInteractive -IsRetry -F Json"; + string fullCallingCmd = string.Equals(fileName, "dotnet", StringComparison.OrdinalIgnoreCase) ? + $"dotnet {credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json" : + $"{credProviderPath} -Uri {uri} -NonInteractive -IsRetry -F Json"; + cmdletPassedIn.WriteVerbose($"Calling Credential Provider with the following: '{fullCallingCmd}'"); + using (Process process = new Process()) + { + // Windows call should look like: "CredentialProvider.Microsoft.exe -Uri -NonInteractive -IsRetry -F Json" + // Unix call should look like: "dotnet CredentialProvider.Microsoft.dll -Uri -NonInteractive -IsRetry -F Json" + process.StartInfo.FileName = fileName; + process.StartInfo.Arguments = arguments; + process.StartInfo.UseShellExecute = false; + process.StartInfo.RedirectStandardOutput = true; + process.StartInfo.RedirectStandardError = true; + process.Start(); + var output = process.StandardOutput.ReadToEnd(); + var stdError = process.StandardError.ReadToEnd(); + + // Timeout in milliseconds (e.g., 5000 ms = 5 seconds) + process.WaitForExit(5000); + + if (process.ExitCode != 0) + { + if (!string.IsNullOrEmpty(stdError)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Standard error: {stdError}"), + "ProcessStandardError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + + cmdletPassedIn.WriteError(new ErrorRecord( + new Exception($"Process exited with code {process.ExitCode}"), + "ProcessExitCodeError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + else if (string.IsNullOrEmpty(output)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + new ArgumentException($"Standard output is empty."), + "ProcessStandardOutputError", + ErrorCategory.InvalidResult, + credProviderPath)); + } + + string username = string.Empty; + SecureString passwordSecure = new SecureString(); + try + { + using (JsonDocument doc = JsonDocument.Parse(output)) + { + JsonElement root = doc.RootElement; + if (root.TryGetProperty("Username", out JsonElement usernameToken)) + { + username = usernameToken.GetString(); + cmdletPassedIn.WriteVerbose("Username retrieved from Credential Provider."); + } + if (String.IsNullOrEmpty(username)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + 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."), + "CredentialProviderUserNameIsNullOrEmpty", + ErrorCategory.InvalidArgument, + credProviderPath)); + return null; + } + + if (root.TryGetProperty("Password", out JsonElement passwordToken)) + { + string password = passwordToken.GetString(); + if (String.IsNullOrEmpty(password)) + { + cmdletPassedIn.WriteError(new ErrorRecord( + 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."), + "CredentialProviderUserNameIsNullOrEmpty", + ErrorCategory.InvalidArgument, + credProviderPath)); + return null; + } + + passwordSecure = Utils.ConvertToSecureString(password); + cmdletPassedIn.WriteVerbose("Password retrieved from Credential Provider."); + } + } + } + catch (Exception e) + { + cmdletPassedIn.WriteError(new ErrorRecord( + 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), + "InvalidCredentialProviderResponse", + ErrorCategory.InvalidResult, + credProviderPath)); + return null; + } + + return new PSCredential(username, passwordSecure); + } + } + } +} diff --git a/src/code/FindHelper.cs b/src/code/FindHelper.cs index 327d0e024..afb2a1b76 100644 --- a/src/code/FindHelper.cs +++ b/src/code/FindHelper.cs @@ -202,7 +202,16 @@ public IEnumerable FindByResourceName( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { @@ -386,7 +395,17 @@ public IEnumerable FindByCommandOrDscResource( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { @@ -590,7 +609,17 @@ public IEnumerable FindByTag( } repositoryNamesToSearch.Add(currentRepository.Name); - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) { diff --git a/src/code/InstallHelper.cs b/src/code/InstallHelper.cs index e31c2b86c..1cf014b98 100644 --- a/src/code/InstallHelper.cs +++ b/src/code/InstallHelper.cs @@ -286,7 +286,16 @@ private List ProcessRepositories( string repoName = currentRepository.Name; sourceTrusted = currentRepository.Trusted || trustRepository; - _networkCredential = Utils.SetNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (currentRepository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(currentRepository, _networkCredential, _cmdletPassedIn); + } + ServerApiCall currentServer = ServerFactory.GetServer(currentRepository, _cmdletPassedIn, _networkCredential); if (currentServer == null) @@ -357,59 +366,6 @@ private List ProcessRepositories( return allPkgsInstalled; } - /// - /// Checks if any of the package versions are already installed and if they are removes them from the list of packages to install. - /// - private List FilterByInstalledPkgs(List packages) - { - // Package install paths. - // _pathsToInstallPkg will only contain the paths specified within the -Scope param (if applicable). - // _pathsToSearch will contain all resource package subdirectories within _pathsToInstallPkg path locations. - // e.g.: - // ./InstallPackagePath1/PackageA - // ./InstallPackagePath1/PackageB - // ./InstallPackagePath2/PackageC - // ./InstallPackagePath3/PackageD - - _cmdletPassedIn.WriteDebug("In InstallHelper::FilterByInstalledPkgs()"); - // Get currently installed packages. - var getHelper = new GetHelper(_cmdletPassedIn); - var installedPackageNames = new HashSet(StringComparer.CurrentCultureIgnoreCase); - foreach (var installedPkg in getHelper.GetInstalledPackages( - pkgs: packages, - pathsToSearch: _pathsToSearch)) - { - installedPackageNames.Add(installedPkg.Name); - } - - if (installedPackageNames.Count is 0) - { - return packages; - } - - // Return only packages that are not already installed. - var filteredPackages = new List(); - foreach (var pkg in packages) - { - if (!installedPackageNames.Contains(pkg.Name)) - { - // Add packages that still need to be installed. - filteredPackages.Add(pkg); - } - else - { - // Remove from tracking list of packages to install. - pkg.AdditionalMetadata.TryGetValue("NormalizedVersion", out string normalizedVersion); - _cmdletPassedIn.WriteWarning($"Resource '{pkg.Name}' with version '{normalizedVersion}' is already installed. If you would like to reinstall, please run the cmdlet again with the -Reinstall parameter"); - - // Remove from tracking list of packages to install. - _pkgNamesToInstall.RemoveAll(x => x.Equals(pkg.Name, StringComparison.InvariantCultureIgnoreCase)); - } - } - - return filteredPackages; - } - /// /// Deletes temp directory and is called at end of install process. /// diff --git a/src/code/PSRepositoryInfo.cs b/src/code/PSRepositoryInfo.cs index b74d52cff..1901356c5 100644 --- a/src/code/PSRepositoryInfo.cs +++ b/src/code/PSRepositoryInfo.cs @@ -27,74 +27,70 @@ public enum APIVersion ContainerRegistry } + public enum CredentialProviderType + { + None, + AzArtifacts + } + #endregion #region Constructor - public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, APIVersion apiVersion, bool allowed) + public PSRepositoryInfo(string name, Uri uri, int priority, bool trusted, PSCredentialInfo credentialInfo, CredentialProviderType credentialProvider, APIVersion apiVersion, bool allowed) { Name = name; Uri = uri; Priority = priority; Trusted = trusted; CredentialInfo = credentialInfo; + CredentialProvider = credentialProvider; ApiVersion = apiVersion; IsAllowedByPolicy = allowed; } #endregion - #region Enum - - public enum RepositoryProviderType - { - None, - ACR, - AzureDevOps - } - - #endregion - #region Properties /// - /// the Name of the repository + /// The Name of the repository. /// public string Name { get; } /// - /// the Uri for the repository + /// The Uri for the repository. /// public Uri Uri { get; } /// - /// whether the repository is trusted + /// Whether the repository is trusted. /// public bool Trusted { get; } /// - /// the priority of the repository + /// The priority of the repository. /// [ValidateRange(0, 100)] public int Priority { get; } /// - /// the type of repository provider (eg, AzureDevOps, ContainerRegistry, etc.) + /// The credential information for repository authentication. /// - public RepositoryProviderType RepositoryProvider { get; } + public PSCredentialInfo CredentialInfo { get; set; } /// - /// the credential information for repository authentication + /// Specifies which credential provider to use. /// - public PSCredentialInfo CredentialInfo { get; } + public CredentialProviderType CredentialProvider { get; set; } /// - /// the API protocol version for the repository + /// The API protocol version for the repository. /// public APIVersion ApiVersion { get; } // - /// is it allowed by policy + /// Specifies whether the repository is allowed by policy. /// public bool IsAllowedByPolicy { get; set; } diff --git a/src/code/PublishHelper.cs b/src/code/PublishHelper.cs index 0eec8e0d9..5d5716a2b 100644 --- a/src/code/PublishHelper.cs +++ b/src/code/PublishHelper.cs @@ -379,7 +379,15 @@ internal void PushResource(string Repository, string modulePrefix, bool SkipDepe return; } - _networkCredential = Utils.SetNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + // Set network credentials via passed in credentials, AzArtifacts CredentialProvider, or SecretManagement. + if (repository.CredentialProvider.Equals(PSRepositoryInfo.CredentialProviderType.AzArtifacts)) + { + _networkCredential = Utils.SetCredentialProviderNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + } + else + { + _networkCredential = Utils.SetSecretManagementNetworkCredential(repository, _networkCredential, _cmdletPassedIn); + } // Check if dependencies already exist within the repo if: // 1) the resource to publish has dependencies and diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 1a86db210..8177d0e7a 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using Dbg = System.Diagnostics.Debug; @@ -23,7 +24,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets SupportsShouldProcess = true, ConfirmImpact = ConfirmImpact.Low)] public sealed - class RegisterPSResourceRepository : PSCmdlet + class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters { #region Members @@ -35,6 +36,7 @@ class RegisterPSResourceRepository : PSCmdlet private const string PSGalleryParameterSet = "PSGalleryParameterSet"; private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private Uri _uri; + private CredentialProviderDynamicParameters _credentialProvider; #endregion @@ -111,6 +113,21 @@ class RegisterPSResourceRepository : PSCmdlet #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + { + return null; + } + + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + #endregion + #region Methods protected override void BeginProcessing() @@ -127,6 +144,8 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; + switch (ParameterSetName) { case NameParameterSet: @@ -140,7 +159,7 @@ protected override void ProcessRecord() try { - items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, Force, this, out string errorMsg)); + items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, credentialProvider, Force, this, out string errorMsg)); if (!string.IsNullOrEmpty(errorMsg)) { @@ -217,7 +236,8 @@ private PSRepositoryInfo PSGalleryParameterSetHelper(int repoPriority, bool repo repoPriority, repoTrusted, apiVersion: null, - repoCredentialInfo: null, + repoCredentialInfo: null, + credentialProvider: null, Force, this, out string errorMsg); @@ -352,6 +372,21 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + if (repo.ContainsKey("CredentialProvider") && + (String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || + !(repo["CredentialProvider"].ToString().Equals("None", StringComparison.OrdinalIgnoreCase) || + repo["CredentialProvider"].ToString().Equals("AzArtifacts", StringComparison.OrdinalIgnoreCase)))) + { + WriteError(new ErrorRecord( + new PSInvalidOperationException("Repository 'CredentialProvider' must be set to either 'None' or 'AzArtifacts'"), + "InvalidCredentialProviderForRepositoriesParameterSetRegistration", + ErrorCategory.InvalidArgument, + this)); + + return null; + } + + try { WriteDebug($"Registering repository '{repo["Name"]}' with uri '{repoUri}'"); @@ -361,6 +396,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) repo.ContainsKey("Trusted") ? Convert.ToBoolean(repo["Trusted"].ToString()) : DefaultTrusted, apiVersion: repo.ContainsKey("Trusted") ? (PSRepositoryInfo.APIVersion?) repo["ApiVersion"] : null, repoCredentialInfo, + repo.ContainsKey("CredentialProvider") ? (PSRepositoryInfo.CredentialProviderType?)repo["CredentialProvider"] : null, Force, this, out string errorMsg); @@ -399,4 +435,25 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) #endregion } + + public class CredentialProviderDynamicParameters + { + PSRepositoryInfo.CredentialProviderType? _credProvider = null; + + /// + /// Specifies which credential provider to use. + /// + [Parameter] + public PSRepositoryInfo.CredentialProviderType? CredentialProvider { + get + { + return _credProvider; + } + + set + { + _credProvider = value; + } + } + } } diff --git a/src/code/RepositorySettings.cs b/src/code/RepositorySettings.cs index e9f2693e2..f3299fb49 100644 --- a/src/code/RepositorySettings.cs +++ b/src/code/RepositorySettings.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. using System; @@ -62,7 +62,7 @@ public static void CheckRepositoryStore() // Add PSGallery to the newly created store Uri psGalleryUri = new Uri(PSGalleryRepoUri); - Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, PSRepositoryInfo.APIVersion.V2, force: false); + Add(PSGalleryRepoName, psGalleryUri, DefaultPriority, DefaultTrusted, repoCredentialInfo: null, repoCredentialProvider: CredentialProviderType.None, APIVersion.V2, force: false); } // Open file (which should exist now), if cannot/is corrupted then throw error @@ -76,7 +76,7 @@ public static void CheckRepositoryStore() } } - public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? repoCredentialProvider, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = String.Empty; if (repoName.Equals("PSGallery", StringComparison.OrdinalIgnoreCase)) @@ -85,11 +85,11 @@ public static PSRepositoryInfo AddRepository(string repoName, Uri repoUri, int r return null; } - return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, force, cmdletPassedIn, out errorMsg); + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoTrusted, apiVersion, repoCredentialInfo, repoCredentialProvider, force, cmdletPassedIn, out errorMsg); } - public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, bool force, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; // remove trailing and leading whitespaces, and if Name is just whitespace Name should become null now and be caught by following condition @@ -106,7 +106,7 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - PSRepositoryInfo.APIVersion resolvedAPIVersion = apiVersion ?? GetRepoAPIVersion(repoUri); + APIVersion resolvedAPIVersion = apiVersion ?? GetRepoAPIVersion(repoUri); if (repoCredentialInfo != null) { @@ -131,6 +131,13 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri } } + CredentialProviderType resolvedCredentialProvider = credentialProvider ?? CredentialProviderType.None; + // If it's an ADO feed with an ADO designated URL (eg: msazure.pkgs.) then add the 'CredentialProvider' attribute to the repository and by default set it to AzArtifacts + if ((repoUri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repoUri.AbsoluteUri.Contains("pkgs.visualstudio.com")) && credentialProvider == null) + { + resolvedCredentialProvider = CredentialProviderType.AzArtifacts; + } + if (!cmdletPassedIn.ShouldProcess(repoName, "Register repository to repository store")) { return null; @@ -141,13 +148,13 @@ public static PSRepositoryInfo AddToRepositoryStore(string repoName, Uri repoUri return null; } - var repo = RepositorySettings.Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, resolvedAPIVersion, force); + var repo = Add(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, resolvedCredentialProvider, resolvedAPIVersion, force); return repo; } - public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, bool isSet, int defaultPriority, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, bool isSet, int defaultPriority, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" @@ -182,7 +189,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr // determine trusted value to pass in (true/false if set, null otherwise, hence the nullable bool variable) bool? _trustedNullable = isSet ? new bool?(repoTrusted) : new bool?(); - + if (repoCredentialInfo != null) { bool isSecretManagementModuleAvailable = Utils.IsSecretManagementModuleAvailable(repoName, cmdletPassedIn); @@ -216,7 +223,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr return null; } - return Update(repoName, repoUri, repoPriority, _trustedNullable, apiVersion, repoCredentialInfo, cmdletPassedIn, out errorMsg); + return Update(repoName, repoUri, repoPriority, _trustedNullable, apiVersion, repoCredentialInfo, credentialProvider, cmdletPassedIn, out errorMsg); } /// @@ -224,7 +231,7 @@ public static PSRepositoryInfo UpdateRepositoryStore(string repoName, Uri repoUr /// Returns: PSRepositoryInfo containing information about the repository just added to the repository store /// /// - public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, PSRepositoryInfo.APIVersion apiVersion, bool force) + public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriority, bool repoTrusted, PSCredentialInfo repoCredentialInfo, CredentialProviderType repoCredentialProvider, APIVersion apiVersion, bool force) { try { @@ -238,7 +245,7 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit } // Delete the existing repository before overwriting it (otherwire multiple repos with the same name will be added) - List removedRepositories = RepositorySettings.Remove(new string[] { repoName }, out string[] errorList); + List removedRepositories = Remove(new string[] { repoName }, out string[] errorList); // Need to load the document again because of changes after removing doc = LoadXDocument(FullRepositoryPath); @@ -261,7 +268,8 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit new XAttribute("Url", repoUri), new XAttribute("APIVersion", apiVersion), new XAttribute("Priority", repoPriority), - new XAttribute("Trusted", repoTrusted) + new XAttribute("Trusted", repoTrusted), + new XAttribute("CredentialProvider", repoCredentialProvider) ); if (repoCredentialInfo != null) @@ -282,14 +290,14 @@ public static PSRepositoryInfo Add(string repoName, Uri repoUri, int repoPriorit bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true; - return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, apiVersion, isAllowed); + return new PSRepositoryInfo(repoName, repoUri, repoPriority, repoTrusted, repoCredentialInfo, repoCredentialProvider, apiVersion, isAllowed); } /// /// Updates a repository name, Uri, priority, installation policy, or credential information /// Returns: void /// - public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPriority, bool? repoTrusted, PSRepositoryInfo.APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, PSCmdlet cmdletPassedIn, out string errorMsg) + public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPriority, bool? repoTrusted, APIVersion? apiVersion, PSCredentialInfo repoCredentialInfo, CredentialProviderType? credentialProvider, PSCmdlet cmdletPassedIn, out string errorMsg) { errorMsg = string.Empty; PSRepositoryInfo updatedRepo; @@ -303,7 +311,7 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio bool repoIsTrusted = !(repoTrusted == null || repoTrusted == false); repoPriority = repoPriority < 0 ? DefaultPriority : repoPriority; - return AddToRepositoryStore(repoName, repoUri, repoPriority, repoIsTrusted, apiVersion, repoCredentialInfo, force:true, cmdletPassedIn, out errorMsg); + return AddToRepositoryStore(repoName, repoUri, repoPriority, repoIsTrusted, apiVersion, repoCredentialInfo, credentialProvider, force:true, cmdletPassedIn, out errorMsg); } // Check that repository node we are attempting to update has all required attributes: Name, Url (or Uri), Priority, Trusted. @@ -417,15 +425,15 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio } // Update APIVersion if necessary - PSRepositoryInfo.APIVersion resolvedAPIVersion = PSRepositoryInfo.APIVersion.Unknown; + APIVersion resolvedAPIVersion = APIVersion.Unknown; if (apiVersion != null) { - resolvedAPIVersion = (PSRepositoryInfo.APIVersion)apiVersion; + resolvedAPIVersion = (APIVersion)apiVersion; node.Attribute("APIVersion").Value = resolvedAPIVersion.ToString(); } else { - resolvedAPIVersion = (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true); + resolvedAPIVersion = (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true); } @@ -445,14 +453,29 @@ public static PSRepositoryInfo Update(string repoName, Uri repoUri, int repoPrio } + // Update CredentialProvider if necessary + CredentialProviderType resolvedCredentialProvider = credentialProvider ?? CredentialProviderType.None; + if (credentialProvider != null) + { + resolvedCredentialProvider = (CredentialProviderType)credentialProvider; + if (node.Attribute("CredentialProvider") == null) + { + node.Add(new XAttribute("CredentialProvider", resolvedCredentialProvider.ToString())); + } + else + { + node.Attribute("CredentialProvider").Value = resolvedCredentialProvider.ToString(); + } + } + bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); updatedRepo = new PSRepositoryInfo(repoName, thisUrl, Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), thisCredentialInfo, + resolvedCredentialProvider, resolvedAPIVersion, isAllowed); @@ -523,6 +546,12 @@ public static List Remove(string[] repoNames, out string[] err continue; } + CredentialProviderType resolvedCredentialProvider = CredentialProviderType.None; + if (node.Attribute("CredentialProvider") != null) + { + resolvedCredentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), node.Attribute("CredentialProvider").Value, ignoreCase: true); + } + // determine if repo had Url or Uri (less likely) attribute bool urlAttributeExists = node.Attribute("Url") != null; bool uriAttributeExists = node.Attribute("Uri") != null; @@ -537,14 +566,14 @@ public static List Remove(string[] repoNames, out string[] err bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(repoUri) : true; - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(repoUri); removedRepos.Add( new PSRepositoryInfo(repo, new Uri(node.Attribute(attributeUrlUriName).Value), Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), repoCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), + resolvedCredentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), isAllowed)); // Remove item from file @@ -630,13 +659,19 @@ public static List Read(string[] repoNames, out string[] error if (repo.Attribute("APIVersion") == null) { - PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + APIVersion apiVersion = GetRepoAPIVersion(thisUrl); XElement repoXElem = FindRepositoryElement(doc, repo.Attribute("Name").Value); repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); doc.Save(FullRepositoryPath); } + CredentialProviderType credentialProvider = CredentialProviderType.None; + if (repo.Attribute("CredentialProvider") != null) + { + credentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), repo.Attribute("CredentialProvider").Value, ignoreCase: true); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {repo.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -669,8 +704,6 @@ public static List Read(string[] repoNames, out string[] error continue; } - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); - bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(repo.Attribute("Name").Value, @@ -678,7 +711,8 @@ public static List Read(string[] repoNames, out string[] error Int32.Parse(repo.Attribute("Priority").Value), Boolean.Parse(repo.Attribute("Trusted").Value), thisCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true), + credentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), repo.Attribute("APIVersion").Value, ignoreCase: true), isAllowed); foundRepos.Add(currentRepoItem); @@ -738,13 +772,19 @@ public static List Read(string[] repoNames, out string[] error if (node.Attribute("APIVersion") == null) { - PSRepositoryInfo.APIVersion apiVersion = GetRepoAPIVersion(thisUrl); + APIVersion apiVersion = GetRepoAPIVersion(thisUrl); XElement repoXElem = FindRepositoryElement(doc, node.Attribute("Name").Value); repoXElem.SetAttributeValue("APIVersion", apiVersion.ToString()); doc.Save(FullRepositoryPath); } + CredentialProviderType credentialProvider = CredentialProviderType.None; + if (node.Attribute("CredentialProvider") != null) + { + credentialProvider = (CredentialProviderType)Enum.Parse(typeof(CredentialProviderType), node.Attribute("CredentialProvider").Value, ignoreCase: true); + } + PSCredentialInfo thisCredentialInfo; string credentialInfoErrorMessage = $"Repository {node.Attribute("Name").Value} has invalid CredentialInfo. {PSCredentialInfo.VaultNameAttribute} and {PSCredentialInfo.SecretNameAttribute} should both be present and non-empty"; // both keys are present @@ -777,8 +817,6 @@ public static List Read(string[] repoNames, out string[] error continue; } - RepositoryProviderType repositoryProvider= GetRepositoryProviderType(thisUrl); - bool isAllowed = GroupPolicyRepositoryEnforcement.IsGroupPolicyEnabled() ? GroupPolicyRepositoryEnforcement.IsRepositoryAllowed(thisUrl) : true; PSRepositoryInfo currentRepoItem = new PSRepositoryInfo(node.Attribute("Name").Value, @@ -786,7 +824,8 @@ public static List Read(string[] repoNames, out string[] error Int32.Parse(node.Attribute("Priority").Value), Boolean.Parse(node.Attribute("Trusted").Value), thisCredentialInfo, - (PSRepositoryInfo.APIVersion)Enum.Parse(typeof(PSRepositoryInfo.APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), + credentialProvider, + (APIVersion)Enum.Parse(typeof(APIVersion), node.Attribute("APIVersion").Value, ignoreCase: true), isAllowed); foundRepos.Add(currentRepoItem); @@ -840,54 +879,38 @@ private static XDocument LoadXDocument(string filePath) return XDocument.Load(xmlReader); } - private static PSRepositoryInfo.APIVersion GetRepoAPIVersion(Uri repoUri) + private static APIVersion GetRepoAPIVersion(Uri repoUri) { if (repoUri.AbsoluteUri.EndsWith("/v2", StringComparison.OrdinalIgnoreCase)) { // Scenario: V2 server protocol repositories (i.e PSGallery) - return PSRepositoryInfo.APIVersion.V2; + return APIVersion.V2; } else if (repoUri.AbsoluteUri.EndsWith("/index.json", StringComparison.OrdinalIgnoreCase)) { // Scenario: V3 server protocol repositories (i.e NuGet.org, Azure Artifacts (ADO), Artifactory, Github Packages, MyGet.org) - return PSRepositoryInfo.APIVersion.V3; + return APIVersion.V3; } else if (repoUri.AbsoluteUri.EndsWith("/nuget", StringComparison.OrdinalIgnoreCase)) { // Scenario: ASP.Net application feed created with NuGet.Server to host packages - return PSRepositoryInfo.APIVersion.NugetServer; + return APIVersion.NugetServer; } else if (repoUri.Scheme.Equals(Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase) || repoUri.Scheme.Equals("temp", StringComparison.OrdinalIgnoreCase)) { // repositories with Uri Scheme "temp" may have PSPath Uri's like: "Temp:\repo" and we should consider them as local repositories. - return PSRepositoryInfo.APIVersion.Local; + return APIVersion.Local; } else if (repoUri.AbsoluteUri.EndsWith(".azurecr.io") || repoUri.AbsoluteUri.EndsWith(".azurecr.io/") || repoUri.AbsoluteUri.Contains("mcr.microsoft.com")) { - return PSRepositoryInfo.APIVersion.ContainerRegistry; + return APIVersion.ContainerRegistry; } else { - return PSRepositoryInfo.APIVersion.Unknown; + return APIVersion.Unknown; } } - private static RepositoryProviderType GetRepositoryProviderType(Uri repoUri) - { - string absoluteUri = repoUri.AbsoluteUri; - // We want to use contains instead of EndsWith to accomodate for trailing '/' - if (absoluteUri.Contains("azurecr.io") || absoluteUri.Contains("mcr.microsoft.com")){ - return RepositoryProviderType.ACR; - } - // TODO: add a regex for this match - // eg: *pkgs.*/_packaging/* - else if (absoluteUri.Contains("pkgs.")){ - return RepositoryProviderType.AzureDevOps; - } - else { - return RepositoryProviderType.None; - } - } #endregion } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index f90646fa2..d3d341181 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -5,6 +5,7 @@ using System; using System.Collections; using System.Collections.Generic; +using System.Linq; using System.Management.Automation; using Dbg = System.Diagnostics.Debug; @@ -18,7 +19,7 @@ namespace Microsoft.PowerShell.PSResourceGet.Cmdlets "PSResourceRepository", DefaultParameterSetName = NameParameterSet, SupportsShouldProcess = true)] - public sealed class SetPSResourceRepository : PSCmdlet + public sealed class SetPSResourceRepository : PSCmdlet, IDynamicParameters { #region Members @@ -26,6 +27,7 @@ public sealed class SetPSResourceRepository : PSCmdlet private const string RepositoriesParameterSet = "RepositoriesParameterSet"; private const int DefaultPriority = -1; private Uri _uri; + private CredentialProviderDynamicParameters _credentialProvider; #endregion @@ -92,7 +94,7 @@ public SwitchParameter Trusted /// Specifies vault and secret names as PSCredentialInfo for the repository. /// [Parameter(ParameterSetName = NameParameterSet)] - public PSCredentialInfo CredentialInfo { get; set; } + public PSCredentialInfo CredentialInfo { get; set; } /// /// When specified, displays the successfully registered repository and its information. @@ -102,6 +104,23 @@ public SwitchParameter Trusted #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); + if (repository is not null && + (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) + { + return null; + } + + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + #endregion + #region Private methods protected override void BeginProcessing() @@ -118,10 +137,11 @@ protected override void ProcessRecord() !MyInvocation.BoundParameters.ContainsKey(nameof(Priority)) && !MyInvocation.BoundParameters.ContainsKey(nameof(Trusted)) && !MyInvocation.BoundParameters.ContainsKey(nameof(ApiVersion)) && - !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialInfo))) + !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialInfo)) && + !MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) { ThrowTerminatingError(new ErrorRecord( - new ArgumentException("Must set Uri, Priority, Trusted, ApiVersion, or CredentialInfo parameter"), + new ArgumentException("Must set Uri, Priority, Trusted, ApiVersion, CredentialInfo, or CredentialProvider parameter"), "SetRepositoryParameterBindingFailure", ErrorCategory.InvalidArgument, this)); @@ -141,6 +161,8 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; + List items = new List(); switch(ParameterSetName) @@ -151,11 +173,12 @@ protected override void ProcessRecord() items.Add(RepositorySettings.UpdateRepositoryStore(Name, _uri, Priority, - Trusted, + Trusted, isSet, DefaultPriority, repoApiVersion, CredentialInfo, + credentialProvider, this, out string errorMsg)); @@ -283,6 +306,8 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; + try { var updatedRepo = RepositorySettings.UpdateRepositoryStore(repo["Name"].ToString(), @@ -293,6 +318,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) DefaultPriority, ApiVersion, repoCredentialInfo, + credentialProvider, this, out string errorMsg); diff --git a/src/code/Utils.cs b/src/code/Utils.cs index da80d3f42..bcd964de1 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -927,7 +927,7 @@ public static bool IsSecretManagementVaultAccessible( } } - public static NetworkCredential SetNetworkCredential( + public static NetworkCredential SetSecretManagementNetworkCredential( PSRepositoryInfo repository, NetworkCredential networkCredential, PSCmdlet cmdletPassedIn) @@ -948,6 +948,30 @@ public static NetworkCredential SetNetworkCredential( return networkCredential; } + public static NetworkCredential SetCredentialProviderNetworkCredential( + PSRepositoryInfo repository, + NetworkCredential networkCredential, + PSCmdlet cmdletPassedIn) + { + // Explicitly passed in Credential takes precedence over repository credential provider. + if (networkCredential == null) + { + cmdletPassedIn.WriteVerbose("Attempting to retrieve credentials from Azure Artifacts Credential Provider."); + PSCredential repoCredential = CredentialProvider.GetCredentialsFromProvider(repository.Uri, cmdletPassedIn); + if (repoCredential == null) + { + cmdletPassedIn.WriteVerbose("Unable to retrieve credentials from Azure Artifacts Credential Provider. Network credentials are null."); + } + else + { + networkCredential = new NetworkCredential(repoCredential.UserName, repoCredential.Password); + cmdletPassedIn.WriteVerbose("Credential successfully read from Azure Artifacts Credential Provider for repository: " + repository.Name); + } + } + + return networkCredential; + } + #endregion #region Path methods @@ -1214,6 +1238,22 @@ internal static void GetMetadataFilesFromPath(string dirPath, string packageName } } } + + internal static string GetCaseInsensitiveFilePath(string directory, string fileName) + { + var files = Directory.GetFiles(directory); + foreach (var file in files) + { + if (string.Equals(Path.GetFileName(file), fileName, StringComparison.OrdinalIgnoreCase)) + { + return file; + } + } + + // File not found + return null; + } + #endregion #region PSDataFile parsing @@ -1522,6 +1562,23 @@ public static bool TryCreateModuleSpecification( return moduleSpecCreatedSuccessfully; } + public static SecureString ConvertToSecureString(string input) + { + if (input == null) { + throw new ArgumentNullException(nameof(input)); + } + + SecureString secureString = new SecureString(); + foreach (char c in input) + { + secureString.AppendChar(c); + } + + secureString.MakeReadOnly(); + + return secureString; + } + #endregion #region Directory and File diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 new file mode 100644 index 000000000..7a651be1f --- /dev/null +++ b/test/CredentialProvider.Tests.ps1 @@ -0,0 +1,74 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +Import-Module "$psscriptroot\PSGetTestUtils.psm1" -Force + +Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { + + BeforeAll{ + $TestModuleName = "PackageManagement" + $ADORepoName = "ADORepository" + $ADORepoUri = "https://pkgs.dev.azure.com/mscodehub/PowerShellCore/_packaging/PowerShellCore_PublicPackages/nuget/v2" + $LocalRepoName = "LocalRepository" + $LocalRepoUri = Join-Path -Path $TestDrive -ChildPath "testdir" + $null = New-Item $LocalRepoUri -ItemType Directory -Force + + Get-NewPSResourceRepositoryFile + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted + } + + AfterAll { + Uninstall-PSResource $TestModuleName -SkipDependencyCheck -ErrorAction SilentlyContinue + + Get-RevertPSResourceRepositoryFile + } + + It "Find resource given specific Name and Repository" { + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName + $res.Name | Should -Be $TestModuleName + } + + It "Install resource given specific Name and Repository" { + Install-PSResource -Name $TestModuleName -Repository $ADORepoName + + Get-InstalledPSResource -Name $TestModuleName | Should -Not -BeNullOrEmpty + } + + It "Register repository with local path (CredentialProvider should be set to 'None')" { + Register-PSResourceRepository -Name $LocalRepoName -Uri $LocalRepoUri -Force + $repo = Get-PSResourceRepository -Name $LocalRepoName + $repo.CredentialProvider | Should -Be "None" + } + + It "Set CredentialProvider for local path repository" { + Register-PSResourceRepository -Name $LocalRepoName -Uri $LocalRepoUri -Trusted -Force + $repo = Get-PSResourceRepository -Name $LocalRepoName + $repo.CredentialProvider | Should -Be "None" + + Set-PSResourceRepository -Name $LocalRepoName -CredentialProvider AzArtifacts + $repo2 = Get-PSResourceRepository -Name $LocalRepoName + $repo2.CredentialProvider | Should -Be "AzArtifacts" + } + + It "Register repository with ADO Uri (CredentialProvider should be set to 'AzArtifacts')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "AzArtifacts" + } + + It "Set CredentialProvider for ADO repository" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -Trusted -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "AzArtifacts" + + Set-PSResourceRepository -Name $ADORepoName -CredentialProvider None + $repo2 = Get-PSResourceRepository -Name $ADORepoName + $repo2.CredentialProvider | Should -Be "None" + } + + It "Register repository with ADO Uri (CredentialProvider should be set to 'AzArtifacts')" { + Register-PSResourceRepository -Name $ADORepoName -Uri $ADORepoUri -CredentialProvider None -Force + $repo = Get-PSResourceRepository -Name $ADORepoName + $repo.CredentialProvider | Should -Be "None" + } +} diff --git a/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 index 253dad68e..a4548a8c8 100644 --- a/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 @@ -14,7 +14,7 @@ Describe 'Test HTTP Find-PSResource for ADO V2 Server Protocol' -tags 'CI' { $ADOV2RepoName = "PSGetTestingPublicFeed" $ADOV2RepoUri = "https://pkgs.dev.azure.com/powershell/PowerShell/_packaging/psresourceget-public-test-ci/nuget/v2" Get-NewPSResourceRepositoryFile - Register-PSResourceRepository -Name $ADOV2RepoName -Uri $ADOV2RepoUri + Register-PSResourceRepository -Name $ADOV2RepoName -Uri $ADOV2RepoUri -CredentialProvider "None" } AfterAll {