From 012a6bbde3288b57b3e1580f3ac10ca03fa90926 Mon Sep 17 00:00:00 2001 From: Andre Toerien Date: Wed, 25 Jun 2025 19:46:49 +0200 Subject: [PATCH] fix(sudo): set sudo flags depending on kind --- locales/app.yml | 8 ++ src/error.rs | 22 ++++++ src/execution_context.rs | 11 ++- src/sudo.rs | 160 ++++++++++++++++++++++++++++++++++++--- 4 files changed, 187 insertions(+), 14 deletions(-) diff --git a/locales/app.yml b/locales/app.yml index 2be905fe..a5a9817d 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -1136,6 +1136,14 @@ _version: 2 zh_CN: "找不到权限管理程序(sudo 等),跳过" zh_TW: "找不到權限管理程式(sudo 等),略過" de: "Benötigt sudo oder Äquivalent, aber nicht gefunden, überspringe" +"{sudo_kind} does not support the {option} option": + en: "%{sudo_kind} does not support the %{option} option" + lt: "%{sudo_kind} nepalaiko parinkties %{option}" + es: "%{sudo_kind} no admite la opción %{option}" + fr: "%{sudo_kind} ne prend pas en charge l’option %{option}" + zh_CN: "%{sudo_kind} 不支持 %{option} 选项" + zh_TW: "%{sudo_kind} 不支援 %{option} 選項" + de: "%{sudo_kind} unterstützt die Option %{option} nicht" "sudo as user '{user}'": en: "sudo as user '%{user}'" lt: "sudo kaip vartotojas '%{user}'" diff --git a/src/error.rs b/src/error.rs index ba6a321b..748edde5 100644 --- a/src/error.rs +++ b/src/error.rs @@ -3,6 +3,8 @@ use std::{fmt::Display, process::ExitStatus}; use rust_i18n::t; use thiserror::Error; +use crate::sudo::SudoKind; + #[derive(Error, Debug, PartialEq, Eq)] pub enum TopgradeError { ProcessFailed(String, ExitStatus), @@ -68,6 +70,26 @@ impl Display for StepFailed { } } +#[derive(Error, Debug)] +pub struct UnsupportedSudo<'a> { + pub sudo_kind: SudoKind, + pub option: &'a str, +} + +impl Display for UnsupportedSudo<'_> { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + t!( + "{sudo_kind} does not support the {option} option", + sudo_kind = self.sudo_kind, + option = self.option + ) + ) + } +} + #[derive(Error, Debug)] pub struct DryRun(); diff --git a/src/execution_context.rs b/src/execution_context.rs index 78995e9a..b60d050a 100644 --- a/src/execution_context.rs +++ b/src/execution_context.rs @@ -11,7 +11,7 @@ use crate::executor::DryCommand; use crate::powershell::Powershell; #[cfg(target_os = "linux")] use crate::steps::linux::Distribution; -use crate::sudo::Sudo; +use crate::sudo::{Sudo, SudoExecuteOpts}; use crate::utils::{get_require_sudo_string, require_option}; use crate::{config::Config, executor::Executor}; @@ -91,7 +91,14 @@ impl<'a> ExecutionContext<'a> { /// using sudo to elevate privileges. pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result { let sudo = require_option(self.sudo.as_ref(), get_require_sudo_string())?; - Ok(sudo.execute_elevated(self, command, interactive)) + sudo.execute_opts( + self, + command, + SudoExecuteOpts { + interactive, + ..Default::default() + }, + ) } pub fn run_type(&self) -> RunType { diff --git a/src/sudo.rs b/src/sudo.rs index 0a78baf4..3de601a4 100644 --- a/src/sudo.rs +++ b/src/sudo.rs @@ -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>(&self, ctx: &ExecutionContext, command: S) -> Result { + self.execute_opts(ctx, command, SudoExecuteOpts::default()) + } + + /// Execute a command with `sudo`, with custom options. + pub fn execute_opts>( + &self, + ctx: &ExecutionContext, + command: S, + opts: SudoExecuteOpts, + ) -> Result { 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 {