feat(sudo): add SudoKind::Null
This commit is contained in:
@@ -27,6 +27,7 @@ use self::error::StepFailed;
|
|||||||
use self::error::Upgraded;
|
use self::error::Upgraded;
|
||||||
#[allow(clippy::wildcard_imports)]
|
#[allow(clippy::wildcard_imports)]
|
||||||
use self::steps::{remote::*, *};
|
use self::steps::{remote::*, *};
|
||||||
|
use self::sudo::{Sudo, SudoKind};
|
||||||
#[allow(clippy::wildcard_imports)]
|
#[allow(clippy::wildcard_imports)]
|
||||||
use self::terminal::*;
|
use self::terminal::*;
|
||||||
use self::utils::{install_color_eyre, install_tracing, is_elevated, update_tracing};
|
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")]
|
#[cfg(target_os = "linux")]
|
||||||
let distribution = linux::Distribution::detect();
|
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 run_type = execution_context::RunType::new(config.dry_run());
|
||||||
let ctx = execution_context::ExecutionContext::new(
|
let ctx = execution_context::ExecutionContext::new(
|
||||||
run_type,
|
run_type,
|
||||||
|
|||||||
139
src/sudo.rs
139
src/sudo.rs
@@ -17,7 +17,7 @@ use crate::utils::which;
|
|||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
pub struct Sudo {
|
pub struct Sudo {
|
||||||
/// The path to the `sudo` binary.
|
/// The path to the `sudo` binary.
|
||||||
path: PathBuf,
|
path: Option<PathBuf>,
|
||||||
/// The type of program being used as `sudo`.
|
/// The type of program being used as `sudo`.
|
||||||
kind: SudoKind,
|
kind: SudoKind,
|
||||||
}
|
}
|
||||||
@@ -105,8 +105,8 @@ impl Sudo {
|
|||||||
/// Get the `sudo` binary for this platform.
|
/// Get the `sudo` binary for this platform.
|
||||||
pub fn detect() -> Option<Self> {
|
pub fn detect() -> Option<Self> {
|
||||||
for kind in DETECT_ORDER {
|
for kind in DETECT_ORDER {
|
||||||
if let Some(path) = kind.which() {
|
if let Some(sudo) = Self::new(kind) {
|
||||||
return Some(Self { path, kind });
|
return Some(sudo);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
None
|
None
|
||||||
@@ -114,7 +114,13 @@ impl Sudo {
|
|||||||
|
|
||||||
/// Create Sudo from SudoKind, if found in the system
|
/// Create Sudo from SudoKind, if found in the system
|
||||||
pub fn new(kind: SudoKind) -> Option<Self> {
|
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
|
/// 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
|
/// This way, sudo options can be specified generically and the actual arguments customized
|
||||||
/// depending on the sudo kind.
|
/// depending on the sudo kind.
|
||||||
#[allow(unused)]
|
#[allow(unused)]
|
||||||
pub fn path(&self) -> &Path {
|
pub fn path(&self) -> Option<&Path> {
|
||||||
self.path.as_ref()
|
self.path.as_deref()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Elevate permissions with `sudo`.
|
/// Elevate permissions with `sudo`.
|
||||||
@@ -133,8 +139,15 @@ impl Sudo {
|
|||||||
///
|
///
|
||||||
/// See: https://github.com/topgrade-rs/topgrade/issues/205
|
/// See: https://github.com/topgrade-rs/topgrade/issues/205
|
||||||
pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> {
|
pub fn elevate(&self, ctx: &ExecutionContext) -> Result<()> {
|
||||||
|
// skip if using null sudo
|
||||||
|
if let SudoKind::Null = self.kind {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
print_separator("Sudo");
|
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 {
|
match self.kind {
|
||||||
SudoKind::Doas => {
|
SudoKind::Doas => {
|
||||||
// `doas` doesn't have anything like `sudo -v` to cache credentials,
|
// `doas` doesn't have anything like `sudo -v` to cache credentials,
|
||||||
@@ -189,6 +202,7 @@ impl Sudo {
|
|||||||
// Warm the access token and exit.
|
// Warm the access token and exit.
|
||||||
cmd.arg("-w");
|
cmd.arg("-w");
|
||||||
}
|
}
|
||||||
|
SudoKind::Null => unreachable!(),
|
||||||
}
|
}
|
||||||
cmd.status_checked().wrap_err("Failed to elevate permissions")
|
cmd.status_checked().wrap_err("Failed to elevate permissions")
|
||||||
}
|
}
|
||||||
@@ -205,7 +219,34 @@ impl Sudo {
|
|||||||
command: S,
|
command: S,
|
||||||
opts: SudoExecuteOpts,
|
opts: SudoExecuteOpts,
|
||||||
) -> Result<Executor> {
|
) -> 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 {
|
if opts.interactive {
|
||||||
match self.kind {
|
match self.kind {
|
||||||
@@ -224,6 +265,7 @@ impl Sudo {
|
|||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
SudoKind::Null => unreachable!(),
|
||||||
}
|
}
|
||||||
} else if let SudoKind::Gsudo = self.kind {
|
} else if let SudoKind::Gsudo = self.kind {
|
||||||
// The `-d` (direct) flag disables shell detection, running the command directly
|
// The `-d` (direct) flag disables shell detection, running the command directly
|
||||||
@@ -249,6 +291,7 @@ impl Sudo {
|
|||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
SudoKind::Null => unreachable!(),
|
||||||
},
|
},
|
||||||
SudoPreserveEnv::Some(vars) => match self.kind {
|
SudoPreserveEnv::Some(vars) => match self.kind {
|
||||||
SudoKind::Sudo => {
|
SudoKind::Sudo => {
|
||||||
@@ -270,6 +313,7 @@ impl Sudo {
|
|||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
SudoKind::Null => unreachable!(),
|
||||||
},
|
},
|
||||||
SudoPreserveEnv::None => {}
|
SudoPreserveEnv::None => {}
|
||||||
}
|
}
|
||||||
@@ -291,6 +335,7 @@ impl Sudo {
|
|||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
SudoKind::Null => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -313,6 +358,7 @@ impl Sudo {
|
|||||||
}
|
}
|
||||||
.into());
|
.into());
|
||||||
}
|
}
|
||||||
|
SudoKind::Null => unreachable!(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,61 +368,68 @@ impl Sudo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// We need separate `SudoKind` definitions for windows and unix,
|
// On unix we use `SudoKind::Sudo`, and on windows `SudoKind::WinSudo`.
|
||||||
// so that we can have serde instantiate `WinSudo` on windows and
|
// We always define both though, so that we don't have to put
|
||||||
// `Sudo` on unix when reading "sudo" from the config file.
|
// #[cfg(...)] everywhere.
|
||||||
// NOTE: when adding a new variant or otherwise changing `SudoKind`,
|
|
||||||
// make sure to keep both definitions in sync.
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, Display, Deserialize)]
|
#[derive(Clone, Copy, Debug, Display, Deserialize)]
|
||||||
#[serde(rename_all = "lowercase")]
|
#[serde(rename_all = "lowercase")]
|
||||||
#[strum(serialize_all = "lowercase")]
|
#[strum(serialize_all = "lowercase")]
|
||||||
#[cfg(target_os = "windows")]
|
|
||||||
pub enum SudoKind {
|
pub enum SudoKind {
|
||||||
Doas,
|
// On unix, "sudo" in the config file means Sudo
|
||||||
#[expect(unused, reason = "Sudo is unix-only")]
|
#[cfg(not(windows))]
|
||||||
#[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,
|
|
||||||
Sudo,
|
Sudo,
|
||||||
|
// and WinSudo is skipped, making it unused.
|
||||||
|
#[cfg(not(windows))]
|
||||||
#[expect(unused, reason = "WinSudo is windows-only")]
|
#[expect(unused, reason = "WinSudo is windows-only")]
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
WinSudo,
|
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,
|
Gsudo,
|
||||||
Pkexec,
|
Pkexec,
|
||||||
Run0,
|
Run0,
|
||||||
Please,
|
Please,
|
||||||
|
/// A "no-op" sudo, used when topgrade itself is running as root
|
||||||
|
Null,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl SudoKind {
|
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 {
|
match self {
|
||||||
SudoKind::Doas => "doas",
|
SudoKind::Doas => Some("doas"),
|
||||||
SudoKind::Sudo => "sudo",
|
SudoKind::Sudo => Some("sudo"),
|
||||||
// hardcode the path to ensure we find Windows Sudo
|
SudoKind::WinSudo => Some(r"C:\Windows\System32\sudo.exe"),
|
||||||
// rather than gsudo masquerading as sudo
|
SudoKind::Gsudo => Some("gsudo"),
|
||||||
SudoKind::WinSudo => r"C:\Windows\System32\sudo.exe",
|
SudoKind::Pkexec => Some("pkexec"),
|
||||||
SudoKind::Gsudo => "gsudo",
|
SudoKind::Run0 => Some("run0"),
|
||||||
SudoKind::Pkexec => "pkexec",
|
SudoKind::Please => Some("please"),
|
||||||
SudoKind::Run0 => "run0",
|
SudoKind::Null => None,
|
||||||
SudoKind::Please => "please",
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Find the full path to the "sudo" binary, if it exists on the system.
|
||||||
fn which(self) -> Option<PathBuf> {
|
fn which(self) -> Option<PathBuf> {
|
||||||
which(self.binary_name())
|
match self.binary_name() {
|
||||||
|
Some(name) => which(name),
|
||||||
|
None => None,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user