use crate::system::interface::UserId;
use crate::system::{Group, User};
use core::fmt;
use std::{
env,
ffi::CStr,
fs, io, ops,
os::unix::prelude::MetadataExt,
path::{Path, PathBuf},
str::FromStr,
};
use super::SudoString;
use super::{Error, context::LaunchType};
#[derive(PartialEq, Debug)]
enum NameOrId<'a, T: FromStr> {
Name(&'a SudoString),
Id(T),
}
impl<'a, T: FromStr> NameOrId<'a, T> {
pub fn parse(input: &'a SudoString) -> Option<Self> {
if input.is_empty() {
None
} else if let Some(stripped) = input.strip_prefix('#') {
stripped.parse::<T>().ok().map(|id| Self::Id(id))
} else {
Some(Self::Name(input))
}
}
}
#[derive(Clone)]
pub struct CurrentUser {
inner: User,
}
impl From<CurrentUser> for User {
fn from(value: CurrentUser) -> Self {
value.inner
}
}
impl fmt::Debug for CurrentUser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_tuple("CurrentUser").field(&self.inner).finish()
}
}
impl ops::Deref for CurrentUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl CurrentUser {
#[cfg(test)]
pub fn fake(user: User) -> Self {
Self { inner: user }
}
pub fn resolve() -> Result<Self, Error> {
Ok(Self {
inner: User::real()?.ok_or(Error::UserNotFound("current user".to_string()))?,
})
}
}
#[derive(Clone, Debug)]
pub struct AuthUser(User);
impl AuthUser {
pub fn from_current_user(user: CurrentUser) -> Self {
Self(user.inner)
}
pub fn resolve_root_for_rootpw() -> Result<Self, Error> {
Ok(Self(
User::from_uid(UserId::ROOT)?.ok_or(Error::UserNotFound("root".to_string()))?,
))
}
pub fn from_user_for_targetpw(user: User) -> Self {
Self(user)
}
}
impl ops::Deref for AuthUser {
type Target = User;
fn deref(&self) -> &Self::Target {
&self.0
}
}
type Shell = Option<PathBuf>;
pub(super) fn resolve_shell(
launch_type: LaunchType,
current_user: &User,
target_user: &User,
) -> Shell {
match launch_type {
LaunchType::Login => Some(target_user.shell.clone()),
LaunchType::Shell => Some(
env::var("SHELL")
.map(|s| s.into())
.unwrap_or_else(|_| current_user.shell.clone()),
),
LaunchType::Direct => None,
}
}
pub(crate) fn resolve_target_user_and_group(
target_user_name_or_id: &Option<SudoString>,
target_group_name_or_id: &Option<SudoString>,
current_user: &CurrentUser,
) -> Result<(User, Group), Error> {
let mut target_user =
resolve_from_name_or_id(target_user_name_or_id, User::from_name, User::from_uid)?;
let mut target_group =
resolve_from_name_or_id(target_group_name_or_id, Group::from_name, Group::from_gid)?;
match (&target_user_name_or_id, &target_group_name_or_id) {
(None, Some(_)) => {
target_user = Some(current_user.clone().into());
}
(Some(_), None) => {
if let Some(user) = &target_user {
target_group = Some(user.primary_group()?);
}
}
(None, None) => {
target_user = User::from_name(c"root")?;
target_group = Group::from_name(if cfg!(target_os = "linux") {
c"root"
} else {
c"wheel"
})?;
}
_ => {}
}
match (target_user, target_group) {
(Some(user), Some(group)) => {
Ok((user, group))
}
(Some(_), None) => Err(Error::GroupNotFound(
target_group_name_or_id
.as_deref()
.unwrap_or_default()
.to_string(),
)),
_ => Err(Error::UserNotFound(
target_user_name_or_id
.as_deref()
.unwrap_or_default()
.to_string(),
)),
}
}
fn resolve_from_name_or_id<T, I, E>(
input: &Option<SudoString>,
from_name: impl FnOnce(&CStr) -> Result<Option<T>, E>,
from_id: impl FnOnce(I) -> Result<Option<T>, E>,
) -> Result<Option<T>, E>
where
I: FromStr,
{
match input.as_ref().and_then(NameOrId::parse) {
Some(NameOrId::Name(name)) => from_name(name.as_cstr()),
Some(NameOrId::Id(id)) => from_id(id),
None => Ok(None),
}
}
pub(crate) fn is_valid_executable(path: &Path) -> bool {
if path.is_file() {
match fs::metadata(path) {
Ok(meta) => meta.mode() & 0o111 != 0,
_ => false,
}
} else {
false
}
}
pub(crate) fn resolve_path(command: &Path, path: &str) -> Option<PathBuf> {
path.split(':')
.map(Path::new)
.filter(|path| path.is_absolute())
.map(|path| path.join(command))
.find(|arg| is_valid_executable(arg))
}
#[cfg(test)]
mod tests {
use std::path::Path;
use crate::common::resolve::CurrentUser;
use crate::system::ROOT_GROUP_NAME;
use super::{NameOrId, is_valid_executable, resolve_path, resolve_target_user_and_group};
#[test]
fn test_resolve_path() {
let path = "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin";
assert!(is_valid_executable(
&resolve_path(Path::new("yes"), path).unwrap()
));
assert!(is_valid_executable(
&resolve_path(Path::new("whoami"), path).unwrap()
));
assert!(is_valid_executable(
&resolve_path(Path::new("env"), path).unwrap()
));
assert_eq!(resolve_path(Path::new("thisisnotonyourfs"), path), None);
assert_eq!(resolve_path(Path::new("thisisnotonyourfs"), "."), None);
}
#[test]
fn test_name_or_id() {
assert_eq!(NameOrId::<u32>::parse(&"".into()), None);
assert_eq!(
NameOrId::<u32>::parse(&"mies".into()),
Some(NameOrId::Name(&"mies".into()))
);
assert_eq!(
NameOrId::<u32>::parse(&"1337".into()),
Some(NameOrId::Name(&"1337".into()))
);
assert_eq!(
NameOrId::<u32>::parse(&"#1337".into()),
Some(NameOrId::Id(1337))
);
assert_eq!(NameOrId::<u32>::parse(&"#-1".into()), None);
}
#[test]
fn test_resolve_target_user_and_group() {
let current_user = CurrentUser::resolve().unwrap();
let (user, group) = resolve_target_user_and_group(&None, &None, ¤t_user).unwrap();
assert_eq!(user.name, "root");
assert_eq!(group.name.unwrap(), ROOT_GROUP_NAME);
let result =
resolve_target_user_and_group(&Some("non_existing_ghost".into()), &None, ¤t_user);
assert!(result.is_err());
let result =
resolve_target_user_and_group(&None, &Some("non_existing_ghost".into()), ¤t_user);
assert!(result.is_err());
let (user, group) =
resolve_target_user_and_group(&None, &Some(ROOT_GROUP_NAME.into()), ¤t_user)
.unwrap();
assert_eq!(user.name, current_user.name);
assert_eq!(group.name.unwrap(), ROOT_GROUP_NAME);
let (user, group) =
resolve_target_user_and_group(&Some(current_user.name.clone()), &None, ¤t_user)
.unwrap();
assert_eq!(user.name, current_user.name);
assert_eq!(group.gid, current_user.gid);
}
}
pub fn canonicalize<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
let reconstructed_path = canonicalize_newfile(path)?;
let _ = fs::metadata(&reconstructed_path)?;
Ok(reconstructed_path)
}
pub fn canonicalize_newfile<P: AsRef<Path>>(path: P) -> io::Result<PathBuf> {
let path = path.as_ref();
let Some(parent) = path.parent() else {
return Ok(path.to_path_buf());
};
let canon_path = fs::canonicalize(parent)?;
let reconstructed_path = if let Some(file_name) = path.file_name() {
canon_path.join(file_name)
} else {
canon_path
};
Ok(reconstructed_path)
}
#[cfg(test)]
mod test {
use super::canonicalize;
use std::path::Path;
#[test]
fn canonicalization() {
assert_eq!(canonicalize("/").unwrap(), Path::new("/"));
assert!(canonicalize("").is_err());
if cfg!(any(target_os = "linux")) {
assert!(Path::new("/bin").is_symlink());
assert!(Path::new("/usr/bin/unxz").is_symlink());
assert_eq!(
canonicalize("/usr/bin/unxz").unwrap(),
Path::new("/usr/bin/unxz")
);
assert_eq!(
canonicalize("/bin/unxz").unwrap(),
Path::new("/usr/bin/unxz")
);
} else if cfg!(target_os = "freebsd") {
assert!(Path::new("/usr/bin/pkill").is_symlink());
assert_eq!(
canonicalize("/usr/bin/pkill").unwrap(),
Path::new("/usr/bin/pkill")
);
assert_eq!(canonicalize("/bin/pkill").unwrap(), Path::new("/bin/pkill"));
} else {
panic!(
"canonicalization test not yet adapted for {}",
std::env::consts::OS
);
}
}
}