//! `redox-users` is designed to be a small, low-ish level interface //! to system user and group information, as well as user password //! authentication. It is OS-specific and will break horribly on platforms //! that are not [Redox-OS](https://redox-os.org). //! //! # Permissions //! Because this is a system level tool dealing with password //! authentication, programs are often required to run with //! escalated priveleges. The implementation of the crate is //! privelege unaware. The only privelege requirements are those //! laid down by the system administrator over these files: //! - `/etc/group` //! - Read: Required to access group information //! - Write: Required to change group information //! - `/etc/passwd` //! - Read: Required to access user information //! - Write: Required to change user information //! - `/etc/shadow` //! - Read: Required to authenticate users //! - Write: Required to set user passwords //! //! # Reimplementation //! This crate is designed to be as small as possible without //! sacrificing critical functionality. The idea is that a small //! enough redox-users will allow easy re-implementation based on //! the same flexible API. This would allow more complicated authentication //! schemes for redox in future without breakage of existing //! software. use std::convert::From; use std::fmt::Debug; use std::fs::{File, OpenOptions}; use std::io::{Read, Seek, SeekFrom, Write}; #[cfg(target_os = "redox")] use std::os::unix::fs::OpenOptionsExt; #[cfg(not(target_os = "redox"))] use std::os::unix::io::AsRawFd; use std::os::unix::process::CommandExt; use std::path::{Path, PathBuf}; use std::process::Command; use std::slice::{Iter, IterMut}; #[cfg(not(test))] #[cfg(feature = "auth")] use std::thread; use std::time::Duration; use thiserror::Error; #[cfg(feature = "auth")] use zeroize::Zeroize; //#[cfg(not(target_os = "redox"))] //use nix::fcntl::{flock, FlockArg}; #[cfg(target_os = "redox")] use syscall::flag::{O_EXLOCK, O_SHLOCK}; const PASSWD_FILE: &'static str = "/etc/passwd"; const GROUP_FILE: &'static str = "/etc/group"; #[cfg(feature = "auth")] const SHADOW_FILE: &'static str = "/etc/shadow"; #[cfg(target_os = "redox")] const DEFAULT_SCHEME: &'static str = "file:"; #[cfg(not(target_os = "redox"))] const DEFAULT_SCHEME: &'static str = ""; const MIN_ID: usize = 1000; const MAX_ID: usize = 6000; const DEFAULT_TIMEOUT: u64 = 3; const USERNAME_LEN_MIN: usize = 3; const USERNAME_LEN_MAX: usize = 32; /// Errors that might happen while using this crate #[derive(Debug, Error)] #[non_exhaustive] pub enum Error { #[error("os error: {reason}")] Os { reason: &'static str }, #[error(transparent)] Io(#[from] std::io::Error), #[error("failed to generate seed: {0}")] Getrandom(#[from] getrandom::Error), #[cfg(feature = "auth")] #[error("")] Argon(#[from] argon2::Error), #[error("parse error line {line}: {reason}")] Parsing { reason: String, line: usize }, #[error(transparent)] ParseInt(#[from] std::num::ParseIntError), #[error("user not found")] UserNotFound, #[error("group not found")] GroupNotFound, #[error("user already exists")] UserAlreadyExists, #[error("group already exists")] GroupAlreadyExists, #[error("invalid name '{name}'")] InvalidName { name: String }, /// Used for invalid string field values of [`User`] #[error("invalid entry element '{data}'")] InvalidData { data: String }, } #[inline] fn parse_error(line: usize, reason: &str) -> Error { Error::Parsing { reason: reason.into(), line, } } impl From for Error { fn from(syscall_error: syscall::Error) -> Error { Error::Os { reason: syscall_error.text() } } } #[derive(Clone, Copy, Debug)] enum Lock { Shared, Exclusive, } impl Lock { fn can_write(&self) -> bool { match self { Lock::Shared => false, Lock::Exclusive => true, } } #[cfg(target_os = "redox")] fn as_olock(self) -> i32 { (match self { Lock::Shared => O_SHLOCK, Lock::Exclusive => O_EXLOCK, }) as i32 } /*#[cfg(not(target_os = "redox"))] fn as_flock(self) -> FlockArg { match self { Lock::Shared => FlockArg::LockShared, Lock::Exclusive => FlockArg::LockExclusive, } }*/ } /// Naive semi-cross platform file locking (need to support linux for tests). #[allow(dead_code)] fn locked_file(file: impl AsRef, lock: Lock) -> Result { #[cfg(test)] println!("Open file: {}", file.as_ref().display()); #[cfg(target_os = "redox")] { Ok(OpenOptions::new() .read(true) .write(lock.can_write()) .custom_flags(lock.as_olock()) .open(file)?) } #[cfg(not(target_os = "redox"))] #[cfg_attr(rustfmt, rustfmt_skip)] { let file = OpenOptions::new() .read(true) .write(lock.can_write()) .open(file)?; let fd = file.as_raw_fd(); eprintln!("Fd: {}", fd); //flock(fd, lock.as_flock())?; Ok(file) } } /// Reset a file for rewriting (user/group dbs must be erased before write-out) fn reset_file(fd: &mut File) -> Result<(), Error> { fd.set_len(0)?; fd.seek(SeekFrom::Start(0))?; Ok(()) } /// Is a string safe to write to `/etc/group` or `/etc/passwd`? fn is_safe_string(s: &str) -> bool { !s.contains(';') } const PORTABLE_FILE_NAME_CHARS: &str = "0123456789._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; /// This function is used by [`UserBuilder`] and [`GroupBuilder`] to determine /// if a name for a user/group is valid. It is provided for convenience. /// /// Usernames must match the [POSIX standard /// for usernames](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_437) /// . The "portable filename character set" is defined as `A-Z`, `a-z`, `0-9`, /// and `._-` (see [here](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282)). /// /// Usernames may not be more than 32 or less than 3 characters in length. pub fn is_valid_name(name: &str) -> bool { if name.len() < USERNAME_LEN_MIN || name.len() > USERNAME_LEN_MAX { false } else if let Some(first) = name.chars().next() { first != '-' && name.chars().all(|c| { PORTABLE_FILE_NAME_CHARS.contains(c) }) } else { false } } /// Marker types for [`User`] and [`AllUsers`]. pub mod auth { #[cfg(feature = "auth")] use std::fmt; #[cfg(feature = "auth")] use zeroize::Zeroize; #[cfg(feature = "auth")] use crate::Error; /// Marker type indicating that a `User` only has access to world-readable /// user information, and cannot authenticate. #[derive(Debug, Default)] pub struct Basic {} /// Marker type indicating that a `User` has access to all user /// information, including password hashes. #[cfg(feature = "auth")] #[derive(Default, Zeroize)] #[zeroize(drop)] pub struct Full { pub(crate) hash: String, } #[cfg(feature = "auth")] impl Full { pub(crate) fn empty() -> Full { Full { hash: "".into() } } pub(crate) fn is_empty(&self) -> bool { &self.hash == "" } pub(crate) fn unset() -> Full { Full { hash: "!".into() } } pub(crate) fn is_unset(&self) -> bool { &self.hash == "!" } pub(crate) fn passwd(pw: &str) -> Result { Ok(if pw != "" { let mut buf = [0u8; 8]; getrandom::getrandom(&mut buf)?; let mut salt = format!("{:X}", u64::from_ne_bytes(buf)); let config = argon2::Config::default(); let hash: String = argon2::hash_encoded( pw.as_bytes(), salt.as_bytes(), &config )?; buf.zeroize(); salt.zeroize(); Full { hash } // note that move == shallow copy in Rust } else { Full::empty() }) } pub(crate) fn verify(&self, pw: &str) -> bool { match self.hash.as_str() { "" => pw == "", "!" => false, //TODO: When does this panic? Should this function return // Result? Or does it need to simply fail to verify if // verify_encoded() fails? hash => argon2::verify_encoded(&hash, pw.as_bytes()) .expect("failed to verify hash"), } } } #[cfg(feature = "auth")] impl fmt::Debug for Full { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { f.debug_struct("Full") .finish() } } } /// A builder pattern for adding [`User`]s to [`AllUsers`]. Fields are verified /// when the group is built via [`AllUsers::add_user`]. See the documentation /// of that function for default values. /// /// Note that this builder is not available when the `auth` feature of the /// crate is disabled. /// /// # Example /// ```no_run /// # use redox_users::{AllGroups, Config, GroupBuilder, UserBuilder}; /// let mut allgs = AllGroups::new(Config::default()).unwrap(); /// /// let g = GroupBuilder::new("foobar") /// .user("foobar"); /// let foobar_g = allgs.add_group(g).unwrap(); /// /// let u = UserBuilder::new("foobar") /// .gid(foobar_g.gid) /// .name("Foo Bar") /// // Note that this directory will not be created /// .home("file:/home/foobar"); /// ``` #[cfg(feature = "auth")] pub struct UserBuilder { user: String, uid: Option, gid: Option, name: Option, home: Option, shell: Option, } #[cfg(feature = "auth")] impl UserBuilder { /// Create a new `UserBuilder` with the login name for the new user. pub fn new(user: impl AsRef) -> UserBuilder { UserBuilder { user: user.as_ref().to_string(), uid: None, gid: None, name: None, home: None, shell: None, } } /// Set the user id for this user. pub fn uid(mut self, uid: usize) -> UserBuilder { self.uid = Some(uid); self } /// Set the primary group id for this user. pub fn gid(mut self, gid: usize) -> UserBuilder { self.gid = Some(gid); self } /// Set the GECOS field for this user. pub fn name(mut self, name: impl AsRef) -> UserBuilder { self.name = Some(name.as_ref().to_string()); self } /// Set the home directory for this user. pub fn home(mut self, home: impl AsRef) -> UserBuilder { self.home = Some(home.as_ref().to_string()); self } /// Set the login shell for this user. pub fn shell(mut self, shell: impl AsRef) -> UserBuilder { self.shell = Some(shell.as_ref().to_string()); self } } /// A struct representing a Redox user. /// Currently maps to an entry in the `/etc/passwd` file. /// /// `A` should be a type from [`crate::auth`]. /// /// # Unset vs. Blank Passwords /// A note on unset passwords vs. blank passwords. A blank password /// is a hash field that is completely blank (aka, `""`). According /// to this crate, successful login is only allowed if the input /// password is blank as well. /// /// An unset password is one whose hash is not empty (`""`), but /// also not a valid serialized argon2rs hashing session. This /// hash always returns `false` upon attempted verification. The /// most commonly used hash for an unset password is `"!"`, but /// this crate makes no distinction. The most common way to unset /// the password is to use [`User::unset_passwd`]. #[derive(Debug)] pub struct User { /// Username (login name) pub user: String, /// User id pub uid: usize, /// Group id pub gid: usize, /// Real name (human readable, can contain spaces) pub name: String, /// Home directory path pub home: String, /// Shell path pub shell: String, // Failed login delay duration auth_delay: Duration, #[allow(dead_code)] auth: A, } impl User { /// Get a Command to run the user's default shell (see [`User::login_cmd`] /// for more docs). pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) } /// Provide a login command for the user, which is any entry point for /// starting a user's session, whether a shell (use [`User::shell_cmd`] /// instead) or a graphical init. /// /// The `Command` will use the user's `uid` and `gid`, its `current_dir` /// will be set to the user's home directory, and the follwing enviroment /// variables will be populated: /// /// - `USER` set to the user's `user` field. /// - `UID` set to the user's `uid` field. /// - `GROUPS` set the user's `gid` field. /// - `HOME` set to the user's `home` field. /// - `SHELL` set to the user's `shell` field. pub fn login_cmd(&self, cmd: T) -> Command where T: std::convert::AsRef + AsRef { let mut command = Command::new(cmd); command .uid(self.uid as u32) .gid(self.gid as u32) .current_dir(&self.home) .env("USER", &self.user) .env("UID", format!("{}", self.uid)) .env("GROUPS", format!("{}", self.gid)) .env("HOME", &self.home) .env("SHELL", &self.shell); command } fn from_passwd_entry(s: &str, line: usize) -> Result, Error> { let mut parts = s.split(';'); let user = parts .next() .ok_or(parse_error(line, "expected user"))?; let uid = parts .next() .ok_or(parse_error(line, "expected uid"))? .parse::()?; let gid = parts .next() .ok_or(parse_error(line, "expected uid"))? .parse::()?; let name = parts .next() .ok_or(parse_error(line, "expected real name"))?; let home = parts .next() .ok_or(parse_error(line, "expected home dir path"))?; let shell = parts .next() .ok_or(parse_error(line, "expected shell path"))?; Ok(User:: { user: user.into(), uid, gid, name: name.into(), home: home.into(), shell: shell.into(), auth: A::default(), auth_delay: Duration::default(), }) } } #[cfg(feature = "auth")] impl User { /// Set the password for a user. Make **sure** that `password` /// is actually what the user wants as their password (this doesn't). /// /// To set the password blank, pass `""` as `password`. /// /// Note that `password` is taken as a reference, so it is up to the caller /// to properly zero sensitive memory (see `zeroize` on crates.io). pub fn set_passwd(&mut self, password: impl AsRef) -> Result<(), Error> { self.auth = auth::Full::passwd(password.as_ref())?; Ok(()) } /// Unset the password ([`User::verify_passwd`] always returns `false`). pub fn unset_passwd(&mut self) { self.auth = auth::Full::unset(); } /// Verify the password. If the hash is empty, this only returns `true` if /// `password` is also empty. /// /// Note that this is a blocking operation if the password is incorrect. /// See [`Config::auth_delay`] to set the wait time. Default is 3 seconds. /// /// Note that `password` is taken as a reference, so it is up to the caller /// to properly zero sensitive memory (see `zeroize` on crates.io). pub fn verify_passwd(&self, password: impl AsRef) -> bool { let verified = self.auth.verify(password.as_ref()); if !verified { #[cfg(not(test))] // Make tests run faster thread::sleep(self.auth_delay); } verified } /// Determine if the hash for the password is blank ([`User::verify_passwd`] /// returns `true` *only* when the password is blank). pub fn is_passwd_blank(&self) -> bool { self.auth.is_empty() } /// Determine if the hash for the password is unset /// ([`User::verify_passwd`] returns `false` regardless of input). pub fn is_passwd_unset(&self) -> bool { self.auth.is_unset() } /// Format this user as an entry in `/etc/passwd`. fn passwd_entry(&self) -> Result { if !is_safe_string(&self.user) { Err(Error::InvalidName { name: self.user.to_string() }) } else if !is_safe_string(&self.name) { Err(Error::InvalidData { data: self.name.to_string() }) } else if !is_safe_string(&self.home) { Err(Error::InvalidData { data: self.home.to_string() }) } else if !is_safe_string(&self.shell) { Err(Error::InvalidData { data: self.shell.to_string() }) } else { #[cfg_attr(rustfmt, rustfmt_skip)] Ok(format!("{};{};{};{};{};{}\n", self.user, self.uid, self.gid, self.name, self.home, self.shell )) } } fn shadow_entry(&self) -> Result { if !is_safe_string(&self.user) { Err(Error::InvalidName { name: self.user.to_string() }) } else { Ok(format!("{};{}\n", self.user, self.auth.hash)) } } } impl Name for User { fn name(&self) -> &str { &self.user } } impl Id for User { fn id(&self) -> usize { self.uid } } /// A builder pattern for adding [`Group`]s to [`AllGroups`]. Fields are /// verified when the `Group` is built, via [`AllGroups::add_group`]. /// /// # Example /// ``` /// # use redox_users::GroupBuilder; /// // When added, this group will use the first available group id /// let mygroup = GroupBuilder::new("group_name"); /// /// // A little more stuff: /// let other = GroupBuilder::new("special") /// .gid(9055) /// .user("some_username"); /// ``` pub struct GroupBuilder { // Group name group: String, gid: Option, users: Vec, } impl GroupBuilder { /// Create a new `GroupBuilder` with the given group name. pub fn new(group: impl AsRef) -> GroupBuilder { GroupBuilder { group: group.as_ref().to_string(), gid: None, users: vec![], } } /// Set the group id of this group. pub fn gid(mut self, gid: usize) -> GroupBuilder { self.gid = Some(gid); self } /// Add a user to this group. Call this function multiple times to add more /// users. pub fn user(mut self, user: impl AsRef) -> GroupBuilder { self.users.push(user.as_ref().to_string()); self } } /// A struct representing a Redox user group. /// Currently maps to an `/etc/group` file entry. #[derive(Debug)] pub struct Group { /// Group name pub group: String, /// Unique group id pub gid: usize, /// Group members' usernames pub users: Vec, } impl Group { fn from_group_entry(s: &str, line: usize) -> Result { let mut parts = s.trim() .split(';'); let group = parts .next() .ok_or(parse_error(line, "expected group"))?; let gid = parts .next() .ok_or(parse_error(line, "expected gid"))? .parse::()?; let users_str = parts.next() .unwrap_or(""); let users = users_str.split(',') .filter_map(|u| if u == "" { None } else { Some(u.into()) }) .collect(); Ok(Group { group: group.into(), gid, users, }) } fn group_entry(&self) -> Result { if !is_safe_string(&self.group) { Err(Error::InvalidName { name: self.group.to_string() }) } else { for username in self.users.iter() { if !is_safe_string(&username) { return Err(Error::InvalidData { data: username.to_string() }); } } #[cfg_attr(rustfmt, rustfmt_skip)] Ok(format!("{};{};{}\n", self.group, self.gid, self.users.join(",").trim_matches(',') )) } } } impl Name for Group { fn name(&self) -> &str { &self.group } } impl Id for Group { fn id(&self) -> usize { self.gid } } /// Gets the current process effective user ID. /// /// This function issues the `geteuid` system call returning the process effective /// user id. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # use redox_users::get_euid; /// let euid = get_euid().unwrap(); /// ``` pub fn get_euid() -> Result { syscall::geteuid() .map_err(From::from) } /// Gets the current process real user ID. /// /// This function issues the `getuid` system call returning the process real /// user id. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # use redox_users::get_uid; /// let uid = get_uid().unwrap(); /// ``` pub fn get_uid() -> Result { syscall::getuid() .map_err(From::from) } /// Gets the current process effective group ID. /// /// This function issues the `getegid` system call returning the process effective /// group id. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # use redox_users::get_egid; /// let egid = get_egid().unwrap(); /// ``` pub fn get_egid() -> Result { syscall::getegid() .map_err(From::from) } /// Gets the current process real group ID. /// /// This function issues the `getegid` system call returning the process real /// group id. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # use redox_users::get_gid; /// let gid = get_gid().unwrap(); /// ``` pub fn get_gid() -> Result { syscall::getgid() .map_err(From::from) } /// A generic configuration that allows fine control of an [`AllUsers`] or /// [`AllGroups`]. /// /// `auth_delay` is not used by [`AllGroups`] /// /// In most situations, [`Config::default`](struct.Config.html#impl-Default) /// will work just fine. The other fields are for finer control if it is /// required. /// /// # Example /// ``` /// # use redox_users::Config; /// use std::time::Duration; /// /// let cfg = Config::default() /// .min_id(500) /// .max_id(1000) /// .auth_delay(Duration::from_secs(5)); /// ``` #[derive(Clone, Debug)] pub struct Config { scheme: String, auth_delay: Duration, min_id: usize, max_id: usize, lock: Lock, } impl Config { /// Set the delay for a failed authentication. Default is 3 seconds. pub fn auth_delay(mut self, delay: Duration) -> Config { self.auth_delay = delay; self } /// Set the smallest ID possible to use when finding an unused ID. pub fn min_id(mut self, id: usize) -> Config { self.min_id = id; self } /// Set the largest possible ID to use when finding an unused ID. pub fn max_id(mut self, id: usize) -> Config { self.max_id = id; self } /// Set the scheme relative to which the [`AllUsers`] or [`AllGroups`] /// should be looking for its data files. This is a compromise between /// exposing implementation details and providing fine enough /// control over the behavior of this API. pub fn scheme(mut self, scheme: String) -> Config { self.scheme = scheme; self } /// Allow writes to group, passwd, and shadow files pub fn writeable(mut self, writeable: bool) -> Config { self.lock = if writeable { Lock::Exclusive } else { Lock::Shared }; self } // Prepend a path with the scheme in this Config fn in_scheme(&self, path: impl AsRef) -> PathBuf { let mut canonical_path = PathBuf::from(&self.scheme); // Should be a little careful here, not sure I want this behavior if path.as_ref().is_absolute() { // This is nasty canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string()); } else { canonical_path.push(path); } canonical_path } } impl Default for Config { /// The default base scheme is `file:`. /// /// The default auth delay is 3 seconds. /// /// The default min and max ids are 1000 and 6000. fn default() -> Config { Config { scheme: String::from(DEFAULT_SCHEME), auth_delay: Duration::new(DEFAULT_TIMEOUT, 0), min_id: MIN_ID, max_id: MAX_ID, lock: Lock::Shared, } } } // Nasty hack to prevent the compiler complaining about // "leaking" `AllInner` mod sealed { use crate::Config; pub trait Name { fn name(&self) -> &str; } pub trait Id { fn id(&self) -> usize; } pub trait AllInner { // Group+User, thanks Dad type Gruser: Name + Id; /// These functions grab internal elements so that the other /// methods of `All` can manipulate them. fn list(&self) -> &Vec; fn list_mut(&mut self) -> &mut Vec; fn config(&self) -> &Config; } } use sealed::{AllInner, Id, Name}; /// This trait is used to remove repetitive API items from /// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait /// so that the implementations of functions can be implemented /// at the trait level. Do not try to implement this trait. pub trait All: AllInner { /// Get an iterator borrowing all [`User`]s or [`Group`]s on the system. fn iter(&self) -> Iter<::Gruser> { self.list().iter() } /// Get an iterator mutably borrowing all [`User`]s or [`Group`]s on the /// system. fn iter_mut(&mut self) -> IterMut<::Gruser> { self.list_mut().iter_mut() } /// Borrow the [`User`] or [`Group`] with a given name. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # use redox_users::{All, AllUsers, Config}; /// let users = AllUsers::basic(Config::default()).unwrap(); /// let user = users.get_by_name("root").unwrap(); /// ``` fn get_by_name(&self, name: impl AsRef) -> Option<&::Gruser> { self.iter() .find(|gruser| gruser.name() == name.as_ref() ) } /// Mutable version of [`All::get_by_name`]. fn get_mut_by_name(&mut self, name: impl AsRef) -> Option<&mut ::Gruser> { self.iter_mut() .find(|gruser| gruser.name() == name.as_ref() ) } /// Borrow the [`User`] or [`Group`] with the given ID. /// /// # Examples /// /// Basic usage: /// /// ```no_run /// # use redox_users::{All, AllUsers, Config}; /// let users = AllUsers::basic(Config::default()).unwrap(); /// let user = users.get_by_id(0).unwrap(); /// ``` fn get_by_id(&self, id: usize) -> Option<&::Gruser> { self.iter() .find(|gruser| gruser.id() == id ) } /// Mutable version of [`All::get_by_id`]. fn get_mut_by_id(&mut self, id: usize) -> Option<&mut ::Gruser> { self.iter_mut() .find(|gruser| gruser.id() == id ) } /// Provides an unused id based on the min and max values in the [`Config`] /// passed to the `All`'s constructor. /// /// # Examples /// /// ```no_run /// # use redox_users::{All, AllUsers, Config}; /// let users = AllUsers::basic(Config::default()).unwrap(); /// let uid = users.get_unique_id().expect("no available uid"); /// ``` fn get_unique_id(&self) -> Option { for id in self.config().min_id..self.config().max_id { if !self.iter().any(|gruser| gruser.id() == id ) { return Some(id) } } None } /// Remove a [`User`] or [`Group`] from this `All` given it's name. If the /// Gruser was removed return `true`, else return `false`. This ensures /// that the Gruser no longer exists. fn remove_by_name(&mut self, name: impl AsRef) -> bool { let list = self.list_mut(); let indx = list.iter() .enumerate() .find_map(|(indx, gruser)| if gruser.name() == name.as_ref() { Some(indx) } else { None }); if let Some(indx) = indx { list.remove(indx); true } else { false } } /// Id version of [`All::remove_by_name`]. fn remove_by_id(&mut self, id: usize) -> bool { let list = self.list_mut(); let indx = list.iter() .enumerate() .find_map(|(indx, gruser)| if gruser.id() == id { Some(indx) } else { None }); if let Some(indx) = indx { list.remove(indx); true } else { false } } } /// `AllUsers` provides (borrowed) access to all the users on the system. /// Note that this struct implements [`All`] for all of its access functions. /// /// # Notes /// Note that everything in this section also applies to [`AllGroups`]. /// /// * If you mutate anything owned by an `AllUsers`, you must call the /// [`AllUsers::save`] in order for those changes to be applied to the system. /// * The API here is kept small. Most mutating actions can be accomplished via /// the [`All::get_mut_by_id`] and [`All::get_mut_by_name`] /// functions. #[derive(Debug)] pub struct AllUsers { users: Vec>, config: Config, // Hold on to the locked fds to prevent race conditions #[allow(dead_code)] passwd_fd: File, #[allow(dead_code)] shadow_fd: Option, } impl AllUsers { pub fn new(config: Config) -> Result, Error> { let mut passwd_fd = locked_file(config.in_scheme(PASSWD_FILE), config.lock)?; let mut passwd_cntnt = String::new(); passwd_fd.read_to_string(&mut passwd_cntnt)?; let mut passwd_entries = Vec::new(); for (indx, line) in passwd_cntnt.lines().enumerate() { let mut user = User::from_passwd_entry(line, indx)?; user.auth_delay = config.auth_delay; passwd_entries.push(user); } Ok(AllUsers:: { users: passwd_entries, config, passwd_fd, shadow_fd: None, }) } } impl AllUsers { /// Provide access to all user information on the system except /// authentication. This is adequate for almost all uses of `AllUsers`. pub fn basic(config: Config) -> Result, Error> { Self::new(config) } } #[cfg(feature = "auth")] impl AllUsers { /// If access to password related methods for the [`User`]s yielded by this /// `AllUsers` is required, use this constructor. pub fn authenticator(config: Config) -> Result, Error> { let mut shadow_fd = locked_file(config.in_scheme(SHADOW_FILE), config.lock)?; let mut shadow_cntnt = String::new(); shadow_fd.read_to_string(&mut shadow_cntnt)?; let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect(); let mut new = Self::new(config)?; new.shadow_fd = Some(shadow_fd); for (indx, entry) in shadow_entries.iter().enumerate() { let mut entry = entry.split(';'); let name = entry.next().ok_or(parse_error(indx, "error parsing shadowfile: expected username" ))?; let hash = entry.next().ok_or(parse_error(indx, "error parsing shadowfile: expected hash" ))?; new.users .iter_mut() .find(|user| user.user == name) .ok_or(parse_error(indx, "error parsing shadowfile: unkown user" ))?.auth.hash = hash.to_string(); } shadow_cntnt.zeroize(); Ok(new) } /// Consumes a builder, adding a new user to this `AllUsers`. Returns a /// reference to the created user. /// /// Make sure to call [`AllUsers::save`] in order for the new user to be /// applied to the system. /// /// Note that the user's password is set unset (see /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords)) /// during this call. /// /// Also note that the user is not added to any groups when this builder is /// consumed. In order to keep the system in a consistent state, it is /// reccomended to also use an instance of [`AllGroups`] to update group /// information when creating new users. /// /// # Defaults /// Fields not passed to the builder before calling this function are as /// follows: /// - `uid`: [`AllUsers::get_unique_id`] is called on self to get the next /// available id. /// - `gid`: `99`. This is the default UID for the group `nobody`. Note /// that the user is NOT added to this group in `/etc/groups`. /// - `name`: The login name passed to [`UserBuilder::new`]. /// - `home`: `"/"` /// - `shell`: `file:/bin/ion` pub fn add_user(&mut self, builder: UserBuilder) -> Result<&User, Error> { if !is_valid_name(&builder.user) { return Err(Error::InvalidName { name: builder.user }); } let uid = builder.uid.unwrap_or_else(|| self.get_unique_id() .expect("no remaining unused user ids") ); if self.iter().any(|user| user.user == builder.user || user.uid == uid) { Err(Error::UserAlreadyExists) } else { self.users.push(User { user: builder.user.clone(), uid, gid: builder.gid.unwrap_or(99), name: builder.name.unwrap_or(builder.user), home: builder.home.unwrap_or("/".to_string()), shell: builder.shell.unwrap_or("file:/bin/ion".to_string()), auth: auth::Full::unset(), auth_delay: self.config.auth_delay }); Ok(&self.users[self.users.len() - 1]) } } /// Syncs the data stored in the `AllUsers` instance to the filesystem. /// To apply changes to the system from an `AllUsers`, you MUST call this /// function! pub fn save(&mut self) -> Result<(), Error> { let mut userstring = String::new(); // Need to be careful to prevent allocations here so that // shadowstring can be zeroed when this process is complete. // 1 is suppossedly parallelism, not sure exactly what this means. // 16 is the max length of a u64, which is used as the salt. // 2 accounts for the semicolon separator and newline let acfg = argon2::Config::default(); let argon_len = argon2::encoded_len( acfg.variant, acfg.mem_cost, acfg.time_cost, 1, 16, acfg.hash_length) as usize; let mut shadowstring = String::with_capacity( self.users.len() * (USERNAME_LEN_MAX + argon_len + 2) ); for user in &self.users { userstring.push_str(&user.passwd_entry()?); let mut shadow_entry = user.shadow_entry()?; shadowstring.push_str(&shadow_entry); shadow_entry.zeroize(); } let mut shadow_fd = self.shadow_fd.as_mut() .expect("shadow_fd should exist for AllUsers"); reset_file(&mut self.passwd_fd)?; self.passwd_fd.write_all(userstring.as_bytes())?; reset_file(&mut shadow_fd)?; shadow_fd.write_all(shadowstring.as_bytes())?; shadowstring.zeroize(); Ok(()) } } impl AllInner for AllUsers { type Gruser = User; fn list(&self) -> &Vec { &self.users } fn list_mut(&mut self) -> &mut Vec { &mut self.users } fn config(&self) -> &Config { &self.config } } impl All for AllUsers {} /* #[cfg(not(target_os = "redox"))] impl Drop for AllUsers { fn drop(&mut self) { eprintln!("Dropping AllUsers"); let _ = flock(self.passwd_fd.as_raw_fd(), FlockArg::Unlock); if let Some(fd) = self.shadow_fd.as_ref() { eprintln!("Shadow"); let _ = flock(fd.as_raw_fd(), FlockArg::Unlock); } } } */ /// `AllGroups` provides (borrowed) access to all groups on the system. Note /// that this struct implements [`All`] for all of its access functions. /// /// General notes that also apply to this struct may be found with /// [`AllUsers`]. #[derive(Debug)] pub struct AllGroups { groups: Vec, config: Config, group_fd: File, } impl AllGroups { /// Create a new `AllGroups`. pub fn new(config: Config) -> Result { let mut group_fd = locked_file(config.in_scheme(GROUP_FILE), config.lock)?; let mut group_cntnt = String::new(); group_fd.read_to_string(&mut group_cntnt)?; let mut entries: Vec = Vec::new(); for (indx, line) in group_cntnt.lines().enumerate() { let group = Group::from_group_entry(line, indx)?; entries.push(group); } Ok(AllGroups { groups: entries, config, group_fd, }) } /// Consumes a builder, adding a new group to this `AllGroups`. Returns a /// reference to the created `Group`. /// /// Make sure to call [`AllGroups::save`] in order for the new group to be /// applied to the system. /// /// # Defaults /// If a builder is not passed a group id ([`GroupBuilder::gid`]) before /// being passed to this function, [`AllGroups::get_unique_id`] is used. /// /// If the builder is not passed any users ([`GroupBuilder::user`]), the /// group will still be created. pub fn add_group(&mut self, builder: GroupBuilder) -> Result<&Group, Error> { let group_exists = self.iter() .any(|group| { let gid_taken = if let Some(gid) = builder.gid { group.gid == gid } else { false }; group.group == builder.group || gid_taken }); if group_exists { Err(Error::GroupAlreadyExists) } else if !is_valid_name(&builder.group) { Err(Error::InvalidName { name: builder.group }) } else { for username in builder.users.iter() { if !is_valid_name(username) { return Err(Error::InvalidName { name: username.to_string() }); } } self.groups.push(Group { group: builder.group, gid: builder.gid.unwrap_or_else(|| self.get_unique_id() .expect("no remaining unused group IDs") ), users: builder.users, }); Ok(&self.groups[self.groups.len() - 1]) } } /// Syncs the data stored in this `AllGroups` instance to the filesystem. /// To apply changes from an `AllGroups`, you MUST call this function! pub fn save(&mut self) -> Result<(), Error> { let mut groupstring = String::new(); for group in &self.groups { groupstring.push_str(&group.group_entry()?); } reset_file(&mut self.group_fd)?; self.group_fd.write_all(groupstring.as_bytes())?; Ok(()) } } impl AllInner for AllGroups { type Gruser = Group; fn list(&self) -> &Vec { &self.groups } fn list_mut(&mut self) -> &mut Vec { &mut self.groups } fn config(&self) -> &Config { &self.config } } impl All for AllGroups {} /* #[cfg(not(target_os = "redox"))] impl Drop for AllGroups { fn drop(&mut self) { eprintln!("Dropping AllGroups"); let _ = flock(self.group_fd.as_raw_fd(), FlockArg::Unlock); } }*/ #[cfg(test)] mod test { use super::*; const TEST_PREFIX: &'static str = "tests"; /// Needed for the file checks, this is done by the library fn test_prefix(filename: &str) -> String { let mut complete = String::from(TEST_PREFIX); complete.push_str(filename); complete } #[test] fn test_safe_string() { assert!(is_safe_string("Hello\\$!")); assert!(!is_safe_string("semicolons are awesome; yeah!")); } #[test] fn test_portable_filename() { let valid = |s| { assert!(is_valid_name(s)); }; let invld = |s| { assert!(!is_valid_name(s)); }; valid("valid"); valid("vld.io"); valid("hyphen-ated"); valid("under_scores"); valid("1334"); invld("-no_flgs"); invld("invalid!"); invld("also:invalid"); invld("coolie-o?"); invld("sh"); invld("avery_very_very_very_loooooooonnggg-username"); } fn test_cfg() -> Config { Config::default() // Since all this really does is prepend `sheme` to the consts .scheme(TEST_PREFIX.to_string()) .writeable(true) } fn read_locked_file(file: impl AsRef) -> Result { let mut fd = locked_file(file, Lock::Shared)?; let mut cntnt = String::new(); fd.read_to_string(&mut cntnt)?; Ok(cntnt) } // *** struct.User *** #[cfg(feature = "auth")] #[test] fn attempt_user_api() { let mut users = AllUsers::authenticator(test_cfg()).unwrap(); let user = users.get_mut_by_id(1000).unwrap(); assert_eq!(user.is_passwd_blank(), true); assert_eq!(user.is_passwd_unset(), false); assert_eq!(user.verify_passwd(""), true); assert_eq!(user.verify_passwd("Something"), false); user.set_passwd("hi,i_am_passwd").unwrap(); assert_eq!(user.is_passwd_blank(), false); assert_eq!(user.is_passwd_unset(), false); assert_eq!(user.verify_passwd(""), false); assert_eq!(user.verify_passwd("Something"), false); assert_eq!(user.verify_passwd("hi,i_am_passwd"), true); user.unset_passwd(); assert_eq!(user.is_passwd_blank(), false); assert_eq!(user.is_passwd_unset(), true); assert_eq!(user.verify_passwd(""), false); assert_eq!(user.verify_passwd("Something"), false); assert_eq!(user.verify_passwd("hi,i_am_passwd"), false); user.set_passwd("").unwrap(); assert_eq!(user.is_passwd_blank(), true); assert_eq!(user.is_passwd_unset(), false); assert_eq!(user.verify_passwd(""), true); assert_eq!(user.verify_passwd("Something"), false); } // *** struct.AllUsers *** #[cfg(feature = "auth")] #[test] fn get_user() { let users = AllUsers::authenticator(test_cfg()).unwrap(); let root = users.get_by_id(0).expect("'root' user missing"); assert_eq!(root.user, "root".to_string()); assert_eq!(root.auth.hash.as_str(), "$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk"); assert_eq!(root.uid, 0); assert_eq!(root.gid, 0); assert_eq!(root.name, "root".to_string()); assert_eq!(root.home, "file:/root".to_string()); assert_eq!(root.shell, "file:/bin/ion".to_string()); let user = users.get_by_name("user").expect("'user' user missing"); assert_eq!(user.user, "user".to_string()); assert_eq!(user.auth.hash.as_str(), ""); assert_eq!(user.uid, 1000); assert_eq!(user.gid, 1000); assert_eq!(user.name, "user".to_string()); assert_eq!(user.home, "file:/home/user".to_string()); assert_eq!(user.shell, "file:/bin/ion".to_string()); println!("{:?}", users); let li = users.get_by_name("loip").expect("'loip' user missing"); println!("got loip"); assert_eq!(li.user, "loip"); assert_eq!(li.auth.hash.as_str(), "!"); assert_eq!(li.uid, 1007); assert_eq!(li.gid, 1007); assert_eq!(li.name, "Lorem".to_string()); assert_eq!(li.home, "file:/home/lorem".to_string()); assert_eq!(li.shell, "file:/bin/ion".to_string()); } #[cfg(feature = "auth")] #[test] fn manip_user() { let mut users = AllUsers::authenticator(test_cfg()).unwrap(); // NOT testing `get_unique_id` let id = 7099; let fb = UserBuilder::new("fbar") .uid(id) .gid(id) .name("Foo Bar") .home("/home/foob") .shell("/bin/zsh"); users .add_user(fb) .expect("failed to add user 'fbar'"); // weirdo ^^^^^^^^ :P users.save().unwrap(); let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); assert_eq!( p_file_content, concat!( "root;0;0;root;file:/root;file:/bin/ion\n", "user;1000;1000;user;file:/home/user;file:/bin/ion\n", "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", "fbar;7099;7099;Foo Bar;/home/foob;/bin/zsh\n" ) ); let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); assert_eq!(s_file_content, concat!( "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", "user;\n", "loip;!\n", "fbar;!\n" )); { println!("{:?}", users); let fb = users.get_mut_by_name("fbar") .expect("'fbar' user missing"); fb.shell = "/bin/fish".to_string(); // That's better fb.set_passwd("").unwrap(); } users.save().unwrap(); let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); assert_eq!( p_file_content, concat!( "root;0;0;root;file:/root;file:/bin/ion\n", "user;1000;1000;user;file:/home/user;file:/bin/ion\n", "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", "fbar;7099;7099;Foo Bar;/home/foob;/bin/fish\n" ) ); let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); assert_eq!(s_file_content, concat!( "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", "user;\n", "loip;!\n", "fbar;\n" )); users.remove_by_id(id); users.save().unwrap(); let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); assert_eq!( file_content, concat!( "root;0;0;root;file:/root;file:/bin/ion\n", "user;1000;1000;user;file:/home/user;file:/bin/ion\n", "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n" ) ); } /* struct.Group */ #[test] fn empty_groups() { let group_trailing = Group::from_group_entry("nobody;2066; ", 0).unwrap(); assert_eq!(group_trailing.users.len(), 0); let group_no_trailing = Group::from_group_entry("nobody;2066;", 0).unwrap(); assert_eq!(group_no_trailing.users.len(), 0); assert_eq!(group_trailing.group, group_no_trailing.group); assert_eq!(group_trailing.gid, group_no_trailing.gid); assert_eq!(group_trailing.users, group_no_trailing.users); } /* struct.AllGroups */ #[test] fn get_group() { let groups = AllGroups::new(test_cfg()).unwrap(); let user = groups.get_by_name("user").unwrap(); assert_eq!(user.group, "user"); assert_eq!(user.gid, 1000); assert_eq!(user.users, vec!["user"]); let wheel = groups.get_by_id(1).unwrap(); assert_eq!(wheel.group, "wheel"); assert_eq!(wheel.gid, 1); assert_eq!(wheel.users, vec!["user", "root"]); } #[test] fn manip_group() { let id = 7099; let mut groups = AllGroups::new(test_cfg()).unwrap(); let fb = GroupBuilder::new("fbar") // NOT testing `get_unique_id` .gid(id) .user("fbar"); groups.add_group(fb).unwrap(); groups.save().unwrap(); let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); assert_eq!( file_content, concat!( "root;0;root\n", "user;1000;user\n", "wheel;1;user,root\n", "loip;1007;loip\n", "fbar;7099;fbar\n" ) ); { let fb = groups.get_mut_by_name("fbar").unwrap(); fb.users.push("user".to_string()); } groups.save().unwrap(); let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); assert_eq!( file_content, concat!( "root;0;root\n", "user;1000;user\n", "wheel;1;user,root\n", "loip;1007;loip\n", "fbar;7099;fbar,user\n" ) ); groups.remove_by_id(id); groups.save().unwrap(); let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); assert_eq!( file_content, concat!( "root;0;root\n", "user;1000;user\n", "wheel;1;user,root\n", "loip;1007;loip\n" ) ); } #[test] fn empty_group() { let mut groups = AllGroups::new(test_cfg()).unwrap(); let nobody = GroupBuilder::new("nobody") .gid(2260); groups.add_group(nobody).unwrap(); groups.save().unwrap(); let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); assert_eq!( file_content, concat!( "root;0;root\n", "user;1000;user\n", "wheel;1;user,root\n", "loip;1007;loip\n", "nobody;2260;\n", ) ); drop(groups); let mut groups = AllGroups::new(test_cfg()).unwrap(); groups.remove_by_name("nobody"); groups.save().unwrap(); let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); assert_eq!( file_content, concat!( "root;0;root\n", "user;1000;user\n", "wheel;1;user,root\n", "loip;1007;loip\n" ) ); } // *** Misc *** #[test] fn users_get_unused_ids() { let users = AllUsers::basic(test_cfg()).unwrap(); let id = users.get_unique_id().unwrap(); if id < users.config.min_id || id > users.config.max_id { panic!("User ID is not between allowed margins") } else if let Some(_) = users.get_by_id(id) { panic!("User ID is used!"); } } #[test] fn groups_get_unused_ids() { let groups = AllGroups::new(test_cfg()).unwrap(); let id = groups.get_unique_id().unwrap(); if id < groups.config.min_id || id > groups.config.max_id { panic!("Group ID is not between allowed margins") } else if let Some(_) = groups.get_by_id(id) { panic!("Group ID is used!"); } } }