use std::{
collections::HashMap,
env,
ffi::OsString,
fs,
path::{Path, PathBuf},
};
use crate::common::{error::Error, resolve::CurrentUser};
use crate::exec::{RunOptions, Umask};
use crate::log::user_warn;
use crate::system::{Group, User};
use crate::{common::resolve::is_valid_executable, system::interface::UserId};
type Environment = HashMap<OsString, OsString>;
use super::cli::SuRunOptions;
const VALID_LOGIN_SHELLS_LIST: &str = "/etc/shells";
const FALLBACK_LOGIN_SHELL: &str = "/bin/sh";
const PATH_DEFAULT: &str = "/usr/local/bin:/usr/bin:/bin:/usr/local/games:/usr/games";
const PATH_DEFAULT_ROOT: &str = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
#[derive(Debug)]
pub(crate) struct SuContext {
command: PathBuf,
arguments: Vec<OsString>,
pub(crate) options: SuRunOptions,
pub(crate) environment: Environment,
pub(crate) user: User,
pub(crate) requesting_user: CurrentUser,
group: Group,
}
fn is_restricted(shell: &Path) -> bool {
if let Some(pattern) = shell.as_os_str().to_str() {
if let Ok(contents) = fs::read_to_string(VALID_LOGIN_SHELLS_LIST) {
return !contents.lines().any(|l| l == pattern);
} else {
return FALLBACK_LOGIN_SHELL != pattern;
}
}
true
}
impl SuContext {
pub(crate) fn from_env(options: SuRunOptions) -> Result<SuContext, Error> {
let mut environment = if options.login {
Environment::default()
} else {
env::vars_os().collect::<Environment>()
};
if options.login {
if let Some(value) = env::var_os("TERM") {
environment.insert("TERM".into(), value);
}
for name in options.whitelist_environment.iter() {
if let Some(value) = env::var_os(name) {
environment.insert(name.into(), value);
}
}
}
let requesting_user = CurrentUser::resolve()?;
let mut user = User::from_name(options.user.as_cstr())?
.ok_or_else(|| Error::UserNotFound(options.user.clone().into()))?;
let is_current_root = User::real_uid() == UserId::ROOT;
let is_target_root = options.user == "root";
if !is_current_root && (!options.supp_group.is_empty() || !options.group.is_empty()) {
return Err(Error::Options(
"only root can specify alternative groups".to_owned(),
));
}
let mut group = user.primary_group()?;
if !options.supp_group.is_empty() || !options.group.is_empty() {
user.groups.clear();
}
for group_name in options.group.iter() {
let primary_group = Group::from_name(group_name.as_cstr())?
.ok_or_else(|| Error::GroupNotFound(group_name.clone().into()))?;
group = primary_group.clone();
user.groups.insert(0, primary_group.gid);
}
for (index, group_name) in options.supp_group.iter().enumerate() {
let supp_group = Group::from_name(group_name.as_cstr())?
.ok_or_else(|| Error::GroupNotFound(group_name.clone().into()))?;
if index == 0 && options.group.is_empty() {
group = supp_group.clone();
}
user.groups.push(supp_group.gid);
}
let user_shell = user.shell.clone();
let mut command = options
.shell
.as_ref()
.cloned()
.or_else(|| {
if options.preserve_environment && is_current_root {
environment.get(&OsString::from("SHELL")).map(|v| v.into())
} else {
None
}
})
.unwrap_or(user_shell.clone());
if is_restricted(user_shell.as_path()) && !is_current_root {
user_warn!(
"using restricted shell {path}",
path = user_shell.as_os_str().to_string_lossy()
);
command = user_shell;
}
if !command.exists() {
return Err(Error::CommandNotFound(command));
}
if !is_valid_executable(&command) {
return Err(Error::InvalidCommand(command));
}
let arguments = if let Some(command) = &options.command {
vec!["-c".into(), command.into()]
} else {
options.arguments.clone()
};
if options.login {
environment.insert(
"PATH".into(),
if is_target_root {
PATH_DEFAULT_ROOT
} else {
PATH_DEFAULT
}
.into(),
);
}
if !options.preserve_environment {
environment.insert("HOME".into(), user.home.clone().into());
environment.insert("SHELL".into(), command.clone().into());
if !is_target_root || options.login {
environment.insert("USER".into(), options.user.clone().into());
environment.insert("LOGNAME".into(), options.user.clone().into());
}
}
Ok(SuContext {
command,
arguments,
options,
environment,
user,
requesting_user,
group,
})
}
}
impl SuContext {
pub(crate) fn as_run_options(&self) -> RunOptions<'_> {
RunOptions {
command: &self.command,
arguments: &self.arguments,
arg0: None,
chdir: None,
is_login: self.options.login,
user: &self.user,
group: &self.group,
umask: Umask::Preserve,
background: false,
use_pty: true,
noexec: false,
}
}
}
#[cfg(test)]
mod tests {
use std::path::PathBuf;
use crate::{
common::Error,
su::cli::{SuAction, SuOptions, SuRunOptions},
};
use super::SuContext;
fn get_options(args: &[&str]) -> SuRunOptions {
let mut args = args.iter().map(|s| s.to_string()).collect::<Vec<String>>();
args.insert(0, "/bin/su".to_string());
let SuAction::Run(options) = SuOptions::parse_arguments(args)
.unwrap()
.validate()
.unwrap()
else {
panic!();
};
options
}
#[test]
fn su_to_root() {
let options = get_options(&["root"]);
let context = SuContext::from_env(options).unwrap();
assert_eq!(context.user.name, "root");
}
#[test]
fn group_as_non_root() {
let options = get_options(&["-g", "root"]);
let result = SuContext::from_env(options);
let expected = Error::Options("only root can specify alternative groups".to_owned());
assert!(result.is_err());
assert_eq!(format!("{}", result.err().unwrap()), format!("{expected}"));
}
#[test]
fn invalid_shell() {
let options = get_options(&["-s", "/not/a/shell"]);
let result = SuContext::from_env(options);
let expected = Error::CommandNotFound(PathBuf::from("/not/a/shell"));
assert!(result.is_err());
assert_eq!(format!("{}", result.err().unwrap()), format!("{expected}"));
}
}