Search code examples
powershellelevated-privileges

Dot-sourcing a self-elevate script


I have a self elevate snippet which is quite wordy, so I decided instead of duplicating it at the top of every script that needs to be run as admin to move it into a separate .ps1:

function Switch-ToAdmin {
    # Self-elevate the script if required
    if (-not ([Security.Principal.WindowsPrincipal] [Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole] 'Administrator')) {
        if ([int](Get-CimInstance -Class Win32_OperatingSystem | Select-Object -ExpandProperty BuildNumber) -ge 6000) {
            $Cmd = @(
                "-Command Set-Location `"$(Get-Location)`"; & `"$PSCommandPath`""
                "-$($PSBoundParameters.Keys)"
            )
            $ProcArgs = @{
                FilePath     = 'PowerShell.exe'
                Verb         = 'RunAs'
                ArgumentList = $Cmd
            }
            Start-Process @ProcArgs
            Exit
        }
    }
}

So for every script that needs elevation I'd prepend

. "$PSScriptRoot\self-elevate.ps1"
Switch-ToAdmin
# rest of script

Doing above successfully procs the UAC prompt, but the rest of the script won't get executed. Is this sorta stuff disallowed?


Solution

  • Darin and iRon have provided the crucial pointers:

    • Darin points out that the automatic $PSCommandPath variable variable in your Switch-ToAdmin function does not contain the full path of the script from which the function is called, but that of the script in which the function is defined, even if that script's definitions are loaded directly into the scope of your main script via ., the dot-sourcing operator.

      • The same applies analogously to the automatic $PSScriptRoot variable, which reflects the defining script's full directory path.
    • Also, more generally, the automatic $PSBoundParameters variable inside a function reflects that function's bound parameters, not its enclosing script's.

    • iRon points out that the Get-PSCallStack cmdlet can be used to get information about a script's callers, starting at index 1; the first object returned - index 0, when Get-PSCallStack output is captured in an array, represents the current command. Index 1 therefore refers to the immediate caller, which from the perspective of your dot-sourced script is your main script.

    Therefore:

    • Replace $PSCommandPath with $MyInvocation.PSCommandPath, via the automatic $MyInvocation variable. $MyInvocation.PSCommandPath truly reflects the caller's full script path, irrespective of where the called function was defined.

      • Alternatively, use (Get-PSCallStack)[1].ScriptName, which despite what the property name suggests, returns the full path of the calling script too.
    • Replace $PSBoundParameters with
      (Get-PSCallStack)[1].InvocationInfo.BoundParameters

      • Note that there's also (Get-PSCallStack)[1].Arguments, but it seems to contain a single string only, containing a representation of all arguments that is only semi-structured and therefore doesn't allow robust reconstruction of the individual parameters.

    As an aside:

    Even if $PSBoundParameters contained the intended information, "-$($PSBoundParameters.Keys)" would only succeed in passing the bound parameters through if your script defines only one parameter, if that parameter is a [switch] parameter, and if it is actually passed in every invocation.

    Passing arguments through robustly in this context is hard to do, and has inherent limitations - see this answer for a - complex - attempt to make it work as well as possible.