From 3dbb6c7327bdf5c45a79b72ed18175d7c30227d3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:07:08 -0800 Subject: [PATCH 01/12] Add azartifacts credential provider integration --- src/code/CredentialProvider.cs | 273 +++++++++++++++++++++++ src/code/FindHelper.cs | 35 ++- src/code/InstallHelper.cs | 64 +----- src/code/PSRepositoryInfo.cs | 40 ++-- src/code/PublishHelper.cs | 10 +- src/code/RegisterPSResourceRepository.cs | 34 ++- src/code/RepositorySettings.cs | 129 ++++++----- src/code/SetPSResourceRepository.cs | 15 +- src/code/Utils.cs | 43 +++- 9 files changed, 504 insertions(+), 139 deletions(-) create mode 100644 src/code/CredentialProvider.cs diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs new file mode 100644 index 000000000..45720380f --- /dev/null +++ b/src/code/CredentialProvider.cs @@ -0,0 +1,273 @@ +using System; +using System.Diagnostics; +using System.IO; +using System.Security; +using System.Management.Automation; +using System.Text.Json; + +namespace Microsoft.PowerShell.PSResourceGet.UtilClasses +{ + internal static class CredentialProvider + { + 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, "CredentialProvider.Microsoft.exe"); + } + else + { + credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll"); + } + } + else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) + { + credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe"); + } + + 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, "CredentialProvider.Microsoft.exe", out error); + } + else if (string.IsNullOrEmpty(credProviderPath)) + { + credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", 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) + { + 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; + } + } + + 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, + null)); + return null; + } + + // Check case sensitivity here + if (!File.Exists(credProviderPath)) + { + 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, + null)); + 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, + null)); + 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, + null)); + 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, + null)); + 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..943e967c2 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -97,6 +97,12 @@ class RegisterPSResourceRepository : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } + /// + /// Specifies which credential provider to use. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + /// /// When specified, displays the succcessfully registered repository and its information. /// @@ -127,6 +133,13 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType? repoCredentialProvider = null; + if (MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) + { + repoCredentialProvider = CredentialProvider; + } + + switch (ParameterSetName) { case NameParameterSet: @@ -140,7 +153,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, repoCredentialProvider, Force, this, out string errorMsg)); if (!string.IsNullOrEmpty(errorMsg)) { @@ -217,7 +230,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 +366,21 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + if (repo.ContainsKey("CredentialProvider") && + (repo["CredentialProvider"] == null || 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 +390,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); 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..aea53adbf 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -94,6 +94,12 @@ public SwitchParameter Trusted [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } + /// + /// Specifies which credential provider to use. + /// + [Parameter(ParameterSetName = NameParameterSet)] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + /// /// When specified, displays the successfully registered repository and its information. /// @@ -118,10 +124,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)); @@ -151,11 +158,12 @@ protected override void ProcessRecord() items.Add(RepositorySettings.UpdateRepositoryStore(Name, _uri, Priority, - Trusted, + Trusted, isSet, DefaultPriority, repoApiVersion, CredentialInfo, + CredentialProvider, this, out string errorMsg)); @@ -293,6 +301,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..5b83ad362 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 @@ -1522,6 +1546,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 From 6d5251409cdbae8ef24f11d60c11dfad63b928bd Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Tue, 17 Dec 2024 16:49:05 -0800 Subject: [PATCH 02/12] Add dynamic parameters to Register-PSResourceRepository and Set-PSResourceRepository --- src/code/RegisterPSResourceRepository.cs | 45 ++++++++++++++++-------- src/code/SetPSResourceRepository.cs | 37 +++++++++++++------ 2 files changed, 57 insertions(+), 25 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 943e967c2..d6186fdef 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 @@ -97,12 +99,6 @@ class RegisterPSResourceRepository : PSCmdlet [Parameter(ParameterSetName = NameParameterSet)] public PSCredentialInfo CredentialInfo { get; set; } - /// - /// Specifies which credential provider to use. - /// - [Parameter(ParameterSetName = NameParameterSet)] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } - /// /// When specified, displays the succcessfully registered repository and its information. /// @@ -117,6 +113,21 @@ class RegisterPSResourceRepository : PSCmdlet #endregion + #region DynamicParameters + + public object GetDynamicParameters() + { + if (Uri.Contains("pkgs.dev.azure.com") || Uri.Contains("pkgs.visualstudio.com")) + { + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + return null; + } + + #endregion + #region Methods protected override void BeginProcessing() @@ -133,12 +144,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType? repoCredentialProvider = null; - if (MyInvocation.BoundParameters.ContainsKey(nameof(CredentialProvider))) - { - repoCredentialProvider = CredentialProvider; - } - + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; switch (ParameterSetName) { @@ -153,7 +159,7 @@ protected override void ProcessRecord() try { - items.Add(RepositorySettings.AddRepository(Name, _uri, Priority, Trusted, repoApiVersion, CredentialInfo, repoCredentialProvider, 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)) { @@ -367,7 +373,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) } if (repo.ContainsKey("CredentialProvider") && - (repo["CredentialProvider"] == null || String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || + (String.IsNullOrEmpty(repo["CredentialProvider"].ToString()) || !(repo["CredentialProvider"].ToString().Equals("None", StringComparison.OrdinalIgnoreCase) || repo["CredentialProvider"].ToString().Equals("AzArtifacts", StringComparison.OrdinalIgnoreCase)))) { @@ -429,4 +435,13 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) #endregion } + + public class CredentialProviderDynamicParameters + { + /// + /// Specifies which credential provider to use. + /// + [Parameter] + public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index aea53adbf..8f35a323b 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,13 +94,7 @@ public SwitchParameter Trusted /// Specifies vault and secret names as PSCredentialInfo for the repository. /// [Parameter(ParameterSetName = NameParameterSet)] - public PSCredentialInfo CredentialInfo { get; set; } - - /// - /// Specifies which credential provider to use. - /// - [Parameter(ParameterSetName = NameParameterSet)] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + public PSCredentialInfo CredentialInfo { get; set; } /// /// When specified, displays the successfully registered repository and its information. @@ -108,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.Contains("pkgs.dev.azure.com") || repository.Uri.AbsoluteUri.Contains("pkgs.visualstudio.com"))) + { + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; + } + + return null; + } + + #endregion + #region Private methods protected override void BeginProcessing() @@ -148,6 +161,8 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + List items = new List(); switch(ParameterSetName) @@ -163,7 +178,7 @@ protected override void ProcessRecord() DefaultPriority, repoApiVersion, CredentialInfo, - CredentialProvider, + credentialProvider, this, out string errorMsg)); @@ -291,6 +306,8 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } + PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + try { var updatedRepo = RepositorySettings.UpdateRepositoryStore(repo["Name"].ToString(), @@ -301,7 +318,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) DefaultPriority, ApiVersion, repoCredentialInfo, - CredentialProvider, + credentialProvider, this, out string errorMsg); From fe270a1fc5e68ced2757bc0e82417c115f7f1c7a Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Wed, 18 Dec 2024 22:04:10 -0800 Subject: [PATCH 03/12] Bug fixes and clean up --- src/code/CredentialProvider.cs | 62 +++++++++++++++++------- src/code/RegisterPSResourceRepository.cs | 24 ++++++--- src/code/SetPSResourceRepository.cs | 14 +++--- src/code/Utils.cs | 16 ++++++ 4 files changed, 86 insertions(+), 30 deletions(-) diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs index 45720380f..3cd1ee738 100644 --- a/src/code/CredentialProvider.cs +++ b/src/code/CredentialProvider.cs @@ -4,11 +4,16 @@ 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" @@ -33,16 +38,16 @@ private static string FindCredProviderFromDefaultLocation() { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.exe"); + credProviderPath = Path.Combine(netCorePath, _credProviderExe); } else { - credProviderPath = Path.Combine(netCorePath, "CredentialProvider.Microsoft.dll"); + credProviderPath = Path.Combine(netCorePath, _credProviderDll); } } else if (Directory.Exists(netFxPath) && Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = Path.Combine(netFxPath, "CredentialProvider.Microsoft.exe"); + credProviderPath = Path.Combine(netFxPath, _credProviderExe); } return credProviderPath; @@ -62,11 +67,11 @@ private static string FindCredProviderFromVSLocation(out ErrorRecord error) { if (Environment.OSVersion.Platform == PlatformID.Win32NT) { - credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.exe", out error); + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderExe, out error); } else if (string.IsNullOrEmpty(credProviderPath)) { - credProviderPath = VSCredentialProviderFile(visualStudioPath, "CredentialProvider.Microsoft.dll", out error); + credProviderPath = VSCredentialProviderFile(visualStudioPath, _credProviderDll, out error); } } @@ -108,8 +113,9 @@ private static string VSCredentialProviderFile(string visualStudioPath, string c 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 @@ -137,25 +143,47 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet } } + 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, - null)); + credProviderPath)); return null; } - // Check case sensitivity here if (!File.Exists(credProviderPath)) { - 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, - null)); - return null; + // 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}'"); @@ -234,7 +262,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet 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, - null)); + credProviderPath)); return null; } @@ -247,7 +275,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet 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, - null)); + credProviderPath)); return null; } @@ -262,7 +290,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet 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, - null)); + credProviderPath)); return null; } diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index d6186fdef..8177d0e7a 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -117,13 +117,13 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { - if (Uri.Contains("pkgs.dev.azure.com") || Uri.Contains("pkgs.visualstudio.com")) + if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { - _credentialProvider = new CredentialProviderDynamicParameters(); - return _credentialProvider; + return null; } - return null; + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; } #endregion @@ -144,7 +144,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; switch (ParameterSetName) { @@ -438,10 +438,22 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) public class CredentialProviderDynamicParameters { + PSRepositoryInfo.CredentialProviderType? _credProvider = null; + /// /// Specifies which credential provider to use. /// [Parameter] - public PSRepositoryInfo.CredentialProviderType CredentialProvider { get; set; } + public PSRepositoryInfo.CredentialProviderType? CredentialProvider { + get + { + return _credProvider; + } + + set + { + _credProvider = value; + } + } } } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 8f35a323b..d3d341181 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -109,14 +109,14 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - if (repository is not null && - (repository.Uri.AbsoluteUri.Contains("pkgs.dev.azure.com") || repository.Uri.AbsoluteUri.Contains("pkgs.visualstudio.com"))) + if (repository is not null && + (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { - _credentialProvider = new CredentialProviderDynamicParameters(); - return _credentialProvider; + return null; } - return null; + _credentialProvider = new CredentialProviderDynamicParameters(); + return _credentialProvider; } #endregion @@ -161,7 +161,7 @@ protected override void ProcessRecord() repoApiVersion = ApiVersion; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; List items = new List(); @@ -306,7 +306,7 @@ private PSRepositoryInfo RepoValidationHelper(Hashtable repo) return null; } - PSRepositoryInfo.CredentialProviderType credentialProvider = _credentialProvider.CredentialProvider; + PSRepositoryInfo.CredentialProviderType? credentialProvider = _credentialProvider?.CredentialProvider; try { diff --git a/src/code/Utils.cs b/src/code/Utils.cs index 5b83ad362..bcd964de1 100644 --- a/src/code/Utils.cs +++ b/src/code/Utils.cs @@ -1238,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 From a99b2b293640fda8fb495b8621fd8662cb1040f3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 13:44:43 -0800 Subject: [PATCH 04/12] Add tests --- test/CredentialProvider.Tests.ps1 | 74 +++++++++++++++++++++++++++++++ 1 file changed, 74 insertions(+) create mode 100644 test/CredentialProvider.Tests.ps1 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" + } +} From fc81545a8c03b991bd5bc073c005d9d4d6aba15e Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:26:07 -0800 Subject: [PATCH 05/12] Update AdoV2 server tests --- test/FindPSResourceTests/FindPSResourceADOV2Server.Tests.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 { From 7f5f0ae92f5cef82f5775eba50ae61e6b9712f7a Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:40:54 -0800 Subject: [PATCH 06/12] Add logging for GH pkgs tests --- test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 index 0d1dc4557..7bc2e09ea 100644 --- a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 @@ -24,6 +24,8 @@ Describe 'Test HTTP Find-PSResource for Github Packages Server' -tags 'CI' { } It "find resource given specific Name, Version null" { + $repo = Get-PSResourceRepository $GithubPackagesRepoName + Write-Error "Repo: $repo; repo credential provider: $($repo.CredentialProvider)" # FindName() $res = Find-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential $res.Name | Should -Be $testModuleName From 01ab7603ef0a664aaa3af04888a232cc9b8e1ad8 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:35:22 -0800 Subject: [PATCH 07/12] Remove GH pkgs logging --- test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 index 7bc2e09ea..0d1dc4557 100644 --- a/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 +++ b/test/FindPSResourceTests/FindPSResourceGithubPackages.Tests.ps1 @@ -24,8 +24,6 @@ Describe 'Test HTTP Find-PSResource for Github Packages Server' -tags 'CI' { } It "find resource given specific Name, Version null" { - $repo = Get-PSResourceRepository $GithubPackagesRepoName - Write-Error "Repo: $repo; repo credential provider: $($repo.CredentialProvider)" # FindName() $res = Find-PSResource -Name $testModuleName -Repository $GithubPackagesRepoName -Credential $credential $res.Name | Should -Be $testModuleName From 43a69e7867c857c283602fc7252a64be6f6185d3 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Thu, 19 Dec 2024 15:58:54 -0800 Subject: [PATCH 08/12] Rename tests --- .../InstallPSResourceADOServer.Tests.ps1 | 4 ++-- .../InstallPSResourceADOV2Server.Tests.ps1 | 4 ++-- .../InstallPSResourceContainerRegistryServer.Tests.ps1 | 2 +- .../InstallPSResourceGithubPackages.Tests.ps1 | 2 +- .../InstallPSResourceV3Server.Tests.ps1 | 2 +- 5 files changed, 7 insertions(+), 7 deletions(-) diff --git a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 index fbf32c59e..834ea8690 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOServer.Tests.ps1 @@ -5,7 +5,7 @@ $ProgressPreference = "SilentlyContinue" $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { +Describe 'Test Install-PSResource for ADO V3Server scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test_local_mod" @@ -216,7 +216,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for ADO V3Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 index 589bff92b..124642f6d 100644 --- a/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceADOV2Server.Tests.ps1 @@ -5,7 +5,7 @@ $ProgressPreference = "SilentlyContinue" $modPath = "$psscriptroot/../PSGetTestUtils.psm1" Import-Module $modPath -Force -Verbose -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { +Describe 'Test Install-PSResource for ADO V2Server scenarios' -tags 'CI' { BeforeAll { $testModuleName = "test_local_mod" @@ -217,7 +217,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for ADO V2Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 index 2ade007f2..3fa1ca304 100644 --- a/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceContainerRegistryServer.Tests.ps1 @@ -261,7 +261,7 @@ Describe 'Test Install-PSResource for ACR scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for Container Registry scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 index 7c4e68d6a..62d92069a 100644 --- a/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceGithubPackages.Tests.ps1 @@ -222,7 +222,7 @@ Describe 'Test Install-PSResource for GitHub packages' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for GitHub Packages scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" diff --git a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 index 3e4f82823..2eef494b5 100644 --- a/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 +++ b/test/InstallPSResourceTests/InstallPSResourceV3Server.Tests.ps1 @@ -377,7 +377,7 @@ Describe 'Test Install-PSResource for V3Server scenarios' -tags 'CI' { } } -Describe 'Test Install-PSResource for V3Server scenarios' -tags 'ManualValidationOnly' { +Describe 'Test Install-PSResource for V3Server scenarios - Manual Validation' -tags 'ManualValidationOnly' { BeforeAll { $testModuleName = "TestModule" From bfeb5e053ed4c360f63da4d20a583c39b9381ba9 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 10:54:34 -0800 Subject: [PATCH 09/12] Add Verbose messaging --- src/code/CredentialProvider.cs | 4 ++-- test/CredentialProvider.Tests.ps1 | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/code/CredentialProvider.cs b/src/code/CredentialProvider.cs index 3cd1ee738..52c731b3c 100644 --- a/src/code/CredentialProvider.cs +++ b/src/code/CredentialProvider.cs @@ -113,7 +113,7 @@ private static string VSCredentialProviderFile(string visualStudioPath, string c internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdletPassedIn) { - cmdletPassedIn.WriteDebug("Enterting CredentialProvider::GetCredentialsFromProvider"); + cmdletPassedIn.WriteVerbose("Enterting CredentialProvider::GetCredentialsFromProvider"); string credProviderPath = string.Empty; // Find credential provider @@ -143,7 +143,7 @@ internal static PSCredential GetCredentialsFromProvider(Uri uri, PSCmdlet cmdlet } } - cmdletPassedIn.WriteDebug($"Credential provider path is '{credProviderPath}'"); + cmdletPassedIn.WriteVerbose($"Credential provider path is '{credProviderPath}'"); if (string.IsNullOrEmpty(credProviderPath)) { cmdletPassedIn.WriteError(new ErrorRecord( diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 7a651be1f..50c26646d 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -24,7 +24,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { } It "Find resource given specific Name and Repository" { - $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName + $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName } From 26441834492ee4575a470ef05c63c1cde410c2fc Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 16:49:56 -0800 Subject: [PATCH 10/12] Update dynamic parameters for Set and Register repositories --- src/code/RegisterPSResourceRepository.cs | 3 ++- src/code/SetPSResourceRepository.cs | 3 ++- .../ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 | 4 ++-- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 8177d0e7a..6a3d69194 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -117,7 +117,8 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { - if(Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + if (ParameterSetName.Equals("PSGalleryParameterSet") || Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { return null; } diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index d3d341181..39b285f15 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -109,7 +109,8 @@ public SwitchParameter Trusted public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); - if (repository is not null && + // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. + if (repository is not null && repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; diff --git a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 index 4f1279b87..fa9120dfe 100644 --- a/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 +++ b/test/ResourceRepositoryTests/SetPSResourceRepository.Tests.ps1 @@ -154,7 +154,7 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { $hashtable4 = @{Name = $PSGalleryName; Trusted = $True}; $arrayOfHashtables = $hashtable1, $hashtable2, $hashtable3, $hashtable4 - Set-PSResourceRepository -Repository $arrayOfHashtables + Set-PSResourceRepository -Repository $arrayOfHashtables -Verbose $res = Get-PSResourceRepository -Name $TestRepoName1 $res.Name | Should -Be $TestRepoName1 $Res.Uri.LocalPath | Should -Contain $tmpDir2Path @@ -189,7 +189,7 @@ Describe "Test Set-PSResourceRepository" -tags 'CI' { It "not set and throw error for trying to set PSGallery Uri (NameParameterSet)" { Unregister-PSResourceRepository -Name $PSGalleryName Register-PSResourceRepository -PSGallery - {Set-PSResourceRepository -Name $PSGalleryName -Uri $tmpDir1Path -ErrorAction Stop} | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" + {Set-PSResourceRepository -Name $PSGalleryName -Uri $tmpDir1Path -Verbose -ErrorAction Stop} | Should -Throw -ErrorId "ErrorInNameParameterSet,Microsoft.PowerShell.PSResourceGet.Cmdlets.SetPSResourceRepository" } It "not set and throw error for trying to set PSGallery CredentialInfo (NameParameterSet)" { From b11eb71c5632c20f13fd1c78b2a28c676599af67 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:20:44 -0800 Subject: [PATCH 11/12] Dynamic param should not appear when using Repositories param set, comment out cred provider tests --- global.json | 2 +- src/code/RegisterPSResourceRepository.cs | 5 ++++- test/CredentialProvider.Tests.ps1 | 3 ++- 3 files changed, 7 insertions(+), 3 deletions(-) diff --git a/global.json b/global.json index 120c43985..b832c3a01 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,5 @@ { "sdk": { - "version": "8.0.403" + "version": "8.0.404" } } diff --git a/src/code/RegisterPSResourceRepository.cs b/src/code/RegisterPSResourceRepository.cs index 6a3d69194..d1b0adf24 100644 --- a/src/code/RegisterPSResourceRepository.cs +++ b/src/code/RegisterPSResourceRepository.cs @@ -118,7 +118,10 @@ class RegisterPSResourceRepository : PSCmdlet, IDynamicParameters public object GetDynamicParameters() { // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. - if (ParameterSetName.Equals("PSGalleryParameterSet") || Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) + // It should also not appear when using the 'Repositories' parameter set. + if (ParameterSetName.Equals(PSGalleryParameterSet) || + ParameterSetName.Equals(RepositoriesParameterSet) || + Uri.EndsWith(".azurecr.io") || Uri.EndsWith(".azurecr.io/") || Uri.Contains("mcr.microsoft.com")) { return null; } diff --git a/test/CredentialProvider.Tests.ps1 b/test/CredentialProvider.Tests.ps1 index 50c26646d..4be26cfe9 100644 --- a/test/CredentialProvider.Tests.ps1 +++ b/test/CredentialProvider.Tests.ps1 @@ -22,7 +22,7 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { Get-RevertPSResourceRepositoryFile } - +<# It "Find resource given specific Name and Repository" { $res = Find-PSResource -Name $TestModuleName -Repository $ADORepoName -Verbose $res.Name | Should -Be $TestModuleName @@ -71,4 +71,5 @@ Describe 'Test Azure Artifacts Credential Provider' -tags 'CI' { $repo = Get-PSResourceRepository -Name $ADORepoName $repo.CredentialProvider | Should -Be "None" } + #> } From d3154edf56750a35697078df5cbfd4fadbab5400 Mon Sep 17 00:00:00 2001 From: alerickson <25858831+alerickson@users.noreply.github.com> Date: Mon, 23 Dec 2024 18:20:56 -0800 Subject: [PATCH 12/12] Set dynamic param updates --- src/code/SetPSResourceRepository.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/code/SetPSResourceRepository.cs b/src/code/SetPSResourceRepository.cs index 39b285f15..3582d571a 100644 --- a/src/code/SetPSResourceRepository.cs +++ b/src/code/SetPSResourceRepository.cs @@ -110,8 +110,11 @@ public object GetDynamicParameters() { PSRepositoryInfo repository = RepositorySettings.Read(new[] { Name }, out string[] _).FirstOrDefault(); // Dynamic parameter '-CredentialProvider' should not appear for PSGallery, or any container registry repository. - if (repository is not null && repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || - (repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) + // It should also not appear when using the 'Repositories' parameter set. + if (repository is not null && + (repository.Name.Equals("PSGallery", StringComparison.OrdinalIgnoreCase) || + RepositoriesParameterSet.Equals(RepositoriesParameterSet) || + repository.Uri.AbsoluteUri.EndsWith(".azurecr.io") || repository.Uri.AbsoluteUri.EndsWith(".azurecr.io/") || repository.Uri.AbsoluteUri.Contains("mcr.microsoft.com"))) { return null; }