fix(sudo): set sudo flags depending on kind
This commit is contained in:
@@ -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}'"
|
||||
|
||||
22
src/error.rs
22
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();
|
||||
|
||||
|
||||
@@ -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<Executor> {
|
||||
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 {
|
||||
|
||||
162
src/sudo.rs
162
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 {
|
||||
let mut cmd = ctx.execute(&self.path);
|
||||
|
||||
if let SudoKind::Sudo = self.kind {
|
||||
cmd.arg("--preserve-env=DIFFPROG");
|
||||
pub fn execute<S: AsRef<OsStr>>(&self, ctx: &ExecutionContext, command: S) -> Result<Executor> {
|
||||
self.execute_opts(ctx, command, SudoExecuteOpts::default())
|
||||
}
|
||||
|
||||
if interactive {
|
||||
/// 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 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 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 {
|
||||
|
||||
Reference in New Issue
Block a user