-
-
Notifications
You must be signed in to change notification settings - Fork 129
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Feature: Improve PowerShell elevation syntax with a wrapper function script #39
Comments
There isn't really a good reason to write a native (i.e. C# compiled) wrapper cmdlet as far as I can see? If you want to marshal parameters using powershell's binder to gsudo's, a wrapper function would do the trick. Or perhaps you're not aware that cmdlet infers compiled code, and function infers script? |
Oh, I didn't mean a native c# cmdlet, but a function script. |
I'd be happy to contribute, but that said, I haven't played with your magical powershell elevation. I suspect it secretly sends the work to an out of process instance of powershell. Am I right? Trying to pretend that a scriptblock is in the same lexical scope as the caller might lead to all kinds of weird situations as the variables wouldn't exist in that other runspace's sessionstate. Or are you doing something sneakier? |
That's right. gsudo.exe does very little to support elevating PS commands. The trick is that it is the user's responsibility to generate a string with a valid PS command (hence the quoting-hell) and then, when gsudo detects it's being called from PS, it elevates the same PowerShell.exe with This can be seen with the --debug parameter:
... executes
In that model, the user should be aware that there is no object marshalling, only strings in and out. In the example you can capture the result because So, what's next? The point of this issue is that I am open to suggestions and gather info on how far we can go.
It should be clear from the ground up that its not. There is a process-hop between elevated an unelevated instances so there will be serializing and deserializing of objects. What is PS-Remoting doing with both local and remote scopes? Can we mimic that? Take a look at this, it serializes a scriptblock while allowing the i.e. AFAIK this would allow this syntax: Serialization will produce huge strings, but we can use |
e.g. Elevate-Command.ps1
returns |
New version (same link), supports $local = "outside scope"
"TestString" | Elevate-Command -ScriptBlock {
$local = "inner scope"
Write-Host "Am I elevated? $([bool](([System.Security.Principal.WindowsIdentity]::GetCurrent()).groups -match 'S-1-5-32-544'))"
Write-Host "Inner scope value: $local"
Write-Host "Outer scope value: $using:local"
Write-Host "Received Args: $args"
Write-Host "Pipeline Input: $Input"
} -ArgumentList 1, 2, 3
returns
I've tried to make Now I've been looking at
Any feedback at this early stage is highly welcome. |
So one thing you could do is to create a proxy command for Invoke-Expression to add an -Elevate parameter. Inventing new verbs (elevate) is frowned upon in the powershell world. You can have powershell automagically generate a wrapper function and pipe it to the clipboard like so: [System.Management.Automation.ProxyCommand]::Create((gcm invoke-expression)) | clip You end up with a function body (which you would wrap in a [CmdletBinding(HelpUri='https://go.microsoft.com/fwlink/?LinkID=2097030')]
param(
[Parameter(Mandatory=$true, Position=0, ValueFromPipeline=$true)]
[string]
${Command})
begin
{
try {
$outBuffer = $null
if ($PSBoundParameters.TryGetValue('OutBuffer', [ref]$outBuffer))
{
$PSBoundParameters['OutBuffer'] = 1
}
$wrappedCmd = $ExecutionContext.InvokeCommand.GetCommand('Microsoft.PowerShell.Utility\Invoke-Expression', [System.Management.Automation.CommandTypes]::Cmdlet)
$scriptCmd = {& $wrappedCmd @PSBoundParameters }
$steppablePipeline = $scriptCmd.GetSteppablePipeline($myInvocation.CommandOrigin)
$steppablePipeline.Begin($PSCmdlet)
} catch {
throw
}
}
process
{
try {
$steppablePipeline.Process($_)
} catch {
throw
}
}
end
{
try {
$steppablePipeline.End()
} catch {
throw
}
}
<#
.ForwardHelpTargetName Microsoft.PowerShell.Utility\Invoke-Expression
.ForwardHelpCategory Cmdlet
#> You would then add a new switch parameter to the param block, |
btw, powershell.exe and pwsh.exe can automatically deserialize psserialized objects from a native command (e.g. cmd batch, exe or other out of proc process) if the stdin stream is prefixed with a special marker sequence. Check this out: Create a batch file @echo #^< CLIXML
@echo ^<Objs Version="1.1.0.1" xmlns="http://schemas.microsoft.com/powershell/2004/04"^>^<I32^>42^</I32^>^</Objs^> Now, pipe this command to foreach % .\foo.cmd | % { $_ }
42 Magic! So if your out of proc elevation serializes the output, then we could pipe an inline elevated expression to another cmdlet/function without having to insert ugly deserialization code. |
Oh this could also help - you can get powershell to serialize AND add the magic marker itself like this: pwsh -noprofile -outputformat xml -command { gi c:\windows }
#< CLIXML
<Objs ... > Example rehydrating in another pwsh process: pwsh -noprofile -outputformat xml -command { gi c:\windows } | pwsh -noprofile -command { $input | % { $_ } }
|
The correct way is to name it in verb-noun (Invoke-Gsudo) manner and set alias to whatever (gsudo). |
For the issue around quoting rules, you can access the invoked command with function Invoke-Gsudo {
gsudo.exe ($MyInvocation.Line -replace "^$($MyInvocation.InvocationName)\s+" -replace '"','""')
}
❯ set-alias sudo Invoke-Gsudo
❯ sudo echo "abc def"
abc def
❯ _ |
The I use it in this function I stole from Lee Holmes in order to run Here is the applicable piece of code which appears in the ## $expression -eq [scriptblock]
## Convert the command into an encoded command for PowerShell
$commandBytes = [System.Text.Encoding]::Unicode.GetBytes($expression)
$encodedCommand = [Convert]::ToBase64String($commandBytes)
$commandLine += "-EncodedCommand $encodedCommand" |
I was playing around with the new build. I couldn't understand the piped
gsudo/src/gsudo.extras/Invoke-gsudo.ps1 Line 110 in e9ad64b
gsudo/src/gsudo.extras/Invoke-gsudo.ps1 Line 95 in e9ad64b
When I changed the above line to ErrorRecord : The input object cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and
its properties do not match any of the parameters that take pipeline input.
WasThrownFromThrowStatement : False
TargetSite : System.Collections.ObjectModel.Collection`1[System.Management.Automation.PSObject] Invoke(System.Collections.IEnumerable)
Message : The running command stopped because the preference variable "ErrorActionPreference" or common parameter is set to Stop: The input object
cannot be bound to any parameters for the command either because the command does not take pipeline input or the input and its properties
do not match any of the parameters that take pipeline input.
Data : {System.Management.Automation.Interpreter.InterpretedFrameInfo}
InnerException :
HelpLink :
Source : System.Management.Automation
HResult : -2146233087
StackTrace : at System.Management.Automation.Runspaces.PipelineBase.Invoke(IEnumerable input)
at Microsoft.PowerShell.Executor.ExecuteCommandHelper(Pipeline tempPipeline, Exception& exceptionThrown, ExecutionOptions options) |
Matt! You anticipated me. I will post what I had in draft and then address your comments: Hi again! There is an experimental This is a glimpse of current status: .DESCRIPTION
Serializes a scriptblock and executes it in an elevated powershell.
The ScriptBlock runs in a different process, so it can´t read/write variables from the invoking scope.
If you reference a variable in a scriptblock using the `$using:variableName` it will be replaced with it´s serialized value.
The elevated command can accept input from the pipeline with $Input. It will be serialized, so size matters.
The script result is serialized, sent back to the non-elevated instance, and returned.
Optionally you can check for "$LastExitCode -eq 999" to find out if gsudo failed to elevate (UAC popup cancelled)
.EXAMPLE
PS> Get-Process notepad | Invoke-gsudo { Stop-Process }
PS> Invoke-gsudo { return Get-Content 'C:\My Secret Folder\My Secret.txt'}
PS> $a=1; $b = Invoke-gsudo { $using:a+10 }; Write-Host "Sum returned: $b";
Sum returned: 11 Problems/Challenges
I'm not able to distinguish between Terminating (
Awaiting feedback mode: ON |
Matt, |
I would think we would let the users determine the As far as telling the difference between the errors, I was reading this and this. Maybe we can use one of these:
I may be confused with the use cases for the error handling. I was playing with the below. Invoke-gsudo.ps1 { write-error 1; write-error 2; 2 } I would expect it to return Invoke-gsudo.ps1: 1
Invoke-gsudo.ps1: 2
2 Currently it is returning 2
Invoke-gsudo.ps1: 1 I guess it is because of the redirection ( As another example, I was playing with the below which does work as I would expect. Invoke-gsudo.ps1 { throw 'hello world'; 2 } |
TIL: Apparently, nobody can. See PowerShell/PowerShell#3158
I agree. I removed specifying the fixed ErrorAction you are referring to.
Thanks, that really helped. Now I do try/catch on the scriptblock to detect the terminating errors, and force Now, should I honor It felt harmless to take the If omitted the invokers ErrorActionPreference is forwarded to the elevated instance.
Me too. I'm learning as I code here. Found this great recap on error handling on PS: See MicrosoftDocs/PowerShell-Docs#1583 In a nutshell, keep the same exception type, keep the same stream. For example:
Now, I still have problems between Script-terminating errors and Statement-terminating errors. For example, Invoke-Command behaves differently surrounded by try/catch:
The later stops execution at So, is it correct to conclude that all statement-terminating errors will turn into script-terminating?
Yes, the redirection changes the output order. No workaround AFAIK. Here are a few test cases. Ideally each pair of lines should behave the same, but they don't. (Invoke-Command vs Invoke-gsudo). Proper PS tests may exists in the future, for now a comment will do.
I've just pushed a new version. Feedback welcomed! |
Uploaded tests, and bugfixing.
Also on the build server: CI test results with inconvenient ordering. And last, but not least: I added a gsudo PowerShell Module: (Add |
Released in v1.1.0 |
For context: when writing this,
gsudo
is invoked as any other console.EXE
app from PowerShell. This means the parsing/quote escaping is not ideal and this rules must be followed.Looking forward to implement an `invoke-gsudo' function for PowerShell and I would like to hear opinions from people with more PowerShell experience than me.
This function would be a wrapper of
gsudo.exe
that would make it feel morePowerShell
native.The function name: What would be the best name for it? I bet people would throw me stones if It doesnt respect the
verb-noun
form. Ideally it should NOT be the same as in Support sudo <PowerShell cmdlet> 2 PowerShell/PowerShell#11343 which is hard to know since that one isn't defined yet either. (reason: to avoid all flows to break when that one is released). From now on I would just sayinvoke-gsudo
as an alias forto be defined function name
. Also, maybe it would be better to leave any alias definitions to the end user.The deployment model: I think I figured out this one: By creating a
Invoke-gsudo.ps1
file in the PATH (e.g. gsudo folder) would be enough. The function should be deployed by the 3 installers (scoop/choco/manual.ps1
)Input command parsing: Ideally one would just prepend
invoke-gsudo
without special quoting rules, but is that doable? Best way to get variable substitution? Would the PS-Remoting model work for gsudo?For example, this difference is unwanted: (related Incorrect quoted string parsing #38)
Output result marshalling: Since marshaling is impossible to avoid, this could be like: The elevated instance serializes the result instead of
.ToString()
it, stream (StdIn/Out) and non-elevated deserialize.Reason I wrote this is here is because I prefer to gather feedback very early on. I don't want to invest time just to learn (after releasing) that I reinvented a wheel already available for free, in any of these areas.
The text was updated successfully, but these errors were encountered: