#[cfg(feature = "serde")]
mod serde_support;
use crate::Error;
use once_cell::sync::Lazy;
use std::{
collections::HashMap,
ffi::{OsStr, OsString},
io,
sync::Mutex,
};
use pwd_grp::{PwdGrp, PwdGrpProvider};
type Id = u32;
#[derive(Default, Debug)]
struct TrustedUsersCache<U: PwdGrpProvider> {
pwd_grp: U,
trusted_uid: HashMap<TrustedUser, Option<Id>>,
trusted_gid: HashMap<TrustedGroup, Option<Id>>,
}
static CACHE: Lazy<Mutex<TrustedUsersCache<PwdGrp>>> =
Lazy::new(|| Mutex::new(TrustedUsersCache::default()));
fn handle_pwd_error(e: io::Error) -> Error {
Error::PasswdGroupIoError(e.into())
}
fn get_self_named_gid_impl<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<u32>> {
let Some(username) = get_own_username(userdb)? else {
return Ok(None);
};
let Some(group) = userdb.getgrnam::<Vec<u8>>(username)? else {
return Ok(None);
};
Ok(if cur_groups()?.contains(&group.gid) {
Some(group.gid)
} else {
None
})
}
fn get_own_username<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<Vec<u8>>> {
use std::os::unix::ffi::OsStringExt as _;
let my_uid = userdb.getuid();
if let Some(username) = std::env::var_os("USER") {
let username = username.into_vec();
if let Some(passwd) = userdb.getpwnam::<Vec<u8>>(&username)? {
if passwd.uid == my_uid {
return Ok(Some(username));
}
}
}
if let Some(passwd) = userdb.getpwuid(my_uid)? {
if passwd.uid == my_uid {
return Ok(Some(passwd.name));
}
}
Ok(None)
}
fn cur_groups() -> io::Result<Vec<u32>> {
PwdGrp.getgroups()
}
#[derive(Clone, Default, Debug, Eq, PartialEq, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "serde_support::Serde", into = "serde_support::Serde")
)]
#[non_exhaustive]
pub enum TrustedUser {
None,
#[default]
Current,
Id(u32),
Name(OsString),
}
impl From<u32> for TrustedUser {
fn from(val: u32) -> Self {
TrustedUser::Id(val)
}
}
impl From<OsString> for TrustedUser {
fn from(val: OsString) -> Self {
TrustedUser::Name(val)
}
}
impl From<&OsStr> for TrustedUser {
fn from(val: &OsStr) -> Self {
val.to_owned().into()
}
}
impl From<String> for TrustedUser {
fn from(val: String) -> Self {
OsString::from(val).into()
}
}
impl From<&str> for TrustedUser {
fn from(val: &str) -> Self {
val.to_owned().into()
}
}
impl TrustedUser {
pub(crate) fn get_uid(&self) -> Result<Option<u32>, Error> {
let mut cache = CACHE.lock().expect("poisoned lock");
if let Some(got) = cache.trusted_uid.get(self) {
return Ok(*got);
}
let calculated = self.get_uid_impl(&cache.pwd_grp)?;
cache.trusted_uid.insert(self.clone(), calculated);
Ok(calculated)
}
fn get_uid_impl<U: PwdGrpProvider>(&self, userdb: &U) -> Result<Option<u32>, Error> {
use std::os::unix::ffi::OsStrExt as _;
match self {
TrustedUser::None => Ok(None),
TrustedUser::Current => Ok(Some(userdb.getuid())),
TrustedUser::Id(id) => Ok(Some(*id)),
TrustedUser::Name(name) => userdb
.getpwnam(name.as_bytes())
.map_err(handle_pwd_error)?
.map(|u: pwd_grp::Passwd<Vec<u8>>| Some(u.uid))
.ok_or_else(|| Error::NoSuchUser(name.to_string_lossy().into_owned())),
}
}
}
#[derive(Clone, Debug, Default, Eq, PartialEq, Hash)]
#[cfg_attr(
feature = "serde",
derive(serde::Serialize, serde::Deserialize),
serde(try_from = "serde_support::Serde", into = "serde_support::Serde")
)]
#[non_exhaustive]
pub enum TrustedGroup {
None,
#[default]
SelfNamed,
Id(u32),
Name(OsString),
}
impl From<u32> for TrustedGroup {
fn from(val: u32) -> Self {
TrustedGroup::Id(val)
}
}
impl From<OsString> for TrustedGroup {
fn from(val: OsString) -> TrustedGroup {
TrustedGroup::Name(val)
}
}
impl From<&OsStr> for TrustedGroup {
fn from(val: &OsStr) -> TrustedGroup {
val.to_owned().into()
}
}
impl From<String> for TrustedGroup {
fn from(val: String) -> TrustedGroup {
OsString::from(val).into()
}
}
impl From<&str> for TrustedGroup {
fn from(val: &str) -> TrustedGroup {
val.to_owned().into()
}
}
impl TrustedGroup {
pub(crate) fn get_gid(&self) -> Result<Option<u32>, Error> {
let mut cache = CACHE.lock().expect("poisoned lock");
if let Some(got) = cache.trusted_gid.get(self) {
return Ok(*got);
}
let calculated = self.get_gid_impl(&cache.pwd_grp)?;
cache.trusted_gid.insert(self.clone(), calculated);
Ok(calculated)
}
fn get_gid_impl<U: PwdGrpProvider>(&self, userdb: &U) -> Result<Option<u32>, Error> {
use std::os::unix::ffi::OsStrExt as _;
match self {
TrustedGroup::None => Ok(None),
TrustedGroup::SelfNamed => get_self_named_gid_impl(userdb).map_err(handle_pwd_error),
TrustedGroup::Id(id) => Ok(Some(*id)),
TrustedGroup::Name(name) => userdb
.getgrnam(name.as_bytes())
.map_err(handle_pwd_error)?
.map(|g: pwd_grp::Group<Vec<u8>>| Some(g.gid))
.ok_or_else(|| Error::NoSuchGroup(name.to_string_lossy().into_owned())),
}
}
}
#[cfg(test)]
mod test {
#![allow(clippy::bool_assert_comparison)]
#![allow(clippy::clone_on_copy)]
#![allow(clippy::dbg_macro)]
#![allow(clippy::mixed_attributes_style)]
#![allow(clippy::print_stderr)]
#![allow(clippy::print_stdout)]
#![allow(clippy::single_char_pattern)]
#![allow(clippy::unwrap_used)]
#![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)]
#![allow(clippy::needless_pass_by_value)]
use super::*;
use pwd_grp::mock::MockPwdGrpProvider;
type Id = u32;
fn mock_users() -> MockPwdGrpProvider {
let mock = MockPwdGrpProvider::new();
mock.set_uids(413.into());
mock
}
fn add_user(mock: &MockPwdGrpProvider, uid: Id, name: &str, gid: Id) {
mock.add_to_passwds([pwd_grp::Passwd::<String> {
name: name.into(),
uid,
gid,
..pwd_grp::Passwd::blank()
}]);
}
fn add_group(mock: &MockPwdGrpProvider, gid: Id, name: &str) {
mock.add_to_groups([pwd_grp::Group::<String> {
name: name.into(),
gid,
..pwd_grp::Group::blank()
}]);
}
#[test]
fn groups() {
let groups = cur_groups().unwrap();
let cur_gid = pwd_grp::getgid();
if groups.is_empty() {
return;
}
assert!(groups.contains(&cur_gid));
}
#[test]
fn username_real() {
let cache = CACHE.lock().expect("poisoned lock");
let uname = get_own_username(&cache.pwd_grp)
.unwrap()
.expect("Running on a misconfigured host");
let user = PwdGrp.getpwnam::<Vec<u8>>(&uname).unwrap().unwrap();
assert_eq!(user.name, uname);
assert_eq!(user.uid, PwdGrp.getuid());
}
#[test]
fn username_from_env() {
let Ok(username_s) = std::env::var("USER")
else {
return;
};
let username = username_s.as_bytes().to_vec();
let other_name = format!("{}2", &username_s);
let db = mock_users();
add_user(&db, 413, &username_s, 413);
add_user(&db, 999, &other_name, 999);
let found = get_own_username(&db).unwrap();
assert_eq!(found.as_ref(), Some(&username));
let db = mock_users();
add_user(&db, 999, &username_s, 999);
add_user(&db, 413, &other_name, 413);
let found = get_own_username(&db).unwrap();
assert_eq!(found, Some(other_name.clone().into_bytes()));
let db = mock_users();
add_user(&db, 999413, &other_name, 999);
let found = get_own_username(&db).unwrap();
assert!(found.is_none());
}
#[test]
fn username_ignoring_env() {
let db = mock_users();
add_user(&db, 413, "aranea", 413413);
add_user(&db, 415, "notyouru!sername", 413413);
let found = get_own_username(&db).unwrap();
assert_eq!(found, Some(b"aranea".to_vec()));
let db = mock_users();
add_user(&db, 999413, "notyourn!ame", 999);
let found = get_own_username(&db).unwrap();
assert!(found.is_none());
}
#[test]
fn selfnamed() {
let cur_groups = cur_groups().unwrap();
if cur_groups.is_empty() {
return;
}
let not_our_gid = (1..65536)
.find(|n| !cur_groups.contains(n))
.expect("We are somehow in all groups 1..65535!");
let db = mock_users();
add_user(&db, 413, "aranea", 413413);
add_group(&db, 413413, "serket");
let found = get_self_named_gid_impl(&db).unwrap();
assert!(found.is_none());
let db = mock_users();
add_user(&db, 413, "aranea", 413413);
add_group(&db, not_our_gid, "aranea");
let found = get_self_named_gid_impl(&db).unwrap();
assert!(found.is_none());
let db = mock_users();
add_user(&db, 413, "aranea", 413413);
add_group(&db, cur_groups[0], "aranea");
let found = get_self_named_gid_impl(&db).unwrap();
assert_eq!(found, Some(cur_groups[0]));
}
#[test]
fn lookup_id() {
let db = mock_users();
add_user(&db, 413, "aranea", 413413);
add_group(&db, 33, "nepeta");
assert_eq!(TrustedUser::None.get_uid_impl(&db).unwrap(), None);
assert_eq!(TrustedUser::Current.get_uid_impl(&db).unwrap(), Some(413));
assert_eq!(TrustedUser::Id(413).get_uid_impl(&db).unwrap(), Some(413));
assert_eq!(
TrustedUser::Name("aranea".into())
.get_uid_impl(&db)
.unwrap(),
Some(413)
);
assert!(TrustedUser::Name("ac".into()).get_uid_impl(&db).is_err());
assert_eq!(TrustedGroup::None.get_gid_impl(&db).unwrap(), None);
assert_eq!(TrustedGroup::Id(33).get_gid_impl(&db).unwrap(), Some(33));
assert_eq!(
TrustedGroup::Name("nepeta".into())
.get_gid_impl(&db)
.unwrap(),
Some(33)
);
assert!(TrustedGroup::Name("ac".into()).get_gid_impl(&db).is_err());
}
}