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

@@ -1136,6 +1136,14 @@ _version: 2
zh_CN: "找不到权限管理程序sudo 等),跳过" zh_CN: "找不到权限管理程序sudo 等),跳过"
zh_TW: "找不到權限管理程式sudo 等),略過" zh_TW: "找不到權限管理程式sudo 等),略過"
de: "Benötigt sudo oder Äquivalent, aber nicht gefunden, überspringe" 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 loption %{option}"
zh_CN: "%{sudo_kind} 不支持 %{option} 选项"
zh_TW: "%{sudo_kind} 不支援 %{option} 選項"
de: "%{sudo_kind} unterstützt die Option %{option} nicht"
"sudo as user '{user}'": "sudo as user '{user}'":
en: "sudo as user '%{user}'" en: "sudo as user '%{user}'"
lt: "sudo kaip vartotojas '%{user}'" lt: "sudo kaip vartotojas '%{user}'"

View File

@@ -3,6 +3,8 @@ use std::{fmt::Display, process::ExitStatus};
use rust_i18n::t; use rust_i18n::t;
use thiserror::Error; use thiserror::Error;
use crate::sudo::SudoKind;
#[derive(Error, Debug, PartialEq, Eq)] #[derive(Error, Debug, PartialEq, Eq)]
pub enum TopgradeError { pub enum TopgradeError {
ProcessFailed(String, ExitStatus), 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)] #[derive(Error, Debug)]
pub struct DryRun(); pub struct DryRun();

View File

@@ -11,7 +11,7 @@ use crate::executor::DryCommand;
use crate::powershell::Powershell; use crate::powershell::Powershell;
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
use crate::steps::linux::Distribution; 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::utils::{get_require_sudo_string, require_option};
use crate::{config::Config, executor::Executor}; use crate::{config::Config, executor::Executor};
@@ -91,7 +91,14 @@ impl<'a> ExecutionContext<'a> {
/// using sudo to elevate privileges. /// using sudo to elevate privileges.
pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> { pub fn execute_elevated(&self, command: &Path, interactive: bool) -> Result<Executor> {
let sudo = require_option(self.sudo.as_ref(), get_require_sudo_string())?; 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 { pub fn run_type(&self) -> RunType {

View File

@@ -1,13 +1,14 @@
use std::ffi::OsStr; use std::ffi::OsStr;
use std::path::Path;
use std::path::PathBuf; use std::path::PathBuf;
use color_eyre::eyre::Context; use color_eyre::eyre::Context;
use color_eyre::eyre::Result; use color_eyre::eyre::Result;
use serde::Deserialize; use serde::Deserialize;
use strum::AsRefStr; use strum::AsRefStr;
use strum::Display;
use crate::command::CommandExt; use crate::command::CommandExt;
use crate::error::UnsupportedSudo;
use crate::execution_context::ExecutionContext; use crate::execution_context::ExecutionContext;
use crate::executor::Executor; use crate::executor::Executor;
use crate::terminal::print_separator; use crate::terminal::print_separator;
@@ -21,6 +22,21 @@ pub struct Sudo {
kind: SudoKind, 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 { impl Sudo {
/// Get the `sudo` binary or the `gsudo` binary in the case of `gsudo` /// Get the `sudo` binary or the `gsudo` binary in the case of `gsudo`
/// masquerading as the `sudo` binary. /// masquerading as the `sudo` binary.
@@ -70,7 +86,7 @@ impl Sudo {
// See: https://man.openbsd.org/doas // See: https://man.openbsd.org/doas
cmd.arg("echo"); cmd.arg("echo");
} }
SudoKind::Sudo => { SudoKind::Sudo if cfg!(not(target_os = "windows")) => {
// From `man sudo` on macOS: // From `man sudo` on macOS:
// -v, --validate // -v, --validate
// Update the user's cached credentials, authenticating the user // Update the user's cached credentials, authenticating the user
@@ -79,12 +95,19 @@ impl Sudo {
// command. Not all security policies support cached credentials. // command. Not all security policies support cached credentials.
cmd.arg("-v"); 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 => { SudoKind::Gsudo => {
// `gsudo` doesn't have anything like `sudo -v` to cache credentials, // `gsudo` doesn't have anything like `sudo -v` to cache credentials,
// so we just execute a dummy `echo` command so we have something // so we just execute a dummy command - the easiest on Windows is
// unobtrusive to run. // `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 // See: https://gerardog.github.io/gsudo/docs/usage
cmd.arg("echo"); cmd.args(["-d", "cmd.exe", "/c", "rem"]);
} }
SudoKind::Pkexec => { SudoKind::Pkexec => {
// I don't think this does anything; `pkexec` usually asks for // I don't think this does anything; `pkexec` usually asks for
@@ -114,24 +137,137 @@ impl Sudo {
} }
/// Execute a command with `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); let mut cmd = ctx.execute(&self.path);
if let SudoKind::Sudo = self.kind { if opts.interactive {
cmd.arg("--preserve-env=DIFFPROG"); 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 { if let Some(preserve_env) = opts.preserve_env {
cmd.arg("-i"); 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.arg(command);
cmd Ok(cmd)
} }
} }
#[derive(Clone, Copy, Debug, Deserialize, AsRefStr)] #[derive(Clone, Copy, Debug, Display, Deserialize, AsRefStr)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")] #[strum(serialize_all = "lowercase")]
pub enum SudoKind { pub enum SudoKind {