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(target_os = "redox")]
use libredox::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";
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;
#[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 },
#[error("invalid entry element '{data}'")]
InvalidData { data: String },
}
pub type Result<T, E = Error> = core::result::Result<T, E>;
#[inline]
fn parse_error(line: usize, reason: &str) -> Error {
Error::Parsing {
reason: reason.into(),
line,
}
}
impl From<libredox::error::Error> for Error {
fn from(syscall_error: libredox::error::Error) -> Error {
Error::Io(std::io::Error::from(syscall_error))
}
}
#[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
}
}
#[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);
Ok(file)
}
}
fn reset_file(fd: &mut File) -> Result<(), Error> {
fd.set_len(0)?;
fd.seek(SeekFrom::Start(0))?;
Ok(())
}
fn is_safe_string(s: &str) -> bool {
!s.contains(';')
}
const PORTABLE_FILE_NAME_CHARS: &str =
"0123456789._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
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
}
}
pub mod auth {
#[cfg(feature = "auth")]
use std::fmt;
#[cfg(feature = "auth")]
use zeroize::Zeroize;
#[cfg(feature = "auth")]
use crate::Error;
#[derive(Debug, Default)]
pub struct Basic {}
#[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 } } else {
Full::empty()
})
}
pub(crate) fn verify(&self, pw: &str) -> bool {
match self.hash.as_str() {
"" => pw == "",
"!" => false,
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()
}
}
}
#[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 {
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,
}
}
pub fn uid(mut self, uid: usize) -> UserBuilder {
self.uid = Some(uid);
self
}
pub fn gid(mut self, gid: usize) -> UserBuilder {
self.gid = Some(gid);
self
}
pub fn name(mut self, name: impl AsRef<str>) -> UserBuilder {
self.name = Some(name.as_ref().to_string());
self
}
pub fn home(mut self, home: impl AsRef<str>) -> UserBuilder {
self.home = Some(home.as_ref().to_string());
self
}
pub fn shell(mut self, shell: impl AsRef<str>) -> UserBuilder {
self.shell = Some(shell.as_ref().to_string());
self
}
}
#[derive(Debug)]
pub struct User<A> {
pub user: String,
pub uid: usize,
pub gid: usize,
pub name: String,
pub home: String,
pub shell: String,
auth_delay: Duration,
#[allow(dead_code)]
auth: A,
}
impl<A: Default> User<A> {
pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) }
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> {
pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<(), Error> {
self.auth = auth::Full::passwd(password.as_ref())?;
Ok(())
}
pub fn unset_passwd(&mut self) {
self.auth = auth::Full::unset();
}
pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool {
let verified = self.auth.verify(password.as_ref());
if !verified {
#[cfg(not(test))] thread::sleep(self.auth_delay);
}
verified
}
pub fn is_passwd_blank(&self) -> bool {
self.auth.is_empty()
}
pub fn is_passwd_unset(&self) -> bool {
self.auth.is_unset()
}
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
}
}
pub struct GroupBuilder {
group: String,
gid: Option<usize>,
users: Vec<String>,
}
impl GroupBuilder {
pub fn new(group: impl AsRef<str>) -> GroupBuilder {
GroupBuilder {
group: group.as_ref().to_string(),
gid: None,
users: vec![],
}
}
pub fn gid(mut self, gid: usize) -> GroupBuilder {
self.gid = Some(gid);
self
}
pub fn user(mut self, user: impl AsRef<str>) -> GroupBuilder {
self.users.push(user.as_ref().to_string());
self
}
}
#[derive(Debug)]
pub struct Group {
pub group: String,
pub password: String,
pub gid: usize,
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 password = parts
.next()
.ok_or(parse_error(line, "expected password"))?;
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(),
password: password.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.password,
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
}
}
pub fn get_euid() -> Result<usize, Error> {
libredox::call::geteuid()
.map_err(From::from)
}
pub fn get_uid() -> Result<usize, Error> {
libredox::call::getruid()
.map_err(From::from)
}
pub fn get_egid() -> Result<usize, Error> {
libredox::call::getegid()
.map_err(From::from)
}
pub fn get_gid() -> Result<usize, Error> {
libredox::call::getrgid()
.map_err(From::from)
}
#[derive(Clone, Debug)]
pub struct Config {
root_fs: PathBuf,
auth_delay: Duration,
min_id: usize,
max_id: usize,
lock: Lock,
}
impl Config {
pub fn auth_delay(mut self, delay: Duration) -> Config {
self.auth_delay = delay;
self
}
pub fn min_id(mut self, id: usize) -> Config {
self.min_id = id;
self
}
pub fn max_id(mut self, id: usize) -> Config {
self.max_id = id;
self
}
pub fn scheme(mut self, scheme: String) -> Config {
self.root_fs = PathBuf::from(scheme);
self
}
pub fn writeable(mut self, writeable: bool) -> Config {
self.lock = if writeable {
Lock::Exclusive
} else {
Lock::Shared
};
self
}
fn in_root_fs(&self, path: impl AsRef<Path>) -> PathBuf {
let mut canonical_path = self.root_fs.clone();
if path.as_ref().is_absolute() {
canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string());
} else {
canonical_path.push(path);
}
canonical_path
}
}
impl Default for Config {
fn default() -> Config {
Config {
root_fs: PathBuf::from("/"),
auth_delay: Duration::new(DEFAULT_TIMEOUT, 0),
min_id: MIN_ID,
max_id: MAX_ID,
lock: Lock::Shared,
}
}
}
mod sealed {
use crate::Config;
pub trait Name {
fn name(&self) -> &str;
}
pub trait Id {
fn id(&self) -> usize;
}
pub trait AllInner {
type Gruser: Name + Id;
fn list(&self) -> &Vec<Self::Gruser>;
fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
fn config(&self) -> &Config;
}
}
use sealed::{AllInner, Id, Name};
pub trait All: AllInner {
fn iter(&self) -> Iter<<Self as AllInner>::Gruser> {
self.list().iter()
}
fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> {
self.list_mut().iter_mut()
}
fn get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser> {
self.iter()
.find(|gruser| gruser.name() == name.as_ref() )
}
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() )
}
fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> {
self.iter()
.find(|gruser| gruser.id() == id )
}
fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> {
self.iter_mut()
.find(|gruser| gruser.id() == id )
}
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
}
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
}
}
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
}
}
}
#[derive(Debug)]
pub struct AllUsers<A> {
users: Vec<User<A>>,
config: Config,
#[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_root_fs(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> {
pub fn basic(config: Config) -> Result<AllUsers<auth::Basic>, Error> {
Self::new(config)
}
}
#[cfg(feature = "auth")]
impl AllUsers<auth::Full> {
pub fn authenticator(config: Config) -> Result<AllUsers<auth::Full>, Error> {
let mut shadow_fd = locked_file(config.in_root_fs(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)
}
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])
}
}
pub fn save(&mut self) -> Result<(), Error> {
let mut userstring = String::new();
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> {}
#[derive(Debug)]
pub struct AllGroups {
groups: Vec<Group>,
config: Config,
group_fd: File,
}
impl AllGroups {
pub fn new(config: Config) -> Result<AllGroups, Error> {
let mut group_fd = locked_file(config.in_root_fs(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,
})
}
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,
password: "x".into(),
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])
}
}
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(test)]
mod test {
use super::*;
const TEST_PREFIX: &'static str = "tests";
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()
.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)
}
#[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);
}
#[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();
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'");
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(); 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"
)
);
}
#[test]
fn empty_groups() {
let group_trailing = Group::from_group_entry("nobody;x;2066; ", 0).unwrap();
assert_eq!(group_trailing.users.len(), 0);
let group_no_trailing = Group::from_group_entry("nobody;x;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);
}
#[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")
.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;x;0;root\n",
"user;x;1000;user\n",
"wheel;x;1;user,root\n",
"loip;x;1007;loip\n",
"fbar;x;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;x;0;root\n",
"user;x;1000;user\n",
"wheel;x;1;user,root\n",
"loip;x;1007;loip\n",
"fbar;x;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;x;0;root\n",
"user;x;1000;user\n",
"wheel;x;1;user,root\n",
"loip;x;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;x;0;root\n",
"user;x;1000;user\n",
"wheel;x;1;user,root\n",
"loip;x;1007;loip\n",
"nobody;x;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;x;0;root\n",
"user;x;1000;user\n",
"wheel;x;1;user,root\n",
"loip;x;1007;loip\n"
)
);
}
#[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!");
}
}
}