fs-mistruct: switch from users to pwd-grp

users is unmaintained.  pwd-grp is the crate I have just written to
replace it. In this commit:

Change the cargo dependency and imports.

Replace the cacheing arrangements.  users has a built-in cache;
pwd-grp doesn't.  Now, instead of cashing individual lookups, we cache
the trusted user and trusted gid calculation results.
This saves on some syscalls, and is also more convenient to write.
(Mocking is still done via the dependency.)

Many systematic consequential changes of details:

 * The entrypoint names to the library are different:
   pwd-grp uses the names of the corresponding Unix functions.

 * pwd-grp's returned structs are transparent, so we don't
   call accessors for .uid(), .name(), etc.

 * pwd-grp's methods are much more often fallible
   (returning io::Result<Option<...>)

 * We're using the non-UTF-8 pwd-grp API, which means we must
   use turbofish syntax in some places.

 * The mocking API is a bit different.
This commit is contained in:
Ian Jackson 2023-07-12 18:30:36 +01:00
parent bf65b7763e
commit 5f46bacbb2
3 changed files with 96 additions and 52 deletions

24
Cargo.lock generated
View File

@ -1509,12 +1509,12 @@ dependencies = [
"educe", "educe",
"libc", "libc",
"once_cell", "once_cell",
"pwd-grp",
"serde", "serde",
"serde_json", "serde_json",
"tempfile", "tempfile",
"thiserror", "thiserror",
"toml 0.7.5", "toml 0.7.5",
"users",
"walkdir", "walkdir",
] ]
@ -2855,6 +2855,18 @@ dependencies = [
"unicode-ident", "unicode-ident",
] ]
[[package]]
name = "pwd-grp"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f051bd6fedf71a440d884906dd1c9b66c678e3a2882652b53c5ec8b0aa4268e4"
dependencies = [
"derive-adhoc",
"libc",
"paste",
"thiserror",
]
[[package]] [[package]]
name = "quick-error" name = "quick-error"
version = "1.2.3" version = "1.2.3"
@ -5121,16 +5133,6 @@ dependencies = [
"percent-encoding", "percent-encoding",
] ]
[[package]]
name = "users"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "24cc0f6d6f267b73e5a2cadf007ba8f9bc39c6a6f9666f8cf25ea809a153b032"
dependencies = [
"libc",
"log",
]
[[package]] [[package]]
name = "valuable" name = "valuable"
version = "0.1.0" version = "0.1.0"

View File

@ -31,7 +31,7 @@ libc = "0.2"
once_cell = "1" once_cell = "1"
[target.'cfg(all(unix, not(target_os="ios"), not(target_os="android")))'.dependencies] [target.'cfg(all(unix, not(target_os="ios"), not(target_os="android")))'.dependencies]
users = "0.11" pwd-grp = "0.1"
[dev-dependencies] [dev-dependencies]
serde_json = "1.0.50" serde_json = "1.0.50"

View File

