fix(sudo): set sudo flags depending on kind

This commit is contained in:
Andre Toerien
2025-06-25 19:46:49 +02:00
committed by Gideon
parent 32197f79f3
commit 012a6bbde3
4 changed files with 187 additions and 14 deletions

View File

@@ -1,13 +1,14 @@
use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf;
use color_eyre::eyre::Context;
use color_eyre::eyre::Result;
use serde::Deserialize;
use strum::AsRefStr;
use strum::Display;
use crate::command::CommandExt;
use crate::error::UnsupportedSudo;
use crate::execution_context::ExecutionContext;
use crate::executor::Executor;
use crate::terminal::print_separator;
@@ -21,6 +22,21 @@ pub struct Sudo {
kind: SudoKind,
}
#[derive(Clone, Debug, Default)]
/// Generic sudo options, translated into flags to pass to `sudo`. Depending on the sudo kind, OS
/// and system config, some options might be specified by default or unsupported.
pub struct SudoExecuteOpts<'a> {
/// Run the command "interactively", i.e. inside a login shell.
pub interactive: bool,
/// Preserve environment variables across the sudo call. If an empty list is given, preserves
/// all existing environment variables.
pub preserve_env: Option<&'a [&'a str]>,
/// Set the HOME environment variable to the target user's home directory.
pub set_home: bool,
/// Run the command as a user other than the root user.
pub user: Option<&'a str>,
}
impl Sudo {
/// Get the `sudo` binary or the `gsudo` binary in the case of `gsudo`
/// masquerading as the `sudo` binary.
@@ -70,7 +86,7 @@ impl Sudo {
// See: https://man.openbsd.org/doas
cmd.arg("echo");
}
SudoKind::Sudo => {
SudoKind::Sudo if cfg!(not(target_os = "windows")) => {
// From `man sudo` on macOS:
// -v, --validate
// Update the user's cached credentials, authenticating the user
@@ -79,12 +95,19 @@ impl Sudo {
// command. Not all security policies support cached credentials.
cmd.arg("-v");
}
SudoKind::Sudo => {
// Windows `sudo` doesn't cache credentials, so we just execute a
// dummy command - the easiest on Windows is `rem` in cmd.
// See: https://learn.microsoft.com/en-us/windows/advanced-settings/sudo/
cmd.args(["cmd.exe", "/c", "rem"]);
}
SudoKind::Gsudo => {
// `gsudo` doesn't have anything like `sudo -v` to cache credentials,
// so we just execute a dummy `echo` command so we have something
// unobtrusive to run.
// so we just execute a dummy command - the easiest on Windows is
// `rem` in cmd. `-d` tells it to run the command directly, without
// going through a shell (which could be powershell) first.
// See: https://gerardog.github.io/gsudo/docs/usage
cmd.arg("echo");
cmd.args(["-d", "cmd.exe", "/c", "rem"]);
}
SudoKind::Pkexec => {
// I don't think this does anything; `pkexec` usually asks for
@@ -114,24 +137,137 @@ impl Sudo {
}
/// Execute a command with `sudo`.
pub fn execute_elevated(&self, ctx: &ExecutionContext, command: &Path, interactive: bool) -> Executor {
pub fn execute<S: AsRef<OsStr>>(&self, ctx: &ExecutionContext, command: S) -> Result<Executor> {
self.execute_opts(ctx, command, SudoExecuteOpts::default())
}
/// Execute a command with `sudo`, with custom options.
pub fn execute_opts<S: AsRef<OsStr>>(
&self,
ctx: &ExecutionContext,
command: S,
opts: SudoExecuteOpts,
) -> Result<Executor> {
let mut cmd = ctx.execute(&self.path);
if let SudoKind::Sudo = self.kind {
cmd.arg("--preserve-env=DIFFPROG");
if opts.interactive {
match self.kind {
SudoKind::Sudo if cfg!(not(target_os = "windows")) => {
cmd.arg("-i");
}
SudoKind::Gsudo => {
// By default, gsudo runs all commands inside a shell, so it's effectively
// always "interactive". If interactive is *not* specified, we add `-d`
// to run outside of a shell - see below.
}
SudoKind::Doas | SudoKind::Sudo | SudoKind::Pkexec | SudoKind::Run0 | SudoKind::Please => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "interactive",
}
.into());
}
}
} else if let SudoKind::Gsudo = self.kind {
// The `-d` (direct) flag disables shell detection, running the command directly
// rather than through the current shell, making it "non-interactive".
// Additionally, if the current shell is pwsh >= 7.3.0, then not including this
// gives errors if the command to run has spaces in it: see
// https://github.com/gerardog/gsudo/issues/297
cmd.arg("-d");
}
if interactive {
cmd.arg("-i");
if let Some(preserve_env) = opts.preserve_env {
if preserve_env.is_empty() {
match self.kind {
SudoKind::Sudo => {
cmd.arg("-E");
}
SudoKind::Gsudo => {
cmd.arg("--copyEV");
}
SudoKind::Doas | SudoKind::Pkexec | SudoKind::Run0 | SudoKind::Please => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "preserve_env",
}
.into());
}
}
} else {
match self.kind {
SudoKind::Sudo if cfg!(not(target_os = "windows")) => {
cmd.arg(format!("--preserve_env={}", preserve_env.join(",")));
}
SudoKind::Run0 => {
for env in preserve_env {
cmd.arg(format!("--setenv={}", env));
}
}
SudoKind::Please => {
cmd.arg("-a");
cmd.arg(preserve_env.join(","));
}
SudoKind::Doas | SudoKind::Sudo | SudoKind::Gsudo | SudoKind::Pkexec => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "preserve_env list",
}
.into());
}
}
}
}
if opts.set_home {
match self.kind {
SudoKind::Sudo if cfg!(not(target_os = "windows")) => {
cmd.arg("-H");
}
SudoKind::Doas
| SudoKind::Sudo
| SudoKind::Gsudo
| SudoKind::Pkexec
| SudoKind::Run0
| SudoKind::Please => {
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "set_home",
}
.into());
}
}
}
if let Some(user) = opts.user {
match self.kind {
SudoKind::Sudo if cfg!(not(target_os = "windows")) => {
cmd.args(["-u", user]);
}
SudoKind::Doas | SudoKind::Gsudo | SudoKind::Run0 | SudoKind::Please => {
cmd.args(["-u", user]);
}
SudoKind::Pkexec => {
cmd.args(["--user", user]);
}
SudoKind::Sudo => {
// Windows sudo is the only one that doesn't have a `-u` flag
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "user",
}
.into());
}
}
}
cmd.arg(command);
cmd
Ok(cmd)
}
}
#[derive(Clone, Copy, Debug, Deserialize, AsRefStr)]
#[derive(Clone, Copy, Debug, Display, Deserialize, AsRefStr)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum SudoKind {