src/world/bin/su/src/bin/su.rs

183 lines
6.3 KiB
Rust

#![warn(clippy::all, clippy::pedantic)]
use clap::{Arg, ArgAction, Command};
use libcrypt_rs::Crypt;
use std::{
env,
ffi::{CStr, CString},
os::unix::process::CommandExt,
path::Path,
process, str,
};
use su::Error;
fn cli() -> Command {
clap::Command::new("su")
.about("run a command with a substitute user and group ID")
.author("Nathan Fisher")
.version(env!("CARGO_PKG_VERSION"))
.before_long_help(
"su allows to run commands with a substitute user and group ID. When \
called without arguments, su defaults to running an interactive shell as \
root. For backward compatibility su defaults to not change the current \
directory and to only set the environment variables HOME and SHELL (plus \
USER and LOGNAME if the target username is not root).",
)
.args([
Arg::new("login")
.short('l')
.long("login")
.help(
"Starts the shell as login shell with an environment similar to a real login.",
)
.action(ArgAction::SetTrue),
Arg::new("preserve")
.short('p')
.long("preserve")
.visible_short_alias('m')
.help("Preserves the whole environment.")
.conflicts_with("login")
.action(ArgAction::SetTrue),
Arg::new("command")
.short('c')
.long("command")
.help("Pass command to the shell")
.num_args(1),
Arg::new("shell")
.short('s')
.long("shell")
.help("Run the specified shell instead of the default.")
.long_help(
"Run the specified shell instead of the default. The shell to
run is selected according to the following rules, in order:
• the shell specified with --shell
• the shell specified in the environment variable SHELL,
if the --preserve-environment option is used
• the shell listed in the passwd entry of the target user
• /bin/sh"
)
.num_args(1),
Arg::new("user").default_value("root").num_args(1),
])
}
#[allow(clippy::similar_names)]
fn main() -> Result<(), Error> {
let mut cmd = {
let matches = cli().get_matches();
// theoretically this can never fail since we have a default value, but
// I'm not a fan of `unwrap()`
let Some(user) = matches.get_one::<String>("user") else {
return Err("no user given".into());
};
'check_user: {
let uid = unsafe { libc::getuid() };
if uid == 0 {
break 'check_user;
}
if let Ok(groups) = su::get_group_names() {
if groups.contains(&"wheel".to_string()) {
validate_pw(user)?;
break 'check_user;
}
}
return Err(Error::PermissionDenied);
}
let pw = unsafe {
let pw = libc::getpwnam(CString::new(user.as_str())?.as_ptr());
if pw.is_null() {
return Err(Error::Other("getpwnam: null pointer".to_string()));
}
*pw
};
let uid = pw.pw_uid;
let gid = pw.pw_gid;
let (shell, args) = if let Some(s) = matches.get_one::<String>("command") {
let mut it = s.split_whitespace();
(it.next().unwrap().to_string(), Some(it))
} else if let Some(s) = matches.get_one::<String>("shell") {
(s.to_string(), None)
} else if matches.get_flag("preserve") {
if let Ok(s) = env::var("SHELL") {
(s, None)
} else {
(get_user_shell(&pw).unwrap_or("/bin/sh").to_string(), None)
}
} else {
(get_user_shell(&pw).unwrap_or("/bin/sh").to_string(), None)
};
let mut cmd = process::Command::new(&shell);
cmd.uid(uid).gid(gid);
if let Some(args) = args {
cmd.args(&args.collect::<Vec<&str>>());
}
if matches.get_flag("login") {
let home = unsafe { CStr::from_ptr(pw.pw_dir).to_str()? };
let path = if uid == 0 {
"/usr/bin:/usr/sbin:/bin:/sbin"
} else {
"/usr/bin:/bin"
};
cmd.env_clear();
cmd.envs([
("SHELL", shell.as_str()),
("USER", user),
("LOGNAME", user),
("HOME", home),
("PATH", path),
]);
if let Ok(term) = env::var("TERM") {
cmd.env("TERM", term);
}
cmd.current_dir(home);
let shellpath = Path::new(&shell);
let shellname = shellpath
.file_name()
.ok_or(Error::Other("Unknown shell name".to_string()))?
.to_str()
.ok_or(Error::Other("Unknown shell name".to_string()))?;
match shellname {
"bash" | "zsh" | "ash" | "mksh" | "fish" | "csh" | "tcsh" | "ksh" | "pdksh"
| "rc" | "nu" => {
cmd.arg("-l");
}
_ => {}
}
}
cmd
};
Err(cmd.exec().into())
}
fn get_user_shell(pw: &libc::passwd) -> Result<&str, str::Utf8Error> {
if pw.pw_shell.is_null() {
Ok("/bin/sh")
} else {
unsafe { CStr::from_ptr(pw.pw_shell).to_str() }
}
}
fn validate_pw(user: &str) -> Result<(), Error> {
let oldpass = unsafe {
let spw = libc::getspnam(CString::new(user)?.as_ptr());
if spw.is_null() {
return Err(Error::Other("null pointer".to_string()));
}
CStr::from_ptr((*spw).sp_pwdp).to_str()?
};
// deny empty passwords
if oldpass.starts_with('\0') {
return Err(Error::InvalidPassword);
}
let pass = rpassword::prompt_password("Password: ")?;
let mut engine = Crypt::new();
// libcrypt gets the encryption salt from the old hashed password
engine.set_salt(oldpass.to_string())?;
engine.encrypt(pass)?;
if oldpass == engine.encrypted.as_str() {
Ok(())
} else {
Err(Error::IncorrectPassword)
}
}