Skip to content

Commit

Permalink
Merge pull request #188 from gerardog/Feature.RunAsUser
Browse files Browse the repository at this point in the history
Feature: Run As User `gsudo -u username`
  • Loading branch information
gerardog authored Oct 23, 2022
2 parents 45296ac + c58c044 commit ce3173a
Show file tree
Hide file tree
Showing 52 changed files with 1,347 additions and 805 deletions.
233 changes: 143 additions & 90 deletions README.md

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions docs/docs/usage/powershell.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,7 @@ $hash = gsudo "(Get-FileHash '$file' -Algorithm $algorithm).Hash"
- Current Location is preserved for non-FileSystem providers.
- `$ErrorActionPreference` is preserved.
- If your command requires accessing a function on your `$PROFILE` add the `-LoadProfile` parameter. [See More](#loading-your-ps-profile-on-command-elevations).
- Add `-Credential` to specify a user & password to run.

Examples:

Expand Down
100 changes: 47 additions & 53 deletions docs/docs/usage/usage.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,73 +6,67 @@ hide_title: true
---
## How to Use

```gsudo``` Opens your shell elevated in the current console.

```gsudo [options] {command} [arguments]```
Executes the specified command with elevated permissions.

Most relevant **`[options]`**:

- **`-n | --new`** Starts the command in a **new** console with elevated rights (and returns immediately).
- **`-w | --wait`** Force wait for the process to end (and return the exitcode).
- **`-s | --system`** Run As Local System account ("NT AUTHORITY\SYSTEM").
- **`-i | --integrity {v}`** Run command with a specific integrity level: `Low`, `Medium`, `MediumPlus`, `High` (default), `System`. For example, use `Low` to launch a restricted process, or use `Medium` to run without Admin rights.
- **`-d | --direct`** Execute {command} directly. Does not wrap it with your current shell (Pwsh/WSL/MinGw/Yori/etc). Assumes it is a `CMD` command (eg. an `.EXE` file).
- **`--copyns`** Reconnect current connected network shares on the elevated session. Warning! This is verbose, affects the elevated user system-wide (other processes), and can prompt for credentials interactively.
- **`--debug`** Debug mode (verbose).

```gsudo config```
Show current user-settings.
``` powershell
gsudo [options] # Elevates your current shell
gsudo [options] {command} [args] # Runs {command} with elevated permissions
gsudo cache [on | off | help] # Starts/Stops an elevated cache session. (reduced UAC popups)
gsudo status # Shows current user, cache and console status.
gsudo !! # Re-run last command as admin. (YMMV)
```

```gsudo config {key} ["value" | --reset]```
Read, write, or reset a user setting to the default value.
``` powershell
General options:
-n | --new # Starts the command in a new console (and returns immediately).
-w | --wait # When in new console, force wait for the command to end.
Security options:
-i | --integrity {v} # Specify integrity level: Untrusted, Low, Medium, MediumPlus, High (default), System
-u | --user {usr} # Run as the specified user. Asks for password. For local admins shows UAC unless '-i Medium'
-s | --system # Run as Local System account (NT AUTHORITY\SYSTEM).
--ti # Run as member of NT SERVICE\TrustedInstaller
-k # Kills all cached credentials. The next time gsudo is run a UAC popup will be appear.
Shell related options:
-d | --direct # Execute {command} directly. Bypass shell wrapper (Pwsh/Yori/etc).
--loadProfile # When elevating PowerShell commands, load user profile.
Other options:
--loglevel {val} # Set minimum log level to display: All, Debug, Info, Warning, Error, None
--debug # Enable debug mode.
--copyns # Connect network drives to the elevated user. Warning: Verbose, interactive asks for credentials
--copyev # (deprecated) Copy environment variables to the elevated process. (not needed on default console mode)
```gsudo status```
Show status information about current user, security, integrity level or other gsudo relevant data.
```

**Note:** You can use anywhere **the `sudo` alias** created by the installers.

### Examples
**Examples:**

``` powershell
# elevate the current shell in the current console window (Cmd/PowerShell/Pwsh Core/Yori/Take Command/git-bash/cygwin)
gsudo
# launch the current shell elevated in a new console window
gsudo -n
# launch in new window and wait for exit
gsudo -n -w powershell ./Do-Something.ps1
gsudo # elevates the current shell in the current console window (Supports Cmd/PowerShell/Pwsh Core/Yori/Take Command/git-bash/cygwin)
gsudo -n # launch the current shell elevated in a new console window
gsudo -n -w powershell ./Do-Something.ps1 # launch in new window and wait for exit
gsudo notepad %windir%\system32\drivers\etc\hosts # launch windows app
# launch windows app
gsudo notepad %windir%\system32\drivers\etc\hosts
sudo notepad # sudo alias built-in
# sudo alias built-in with choco/scoop/manual installers:
sudo notepad %windir%\system32\drivers\etc\hosts
# Cmd Commands:
gsudo type MySecretFile.txt
gsudo md "C:\Program Files\MyApp"
# redirect/pipe input/output/error
# redirect/pipe input/output/error example
gsudo dir | findstr /c:"bytes free" > FreeSpace.txt
# Elevate last command (sudo bang bang)
gsudo !!
gsudo config LogLevel "Error" # Configure Reduced logging
gsudo config Prompt "$P [elevated]$G " # Configure a custom Elevated Prompt
gsudo config Prompt --reset # Reset to default value
# Enable credentials cache (less UAC popups):
gsudo config CacheMode Auto
```

### Configuration

``` powershell
# See current configuration
gsudo config
# Configure Reduced logging
gsudo config LogLevel "Error"
# Configure a custom Elevated Prompt
gsudo config Prompt "$P [elevated]$G "
# Reset to default value
gsudo config Prompt --reset

# Enable credentials cache (less UAC popups):
gsudo config CacheMode Auto
``` powershell
gsudo config # Show current config settings & values.
gsudo config {key} [--global] [value] # Read or write a user setting
gsudo config {key} [--global] --reset # Reset config to default value
--global # Affects all users (overrides user settings)
```
17 changes: 9 additions & 8 deletions src/gsudo.Tests/CmdTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public void Cmd_DebugTestHelper()
//[TestMethod]
public void Cmd_AdminUserTest()
{
Assert.IsTrue(ProcessHelper.IsAdministrator(), "This test suite is intended to be run as an administrator, otherwise it will not work.");
Assert.IsTrue(SecurityHelper.IsAdministrator(), "This test suite is intended to be run as an administrator, otherwise it will not work.");
}

[TestMethod]
Expand All @@ -42,10 +42,10 @@ public void Cmd_ChangeDirTest()
// TODO: Test --raw, --vt, --attached
var testDir = Environment.CurrentDirectory;
var p1 = new TestProcess(
$"\"{testDir}\\gsudo\" cmd /c cd \r\n"
$"\"{testDir}\\gsudo\" cmd /c cd \r\n" // => show current path
+ $"cd .. \r\n"
+ $"\"{testDir}\\gsudo\" cmd /c cd \r\n"
);
); ;
p1.WaitForExit();

var otherDir = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory,".."));
Expand Down Expand Up @@ -102,12 +102,12 @@ public void Cmd_CommandLineAppNoWaitTest()
public void Cmd_WindowsAppWaitTest()
{
bool stillWaiting = false;
var p = new TestProcess("gsudo \"c:\\Program Files (x86)\\Windows NT\\Accessories\\wordpad.exe\"");
var p = new TestProcess("gsudo -w \"c:\\Program Files (x86)\\Windows NT\\Accessories\\wordpad.exe\"");
try
{
p.WaitForExit(3000);
}
finally
catch
{
stillWaiting = true;
}
Expand All @@ -120,16 +120,17 @@ public void Cmd_WindowsAppWaitTest()
[TestMethod]
public void Cmd_WindowsAppNoWaitTest()
{
var p = new TestProcess("gsudo notepad");
var p = new TestProcess("gsudo \"c:\\Program Files (x86)\\Windows NT\\Accessories\\wordpad.exe\"");
try
{
p.WaitForExit();
}
finally
{
Process.Start("gsudo", "taskkill.exe /FI \"WINDOWTITLE eq Untitled - Notepad\" ").WaitForExit();
p.WaitForExit();
Process.Start("gsudo", "taskkill.exe /IM Wordpad.exe").WaitForExit();
}

p.WaitForExit();
}

[TestMethod]
Expand Down
22 changes: 20 additions & 2 deletions src/gsudo.Tests/PowershellTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using gsudo.Commands;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;

namespace gsudo.Tests
Expand All @@ -11,7 +12,24 @@ public class PowerShellCoreTests : PowerShellTests

public PowerShellCoreTests()
{
PS_FILENAME = "pwsh.exe";
PS_FILENAME = "pwsh.exe";
}
}

[TestClass]
public class PowerShellCoreAttachedTests : PowerShellTests
{
[ClassInitialize]
public static new void ClassInitialize(TestContext context)
{
TestShared.StartCacheSession();
new ConfigCommand() { key = "ForceAttachedConsole", value = new string[] { "true" } }.Execute();
}

[ClassCleanup]
public static new void ClassCleanup()
{
new ConfigCommand() { key = "ForceAttachedConsole", value = new string[] { "--reset" } }.Execute();
}
}

Expand Down
3 changes: 1 addition & 2 deletions src/gsudo.Tests/TestProcess.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,6 @@ class TestProcess
public uint ProcessId { get; set; }
public int ExitCode;

static int TestNumber = 1;
private readonly string _testId = Random.Shared.Next(1,999999).ToString() ;// DateTime.Now.ToString("yyyyMMddHHmmssff");
string _sIn => $"in{_testId}";
string _sOut => $"out{_testId}";
Expand Down Expand Up @@ -41,7 +40,7 @@ public TestProcess(string inputScript, string shell = "cmd /k")

File.WriteAllText($"{_sIn}", inputScript + "\r\nExit /b %errorlevel%\r\n");

_process = ProcessFactory.StartDetached(_batchFile, arguments, Environment.CurrentDirectory, false);
_process = ProcessFactory.StartDetached(_batchFile, arguments, Environment.CurrentDirectory, false);
_testProcessHandle = new SafeProcessHandle(_process.Handle, false);

ProcessId = (uint) _process.Id;
Expand Down
120 changes: 66 additions & 54 deletions src/gsudo.Wrappers/Invoke-gsudo.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -7,24 +7,28 @@ 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)
The command result is serialized, sent back to the non-elevated instance, deserealized and returned.
Optionally you can check for "$LastExitCode -eq 999" to find out if gsudo failed to elevate (for example, UAC popup cancelled)
.PARAMETER ScriptBlock
Specifies a ScriptBlock that will be run in an elevated PowerShell instance. '
e.g. { Get-Process Notepad }
.PARAMETER ArgumentList
An list of elements that will be accesible inside the script as: $args
An list of elements that will be accesible inside the script as: $args[0] ... $args[n]
.PARAMETER LoadProfile
Load the user profile in the elevated powershell instance. (regardless of `gsudo config PowerShellLoadProfile`)
.PARAMETER NoElevate
A test mode where the command is executed out-of-scope but without real elevation: The serialization/marshalling is still done.
.PARAMETER NoProfile
Do not load the user profile in the elevated powershell instance. (regardless of `gsudo config PowerShellLoadProfile`)
.INPUTS
You can pipe any object to Invoke-Gsudo. It will be serialized and available in the userScript as $Input.
.OUTPUTS
Whatever the scriptblock returns. Use explicit "return" in your scriptblock.
Whatever the scriptblock returns.
.EXAMPLE
PS> Get-Process notepad | Invoke-gsudo { Stop-Process }
Expand Down Expand Up @@ -63,10 +67,9 @@ param
[switch]
$NoProfile = $false,

#test mode
[Parameter()]
[switch]
$NoElevate = $false
[System.Management.Automation.PSCredential]
$Credential
)

# Replaces $using:variableName with the serialized value of $variableName.
Expand Down Expand Up @@ -137,57 +140,66 @@ if ($Debug) {
Write-Debug "Full Script to run on the isolated instance: { $remoteCmd }"
}

if($NoElevate) {
# We could invoke using Invoke-Command:
# $result = $InputObject | Invoke-Command (Deserialize-Scriptblock $remoteCmd) -ArgumentList $ArgumentList
# Or run in a Job to ensure same variable isolation:
$pwsh = ("""$([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)""") # Get same running powershell EXE.

if ($host.Name -notmatch 'consolehost') { # Workaround for PowerShell ISE, or PS hosted inside other process
if ($PSVersionTable.PSVersion.Major -le 5)
{ $pwsh = "powershell.exe" }
else
{ $pwsh = "pwsh.exe" }
}

$windowTitle = $host.ui.RawUI.WindowTitle;

$dbg = if ($debug) {"--debug "} else {" "}

$job = Start-Job -ScriptBlock (Deserialize-Scriptblock $remoteCmd) -errorAction $errorAction | Wait-Job;
$result = Receive-Job $job -errorAction $errorAction
if ($LoadProfile -and (-not $NoProfile -or (gsudo.exe --loglevel None config PowerShellLoadProfile).Split(" = ")[1] -like "*true*")) {
$sNoProfile = ""
} else {
$pwsh = ("""$([System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName)""") # Get same running powershell EXE.

if ($host.Name -notmatch 'consolehost') { # Workaround for PowerShell ISE, or PS hosted inside other process
if ($PSVersionTable.PSVersion.Major -le 5)
{ $pwsh = "powershell.exe" }
else
{ $pwsh = "pwsh.exe" }
}

$windowTitle = $host.ui.RawUI.WindowTitle;
$sNoProfile = "-NoProfile "
}

$dbg = if ($debug) {"--debug "} else {" "}
if ($credential) {
$currentSid = ([System.Security.Principal.WindowsIdentity]::GetCurrent()).User.Value;
$user = "-u $($credential.UserName) "

if ($LoadProfile -or ((gsudo.exe --loglevel None config Powershellloadprofile).Split(" = ")[1] -like "*true*" -and -not $NoProfile)) {
$sNoProfile = ""
} else {
$sNoProfile = "-NoProfile "
}
# At the time of writing this, there is no way (considered secure) to send the password to gsudo. So instead of sending the password, lets start a credentials cache instance.
Start-Process "gsudo.exe" -Args "$dbg -u $($credential.UserName) gsudoservice $PID $CurrentSid All 00:05:00" -credential $Credential -LoadUserProfile -WorkingDirectory "$env:windir" *> $null
# This may fail with `The specified drive root "C:\Users\gerar\AppData\Local\Temp\" either does not exist, or it is not a folder.` https://github.com/PowerShell/PowerShell/issues/18333

$arguments = "-d --LogLevel Error $dbg$pwsh -nologo $sNoProfile-NonInteractive -OutputFormat Xml -InputFormat Text -encodedCommand IAAoACQAaQBuAHAAdQB0ACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAKQAgAHwAIABpAGUAeAAgAA==".Split(" ")

# Must Read: https://stackoverflow.com/questions/68136128/how-do-i-call-the-powershell-cli-robustly-with-respect-to-character-encoding-i?noredirect=1&lq=1
$result = $remoteCmd | & gsudo.exe $arguments *>&1

$host.ui.RawUI.WindowTitle = $windowTitle;
#$p.WaitForExit();
Start-Sleep -Seconds 1
} else {
$user = "";
}