@ -6,40 +6,60 @@ mod serde_support;
use crate::Error; use crate::Error;
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use std::{ use std::{
collections::HashMap,
ffi::{OsStr, OsString}, ffi::{OsStr, OsString},
io, io,
sync::Mutex, sync::Mutex,
}; };
// TODO Unix usernames are byte strings. OsStr and OsString are very awkward because
// Windows env vars etc. can be WTF-16. We should use Vec<u8> throughout, on Unix, where we can.
use std::os::unix::ffi::{OsStrExt as _, OsStringExt as _};
/// XXXX temporary alias use pwd_grp::{PwdGrp, PwdGrpProvider};
trait PwdGrpProvider: users::Users + users::Groups {}
impl<U: users::Users + users::Groups> PwdGrpProvider for U {}
/// Cached values of user db entries we've looked up. /// uids and gids, convenient type alias
type Id = u32;
/// Cache for the trusted uid/gid answers
#[derive(Default, Debug)]
struct TrustedUsersCache<U: PwdGrpProvider> {
/// The passwd/group provider (possibly mocked)
pwd_grp: U,
/// Cached trusted uid determination
trusted_uid: HashMap<TrustedUser, Option<Id>>,
/// Cached trusted gid determination
trusted_gid: HashMap<TrustedGroup, Option<Id>>,
}
/// Cached trusted id determinations
/// ///
/// Caching here saves time, AND makes our code testable. /// Caching here saves time - including passwd/group lookups, which can be slow enough
/// we don't want to do them often.
/// ///
/// Though this type has interior mutability, it isn't Sync, so we need to add a mutex. /// It isn't 100% correct since we don't track changes to the passwd/group databases.
static CACHE: Lazy<Mutex<users::UsersCache>> = Lazy::new(|| Mutex::new(users::UsersCache::new())); /// That might not be OK everywhere, but it is OK in this application.
static CACHE: Lazy<Mutex<TrustedUsersCache<PwdGrp>>> = Lazy::new(
|| Mutex::new(TrustedUsersCache::default())
);
/// Convert an [`io::Error `] representing a user/group handling failure into an [`Error`] /// Convert an [`io::Error `] representing a user/group handling failure into an [`Error`]
fn handle_pwd_error(e: io::Error) -> Error { fn handle_pwd_error(e: io::Error) -> Error {
Error::PasswdGroupIoError(e.into()) Error::PasswdGroupIoError(e.into())
} }
/// Like get_self_named_gid(), but use a provided user database. /// Obtain the gid of a group named after the current user
fn get_self_named_gid_impl<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<u32>> { 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(username) = get_own_username(userdb)? else { return Ok(None) };
let Some(group) = userdb.get_group_by_name(username.as_os_str()) let Some(group) = userdb.getgrnam::<Vec<u8>>(username.as_bytes())?
else { return Ok(None) }; else { return Ok(None) };
// TODO: Perhaps we should enforce a requirement that the group contains // TODO: Perhaps we should enforce a requirement that the group contains
// _only_ the current users. That's kinda tricky to do, though, without // _only_ the current users. That's kinda tricky to do, though, without
// walking the entire user db. // walking the entire user db.
Ok(if cur_groups()?.contains(&group.gid()) { Ok(if cur_groups()?.contains(&group.gid) {
Some(group.gid()) Some(group.gid)
} else { } else {
None None
}) })
@ -53,20 +73,20 @@ fn get_self_named_gid_impl<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<u
/// Failing that, we look for a user entry for our current UID. /// Failing that, we look for a user entry for our current UID.
#[allow(clippy::unnecessary_wraps)] // XXXX #[allow(clippy::unnecessary_wraps)] // XXXX
fn get_own_username<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<OsString>> { fn get_own_username<U: PwdGrpProvider>(userdb: &U) -> io::Result<Option<OsString>> {
let my_uid = userdb.get_current_uid(); let my_uid = userdb.getuid();
if let Some(username) = std::env::var_os("USER") { if let Some(username) = std::env::var_os("USER") {
if let Some(passwd) = userdb.get_user_by_name(username.as_os_str()) { if let Some(passwd) = userdb.getpwnam::<Vec<u8>>(username.as_bytes())? {
if passwd.uid() == my_uid { if passwd.uid == my_uid {
return Ok(Some(username)); return Ok(Some(username));
} }
} }
} }
if let Some(passwd) = userdb.get_user_by_uid(my_uid) { if let Some(passwd) = userdb.getpwuid::<Vec<u8>>(my_uid)? {
// This check should always pass, but let's be extra careful. // This check should always pass, but let's be extra careful.
if passwd.uid() == my_uid { if passwd.uid == my_uid {
return Ok(Some(passwd.name().to_owned())); return Ok(Some(OsString::from_vec(passwd.name)));
} }
} }
@ -84,7 +104,7 @@ fn cur_groups() -> io::Result<Vec<u32>> {
if n_groups <= 0 { if n_groups <= 0 {
return Ok(Vec::new()); return Ok(Vec::new());
} }
let mut buf: Vec<users::gid_t> = vec![0; n_groups as usize]; let mut buf: Vec<Id> = vec![0; n_groups as usize];
let n_groups2 = unsafe { libc::getgroups(buf.len() as i32, buf.as_mut_ptr()) }; let n_groups2 = unsafe { libc::getgroups(buf.len() as i32, buf.as_mut_ptr()) };
if n_groups2 <= 0 { if n_groups2 <= 0 {
return Ok(Vec::new()); return Ok(Vec::new());
@ -94,7 +114,7 @@ fn cur_groups() -> io::Result<Vec<u32>> {
} }
// It's not guaranteed that our current GID is necessarily one of our // It's not guaranteed that our current GID is necessarily one of our
// current groups. So, we add it. // current groups. So, we add it.
let cur_gid = users::get_current_gid(); let cur_gid = PwdGrp.getgid();
if !buf.contains(&cur_gid) { if !buf.contains(&cur_gid) {
buf.push(cur_gid); buf.push(cur_gid);
} }
@ -171,18 +191,23 @@ impl From<&str> for TrustedUser {
impl TrustedUser { impl TrustedUser {
/// Try to convert this `User` into an optional UID. /// Try to convert this `User` into an optional UID.
pub(crate) fn get_uid(&self) -> Result<Option<u32>, Error> { pub(crate) fn get_uid(&self) -> Result<Option<u32>, Error> {
let userdb = CACHE.lock().expect("poisoned lock"); let mut cache = CACHE.lock().expect("poisoned lock");
self.get_uid_impl(&*userdb) 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)
} }
/// As `get_uid`, but take a userdb. /// As `get_uid`, but take a userdb.
fn get_uid_impl<U: PwdGrpProvider>(&self, userdb: &U) -> Result<Option<u32>, Error> { fn get_uid_impl<U: PwdGrpProvider>(&self, userdb: &U) -> Result<Option<u32>, Error> {
match self { match self {
TrustedUser::None => Ok(None), TrustedUser::None => Ok(None),
TrustedUser::Current => Ok(Some(userdb.get_current_uid())), TrustedUser::Current => Ok(Some(userdb.getuid())),
TrustedUser::Id(id) => Ok(Some(*id)), TrustedUser::Id(id) => Ok(Some(*id)),
TrustedUser::Name(name) => userdb TrustedUser::Name(name) => userdb
.get_user_by_name(&name) .getpwnam(name.as_bytes()).map_err(handle_pwd_error)?
.map(|u| Some(u.uid())) .map(|u: pwd_grp::Passwd<Vec<u8>>| Some(u.uid))
.ok_or_else(|| Error::NoSuchUser(name.to_string_lossy().into_owned())), .ok_or_else(|| Error::NoSuchUser(name.to_string_lossy().into_owned())),
} }
} }
@ -250,8 +275,13 @@ impl From<&str> for TrustedGroup {
impl TrustedGroup { impl TrustedGroup {
/// Try to convert this `Group` into an optional GID. /// Try to convert this `Group` into an optional GID.
pub(crate) fn get_gid(&self) -> Result<Option<u32>, Error> { pub(crate) fn get_gid(&self) -> Result<Option<u32>, Error> {
let userdb = CACHE.lock().expect("poisoned lock"); let mut cache = CACHE.lock().expect("poisoned lock");
self.get_gid_impl(&*userdb) 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)
} }
/// Like `get_gid`, but take a user db as an argument. /// Like `get_gid`, but take a user db as an argument.
fn get_gid_impl<U: PwdGrpProvider>( fn get_gid_impl<U: PwdGrpProvider>(
@ -263,8 +293,8 @@ impl TrustedGroup {
TrustedGroup::SelfNamed => get_self_named_gid_impl(userdb).map_err(handle_pwd_error), TrustedGroup::SelfNamed => get_self_named_gid_impl(userdb).map_err(handle_pwd_error),
TrustedGroup::Id(id) => Ok(Some(*id)), TrustedGroup::Id(id) => Ok(Some(*id)),
TrustedGroup::Name(name) => userdb TrustedGroup::Name(name) => userdb
.get_group_by_name(&name) .getgrnam(name.as_bytes()).map_err(handle_pwd_error)?
.map(|u| Some(u.gid())) .map(|g: pwd_grp::Group<Vec<u8>>| Some(g.gid))
.ok_or_else(|| Error::NoSuchGroup(name.to_string_lossy().into_owned())), .ok_or_else(|| Error::NoSuchGroup(name.to_string_lossy().into_owned())),
} }
} }
@ -283,24 +313,36 @@ mod test {
#![allow(clippy::unchecked_duration_subtraction)] #![allow(clippy::unchecked_duration_subtraction)]
#![allow(clippy::useless_vec)] #![allow(clippy::useless_vec)]
//! <!-- @@ end test lint list maintained by maint/add_warning @@ --> //! <!-- @@ end test lint list maintained by maint/add_warning @@ -->
#![allow(clippy::unnecessary_mut_passed)] // XXXX
use super::*; use super::*;
use users::mock::{Group, MockUsers, User}; use pwd_grp::mock::MockPwdGrpProvider;
type Id = u32; type Id = u32;
fn mock_users() -> MockUsers { fn mock_users() -> MockPwdGrpProvider {
MockUsers::with_current_uid(413) let mock = MockPwdGrpProvider::new();
mock.set_uids(413.into());
mock
} }
fn add_user(mock: &mut MockUsers, uid: Id, name: &str, gid: Id) { fn add_user(mock: &MockPwdGrpProvider, uid: Id, name: &str, gid: Id) {
mock.add_user(User::new(uid, name, gid)); mock.add_to_passwds([pwd_grp::Passwd::<String> {
name: name.into(),
uid,
gid,
..pwd_grp::Passwd::blank()
}]);
} }
fn add_group(mock: &mut MockUsers, gid: Id, name: &str) { fn add_group(mock: &MockPwdGrpProvider, gid: Id, name: &str) {
mock.add_group(Group::new(gid, name)); mock.add_to_groups([pwd_grp::Group::<String> {
name: name.into(),
gid,
..pwd_grp::Group::blank()
}]);
} }
#[test] #[test]
fn groups() { fn groups() {
let groups = cur_groups().unwrap(); let groups = cur_groups().unwrap();
let cur_gid = users::get_current_gid(); let cur_gid = pwd_grp::getgid();
if groups.is_empty() { if groups.is_empty() {
// Some container/VM setups forget to put the (root) user into any // Some container/VM setups forget to put the (root) user into any
// groups at all. // groups at all.
@ -314,10 +356,10 @@ mod test {
// Here we'll do tests with our real username. THere's not much we can // Here we'll do tests with our real username. THere's not much we can
// actually test there, but we'll try anyway. // actually test there, but we'll try anyway.
let cache = CACHE.lock().expect("poisoned lock"); let cache = CACHE.lock().expect("poisoned lock");
let uname = get_own_username(&*cache).unwrap().expect("Running on a misconfigured host"); let uname = get_own_username(&cache.pwd_grp).unwrap().expect("Running on a misconfigured host");
let user = users::get_user_by_name(uname.as_os_str()).unwrap(); let user = PwdGrp.getpwnam::<Vec<u8>>(uname.as_bytes()).unwrap().unwrap();
assert_eq!(user.name(), uname); assert_eq!(user.name, uname.as_bytes());
assert_eq!(user.uid(), users::get_current_uid()); assert_eq!(user.uid, PwdGrp.getuid());
} }
#[test] #[test]