Skip to content
This repository was archived by the owner on Feb 19, 2019. It is now read-only.

Improve Chocolatey setup as administrator and add Test-ProcessAdminRights helper #486

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 4 additions & 8 deletions nuget/tools/chocolateysetup.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -89,10 +89,8 @@ param(
[string]$folder
)
$environmentTarget = [System.EnvironmentVariableTarget]::User
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UACEnabled = Get-UACEnabled
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -and !$UACEnabled) {
Write-Debug "Administrator installing with UAC disabled so using Machine environment variable target instead of User."
if (Test-ProcessAdminRights) {
Write-Debug "Administrator installing so using Machine environment variable target instead of User."
$environmentTarget = [System.EnvironmentVariableTarget]::Machine
}
Write-Host "Creating $chocInstallVariableName as an Environment variable (targeting `'$environmentTarget`') and setting it to `'$folder`'"
Expand Down Expand Up @@ -156,10 +154,8 @@ param(
)

$environmentTarget = [System.EnvironmentVariableTarget]::User
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UACEnabled = Get-UACEnabled
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -and !$UACEnabled) {
Write-Debug "Administrator installing with UAC disabled so using Machine environment variable target instead of User."
if (Test-ProcessAdminRights) {
Write-Debug "Administrator installing so using Machine environment variable target instead of User."
$environmentTarget = [System.EnvironmentVariableTarget]::Machine
}

Expand Down
2 changes: 1 addition & 1 deletion src/functions/Chocolatey-Python.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ param(
Chocolatey-InstallIfMissing 'python'

if ($($env:Path).ToLower().Contains("python") -eq $false) {
$env:Path = [Environment]::GetEnvironmentVariable('Path',[System.EnvironmentVariableTarget]::Machine);
$env:Path = Get-EnvironmentVariable -Name 'Path' -Scope Machine
}

Chocolatey-InstallIfMissing 'easy.install'
Expand Down
2 changes: 1 addition & 1 deletion src/functions/Chocolatey-RubyGem.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ param(
Chocolatey-InstallIfMissing 'ruby'

if ($($env:Path).ToLower().Contains("ruby") -eq $false) {
$env:Path = [Environment]::GetEnvironmentVariable('Path',[System.EnvironmentVariableTarget]::Machine);
$env:Path = Get-EnvironmentVariable -Name 'Path' -Scope Machine
}


Expand Down
6 changes: 5 additions & 1 deletion src/helpers/chocolateyInstaller.psm1
Original file line number Diff line number Diff line change
Expand Up @@ -31,5 +31,9 @@ Export-ModuleMember -Function `
Write-ChocolateyFailure,`
Write-Host,`Write-Debug,`Write-Error,`
Start-ChocolateyProcessAsAdmin,`
Test-ProcessAdminRights,`
Uninstall-ChocolateyPackage,`
Update-SessionEnvironment
Update-SessionEnvironment,`
Get-EnvironmentVariableNames,`
Get-EnvironmentVariable,`
Set-EnvironmentVariable
2 changes: 1 addition & 1 deletion src/helpers/functions/Get-BinRoot.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ function Get-BinRoot {

# Now that we figured out the binRoot, let's store it as per proposal #3 line #7
if (-not($env:ChocolateyBinRoot -eq $binRoot)) {
[Environment]::SetEnvironmentVariable("ChocolateyBinRoot", "$binRoot", "User")
Set-EnvironmentVariable -Name "ChocolateyBinRoot" -Value $binRoot -Scope User
# Note that user variables pose a problem when there are two admins on one computer. But this is what was decided upon.
}

Expand Down
3 changes: 3 additions & 0 deletions src/helpers/functions/Get-EnvironmentVariable.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function Get-EnvironmentVariable([string] $Name, [System.EnvironmentVariableTarget] $Scope) {
[Environment]::GetEnvironmentVariable($Name, $Scope)
}
8 changes: 8 additions & 0 deletions src/helpers/functions/Get-EnvironmentVariableNames.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
function Get-EnvironmentVariableNames([System.EnvironmentVariableTarget] $Scope) {
switch ($Scope) {
'User' { Get-Item 'HKCU:\Environment' | Select-Object -ExpandProperty Property }
'Machine' { Get-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment' | Select-Object -ExpandProperty Property }
'Process' { Get-ChildItem Env:\ | Select-Object -ExpandProperty Key }
default { throw "Unsupported environment scope: $Scope" }
}
}
10 changes: 4 additions & 6 deletions src/helpers/functions/Install-ChocolateyEnvironmentVariable.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -41,16 +41,14 @@ param(
Write-Debug "Running 'Install-ChocolateyEnvironmentVariable' with variableName:`'$variableName`' and variableValue:`'$variableValue`'";

if ($variableType -eq [System.EnvironmentVariableTarget]::Machine) {
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UACEnabled = Get-UACEnabled
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -and !$UACEnabled) {
[Environment]::SetEnvironmentVariable($variableName, $variableValue, $variableType)
if (Test-ProcessAdminRights) {
Set-EnvironmentVariable -Name $variableName -Value $variableValue -Scope $variableType
} else {
$psArgs = "[Environment]::SetEnvironmentVariable(`'$variableName`',`'$variableValue`', `'$variableType`')"
$psArgs = "Install-ChocolateyEnvironmentVariable -variableName `'$variableName`' -variableValue `'$variableValue`' -variableType `'$variableType`'"
Start-ChocolateyProcessAsAdmin "$psArgs"
}
} else {
[Environment]::SetEnvironmentVariable($variableName, $variableValue, $variableType)
Set-EnvironmentVariable -Name $variableName -Value $variableValue -Scope $variableType
}

Set-Content env:\$variableName $variableValue
Expand Down
14 changes: 6 additions & 8 deletions src/helpers/functions/Install-ChocolateyPath.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@ param(
[System.EnvironmentVariableTarget] $pathType = [System.EnvironmentVariableTarget]::User
)
Write-Debug "Running 'Install-ChocolateyPath' with pathToInstall:`'$pathToInstall`'";
$originalPathToInstall = $pathToInstall

#get the PATH variable
$envPath = $env:PATH
#$envPath = [Environment]::GetEnvironmentVariable('Path', $pathType)
if (!$envPath.ToLower().Contains($pathToInstall.ToLower()))
{
Write-Host "PATH environment variable does not have $pathToInstall in it. Adding..."
$actualPath = [Environment]::GetEnvironmentVariable('Path', $pathType)
$actualPath = Get-EnvironmentVariable -Name 'Path' -Scope $pathType

$statementTerminator = ";"
#does the path end in ';'?
Expand All @@ -22,16 +22,14 @@ param(
$actualPath = $actualPath + $pathToInstall

if ($pathType -eq [System.EnvironmentVariableTarget]::Machine) {
$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent())
$UACEnabled = Get-UACEnabled
if ($currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator) -and !$UACEnabled) {
[Environment]::SetEnvironmentVariable('Path', $actualPath, $pathType)
if (Test-ProcessAdminRights) {
Set-EnvironmentVariable -Name 'Path' -Value $actualPath -Scope $pathType
} else {
$psArgs = "[Environment]::SetEnvironmentVariable('Path',`'$actualPath`', `'$pathType`')"
$psArgs = "Install-ChocolateyPath -pathToInstall `'$originalPathToInstall`' -pathType `'$pathType`'"
Start-ChocolateyProcessAsAdmin "$psArgs"
}
} else {
[Environment]::SetEnvironmentVariable('Path', $actualPath, $pathType)
Set-EnvironmentVariable -Name 'Path' -Value $actualPath -Scope $pathType
}

#add it to the local path as well so users will be off and running
Expand Down
3 changes: 3 additions & 0 deletions src/helpers/functions/Set-EnvironmentVariable.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
function Set-EnvironmentVariable([string] $Name, [string] $Value, [System.EnvironmentVariableTarget] $Scope) {
[Environment]::SetEnvironmentVariable($Name, $Value, $Scope)
}
25 changes: 25 additions & 0 deletions src/helpers/functions/Test-ProcessAdminRights.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
function Test-ProcessAdminRights {
<#
.SYNOPSIS
Tests whether the current process is running with administrative rights.

.DESCRIPTION
This function checks whether the current process has administrative rights
by checking if the current user identity is a member of the Administrators group.
It returns $true if the current process is running with administrative rights,
$false otherwise.

On Windows Vista and later, with UAC enabled, the returned value represents the
actual rights available to the process, i.e. if it returns $true, the process is
running elevated.

.OUTPUTS
System.Boolean

#>

$currentPrincipal = New-Object Security.Principal.WindowsPrincipal([Security.Principal.WindowsIdentity]::GetCurrent([Security.Principal.TokenAccessLevels]'Query,Duplicate'))
$isAdmin = $currentPrincipal.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
Write-Debug "Test-ProcessAdminRights: returning $isAdmin"
return $isAdmin
}
18 changes: 5 additions & 13 deletions src/helpers/functions/Update-SessionEnvironment.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -18,29 +18,21 @@ powershell session with all environment settings possibly performed by
chocolatey package installs.

#>
$user = 'HKCU:\Environment'
$machine ='HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
#ordering is important here, $user comes after so we can override $machine
$machine, $user |
Get-Item |
'Machine', 'User' |
% {
$regPath = $_.PSPath
$_ |
Select -ExpandProperty Property |
$scope = $_
Get-EnvironmentVariableNames -Scope $scope |
% {
Set-Item "Env:$($_)" -Value (Get-ItemProperty $regPath -Name $_).$_
Set-Item "Env:$($_)" -Value (Get-EnvironmentVariable -Scope $scope -Name $_)
}
}

#Path gets special treatment b/c it munges the two together
$paths = 'Machine', 'User' |
% {
(Get-EnvironmentVar 'PATH' $_) -split ';'
(Get-EnvironmentVariable -Name 'PATH' -Scope $_) -split ';'
} |
Select -Unique
$Env:PATH = $paths -join ';'
}

function Get-EnvironmentVar($key, $scope) {
[Environment]::GetEnvironmentVariable($key, $scope)
}
2 changes: 2 additions & 0 deletions tests/_Common.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@ $src = Join-Path (Split-Path $here) 'src'
$script = Join-Path $src 'chocolatey.ps1'
$setup = Join-Path $here '_Setup.ps1'

. (Join-Path $here '_TestHelpers.ps1')

Get-Module ChocolateyInstaller -All | Remove-Module
Get-Module chocolatey | Remove-Module
Import-Module $script
Expand Down
182 changes: 182 additions & 0 deletions tests/_TestHelpers.ps1
Original file line number Diff line number Diff line change
@@ -0,0 +1,182 @@
$here = Split-Path -Parent $MyInvocation.MyCommand.Definition

Get-ChildItem "$here\mocks" -Filter *.ps1 -Recurse | ForEach-Object { Write-Debug "Importing $($_.FullName)"; . $_.FullName }

function Get-EnvironmentSnapshot()
{
Write-Debug 'Obtaining snapshot of the environment'
$machineEnv = @{}
$key = Get-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
$key.GetValueNames() | ForEach-Object { $machineEnv[$_] = $key.GetValue($_) }

$userEnv = @{}
$key = Get-Item 'HKCU:\Environment'
$key.GetValueNames() | ForEach-Object { $userEnv[$_] = $key.GetValue($_) }

$processEnv = @{}
Get-ChildItem Env:\ | ForEach-Object { $processEnv[$_.Key] = $_.Value }

return New-Object PSCustomObject -Property @{ machine = $machineEnv; user = $userEnv; process = $processEnv }
}

function Restore-Environment($state)
{
Write-Debug 'Restoring the environment'
$state.machine.GetEnumerator() | ForEach-Object {
$current = [Environment]::GetEnvironmentVariable($_.Key, 'Machine')
if ($current -ne $_.Value) {
Write-Warning "Restoring value of environment variable $($_.Key) at Machine scope. The need to do that means that some code did not use the environment manipulation functions *-EnvironmentVariable*."
[Environment]::SetEnvironmentVariable($_.Key, $_.Value, 'Machine')
}
}

$key = Get-Item 'HKLM:\SYSTEM\CurrentControlSet\Control\Session Manager\Environment'
$key.GetValueNames() | Where-Object { -not $state.machine.ContainsKey($_) } | ForEach-Object {
Write-Warning "Deleting environment variable $_ at Machine scope. The need to do that means that some code did not use the environment manipulation functions *-EnvironmentVariable*."
[Environment]::SetEnvironmentVariable($_, $null, 'Machine')
}

$state.user.GetEnumerator() | ForEach-Object {
$current = [Environment]::GetEnvironmentVariable($_.Key, 'User')
if ($current -ne $_.Value) {
Write-Warning "Restoring value of environment variable $($_.Key) at User scope. The need to do that means that some code did not use the environment manipulation functions *-EnvironmentVariable*."
[Environment]::SetEnvironmentVariable($_.Key, $_.Value, 'User')
}
}

$key = Get-Item 'HKCU:\Environment'
$key.GetValueNames() | Where-Object { -not $state.user.ContainsKey($_) } | ForEach-Object {
Write-Warning "Deleting environment variable $_ at User scope. The need to do that means that some code did not use the environment manipulation functions *-EnvironmentVariable*."
[Environment]::SetEnvironmentVariable($_, $null, 'User')
}

$state.process.GetEnumerator() | ForEach-Object {
$current = [Environment]::GetEnvironmentVariable($_.Key, 'Process')
if ($current -ne $_.Value) {
Write-Debug "Restoring value of environment variable $($_.Key) at Process scope"
[Environment]::SetEnvironmentVariable($_.Key, $_.Value, 'Process')
}
}

Get-ChildItem Env:\ | Select-Object -ExpandProperty Name | Where-Object { -not $state.process.ContainsKey($_) } | ForEach-Object {
Write-Debug "Deleting environment variable $_ at Process scope"
[Environment]::SetEnvironmentVariable($_, $null, 'Process')
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Tests that are actually making changes to the environment scare me a bit. A glitch where it doesn't get to clean up is going to leave the system in an unusable state.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we could find a way to mock out the actual calls to the system and test the behavior of chocolatey based on what is returned from those calls, I think we can call these unit tests.

Actual hits to the system are considered physical integration tests and should probably be moved somewhere else.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I implemented a very thin abstraction layer, wrapping [Environment]::Get/SetEnvironmentVariable and obtaining a list of environment variable names (needed by one helper). In test code I can now replace that layer with mocks that read and write in-memory state.
I've left the existing backup/restore mechanism in place, first to protect against bugs and code not going through my layer (no such code now, but someone might forget and write a direct call to [Environment] in the future), second to restore process-level environment between test cases.
The restore mechanism will now warn if it detects a change at user or machine level, because that means some code did not use the layer and should be fixed.


function Setup-EnvironmentMockup
{
$global:ChocolateyTestEnvironmentVariables = Get-EnvironmentSnapshot
}

function Cleanup-EnvironmentMockup
{
$global:ChocolateyTestEnvironmentVariables = $null
}

function Execute-WithEnvironmentProtection($scriptBlock)
{
$savedEnvironment = Get-EnvironmentSnapshot
try
{
Setup-EnvironmentMockup
try
{
& $scriptBlock
}
finally
{
Cleanup-EnvironmentMockup
}
}
finally
{
Restore-Environment $savedEnvironment
}
}

function Add-EnvironmentVariable($name, $value, $targetScope)
{
Write-Debug "Setting $name to $value at $targetScope scope"
Set-EnvironmentVariable -Name $name -Value $Value -Scope $targetScope
if ($targetScope -eq 'Process') {
Write-Debug "Current $name value is '$value' (from Process scope)"
return
}
# find lowest scope with $name set and use that value as current
foreach ($currentScope in @('User', 'Machine')) {
$valueAtCurrentScope = Get-EnvironmentVariable -Name $name -Scope $currentScope
if ($valueAtCurrentScope -ne $null) {
Write-Debug "Current $name value is '$valueAtCurrentScope' (from $currentScope scope)"
Set-EnvironmentVariable -Name $name -Value $valueAtCurrentScope -Scope Process
break
}
}
}

function Remove-EnvironmentVariable($name)
{
Write-Debug "Ensuring environment variable $name is not set at any scope"
'Machine','User','Process' | ForEach-Object {
if (-not ([String]::IsNullOrEmpty((Get-EnvironmentVariable -Name $name -Scope $_)))) {
Write-Debug "Deleting environment variable $name at $_ scope"
Set-EnvironmentVariable -Name $name -Value $null -Scope $_
}
}
}

function Add-DirectoryToPath($directory, $scope)
{
$curPath = Get-EnvironmentVariable -Name 'PATH' -Scope $scope
$newPath = ($curPath -split ';' | Where-Object { $_.TrimEnd('\') -ne $directory.TrimEnd('\') }) -join ';'
if ($newPath -ne $curPath) {
Write-Debug "Directory $directory is already on PATH at $scope scope"
} else {
Write-Debug "Adding directory $directory to PATH at $scope scope"
if ([String]::IsNullOrEmpty($newPath)) {
Set-EnvironmentVariable -Name 'PATH' -Value $directory -Scope $scope
} else {
Set-EnvironmentVariable -Name 'PATH' -Value "$($newPath.TrimEnd(';'));$directory" -Scope $scope
}
}
if ($scope -ne 'Process') {
$curPath = Get-EnvironmentVariable -Name 'PATH' -Scope Process
$newPath = ($curPath -split ';' | Where-Object { $_.TrimEnd('\') -ne $directory.TrimEnd('\') }) -join ';'
if ($newPath -eq $curPath) {
Write-Debug "Adding directory $directory to PATH at Process scope"
if ([String]::IsNullOrEmpty($newPath)) {
Set-EnvironmentVariable -Name 'PATH' -Value $directory -Scope Process
} else {
Set-EnvironmentVariable -Name 'PATH' -Value "$($newPath.TrimEnd(';'));$directory" -Scope Process
}
}
}
}

function Remove-DirectoryFromPath($directory)
{
Write-Debug "Ensuring directory $directory is not on PATH at any scope"
'Machine','User','Process' | ForEach-Object {
$scope = $_
$curPath = Get-EnvironmentVariable -Name 'PATH' -Scope $scope
$newPath = ($curPath -split ';' | Where-Object { $_.TrimEnd('\') -ne $directory.TrimEnd('\') }) -join ';'
if ($newPath -ne $curPath) {
Write-Debug "Removing directory $directory from PATH at $scope scope"
Set-EnvironmentVariable -Name 'PATH' -Value $newPath -Scope $scope
}
}
}

function Assert-OnPath($directory, $pathScope)
{
$path = Get-EnvironmentVariable -Name 'PATH' -Scope $pathScope
$dirInPath = $path -split ';' | Where-Object { $_ -eq $directory }
"$dirInPath" | Should not BeNullOrEmpty
}

function Assert-NotOnPath($directory, $pathScope)
{
$path = Get-EnvironmentVariable -Name 'PATH' -Scope $pathScope
$dirInPath = $path -split ';' | Where-Object { $_ -eq $directory }
"$dirInPath" | Should BeNullOrEmpty
}
Loading