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..4fc2b3b3 --- /dev/null +++ b/src/PowerShellGet/public/psgetfunctions/Get-CredsFromCredentialProvider.ps1 @@ -0,0 +1,149 @@ + +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)) { + 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:)"); + $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 "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" ` + -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 + + $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