1

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?

gargoylebident
  • 373
  • 1
  • 2
  • 12
  • 2
    You might want to check what file $PSCommandPath is pointing to. I'm thinking you are elevating, and calling, "self-elevate.ps1" instead the calling script. – Darin May 01 '22 at 05:10
  • 3
    That is because the `Switch-ToAdmin` function starts a new session. In your `Switch-ToAdmin` script you need to find the caller, see [`Get-PSCallStack](https://learn.microsoft.com/powershell/module/microsoft.powershell.utility/get-pscallstack) and restart that. – iRon May 01 '22 at 07:18
  • 1
    Nice catch, @Darin - that's worthy of being written up as an answer, given that the behavior is counterintuitive. – mklement0 May 01 '22 at 13:27
  • 1
    Thanks folks. Swapping `$PSCommandPath` for `(Get-PSCallStack)[1].ScriptName` makes it successfully continue on to the rest of the script! I'm not actually sure if using a literal index (`[1]`) is the proper way of doing it but it seems to do the job.. Open to suggestions though! Slightly off-topic but `self-elevate.ps1` is actually in a different directory, and similarly swapping `Get-Location` for `Split-Path (Get-PSCallStack)[1].ScriptName` makes that work as well. – gargoylebident May 02 '22 at 06:47
  • 1
    I agree with @mklement0 the two commenters should probably expand their comment to an answer cuz that was pretty much the culprit. – gargoylebident May 02 '22 at 06:49
  • @gargoylebident, `(Get-PSCallStack)[1].ScriptName`, despite the property's name, reports a full path.`$(Get-Location)` (which you could simplify to `$PWD`) should be fine, assuming you want to preserve the _current location_. – mklement0 May 02 '22 at 19:49

1 Answers1

1

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.

mklement0
  • 382,024
  • 64
  • 607
  • 775