ForEach ($item in $result)
{
if (
$item.psobject.Properties['Exception'] -and
($item.Exception.SerializedRemoteException.WasThrownFromThrowStatement -or
$item.Exception.WasThrownFromThrowStatement)
)
$arguments = "-d --LogLevel Error $user$dbg$pwsh $sNoProfile -nologo -NonInteractive -OutputFormat Xml -InputFormat Text -encodedCommand IAAoACQAaQBuAHAAdQB0ACAAfAAgAE8AdQB0AC0AUwB0AHIAaQBuAGcAKQAgAHwAIABpAGUAeAAgAA==".Split(" ")
# Must Read: https://stackoverflow.com/questions/68136128/how-do-i-call-the-powershell-cli-robustly-with-respect-to-character-encoding-i?noredirect=1&lq=1

$result = $remoteCmd | & gsudo.exe $arguments *>&1

$host.ui.RawUI.WindowTitle = $windowTitle;

& {
Set-StrictMode -Off #within this scope

ForEach ($item in $result)
{
throw $item
}
if ($item -is [System.Management.Automation.ErrorRecord])
{
Write-Error $item
}
else
{
Write-Output $item;
if (
$item.psobject.Properties['Exception'] -and
($item.Exception.SerializedRemoteException.WasThrownFromThrowStatement -or
$item.Exception.WasThrownFromThrowStatement)
)
{
throw $item
}
if ($item -is [System.Management.Automation.ErrorRecord])
{
Write-Error $item
}
else
{
Write-Output $item;
}
}
}
}
Loading

0 comments on commit ce3173a

Please sign in to comment.