elevate.ps1

function Test-Elevated {
    <#
    .SYNOPSIS
        Tests whether the current session runs as administrator.

    .DESCRIPTION
        Returns whether the current session is elevated, so a caller can offer an unelevated path
        instead of failing. Assert-Elevated reports an error instead.

    .OUTPUTS
        Boolean indicating whether the current session runs as administrator.

    .EXAMPLE
        Test-Elevated

    .EXAMPLE
        if (-not (Test-Elevated)) { Invoke-Elevated Restart-Service -Name Spooler }
    #>

    [CmdletBinding()]
    [OutputType([bool])]
    param ()

    $identity = [Security.Principal.WindowsPrincipal] [System.Security.Principal.WindowsIdentity]::GetCurrent()
    return $identity.IsInRole([System.Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Assert-Elevated {
    <#
    .SYNOPSIS
        Requires the current session to run as administrator.

    .DESCRIPTION
        Reports a terminating error when the current session is not elevated, ending a command that has
        no unelevated path. An elevated session passes silently. Test-Elevated returns the state instead.

    .EXAMPLE
        Assert-Elevated
    #>

    [CmdletBinding()]
    param ()

    if (! (Test-Elevated)) {
        Write-Error "Operation requires administrator privileges." `
            -ErrorId "ElevationRequired" `
            -Category PermissionDenied `
            -TargetObject ([System.Security.Principal.WindowsIdentity]::GetCurrent().Name) `
            -ErrorAction Stop
    }
}

function local:Get-SudoModeValue {
    <#
    .SYNOPSIS
        Reads the sudo mode from a registry key.

    .DESCRIPTION
        Returns the Enabled value of the given machine key, or $null where key or value is missing.

    .PARAMETER Key
        Machine registry key holding the value, without the hive.

    .OUTPUTS
        Mode as integer, or $null where key or value is missing.

    .EXAMPLE
        Get-SudoModeValue -Key "SOFTWARE\Policies\Microsoft\Windows\Sudo"
    #>

    [CmdletBinding()]
    [OutputType([int])]
    param (
        [Parameter(Mandatory)]
        [string] $Key
    )

    return [Microsoft.Win32.Registry]::GetValue("HKEY_LOCAL_MACHINE\$Key", "Enabled", $null)
}

function local:Assert-SudoAvailable {
    <#
    .SYNOPSIS
        Requires the Windows sudo feature to be usable.

    .DESCRIPTION
        Reports a terminating error when Invoke-Elevated could not elevate: sudo missing from the system,
        the sudo feature switched off in Settings or by group policy, or its mode capped below the inline
        mode the elevation runs in. A command that will elevate asserts before it reads or writes anything,
        so an impossible elevation fails it up front rather than halfway through.

    .EXAMPLE
        Assert-SudoAvailable

    .EXAMPLE
        if ($Machine -and -not (Test-Elevated)) { Assert-SudoAvailable }
    #>

    [CmdletBinding()]
    param ()

    if (-not (Get-Command sudo -ErrorAction SilentlyContinue)) {
        Write-Error "Elevation denied: sudo not found." `
            -ErrorId "SudoNotAvailable" `
            -Category NotInstalled `
            -TargetObject "sudo" `
            -ErrorAction Stop
    }

    # the values sudo itself reads: Settings writes the first, group policy the second.
    # Each names a mode - 0 disabled, 1 new window, 2 input closed, 3 inline - capped at 3.
    # Sudo takes an unset toggle for disabled, an unset policy for every mode allowed,
    # and runs in the lower of the two.
    $settingValue = Get-SudoModeValue -Key "SOFTWARE\Microsoft\Windows\CurrentVersion\Sudo"
    $policyValue = Get-SudoModeValue -Key "SOFTWARE\Policies\Microsoft\Windows\Sudo"

    $setting = $null -eq $settingValue ? 0 : [Math]::Min([int] $settingValue, 3)
    $policy = $null -eq $policyValue ? 3 : [Math]::Min([int] $policyValue, 3)
    $mode = [Math]::Min($setting, $policy)

    if ($mode -le 0) {
        Write-Error "Elevation denied: sudo disabled. mode: $mode, setting: $setting, policy: $policy" `
            -ErrorId "SudoDisabled" `
            -Category NotEnabled `
            -TargetObject "sudo" `
            -ErrorAction Stop
    }

    # elevation runs sudo --inline, the mode sudo calls Normal
    if ($mode -lt 3) {
        Write-Error "Elevation denied: sudo inline mode forbidden. mode: $mode, setting: $setting, policy: $policy" `
            -ErrorId "SudoInlineNotAllowed" `
            -Category NotEnabled `
            -TargetObject "sudo" `
            -ErrorAction Stop
    }
}

function local:ConvertTo-ElevatedCommand {
    <#
    .SYNOPSIS
        Builds the command line that re-runs a command in an elevated session.

    .DESCRIPTION
        Returns the command name followed by the parameters it was called with, as a string array ready
        for Invoke-Elevated. A switch contributes its name alone and only where it is present; every
        other parameter contributes its name and its value. Common parameters are left out, the elevated
        session taking its own.

    .PARAMETER Name
        Name of the command to re-run, as the elevated session resolves it - an exported one.

    .PARAMETER BoundParameters
        The calling command's $PSBoundParameters.

    .OUTPUTS
        The command and its arguments as a string array.

    .EXAMPLE
        Invoke-Elevated (ConvertTo-ElevatedCommand -Name New-StartMenuShortcut -BoundParameters $PSBoundParameters)
    #>

    [CmdletBinding()]
    [OutputType([string[]])]
    param (
        [Parameter(Mandatory)]
        [string] $Name,
        [Parameter(Mandatory)]
        [System.Collections.IDictionary] $BoundParameters
    )

    $common = @([System.Management.Automation.PSCmdlet]::CommonParameters) +
        @([System.Management.Automation.PSCmdlet]::OptionalCommonParameters)

    $command = @($Name)

    foreach ($parameter in $BoundParameters.GetEnumerator()) {
        if ($parameter.Key -in $common) {
            continue
        }

        if ($parameter.Value -is [switch]) {
            if ($parameter.Value.IsPresent) {
                $command += "-$($parameter.Key)"
            }
            continue
        }

        $command += "-$($parameter.Key)"
        $command += [string] $parameter.Value
    }

    # comma keeps the array whole where the caller assigns a single value
    return , $command
}

function Invoke-Elevated {
    <#
    .SYNOPSIS
        Runs a command as administrator.

    .DESCRIPTION
        Runs the given command with its arguments as administrator through the Windows sudo command,
        forced into inline mode (sudo --inline) so it runs in the current terminal instead of a
        separate window, whatever mode sudo is configured for. Windows prompts for confirmation with a
        User Account Control dialog. Waits for the command to finish and reports a terminating error if
        it fails - a terminating error or a non-zero exit code, but not a non-terminating error on its own.

        Every argument is single-quoted, and an embedded single quote doubled, so it reaches the
        elevated session as one literal token whatever it holds - whitespace, a semicolon or a quote.
        The command name itself and anything written as a parameter, -Like -This, are passed through
        as typed, so the elevated session parses them as the command and its parameters.

    .PARAMETER Command
        The command to run elevated, followed by its arguments, exactly as it would be typed at the prompt.

    .EXAMPLE
        Invoke-Elevated New-Item -ItemType Directory 'C:\Program Files\MyTool'

    .EXAMPLE
        sudops Restart-Service -Name Spooler

    .NOTES
        Aliases: sudops, sups
        Requires the Windows sudo feature.
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [Parameter(Mandatory, Position = 0, ValueFromRemainingArguments = $true)]
        [string[]] $Command
    )

    # the command name and parameter names have to stay bare to parse as such; every other argument is
    # quoted literally, so a semicolon, a space or a quote in a value cannot reach the child as syntax
    $arguments = @(
        $Command `
            | Select-Object -Skip 1 `
            | ForEach-Object {
                if ($_ -match '^-\w') {
                    $_
                }
                else {
                    "'{0}'" -f ($_ -replace "'", "''")
                }
            }
    )
    $line = (@($Command[0]) + $arguments) -join ' '

    if ($PSCmdlet.ShouldProcess($line, "Run elevated")) {
        Assert-SudoAvailable
        # only a real failure sets the exit code: a terminating error - including a command the elevated
        # session cannot resolve, which would otherwise fall through to a successful exit - or a native
        # non-zero exit. A non-terminating error alone does not, and an unset $LASTEXITCODE after a
        # cmdlet means success rather than failure.
        $script = "try { $line } catch { Write-Error -ErrorRecord `$_; exit 1 }; exit (`$LASTEXITCODE ?? 0)"
        $encodedCommand = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($script))
        $powershell = (Get-Process -Id $PID).Path
        # --inline forces sudo to run in the current terminal, whatever mode the system is configured for
        sudo --inline $powershell -NoProfile -EncodedCommand $encodedCommand
        if ($LASTEXITCODE -ne 0) {
            Write-Error "Elevated command failed. exitCode: $LASTEXITCODE, command: $line" `
                -ErrorId "ElevatedCommandFailed" `
                -Category OperationStopped `
                -TargetObject $line `
                -ErrorAction Stop
        }
    }
}

New-Alias -Name sudops -Value Invoke-Elevated -ErrorAction SilentlyContinue | Out-Null
New-Alias -Name sups -Value Invoke-Elevated -ErrorAction SilentlyContinue | Out-Null