183 lines
6.3 KiB
Rust
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)
|
|
}
|
|
}
|