From 47b51a8be06a6d084dfe78ca80335e825181546b Mon Sep 17 00:00:00 2001 From: Andre Toerien Date: Fri, 26 Sep 2025 14:49:12 +0200 Subject: [PATCH] feat: detect and warn if running as root --- Cargo.lock | 10 ++++++++++ Cargo.toml | 3 ++- config.example.toml | 7 +++++++ locales/app.yml | 18 ++++++++++++++++++ src/config.rs | 16 ++++++++++++++++ src/main.rs | 16 ++++++++++++++-- src/utils.rs | 16 ++++++++++++++++ 7 files changed, 83 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 4bf250df..fdcb463e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1453,6 +1453,15 @@ version = "2.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8f518f335dce6725a761382244631d86cf0ccb2863413590b31338feb467f9c3" +[[package]] +name = "is_elevated" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5299060ff5db63e788015dcb9525ad9b84f4fd9717ed2cbdeba5018cbf42f9b5" +dependencies = [ + "winapi", +] + [[package]] name = "is_terminal_polyfill" version = "1.70.0" @@ -2883,6 +2892,7 @@ dependencies = [ "glob", "home", "indexmap 2.9.0", + "is_elevated", "jetbrains-toolbox-updater", "merge", "nix 0.29.0", diff --git a/Cargo.toml b/Cargo.toml index 9471eaa6..d5bd7a81 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -78,9 +78,10 @@ rust-ini = "~0.21" self_update_crate = { version = "~0.40", default-features = false, optional = true, package = "self_update", features = ["archive-tar", "compression-flate2", "rustls"] } [target.'cfg(windows)'.dependencies] +is_elevated = "~0.1" +parselnk = "~0.1" self_update_crate = { version = "~0.40", default-features = false, optional = true, package = "self_update", features = ["archive-zip", "compression-zip-deflate", "rustls"] } winapi = { version = "~0.3", features = ["consoleapi", "wincon"] } -parselnk = "~0.1" [profile.release] lto = true diff --git a/config.example.toml b/config.example.toml index 720d4dd6..6540f726 100644 --- a/config.example.toml +++ b/config.example.toml @@ -6,6 +6,13 @@ [misc] +# On Unix systems, Topgrade should not be run as root, it +# will run commands with sudo or equivalent where needed. +# Set this to true to suppress the warning and confirmation +# prompt if Topgrade detects it is being run as root. +# (default: false) +# allow_root = false + # Run `sudo -v` to cache credentials at the start of the run # This avoids a blocking password prompt in the middle of an unattended run # (default: false) diff --git a/locales/app.yml b/locales/app.yml index a3492893..0c924570 100644 --- a/locales/app.yml +++ b/locales/app.yml @@ -1120,6 +1120,24 @@ _version: 2 zh_CN: "\n(R)重启\n(S)Shell\n(Q)退出" zh_TW: "\n(R)重新啟動\n(S)殼層\n(Q)退出" de: "\n(R) Neustarten\n(S)hell\n(Q)uit beenden" + +"Continue?": + en: "Continue?" + lt: "Tęsti?" + es: "¿Continuar?" + fr: "Continuer ?" + zh_CN: "继续?" + zh_TW: "繼續?" + de: "Fortfahren?" + +"Topgrade should not be run as root, it will run commands with sudo or equivalent where needed.": + en: "Topgrade should not be run as root, it will run commands with sudo or equivalent where needed." + lt: "Topgrade neturėtų būti paleistas kaip root, jis vykdys komandas su sudo ar atitikmeniu, kai to reikės." + es: "Topgrade no debe ejecutarse como root, ejecutará comandos con sudo o equivalente cuando sea necesario." + fr: "Topgrade ne doit pas être exécuté en tant que root, il exécutera les commandes avec sudo ou équivalent si nécessaire." + zh_CN: "Topgrade 不应以 root 身份运行,它会在需要时使用 sudo 或等效工具执行命令。" + zh_TW: "Topgrade 不應以 root 身份執行,它會在需要時使用 sudo 或等效工具執行命令。" + de: "Topgrade sollte nicht als Root ausgeführt werden, es führt Befehle mit sudo oder einem Äquivalent aus, wenn erforderlich." "Require sudo or counterpart but not found, skip": en: "Require sudo or counterpart but not found, skip" lt: "Reikalingas sudo arba atitikmuo, bet nerasta, praleidžiama" diff --git a/src/config.rs b/src/config.rs index 5d86d32c..a47f9e71 100644 --- a/src/config.rs +++ b/src/config.rs @@ -295,6 +295,8 @@ pub struct Vim { #[derive(Deserialize, Default, Debug, Merge)] #[serde(deny_unknown_fields)] pub struct Misc { + allow_root: Option, + pre_sudo: Option, sudo_command: Option, @@ -767,6 +769,10 @@ pub struct CommandLineArgs { #[arg(long = "show-skipped")] show_skipped: bool, + /// Suppress warning and confirmation prompt if running as root + #[arg(long = "allow-root")] + allow_root: bool, + /// Tracing filter directives. /// /// See: https://docs.rs/tracing-subscriber/latest/tracing_subscriber/filter/struct.EnvFilter.html#directives @@ -1535,6 +1541,16 @@ impl Config { .unwrap_or(true) } + pub fn allow_root(&self) -> bool { + self.opt.allow_root + || self + .config_file + .misc + .as_ref() + .and_then(|misc| misc.allow_root) + .unwrap_or(false) + } + pub fn sudo_command(&self) -> Option { self.config_file.misc.as_ref().and_then(|misc| misc.sudo_command) } diff --git a/src/main.rs b/src/main.rs index 7b48405c..a5fc46d5 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,7 +29,7 @@ use self::error::Upgraded; use self::steps::{remote::*, *}; #[allow(clippy::wildcard_imports)] use self::terminal::*; -use self::utils::{install_color_eyre, install_tracing, update_tracing}; +use self::utils::{install_color_eyre, install_tracing, is_elevated, update_tracing}; mod breaking_changes; mod command; @@ -134,6 +134,18 @@ fn run() -> Result<()> { } } + let elevated = is_elevated(); + + #[cfg(unix)] + if !config.allow_root() && elevated { + print_warning(t!( + "Topgrade should not be run as root, it will run commands with sudo or equivalent where needed." + )); + if !prompt_yesno(&t!("Continue?"))? { + exit(1) + } + } + #[cfg(target_os = "linux")] let distribution = linux::Distribution::detect(); @@ -157,7 +169,7 @@ fn run() -> Result<()> { if !should_skip() && first_run_of_major_release()? { print_breaking_changes(); - if prompt_yesno("Confirmed?")? { + if prompt_yesno(&t!("Continue?"))? { write_keep_file()?; } else { exit(1); diff --git a/src/utils.rs b/src/utils.rs index 4e2252a2..0b52f192 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -169,6 +169,22 @@ pub fn hostname() -> Result { .map(|output| output.stdout.trim().to_owned()) } +#[cfg(unix)] +pub fn is_elevated() -> bool { + let euid = nix::unistd::Uid::effective(); + debug!("Running with euid: {euid}"); + euid.is_root() +} + +#[cfg(windows)] +pub fn is_elevated() -> bool { + let elevated = is_elevated::is_elevated(); + if elevated { + debug!("Detected elevated process"); + } + elevated +} + pub mod merge_strategies { use merge::Merge;