From ccd4288c7967c8a85a6f2f94fc0aee3f1934f9de Mon Sep 17 00:00:00 2001 From: sisungo Date: Sun, 19 Jul 2026 16:09:03 +0800 Subject: [PATCH] feat: add command su Signed-off-by: sisungo --- bin/account/src/authenticate.rs | 22 +----- bin/account/src/main.rs | 27 ++++++- bin/account/src/su.rs | 134 ++++++++++++++++++++++++++++++++ bin/account/src/util.rs | 33 +++++++- 4 files changed, 193 insertions(+), 23 deletions(-) create mode 100644 bin/account/src/su.rs diff --git a/bin/account/src/authenticate.rs b/bin/account/src/authenticate.rs index 43616f4..8a56509 100644 --- a/bin/account/src/authenticate.rs +++ b/bin/account/src/authenticate.rs @@ -21,25 +21,9 @@ pub fn main(cli: Cli) -> anyhow::Result<()> { let mut client = client()?; let user = uuid_by_one_user_filter(&mut client, &cli.user) .ok_or_else(|| anyhow!("user \"{}\" not found", cli.user))?; - let available_methods = client.get_user_auth_methods(GetUserAuthMethodsArgs { user })?; - let main_method = available_methods - .iter() - .find(|x| x.flags.contains(UserAuthMethodFlags::MAIN)); - let Some(method) = cli.method.or(main_method.map(|x| x.name.clone())) else { - return Err(anyhow!("no auth method available")); - }; - if !available_methods.iter().any(|x| x.name == method) { - return Err(anyhow!("unregistered auth method {method}")); + let ret = crate::util::authenticate(&mut client, user, cli.method, &cli.mode)?; + if !ret { + std::process::exit(1); } - let executable = client.get_auth_method_path(GetAuthMethodPathArgs { name: method })?; - let args = AuthCli::Authenticate(AuthenticateArgs { - user_uuid: user, - mode: cli.mode, - }) - .compose(); - std::process::Command::new(executable) - .args(args) - .spawn()? - .wait()?; Ok(()) } diff --git a/bin/account/src/main.rs b/bin/account/src/main.rs index 1c1aec8..c208415 100644 --- a/bin/account/src/main.rs +++ b/bin/account/src/main.rs @@ -2,6 +2,7 @@ mod authenticate; mod create_group; mod create_user; mod info; +mod su; mod update_auth; mod util; @@ -27,8 +28,14 @@ pub enum Cli { } fn main() { - let cli = Cli::parse(); + // Invoke non-default utility if required + if progname() == "su" { + su::main(); + std::process::exit(0); + } + // Invoke the default `account` utility + let cli = Cli::parse(); let result = match cli { Cli::Info(cli) => info::main(cli), Cli::CreateUser(cli) => create_user::main(cli), @@ -36,15 +43,29 @@ fn main() { Cli::Authenticate(cli) => authenticate::main(cli), Cli::UpdateAuth(cli) => update_auth::main(cli), }; - if let Err(err) = result { eprintln!("error: {err}"); std::process::exit(1); } } +fn progname() -> String { + std::env::args() + .next() + .as_deref() + .unwrap_or_else(|| "account") + .split('/') + .last() + .unwrap() + .into() +} + +fn has_root_privs() -> bool { + unsafe { libc::geteuid() == 0 } +} + fn is_suid_mode() -> bool { - unsafe { libc::getuid() != libc::geteuid() && libc::geteuid() == 0 } + unsafe { libc::getuid() != libc::geteuid() && has_root_privs() } } fn client() -> std::io::Result { diff --git a/bin/account/src/su.rs b/bin/account/src/su.rs new file mode 100644 index 0000000..b0e3469 --- /dev/null +++ b/bin/account/src/su.rs @@ -0,0 +1,134 @@ +use semios_account::{protocol::GetUserInfoArgs, wellknown::CLI_LOGIN_SHELL}; + +use crate::client; +use std::{ffi::OsString, os::unix::process::CommandExt}; + +#[derive(Debug)] +struct Cli { + login: bool, + preserve_environment: bool, + fast: bool, + user: String, + command: Vec, +} +impl Cli { + const USAGE: &str = "usage: su [options] [-] [login [args]]"; + + fn parse() -> Self { + let mut this = Self { + login: false, + preserve_environment: false, + fast: true, + user: "root".into(), + command: Vec::new(), + }; + + let mut positional = false; + + for i in std::env::args_os().skip(1) { + let bytes = i.as_encoded_bytes(); + if positional { + this.command.push(i); + continue; + } + if bytes == b"--login" { + this.login = true; + } else if bytes == b"--preserve-environment" { + this.preserve_environment = true; + } else if bytes == b"--fast" { + this.fast = true; + } else if bytes.starts_with(b"--") { + Self::parse_error(bytes); + } else if bytes.starts_with(b"-") { + if i == "-" { + this.login = true; + continue; + } + for &byte in &bytes[1..] { + match byte { + b'l' => { + this.login = true; + } + b'm' | b'p' => { + this.preserve_environment = true; + } + b'f' => { + this.fast = true; + } + _ => Self::parse_error(&[byte]), + }; + } + } else { + debug_assert!(!positional); + positional = true; + this.user = String::from_utf8_lossy(bytes).to_string(); + } + } + + this + } + + fn parse_error(bytes: &[u8]) -> ! { + eprintln!("su: illegal option -- {}", String::from_utf8_lossy(bytes)); + eprintln!("{}", Self::USAGE); + std::process::exit(1); + } +} + +pub fn main() { + let cli = Cli::parse(); + let mut client = match client() { + Ok(val) => val, + Err(err) => { + eprintln!("su: Service error: {err}"); + std::process::exit(1); + } + }; + let Ok(user_info) = client.get_user_info(GetUserInfoArgs::Username(cli.user.clone())) else { + eprintln!("su: user {} does not exist", cli.user); + std::process::exit(1); + }; + let auth_result = + crate::util::authenticate(&mut client, user_info.uuid, Some("password".into()), "CLI"); + match auth_result { + Ok(true) => (), + Ok(false) => std::process::exit(1), + Err(err) => { + eprintln!("su: cannot authenticate user: {err}"); + std::process::exit(1); + } + }; + let Some(uid) = user_info.host_uid else { + eprintln!("su: user {} is not configured on this host", cli.user); + std::process::exit(1); + }; + let Some(shell) = user_info.defaults.get(CLI_LOGIN_SHELL) else { + eprintln!("su: user {} has no login shell", cli.user); + std::process::exit(1); + }; + let home_dir = user_info.home_directory.unwrap_or_else(|| "/".into()); + + if !crate::has_root_privs() { + eprintln!("su: the `su` binary is only available with root privileges"); + std::process::exit(1); + } + + // Set environment variables + if !cli.preserve_environment || cli.login { + // SAFETY: The program is single-threaded, so no data race is possible here. + unsafe { + std::env::set_var("HOME", &home_dir); + std::env::set_var("USER", &cli.user); + std::env::set_var("SHELL", &shell); + std::env::set_var("LOGNAME", &cli.user); + } + } + + let err = std::process::Command::new(shell) + .uid(uid) + .args(cli.command) + .exec(); + + eprintln!("su: exec error: {err}"); + std::process::exit(1); +} diff --git a/bin/account/src/util.rs b/bin/account/src/util.rs index 6f0d423..5373748 100644 --- a/bin/account/src/util.rs +++ b/bin/account/src/util.rs @@ -1,6 +1,8 @@ +use anyhow::anyhow; use semios_account::{ + auth::{AuthCli, AuthenticateArgs, UserAuthMethodFlags}, client::Client, - protocol::{GetGroupInfoArgs, GetUserInfoArgs}, + protocol::{GetAuthMethodPathArgs, GetGroupInfoArgs, GetUserAuthMethodsArgs, GetUserInfoArgs}, record::HostUid, }; use std::fmt::Display; @@ -68,3 +70,32 @@ pub fn uuid_by_one_group_filter(client: &mut Client, filter: &str) -> Option, + mode: &str, +) -> anyhow::Result { + let available_methods = client.get_user_auth_methods(GetUserAuthMethodsArgs { user })?; + let main_method = available_methods + .iter() + .find(|x| x.flags.contains(UserAuthMethodFlags::MAIN)); + let Some(method) = method.or(main_method.map(|x| x.name.clone())) else { + return Err(anyhow!("no auth method available")); + }; + if !available_methods.iter().any(|x| x.name == method) { + return Err(anyhow!("unregistered auth method {method}")); + } + let executable = client.get_auth_method_path(GetAuthMethodPathArgs { name: method })?; + let args = AuthCli::Authenticate(AuthenticateArgs { + user_uuid: user, + mode: mode.to_string(), + }) + .compose(); + let wait_status = std::process::Command::new(executable) + .args(args) + .spawn()? + .wait()?; + Ok(wait_status.success()) +}