feat: add command su

Signed-off-by: sisungo <[email protected]>
This commit is contained in:
2026-07-19 16:09:03 +08:00
parent c72134a89e
commit ccd4288c79
4 changed files with 193 additions and 23 deletions
+3 -19
View File
@@ -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(())
}
+24 -3
View File
@@ -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<Client> {
+134
View File
@@ -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<OsString>,
}
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);
}
+32 -1
View File
@@ -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<Uui
Some(client.get_group_info(args).ok()?.uuid)
}
pub fn authenticate(
client: &mut Client,
user: Uuid,
method: Option<String>,
mode: &str,
) -> anyhow::Result<bool> {
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())
}