feat(sudo): add SudoKind::Null

This commit is contained in:
Andre Toerien
2025-09-26 14:58:35 +02:00
committed by Gideon
parent 47b51a8be0
commit a2afdb821f
2 changed files with 104 additions and 44 deletions

View File

@@ -27,6 +27,7 @@ use self::error::StepFailed;
use self::error::Upgraded;
#[allow(clippy::wildcard_imports)]
use self::steps::{remote::*, *};
use self::sudo::{Sudo, SudoKind};
#[allow(clippy::wildcard_imports)]
use self::terminal::*;
use self::utils::{install_color_eyre, install_tracing, is_elevated, update_tracing};
@@ -146,10 +147,16 @@ fn run() -> Result<()> {
}
}
let sudo = match config.sudo_command() {
Some(kind) => Sudo::new(kind),
None if elevated => Sudo::new(SudoKind::Null),
None => Sudo::detect(),
};
debug!("Sudo: {:?}", sudo);
#[cfg(target_os = "linux")]
let distribution = linux::Distribution::detect();
let sudo = config.sudo_command().map_or_else(sudo::Sudo::detect, sudo::Sudo::new);
let run_type = execution_context::RunType::new(config.dry_run());
let ctx = execution_context::ExecutionContext::new(
run_type,

View File

@@ -17,7 +17,7 @@ use crate::utils::which;
#[derive(Clone, Debug)]
pub struct Sudo {
/// The path to the `sudo` binary.
path: PathBuf,
path: Option<PathBuf>,
/// The type of program being used as `sudo`.
kind: SudoKind,
}
@@ -105,8 +105,8 @@ impl Sudo {
/// Get the `sudo` binary for this platform.
pub fn detect() -> Option<Self> {
for kind in DETECT_ORDER {
if let Some(path) = kind.which() {
return Some(Self { path, kind });
if let Some(sudo) = Self::new(kind) {
return Some(sudo);
}
}
None
@@ -114,7 +114,13 @@ impl Sudo {
/// Create Sudo from SudoKind, if found in the system
pub fn new(kind: SudoKind) -> Option<Self> {
kind.which().map(|path| Self { path, kind })
match kind {
SudoKind::Null => Some(Self {
path: None, // no actual binary for null sudo
kind,
}),
_ => kind.which().map(|path| Self { path: Some(path), kind }),
}
}
/// Gets the path to the `sudo` binary. Do not use this to execute `sudo` directly - either use
@@ -122,8 +128,8 @@ impl Sudo {
/// This way, sudo options can be specified generically and the actual arguments customized
/// depending on the sudo kind.
#[allow(unused)]
pub fn path(&self) -> &Path {
self.path.as_ref()
pub fn path(&self) -> Option<&Path> {
self.path.as_deref()
}
/// Elevate permissions with `sudo`.
@@ -133,8 +139,15 @@ impl Sudo {
///
/// See: https://github.com/topgrade-rs/topgrade/issues/205
pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> {
// skip if using null sudo
if let SudoKind::Null = self.kind {
return Ok(());
}
print_separator("Sudo");
let mut cmd = ctx.execute(&self.path);
// self.path is only None for null sudo, which we've handled above
let mut cmd = ctx.execute(self.path.as_deref().unwrap());
match self.kind {
SudoKind::Doas => {
// `doas` doesn't have anything like `sudo -v` to cache credentials,
@@ -189,6 +202,7 @@ impl Sudo {
// Warm the access token and exit.
cmd.arg("-w");
}
SudoKind::Null => unreachable!(),
}
cmd.status_checked().wrap_err("Failed to elevate permissions")
}
@@ -205,7 +219,34 @@ impl Sudo {
command: S,
opts: SudoExecuteOpts,
) -> Result<Executor> {
let mut cmd = ctx.execute(&self.path);
// null sudo is very different, do separately
if let SudoKind::Null = self.kind {
if opts.interactive {
// TODO: emulate running in a login shell with su/runuser
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "interactive",
}
.into());
}
if opts.user.is_some() {
// TODO: emulate running as a different user with su/runuser
return Err(UnsupportedSudo {
sudo_kind: self.kind,
option: "user",
}
.into());
}
// NOTE: we ignore preserve_env and set_home, using
// no sudo effectively preserves these by default
// run command directly
return Ok(ctx.execute(command));
}
// self.path is only None for null sudo, which we've handled above
let mut cmd = ctx.execute(self.path.as_ref().unwrap());
if opts.interactive {
match self.kind {
@@ -224,6 +265,7 @@ impl Sudo {
}
.into());
}
SudoKind::Null => unreachable!(),
}
} else if let SudoKind::Gsudo = self.kind {
// The `-d` (direct) flag disables shell detection, running the command directly
@@ -249,6 +291,7 @@ impl Sudo {
}
.into());
}
SudoKind::Null => unreachable!(),
},
SudoPreserveEnv::Some(vars) => match self.kind {
SudoKind::Sudo => {
@@ -270,6 +313,7 @@ impl Sudo {
}
.into());
}
SudoKind::Null => unreachable!(),
},
SudoPreserveEnv::None => {}
}
@@ -291,6 +335,7 @@ impl Sudo {
}
.into());
}
SudoKind::Null => unreachable!(),
}
}
@@ -313,6 +358,7 @@ impl Sudo {
}
.into());
}
SudoKind::Null => unreachable!(),
}
}
@@ -322,61 +368,68 @@ impl Sudo {
}
}
// We need separate `SudoKind` definitions for windows and unix,
// so that we can have serde instantiate `WinSudo` on windows and
// `Sudo` on unix when reading "sudo" from the config file.
// NOTE: when adding a new variant or otherwise changing `SudoKind`,
// make sure to keep both definitions in sync.
// On unix we use `SudoKind::Sudo`, and on windows `SudoKind::WinSudo`.
// We always define both though, so that we don't have to put
// #[cfg(...)] everywhere.
#[derive(Clone, Copy, Debug, Display, Deserialize)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[cfg(target_os = "windows")]
pub enum SudoKind {
Doas,
#[expect(unused, reason = "Sudo is unix-only")]
#[serde(skip)]
Sudo,
#[serde(rename = "sudo")]
WinSudo,
Gsudo,
Pkexec,
Run0,
Please,
}
#[derive(Clone, Copy, Debug, Display, Deserialize)]
#[serde(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
#[cfg(not(target_os = "windows"))]
pub enum SudoKind {
Doas,
// On unix, "sudo" in the config file means Sudo
#[cfg(not(windows))]
Sudo,
// and WinSudo is skipped, making it unused.
#[cfg(not(windows))]
#[expect(unused, reason = "WinSudo is windows-only")]
#[serde(skip)]
WinSudo,
// On unix, Sudo is skipped and unused
#[cfg(windows)]
#[expect(unused, reason = "Sudo is unix-only")]
#[serde(skip)]
Sudo,
// and "sudo" in the config file means WinSudo.
#[cfg(windows)]
#[serde(rename = "sudo")]
WinSudo,
Doas,
Gsudo,
Pkexec,
Run0,
Please,
/// A "no-op" sudo, used when topgrade itself is running as root
Null,
}
impl SudoKind {
fn binary_name(self) -> &'static str {
/// Get the name of the "sudo" binary.
///
/// For `SudoKind::WinSudo`, returns the full hardcoded path
/// instead to ensure we find Windows Sudo rather than gsudo
/// masquerading as sudo.
///
/// Only returns `None` for `SudoKind::Null`.
fn binary_name(self) -> Option<&'static str> {
match self {
SudoKind::Doas => "doas",
SudoKind::Sudo => "sudo",
// hardcode the path to ensure we find Windows Sudo
// rather than gsudo masquerading as sudo
SudoKind::WinSudo => r"C:\Windows\System32\sudo.exe",
SudoKind::Gsudo => "gsudo",
SudoKind::Pkexec => "pkexec",
SudoKind::Run0 => "run0",
SudoKind::Please => "please",
SudoKind::Doas => Some("doas"),
SudoKind::Sudo => Some("sudo"),
SudoKind::WinSudo => Some(r"C:\Windows\System32\sudo.exe"),
SudoKind::Gsudo => Some("gsudo"),
SudoKind::Pkexec => Some("pkexec"),
SudoKind::Run0 => Some("run0"),
SudoKind::Please => Some("please"),
SudoKind::Null => None,
}
}
/// Find the full path to the "sudo" binary, if it exists on the system.
fn which(self) -> Option<PathBuf> {
which(self.binary_name())
match self.binary_name() {
Some(name) => which(name),
None => None,
}
}
}