diff --git a/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs b/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs index 05ef4ced068..8bb408cecbf 100644 --- a/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs +++ b/src/System.Management.Automation/engine/remoting/commands/CustomShellCommands.cs @@ -188,14 +188,13 @@ function Register-PSSessionConfiguration }} }} if (([System.Management.Automation.Runspaces.PSSessionConfigurationAccessMode]::Local.Equals($accessMode) -or - ([System.Management.Automation.Runspaces.PSSessionConfigurationAccessMode]::Remote.Equals($accessMode)-and $disableNetworkExists)) -and - !$haveDisableACE) + ([System.Management.Automation.Runspaces.PSSessionConfigurationAccessMode]::Remote.Equals($accessMode))) -and !$haveDisableACE) {{ - # Add network deny ACE for local access or remote access with PSRemoting disabled ($disableNetworkExists) + # Add network deny ACE for local access or remote access with PSRemoting disabled. $sd.DiscretionaryAcl.AddAccess(""deny"", $networkSID, 268435456, ""None"", ""None"") $newSDDL = $sd.GetSddlForm(""all"") }} - if ([System.Management.Automation.Runspaces.PSSessionConfigurationAccessMode]::Remote.Equals($accessMode) -and -not $disableNetworkExists -and $haveDisableACE) + if ([System.Management.Automation.Runspaces.PSSessionConfigurationAccessMode]::Remote.Equals($accessMode) -and $haveDisableACE) {{ # Remove the specific ACE $sd.discretionaryacl.RemoveAccessSpecific('Deny', $securityIdentifierToPurge, 268435456, 'none', 'none') @@ -4796,16 +4795,119 @@ public sealed class EnablePSRemotingCommand : PSCmdlet //TODO: CLR4: Remove the logic for setting the MaxMemoryPerShellMB to 200 MB once IPMO->Get-Command->Get-Help memory usage issue is fixed. private const string enableRemotingSbFormat = @" -function Generate-PluginConfigFile +Set-StrictMode -Version Latest + +function New-PluginConfigFile {{ +[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact=""Medium"")] param( [Parameter()] [string] $pluginInstallPath ) $pluginConfigFile = Join-Path $pluginInstallPath ""RemotePowerShellConfig.txt"" # This always overwrites the file with a new version of it (if it already exists) - Set-Content -Path $pluginConfigFile -Value ""PSHOMEDIR=$PSHOME"" - Add-Content -Path $pluginConfigFile -Value ""CORECLRDIR=$PSHOME"" + Set-Content -Path $pluginConfigFile -Value ""PSHOMEDIR=$PSHOME"" -ErrorAction Stop + Add-Content -Path $pluginConfigFile -Value ""CORECLRDIR=$PSHOME"" -ErrorAction Stop +}} + +function Copy-PluginToEndpoint +{{ +param( + [Parameter()] [string] $endpointDir +) + $resolvedPluginInstallPath = """" + $pluginInstallPath = Join-Path ([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + ""\System32\PowerShell"") $endpointDir + if (!(Test-Path $pluginInstallPath)) + {{ + $resolvedPluginInstallPath = New-Item -Type Directory -Path $pluginInstallPath + }} + else + {{ + $resolvedPluginInstallPath = Resolve-Path $pluginInstallPath + }} + if (!(Test-Path $resolvedPluginInstallPath\{5})) + {{ + Copy-Item -Path $PSHOME\{5} -Destination $resolvedPluginInstallPath -Force -ErrorAction Stop + if (!(Test-Path $resolvedPluginInstallPath\{5})) + {{ + Write-Error ($errorMsgUnableToInstallPlugin -f ""{5}"", $resolvedPluginInstallPath) + return $null + }} + }} + return $resolvedPluginInstallPath +}} + +function Register-Endpoint +{{ +param( + [Parameter()] [string] $configurationName +) + # + # Section 1: + # Move pwrshplugin.dll from $PSHOME to the endpoint directory + # + # The plugin directory pattern for endpoint configuration is: + # '$env:WINDIR\System32\PowerShell\' + powershell_version, + # so we call Copy-PluginToEndpoint function only with the PowerShell version argument. + + $pwshVersion = $configurationName.Replace(""PowerShell."", """") + $resolvedPluginInstallPath = Copy-PluginToEndpoint $pwshVersion + if (!$resolvedPluginInstallPath) {{ + return + }} + + # + # Section 2: + # Generate the Plugin Configuration File + # + New-PluginConfigFile $resolvedPluginInstallPath + + # + # Section 3: + # Register the endpoint + # + $null = Register-PSSessionConfiguration -Name $configurationName -force -ErrorAction Stop + + set-item -WarningAction SilentlyContinue wsman:\localhost\plugin\$configurationName\Quotas\MaxShellsPerUser -value ""25"" -confirm:$false + set-item -WarningAction SilentlyContinue wsman:\localhost\plugin\$configurationName\Quotas\MaxIdleTimeoutms -value {4} -confirm:$false + restart-service winrm -confirm:$false +}} + +function Register-EndpointIfNotPresent +{{ +[CmdletBinding(SupportsShouldProcess=$true, ConfirmImpact=""Medium"")] +param( + [Parameter()] [string] $Name, + [Parameter()] [bool] $Force, + [Parameter()] [string] $queryForRegisterDefault, + [Parameter()] [string] $captionForRegisterDefault +) + # + # This cmdlet will make sure default powershell end points exist upon successful completion. + # + # Windows PowerShell: + # Microsoft.PowerShell + # Microsoft.PowerShell32 (wow64) + # + # PowerShell Core: + # PowerShell. + # + $errorCount = $error.Count + $endPoint = Get-PSSessionConfiguration $Name -Force:$Force -ErrorAction silentlycontinue 2>&1 + $newErrorCount = $error.Count + + # remove the 'No Session Configuration matches criteria' errors + for ($index = 0; $index -lt ($newErrorCount - $errorCount); $index ++) + {{ + $error.RemoveAt(0) + }} + + $qMessage = $queryForRegisterDefault -f ""$Name"",""Register-PSSessionConfiguration {0} -force"" + if ((!$endpoint) -and + ($force -or $pscmdlet.ShouldProcess($qMessage, $captionForRegisterDefault))) + {{ + Register-Endpoint $Name + }} }} function Enable-PSRemoting @@ -4836,70 +4938,16 @@ function Enable-PSRemoting # first try to enable all the sessions Enable-PSSessionConfiguration @PSBoundParameters - # - # This cmdlet will make sure default powershell end points exist upon successful completion. - # - # Windows PowerShell: - # Microsoft.PowerShell - # Microsoft.PowerShell32 (wow64) - # - # PowerShell Core: - # PowerShell. - # - $errorCount = $error.Count - $endPoint = Get-PSSessionConfiguration {0} -Force:$Force -ErrorAction silentlycontinue 2>&1 - $newErrorCount = $error.Count - - # remove the 'No Session Configuration matches criteria' errors - for ($index = 0; $index -lt ($newErrorCount - $errorCount); $index ++) - {{ - $error.RemoveAt(0) - }} - - $qMessage = $queryForRegisterDefault -f ""{0}"",""Register-PSSessionConfiguration {0} -force"" - if ((!$endpoint) -and - ($force -or $pscmdlet.ShouldProcess($qMessage, $captionForRegisterDefault))) - {{ - $resolvedPluginInstallPath = """" - # - # Section 1: - # Move pwrshplugin.dll from $PSHOME to the endpoint directory - # - $pluginInstallPath = Join-Path ""$env:WINDIR\System32\PowerShell"" $psversiontable.GitCommitId - if (!(Test-Path $pluginInstallPath)) - {{ - $resolvedPluginInstallPath = New-Item -Type Directory -Path $pluginInstallPath - }} - else - {{ - $resolvedPluginInstallPath = Resolve-Path $pluginInstallPath - }} - if (!(Test-Path $resolvedPluginInstallPath\{5})) - {{ - Copy-Item $PSHOME\{5} $resolvedPluginInstallPath -Force - if (!(Test-Path $resolvedPluginInstallPath\{5})) - {{ - Write-Error ($errorMsgUnableToInstallPlugin -f ""{5}"", $resolvedPluginInstallPath) - return - }} - }} + Register-EndpointIfNotPresent -Name {0} $Force $queryForRegisterDefault $captionForRegisterDefault - # - # Section 2: - # Generate the Plugin Configuration File - # - Generate-PluginConfigFile $resolvedPluginInstallPath - - # - # Section 3: - # Register the endpoint - # - $null = Register-PSSessionConfiguration -Name {0} -force - - set-item -WarningAction SilentlyContinue wsman:\localhost\plugin\{0}\Quotas\MaxShellsPerUser -value ""25"" -confirm:$false - set-item -WarningAction SilentlyContinue wsman:\localhost\plugin\{0}\Quotas\MaxIdleTimeoutms -value {4} -confirm:$false - restart-service winrm -confirm:$false + # Create the default PSSession configuration, not tied to specific PowerShell version + # e. g. 'PowerShell.6'. + $powershellVersionMajor = $PSVersionTable.PSVersion.ToString() + $dotPos = $powershellVersionMajor.IndexOf(""."") + if ($dotPos -ne -1) {{ + $powershellVersionMajor = $powershellVersionMajor.Substring(0, $dotPos) }} + Register-EndpointIfNotPresent -Name (""PowerShell."" + $powershellVersionMajor) $Force $queryForRegisterDefault $captionForRegisterDefault # PowerShell Workflow and WOW are not supported for PowerShell Core if (![System.Management.Automation.Platform]::IsCoreCLR) diff --git a/src/powershell-native/Install-PowerShellRemoting.ps1 b/src/powershell-native/Install-PowerShellRemoting.ps1 index 30bc2f1a100..6c3d01285fd 100644 --- a/src/powershell-native/Install-PowerShellRemoting.ps1 +++ b/src/powershell-native/Install-PowerShellRemoting.ps1 @@ -21,10 +21,18 @@ param ( [parameter(Mandatory = $true, ParameterSetName = "ByPath")] + [switch]$Force, [string] $PowerShellHome ) +Set-StrictMode -Version Latest + +if (! ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) +{ + Write-Error "WinRM registration requires Administrator rights. To run this cmdlet, start PowerShell with the `"Run as administrator`" option." + return +} function Register-WinRmPlugin { param @@ -49,8 +57,6 @@ function Register-WinRmPlugin $regKey = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WSMAN\Plugin\$pluginEndpointName" - $regKeyName = '"ConfigXML"="{0}"' - $pluginArchitecture = "64" if ($env:PROCESSOR_ARCHITECTURE -match "x86" -or $env:PROCESSOR_ARCHITECTURE -eq "ARM") { @@ -80,8 +86,9 @@ function Register-WinRmPlugin New-ItemProperty -Path $regKey -Name ConfigXML -Value $valueString > $null } -function Generate-PluginConfigFile +function New-PluginConfigFile { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact="Medium")] param ( [string] @@ -97,90 +104,119 @@ function Generate-PluginConfigFile # This always overwrites the file with a new version of it if the # script is invoked multiple times. - Set-Content -Path $pluginFile -Value "PSHOMEDIR=$targetPsHomeDir" - Add-Content -Path $pluginFile -Value "CORECLRDIR=$targetPsHomeDir" + Set-Content -Path $pluginFile -Value "PSHOMEDIR=$targetPsHomeDir" -ErrorAction Stop + Add-Content -Path $pluginFile -Value "CORECLRDIR=$targetPsHomeDir" -ErrorAction Stop Write-Verbose "Created Plugin Config File: $pluginFile" -Verbose } -###################### -# # -# Install the plugin # -# # -###################### +function Install-PluginEndpoint { + [CmdletBinding(SupportsShouldProcess, ConfirmImpact="Medium")] + param ( + [Parameter()] [bool] $Force, + [switch] + $VersionIndependent + ) -if (! ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] "Administrator")) -{ - Write-Error "WinRM registration requires Administrator rights. To run this cmdlet, start PowerShell with the `"Run as administrator`" option." - Break -} + ###################### + # # + # Install the plugin # + # # + ###################### -if ($PsCmdlet.ParameterSetName -eq "ByPath") -{ - $targetPsHome = $PowerShellHome - $targetPsVersion = & "$targetPsHome\pwsh" -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' -} -else -{ - ## Get the PSHome and PSVersion using the current powershell instance - $targetPsHome = $PSHOME - $targetPsVersion = $PSVersionTable.PSVersion.ToString() -} + if ($PsCmdlet.ParameterSetName -eq "ByPath") + { + $targetPsHome = $PowerShellHome + $targetPsVersion = & "$targetPsHome\pwsh" -NoProfile -Command '$PSVersionTable.PSVersion.ToString()' + } + else + { + ## Get the PSHome and PSVersion using the current powershell instance + $targetPsHome = $PSHOME + $targetPsVersion = $PSVersionTable.PSVersion.ToString() + } -Write-Verbose "Using PowerShell Version: $targetPsVersion" -Verbose + # For default, not tied to the specific version endpoint, we apply + # only first number in the PSVersion string to the endpoint name. + # Example name: 'PowerShell.6'. + if ($VersionIndependent) { + $dotPos = $targetPsVersion.IndexOf(".") + if ($dotPos -ne -1) { + $targetPsVersion = $targetPsVersion.Substring(0, $dotPos) + } + } -$pluginBasePath = Join-Path "$env:WINDIR\System32\PowerShell" $targetPsVersion + Write-Verbose "Using PowerShell Version: $targetPsVersion" -Verbose -$resolvedPluginAbsolutePath = "" -if (! (Test-Path $pluginBasePath)) -{ - Write-Verbose "Creating $pluginBasePath" - $resolvedPluginAbsolutePath = New-Item -Type Directory -Path $pluginBasePath -} -else -{ - $resolvedPluginAbsolutePath = Resolve-Path $pluginBasePath -} + $pluginEndpointName = "PowerShell.$targetPsVersion" -$pluginPath = Join-Path $resolvedPluginAbsolutePath "pwrshplugin.dll" + $endPoint = Get-PSSessionConfiguration $pluginEndpointName -Force:$Force -ErrorAction silentlycontinue 2>&1 -# This is forced to ensure the the file is placed correctly -Copy-Item $targetPsHome\pwrshplugin.dll $resolvedPluginAbsolutePath -Force -Verbose + # If endpoint exists and -Force parameter was not used, the endpoint would not be overwritten. + if ($endpoint -and !$Force) + { + Write-Error -Category ResourceExists -ErrorId "PSSessionConfigurationExists" -Message "Endpoint $pluginEndpointName already exists." + return + } -$pluginFile = Join-Path $resolvedPluginAbsolutePath "RemotePowerShellConfig.txt" -Generate-PluginConfigFile $pluginFile (Resolve-Path $targetPsHome) + if (!$PSCmdlet.ShouldProcess($pluginEndpointName)) { + return + } -$pluginEndpointName = "powershell.$targetPsVersion" + $pluginBasePath = Join-Path ([System.Environment]::GetFolderPath([System.Environment+SpecialFolder]::Windows) + "\System32\PowerShell") $targetPsVersion -# Register the plugin -Register-WinRmPlugin $pluginPath $pluginEndpointName + $resolvedPluginAbsolutePath = "" + if (! (Test-Path $pluginBasePath)) + { + Write-Verbose "Creating $pluginBasePath" + $resolvedPluginAbsolutePath = New-Item -Type Directory -Path $pluginBasePath + } + else + { + $resolvedPluginAbsolutePath = Resolve-Path $pluginBasePath + } -#################################################################### -# # -# Validations to confirm that everything was registered correctly. # -# # -#################################################################### + $pluginPath = Join-Path $resolvedPluginAbsolutePath "pwrshplugin.dll" -if (! (Test-Path $pluginFile)) -{ - throw "WinRM Plugin configuration file not created. Expected = $pluginFile" -} + # This is forced to ensure the the file is placed correctly + Copy-Item $targetPsHome\pwrshplugin.dll $resolvedPluginAbsolutePath -Force -Verbose -ErrorAction Stop -if (! (Test-Path $resolvedPluginAbsolutePath\pwrshplugin.dll)) -{ - throw "WinRM Plugin DLL missing. Expected = $resolvedPluginAbsolutePath\pwrshplugin.dll" -} + $pluginFile = Join-Path $resolvedPluginAbsolutePath "RemotePowerShellConfig.txt" + New-PluginConfigFile $pluginFile (Resolve-Path $targetPsHome) -try -{ - Write-Host "`nGet-PSSessionConfiguration $pluginEndpointName" -foregroundcolor "green" - Get-PSSessionConfiguration $pluginEndpointName -ErrorAction Stop -} -catch [Microsoft.PowerShell.Commands.WriteErrorException] -{ - throw "No remoting session configuration matches the name $pluginEndpointName." + # Register the plugin + Register-WinRmPlugin $pluginPath $pluginEndpointName + + #################################################################### + # # + # Validations to confirm that everything was registered correctly. # + # # + #################################################################### + + if (! (Test-Path $pluginFile)) + { + throw "WinRM Plugin configuration file not created. Expected = $pluginFile" + } + + if (! (Test-Path $resolvedPluginAbsolutePath\pwrshplugin.dll)) + { + throw "WinRM Plugin DLL missing. Expected = $resolvedPluginAbsolutePath\pwrshplugin.dll" + } + + try + { + Write-Host "`nGet-PSSessionConfiguration $pluginEndpointName" -foregroundcolor "green" + Get-PSSessionConfiguration $pluginEndpointName -ErrorAction Stop + } + catch [Microsoft.PowerShell.Commands.WriteErrorException] + { + throw "No remoting session configuration matches the name $pluginEndpointName." + } } +Install-PluginEndpoint -Force $Force +Install-PluginEndpoint -Force $Force -VersionIndependent + Write-Host "Restarting WinRM to ensure that the plugin configuration change takes effect.`nThis is required for WinRM running on Windows SKUs prior to Windows 10." -foregroundcolor Magenta Restart-Service winrm diff --git a/test/powershell/Modules/Microsoft.PowerShell.Core/PSSessionConfiguration.Tests.ps1 b/test/powershell/Modules/Microsoft.PowerShell.Core/PSSessionConfiguration.Tests.ps1 index 8b87396e82f..3a2b42f366a 100644 --- a/test/powershell/Modules/Microsoft.PowerShell.Core/PSSessionConfiguration.Tests.ps1 +++ b/test/powershell/Modules/Microsoft.PowerShell.Core/PSSessionConfiguration.Tests.ps1 @@ -129,7 +129,7 @@ try $Result = Get-PSSessionConfiguration $Result.Name -contains $endpointName | Should -BeTrue - $Result.PSVersion | Should -BeExactly $expectedPSVersion + $Result.PSVersion -contains $expectedPSVersion | Should -BeTrue } It "Get-PSSessionConfiguration with Name parameter" { @@ -147,7 +147,7 @@ try $Result = Get-PSSessionConfiguration -Name $endpointWildcard $Result.Name -contains $endpointName | Should -BeTrue - $Result.PSVersion | Should -BeExactly $expectedPSVersion + $Result.PSVersion -contains $expectedPSVersion | Should -BeTrue } It "Get-PSSessionConfiguration -Name with Non-Existent session configuration" { @@ -838,6 +838,27 @@ namespace PowershellTestConfigNamespace $result | Should -BeTrue } } + + Describe "Validate Enable-PSSession Cmdlet" -Tags @("Feature", 'RequireAdminOnWindows') { + BeforeAll { + if ($IsNotSkipped) { + Enable-PSRemoting + } + } + + It "Enable-PSSession Cmdlet creates a PSSession configuration with a name tied to PowerShell version." { + $endpointName = "PowerShell." + $PSVersionTable.GitCommitId + $matchedEndpoint = Get-PSSessionConfiguration $endpointName -ErrorAction SilentlyContinue + $matchedEndpoint | Should -Not -BeNullOrEmpty + } + + It "Enable-PSSession Cmdlet creates a default PSSession configuration untied to a specific PowerShell version." { + $dotPos = $PSVersionTable.PSVersion.ToString().IndexOf(".") + $endpointName = "PowerShell." + $PSVersionTable.PSVersion.ToString().Substring(0, $dotPos) + $matchedEndpoint = Get-PSSessionConfiguration $endpointName -ErrorAction SilentlyContinue + $matchedEndpoint | Should -Not -BeNullOrEmpty + } + } } finally {