1698 lines
52 KiB
Rust
1698 lines
52 KiB
Rust
//! `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<syscall::Error> 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<Path>, lock: Lock) -> Result<File, Error> {
|
|
#[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<Full, Error> {
|
|
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<usize>,
|
|
gid: Option<usize>,
|
|
name: Option<String>,
|
|
home: Option<String>,
|
|
shell: Option<String>,
|
|
}
|
|
|
|
#[cfg(feature = "auth")]
|
|
impl UserBuilder {
|
|
/// Create a new `UserBuilder` with the login name for the new user.
|
|
pub fn new(user: impl AsRef<str>) -> 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<str>) -> 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<str>) -> 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<str>) -> 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<A> {
|
|
/// 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<A: Default> User<A> {
|
|
/// 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<T>(&self, cmd: T) -> Command
|
|
where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str>
|
|
{
|
|
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<User<A>, 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::<usize>()?;
|
|
let gid = parts
|
|
.next()
|
|
.ok_or(parse_error(line, "expected uid"))?
|
|
.parse::<usize>()?;
|
|
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::<A> {
|
|
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<auth::Full> {
|
|
/// 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<str>) -> 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<str>) -> 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<String, Error> {
|
|
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<String, Error> {
|
|
if !is_safe_string(&self.user) {
|
|
Err(Error::InvalidName { name: self.user.to_string() })
|
|
} else {
|
|
Ok(format!("{};{}\n", self.user, self.auth.hash))
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<A> Name for User<A> {
|
|
fn name(&self) -> &str {
|
|
&self.user
|
|
}
|
|
}
|
|
|
|
impl<A> Id for User<A> {
|
|
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<usize>,
|
|
|
|
users: Vec<String>,
|
|
}
|
|
|
|
impl GroupBuilder {
|
|
/// Create a new `GroupBuilder` with the given group name.
|
|
pub fn new(group: impl AsRef<str>) -> 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<str>) -> 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<String>,
|
|
}
|
|
|
|
impl Group {
|
|
fn from_group_entry(s: &str, line: usize) -> Result<Group, Error> {
|
|
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::<usize>()?;
|
|
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<String, Error> {
|
|
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<usize, Error> {
|
|
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<usize, Error> {
|
|
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<usize, Error> {
|
|
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<usize, Error> {
|
|
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<Path>) -> 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<Self::Gruser>;
|
|
fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
|
|
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<<Self as AllInner>::Gruser> {
|
|
self.list().iter()
|
|
}
|
|
|
|
/// Get an iterator mutably borrowing all [`User`]s or [`Group`]s on the
|
|
/// system.
|
|
fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::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<str>) -> Option<&<Self as AllInner>::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<str>) -> Option<&mut <Self as AllInner>::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<&<Self as AllInner>::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 <Self as AllInner>::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<usize> {
|
|
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<str>) -> 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<A> {
|
|
users: Vec<User<A>>,
|
|
config: Config,
|
|
|
|
// Hold on to the locked fds to prevent race conditions
|
|
#[allow(dead_code)]
|
|
passwd_fd: File,
|
|
#[allow(dead_code)]
|
|
shadow_fd: Option<File>,
|
|
}
|
|
|
|
impl<A: Default> AllUsers<A> {
|
|
pub fn new(config: Config) -> Result<AllUsers<A>, 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::<A> {
|
|
users: passwd_entries,
|
|
config,
|
|
passwd_fd,
|
|
shadow_fd: None,
|
|
})
|
|
}
|
|
}
|
|
|
|
impl AllUsers<auth::Basic> {
|
|
/// 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<AllUsers<auth::Basic>, Error> {
|
|
Self::new(config)
|
|
}
|
|
}
|
|
|
|
#[cfg(feature = "auth")]
|
|
impl AllUsers<auth::Full> {
|
|
/// 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<AllUsers<auth::Full>, 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<auth::Full>, 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<auth::Full>");
|
|
|
|
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<A> AllInner for AllUsers<A> {
|
|
type Gruser = User<A>;
|
|
|
|
fn list(&self) -> &Vec<Self::Gruser> {
|
|
&self.users
|
|
}
|
|
|
|
fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
|
|
&mut self.users
|
|
}
|
|
|
|
fn config(&self) -> &Config {
|
|
&self.config
|
|
}
|
|
}
|
|
|
|
impl<A> All for AllUsers<A> {}
|
|
/*
|
|
#[cfg(not(target_os = "redox"))]
|
|
impl<A> Drop for AllUsers<A> {
|
|
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<Group>,
|
|
config: Config,
|
|
|
|
group_fd: File,
|
|
}
|
|
|
|
impl AllGroups {
|
|
/// Create a new `AllGroups`.
|
|
pub fn new(config: Config) -> Result<AllGroups, Error> {
|
|
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<Group> = 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::Gruser> {
|
|
&self.groups
|
|
}
|
|
|
|
fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
|
|
&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<Path>) -> Result<String, Error> {
|
|
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!");
|
|
}
|
|
}
|
|
}
|