From 99ec1cba1209ebd7a0787f9b591eb6f64af7b125 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Fri, 28 Jun 2019 16:51:52 -0700 Subject: [PATCH 1/2] Bug fixes for registering repositories with credentil provider --- src/PowerShellGet/PSGet.Resource.psd1 | 1 + .../Get-CredsFromCredentialProvider.ps1 | 151 ++++++++++++++++++ .../psgetfunctions/Register-PSRepository.ps1 | 59 ++++++- 3 files changed, 209 insertions(+), 2 deletions(-) create mode 100644 src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 diff --git a/src/PowerShellGet/PSGet.Resource.psd1 b/src/PowerShellGet/PSGet.Resource.psd1 index 8d472b70..4cc14115 100644 --- a/src/PowerShellGet/PSGet.Resource.psd1 +++ b/src/PowerShellGet/PSGet.Resource.psd1 @@ -110,6 +110,7 @@ ConvertFrom-StringData @' UseDefaultParameterSetOnRegisterPSRepository=Use 'Register-PSRepository -Default' to register the PSGallery repository. RepositoryNameContainsWildCards=The repository name '{0}' should not have wildcards, correct it and try again. InvalidRepository=The specified repository '{0}' is not a valid registered repository name. Please ensure that '{1}' is a registered repository. + RepositoryCannotBeRegistered=The specified repository '{0}' is unauthorized and cannot be registered. Try running with -Credential. RepositoryRegistered=Successfully registered the repository '{0}' with source location '{1}'. RepositoryUnregistered=Successfully unregistered the repository '{0}'. PSGalleryPublishLocationIsMissing=The specified repository '{0}' does not have a valid PublishLocation. Retry after setting the PublishLocation for repository '{1}' to a valid NuGet publishing endpoint using the Set-PSRepository cmdlet. diff --git a/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 b/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 new file mode 100644 index 00000000..4034195e --- /dev/null +++ b/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 @@ -0,0 +1,151 @@ + +function Get-CredsFromCredentialProvider { + [CmdletBinding()] + Param + ( + [Parameter()] + [ValidateNotNullOrEmpty()] + [Uri] + $SourceLocation, + + [Parameter()] + [bool] + $isRetry = $false + ) + + + Write-Verbose "PowerShellGet Calling 'CallCredProvider' on $SourceLocation" + # Example query: https://pkgs.dev.azure.com/onegettest/_packaging/onegettest/nuget/v2 + $regex = [regex] '^(\S*pkgs.dev.azure.com\S*/v2)$|^(\S*pkgs.visualstudio.com\S*/v2)$' + + if (!($SourceLocation -match $regex)) { + Write-Debug "SourceLocation did not match regex" + return $null; + } + + # 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 + # Note: OSX and Linux can only use option 1 + # Nuget prioritizes credential providers stored in the NUGET_PLUGIN_PATHS env var + $credProviderPath = $null + $defaultEnvPath = "NUGET_PLUGIN_PATHS" + $nugetPluginPath = Get-Childitem env:$defaultEnvPath -ErrorAction SilentlyContinue + $callDotnet = $true; + + if ($nugetPluginPath -and $nugetPluginPath.value) { + # Obtion 1a) The environment variable NUGET_PLUGIN_PATHS should contain a full path to the executable, + # .exe in the .NET Framework case and .dll in the .NET Core case + $credProviderPath = $nugetPluginPath.value + $extension = $credProviderPath.Substring($credProviderPath.get_Length()-4) + if ($extension -eq ".exe") { + $callDotnet = $false + } + } + else { + # Option 1b) Find User-location - The NuGet Home location - %UserProfile%/.nuget/plugins/ + $path = "$env:UserProfile/.nuget/plugins/netcore/CredentialProvider.Microsoft/CredentialProvider.Microsoft.dll"; + + if ($script:IsLinux -or $script:IsMacOS) { + $path = "$HOME/.nuget/plugins/netcore/CredentialProvider.Microsoft/CredentialProvider.Microsoft.dll"; + } + if (Test-Path $path -PathType Leaf) { + $credProviderPath = $path + } + } + + # Option 2. Use Visual Studio path to find credential provider + # Visual Studio comes pre-installed with the Azure Artifacts credential provider, so we'll search for that file using vswhere.exe + # If Windows (ie not unix), we'll use vswhere.exe to find installation path of VsWhere + # If credProviderPath is already set we can skip option 2 + if (!$credProviderPath -and $script:IsWindows) { + if (${Env:ProgramFiles(x86)}) { + $programFiles = ${Env:ProgramFiles(x86)} + } + elseif ($Env:Programfiles) { + $programFiles = $Env:Programfiles + } + else { + return $null + } + + $vswhereExePath = $programFiles + "\\Microsoft Visual Studio\\Installer\\vswhere.exe"; + if (!(Test-Path $vswhereExePath -PathType Leaf)) { + return $null + } + + $RedirectedOutput = Join-Path ([System.IO.Path]::GetTempPath()) 'RedirectedOutput.txt' + Start-Process $vswhereExePath ` + -Wait ` + -WorkingDirectory $PSHOME ` + -RedirectStandardOutput $RedirectedOutput ` + -NoNewWindow + + $content = Get-Content $RedirectedOutput + Remove-Item $RedirectedOutput -Force -Recurse -ErrorAction SilentlyContinue + + $vsInstallationPath = "" + if ([System.Text.RegularExpressions.Regex]::IsMatch($content, "installationPath")) { + $vsInstallationPath = [System.Text.RegularExpressions.Regex]::Match($content, "(?<=installationPath: ).*(?= installationVersion:)"); + Write-Debug "vsinstallpathmatch: $vsInstallationPath." + $vsInstallationPath = $vsInstallationPath.ToString() + } + + # Then use the installation path discovered by vswhere.exe to create the path to search for credential provider + # ex: "C:\Program Files (x86)\Microsoft Visual Studio\2017\Enterprise" + "\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe" + if ($vsInstallationPath) { + $credProviderPath = ($vsInstallationPath + '\Common7\IDE\CommonExtensions\Microsoft\NuGet\Plugins\CredentialProvider.Microsoft\CredentialProvider.Microsoft.exe') + if (!(Test-Path $credProviderPath -PathType Leaf)) { + return $null + } + $callDotnet = $false; + } + } + + if (!(Test-Path $credProviderPath -PathType Leaf)) { + return $null + } + + $filename = $credProviderPath + $arguments = "-U $SourceLocation" + if ($callDotnet) { + $filename = "dotnet" + $arguments = "$credProviderPath $arguments" + } + $argumentsNoRetry = $arguments + if ($isRetry) { + $arguments = "$arguments -I"; + Write-Debug "Is retry" + } + + # Using a process to run CredentialProvider.Microsoft.exe with arguments -V verbose -U query (and -IsRetry when appropriate) + # See: https://github.com/Microsoft/artifacts-credprovider + Start-Process $filename -ArgumentList "$arguments -V minimal" ` + -Wait ` + -WorkingDirectory $PSHOME ` + -NoNewWindow + + # This should never run IsRetry + $RedirectedOutput = Join-Path ([System.IO.Path]::GetTempPath()) 'RedirectedOutput.txt' + Start-Process $filename -ArgumentList "$argumentsNoRetry -V verbose" ` + -Wait ` + -WorkingDirectory $PSHOME ` + -RedirectStandardOutput $RedirectedOutput ` + -NoNewWindow + + $content = Get-Content $RedirectedOutput + Remove-Item $RedirectedOutput -Force -Recurse -ErrorAction SilentlyContinue + + Write-Debug "content is: $content" + $username = [System.Text.RegularExpressions.Regex]::Match($content, '(?<=Username: )\S*') + $password = [System.Text.RegularExpressions.Regex]::Match($content, '(?<=Password: ).*') + + if ($username -and $password) { + $secstr = ConvertTo-SecureString $password -AsPlainText -Force + $credential = new-object -typename System.Management.Automation.PSCredential -argumentlist $username, $secstr + + return $credential + } + + return $null +} diff --git a/src/PowerShellGet/public/psgetfunctions/Register-PSRepository.ps1 b/src/PowerShellGet/public/psgetfunctions/Register-PSRepository.ps1 index dce3f0c8..2b99546d 100644 --- a/src/PowerShellGet/public/psgetfunctions/Register-PSRepository.ps1 +++ b/src/PowerShellGet/public/psgetfunctions/Register-PSRepository.ps1 @@ -139,6 +139,61 @@ function Register-PSRepository { return } + $pingResult = Ping-Endpoint -Endpoint (Get-LocationString -LocationUri $SourceLocation) -Credential $Credential -Proxy $Proxy -ProxyCredential $ProxyCredential + + $retrievedCredential = $null + if (!$Credential -and $pingResult -and $pingResult.ContainsKey($Script:StatusCode) ` + -and ($pingResult[$Script:StatusCode] -eq 401)) { + + # Try pulling credentials from credential provider + $retrievedCredential = Get-CredsFromCredentialProvider -SourceLocation $SourceLocation + + # Ping and resolve the specified location + $SourceLocation = Resolve-Location -Location (Get-LocationString -LocationUri $SourceLocation) ` + -LocationParameterName 'SourceLocation' ` + -Credential $retrievedCredential ` + -Proxy $Proxy ` + -ProxyCredential $ProxyCredential ` + -CallerPSCmdlet $PSCmdlet + if (-not $SourceLocation) { + # Above Resolve-Location function throws an error when it is not able to resolve a location + return + } + + $pingResult = Ping-Endpoint -Endpoint (Get-LocationString -LocationUri $SourceLocation) -Credential $retrievedCredential -Proxy $Proxy -ProxyCredential $ProxyCredential + + if (!$retrievedCredential -or ($pingResult -and $pingResult.ContainsKey($Script:StatusCode) ` + -and ($pingResult[$Script:StatusCode] -eq 401))) { + + # Try again + $retriedRetrievedCredential = Get-CredsFromCredentialProvider -SourceLocation $SourceLocation -IsRetry $true + + # Ping and resolve the specified location + $SourceLocation = Resolve-Location -Location (Get-LocationString -LocationUri $SourceLocation) ` + -LocationParameterName 'SourceLocation' ` + -Credential $retriedRetrievedCredential ` + -Proxy $Proxy ` + -ProxyCredential $ProxyCredential ` + -CallerPSCmdlet $PSCmdlet + + if (-not $SourceLocation) { + # Above Resolve-Location function throws an error when it is not able to resolve a location + return + } + + $pingResult = Ping-Endpoint -Endpoint (Get-LocationString -LocationUri $SourceLocation) -Credential $retrievedCredential -Proxy $Proxy -ProxyCredential $ProxyCredential + + if (!$retriedRetrievedCredential -or ($pingResult -and $pingResult.ContainsKey($Script:StatusCode) ` + -and ($pingResult[$Script:StatusCode] -eq 401))) { + + $message = $LocalizedData.RepositoryCannotBeRegistered -f ($Name) + Write-Error -Message $message -ErrorId "RepositoryCannotBeRegistered" -Category InvalidOperation + + return + } + } + } + $providerName = $null if ($PackageManagementProvider) { @@ -183,9 +238,9 @@ function Register-PSRepository { # add nuget based repo as a nuget source $nugetCmd = Microsoft.PowerShell.Core\Get-Command -Name $script:NuGetExeName ` - -ErrorAction SilentlyContinue -WarningAction SilentlyContinue + -ErrorAction SilentlyContinue -WarningAction SilentlyContinue - if ($nugetCmd){ + if ($nugetCmd) { $nugetSourceExists = nuget sources list | where-object { $_.Trim() -in $SourceLocation } if (!$nugetSourceExists) { nuget sources add -name $Name -source $SourceLocation From 6f22410759534fbcd5a313bb8686852f9cb109e7 Mon Sep 17 00:00:00 2001 From: Amber Erickson Date: Sun, 30 Jun 2019 21:07:23 -0700 Subject: [PATCH 2/2] Update debug messages --- .../psgetfunctions/Get-CredsFromCredentialProvider.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 b/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 index 4034195e..4fc2b3b3 100644 --- a/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 +++ b/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 @@ -19,7 +19,6 @@ function Get-CredsFromCredentialProvider { $regex = [regex] '^(\S*pkgs.dev.azure.com\S*/v2)$|^(\S*pkgs.visualstudio.com\S*/v2)$' if (!($SourceLocation -match $regex)) { - Write-Debug "SourceLocation did not match regex" return $null; } @@ -37,7 +36,7 @@ function Get-CredsFromCredentialProvider { # Obtion 1a) The environment variable NUGET_PLUGIN_PATHS should contain a full path to the executable, # .exe in the .NET Framework case and .dll in the .NET Core case $credProviderPath = $nugetPluginPath.value - $extension = $credProviderPath.Substring($credProviderPath.get_Length()-4) + $extension = $credProviderPath.Substring($credProviderPath.get_Length() - 4) if ($extension -eq ".exe") { $callDotnet = $false } @@ -87,7 +86,6 @@ function Get-CredsFromCredentialProvider { $vsInstallationPath = "" if ([System.Text.RegularExpressions.Regex]::IsMatch($content, "installationPath")) { $vsInstallationPath = [System.Text.RegularExpressions.Regex]::Match($content, "(?<=installationPath: ).*(?= installationVersion:)"); - Write-Debug "vsinstallpathmatch: $vsInstallationPath." $vsInstallationPath = $vsInstallationPath.ToString() } @@ -115,9 +113,10 @@ function Get-CredsFromCredentialProvider { $argumentsNoRetry = $arguments if ($isRetry) { $arguments = "$arguments -I"; - Write-Debug "Is retry" + Write-Debug "Credential provider is re-running with -IsRetry" } + Write-Debug "Credential provider path is: $credProviderPath" # Using a process to run CredentialProvider.Microsoft.exe with arguments -V verbose -U query (and -IsRetry when appropriate) # See: https://github.com/Microsoft/artifacts-credprovider Start-Process $filename -ArgumentList "$arguments -V minimal" ` @@ -136,7 +135,6 @@ function Get-CredsFromCredentialProvider { $content = Get-Content $RedirectedOutput Remove-Item $RedirectedOutput -Force -Recurse -ErrorAction SilentlyContinue - Write-Debug "content is: $content" $username = [System.Text.RegularExpressions.Regex]::Match($content, '(?<=Username: )\S*') $password = [System.Text.RegularExpressions.Regex]::Match($content, '(?<=Password: ).*')