extern crate argon2rs;
extern crate rand_os;
extern crate syscall;
#[macro_use]
extern crate failure;
use std::convert::From;
use std::fmt::{self, Debug, Display};
use std::fs::OpenOptions;
use std::io::{Read, Write};
#[cfg(target_os = "redox")]
use std::os::unix::fs::OpenOptionsExt;
use std::os::unix::process::CommandExt;
use std::path::{Path, PathBuf};
use std::process::Command;
use std::slice::{Iter, IterMut};
use std::str::FromStr;
use std::thread;
use std::time::Duration;
use argon2rs::verifier::Encoded;
use argon2rs::{Argon2, Variant};
use failure::Error;
use rand_os::OsRng;
use rand_os::rand_core::RngCore;
use syscall::Error as SyscallError;
#[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";
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;
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Fail, PartialEq)]
pub enum UsersError {
#[fail(display = "os error: code {}", reason)]
Os { reason: String },
#[fail(display = "parse error: {}", reason)]
Parsing { reason: String },
#[fail(display = "user/group not found")]
NotFound,
#[fail(display = "user/group already exists")]
AlreadyExists
}
#[inline]
fn parse_error(reason: &str) -> UsersError {
UsersError::Parsing {
reason: reason.into()
}
}
#[inline]
fn os_error(reason: &str) -> UsersError {
UsersError::Os {
reason: reason.into()
}
}
impl From<SyscallError> for UsersError {
fn from(syscall_error: SyscallError) -> UsersError {
UsersError::Os {
reason: format!("{}", syscall_error)
}
}
}
fn read_locked_file(file: impl AsRef<Path>) -> Result<String> {
#[cfg(test)]
println!("Reading file: {}", file.as_ref().display());
#[cfg(target_os = "redox")]
let mut file = OpenOptions::new()
.read(true)
.custom_flags(O_SHLOCK as i32)
.open(file)?;
#[cfg(not(target_os = "redox"))]
#[cfg_attr(rustfmt, rustfmt_skip)]
let mut file = OpenOptions::new()
.read(true)
.open(file)?;
let len = file.metadata()?.len();
let mut file_data = String::with_capacity(len as usize);
file.read_to_string(&mut file_data)?;
Ok(file_data)
}
fn write_locked_file(file: impl AsRef<Path>, data: String) -> Result<()> {
#[cfg(test)]
println!("Writing file: {}", file.as_ref().display());
#[cfg(target_os = "redox")]
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.custom_flags(O_EXLOCK as i32)
.open(file)?;
#[cfg(not(target_os = "redox"))]
#[cfg_attr(rustfmt, rustfmt_skip)]
let mut file = OpenOptions::new()
.write(true)
.truncate(true)
.open(file)?;
file.write(data.as_bytes())?;
Ok(())
}
pub struct User {
pub user: String,
hash: Option<(String, Option<Encoded>)>,
pub uid: usize,
pub gid: usize,
pub name: String,
pub home: String,
pub shell: String,
auth_delay: Duration,
}
impl User {
pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<()> {
self.panic_if_unpopulated();
let password = password.as_ref();
self.hash = if password != "" {
let a2 = Argon2::new(10, 1, 4096, Variant::Argon2i)?;
let salt = format!("{:X}", OsRng::new()?.next_u64());
let enc = Encoded::new(
a2,
password.as_bytes(),
salt.as_bytes(),
&[],
&[]
);
Some((String::from_utf8(enc.to_u8())?, Some(enc)))
} else {
Some(("".into(), None))
};
Ok(())
}
pub fn unset_passwd(&mut self) {
self.panic_if_unpopulated();
self.hash = Some(("!".into(), None));
}
pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool {
self.panic_if_unpopulated();
let &(ref hash, ref encoded) = self.hash.as_ref().unwrap();
let password = password.as_ref();
let verified = if let &Some(ref encoded) = encoded {
encoded.verify(password.as_bytes())
} else {
hash == "" && password == ""
};
if !verified {
#[cfg(not(test))]
thread::sleep(self.auth_delay);
}
verified
}
pub fn is_passwd_blank(&self) -> bool {
self.panic_if_unpopulated();
let &(ref hash, ref encoded) = self.hash.as_ref().unwrap();
hash == "" && encoded.is_none()
}
pub fn is_passwd_unset(&self) -> bool {
self.panic_if_unpopulated();
let &(ref hash, ref encoded) = self.hash.as_ref().unwrap();
hash != "" && encoded.is_none()
}
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 shadowstring(&self) -> String {
self.panic_if_unpopulated();
let hashstring = match self.hash {
Some((ref hash, _)) => hash,
None => panic!("Shadowfile not read!")
};
format!("{};{}", self.user, hashstring)
}
fn populate_hash(&mut self, hash: &str) -> Result<()> {
let encoded = match hash {
"" => None,
"!" => None,
_ => Some(Encoded::from_u8(hash.as_bytes())?)
};
self.hash = Some((hash.to_string(), encoded));
Ok(())
}
#[inline]
fn panic_if_unpopulated(&self) {
if self.hash.is_none() {
panic!("Hash not populated!");
}
}
}
impl Name for User {
fn name(&self) -> &str {
&self.user
}
}
impl Id for User {
fn id(&self) -> usize {
self.uid
}
}
impl Debug for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,
"User {{\n\tuser: {:?}\n\tuid: {:?}\n\tgid: {:?}\n\tname: {:?}
home: {:?}\n\tshell: {:?}\n\tauth_delay: {:?}\n}}",
self.user, self.uid, self.gid, self.name, self.home, self.shell, self.auth_delay
)
}
}
impl Display for User {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
#[cfg_attr(rustfmt, rustfmt_skip)]
write!(f, "{};{};{};{};{};{}",
self.user, self.uid, self.gid, self.name, self.home, self.shell
)
}
}
impl FromStr for User {
type Err = failure::Error;
fn from_str(s: &str) -> Result<Self> {
let mut parts = s.split(';');
let user = parts
.next()
.ok_or(parse_error("expected user"))?;
let uid = parts
.next()
.ok_or(parse_error("expected uid"))?
.parse::<usize>()?;
let gid = parts
.next()
.ok_or(parse_error("expected uid"))?
.parse::<usize>()?;
let name = parts
.next()
.ok_or(parse_error("expected real name"))?;
let home = parts
.next()
.ok_or(parse_error("expected home dir path"))?;
let shell = parts
.next()
.ok_or(parse_error("expected shell path"))?;
Ok(User {
user: user.into(),
hash: None,
uid,
gid,
name: name.into(),
home: home.into(),
shell: shell.into(),
auth_delay: Duration::default(),
})
}
}
#[derive(Debug)]
pub struct Group {
pub group: String,
pub gid: usize,
pub users: Vec<String>,
}
impl Name for Group {
fn name(&self) -> &str {
&self.group
}
}
impl Id for Group {
fn id(&self) -> usize {
self.gid
}
}
impl Display for Group {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
#[cfg_attr(rustfmt, rustfmt_skip)]
write!(f, "{};{};{}",
self.group,
self.gid,
self.users.join(",").trim_matches(',')
)
}
}
impl FromStr for Group {
type Err = failure::Error;
fn from_str(s: &str) -> Result<Self> {
let mut parts = s.split(';');
let group = parts
.next()
.ok_or(parse_error("expected group"))?;
let gid = parts
.next()
.ok_or(parse_error("expected gid"))?
.parse::<usize>()?;
let users_str = parts.next().unwrap_or(" ");
let users = users_str.split(',').map(|u| u.into()).collect();
Ok(Group {
group: group.into(),
gid,
users,
})
}
}
pub fn get_euid() -> Result<usize> {
match syscall::geteuid() {
Ok(euid) => Ok(euid),
Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
}
}
pub fn get_uid() -> Result<usize> {
match syscall::getuid() {
Ok(uid) => Ok(uid),
Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
}
}
pub fn get_egid() -> Result<usize> {
match syscall::getegid() {
Ok(egid) => Ok(egid),
Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
}
}
pub fn get_gid() -> Result<usize> {
match syscall::getgid() {
Ok(gid) => Ok(gid),
Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
}
}
#[derive(Clone)]
pub struct Config {
auth_enabled: bool,
scheme: String,
auth_delay: Duration,
min_id: usize,
max_id: usize,
}
impl Config {
pub fn with_auth() -> Config {
Config {
auth_enabled: true,
..Default::default()
}
}
pub fn auth(mut self, auth: bool) -> Config {
self.auth_enabled = auth;
self
}
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.scheme = scheme;
self
}
fn in_scheme(&self, path: impl AsRef<Path>) -> PathBuf {
let mut canonical_path = PathBuf::from(&self.scheme);
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 {
auth_enabled: false,
scheme: String::from(DEFAULT_SCHEME),
auth_delay: Duration::new(DEFAULT_TIMEOUT, 0),
min_id: MIN_ID,
max_id: MAX_ID,
}
}
}
mod sealed {
use 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>) {
self.list_mut()
.retain(|gruser| gruser.name() != name.as_ref() );
}
fn remove_by_id(&mut self, id: usize) {
self.list_mut()
.retain(|gruser| gruser.id() != id );
}
}
pub struct AllUsers {
users: Vec<User>,
config: Config,
}
impl AllUsers {
pub fn new(config: Config) -> Result<AllUsers> {
let passwd_cntnt = read_locked_file(config.in_scheme(PASSWD_FILE))?;
let mut passwd_entries: Vec<User> = Vec::new();
for line in passwd_cntnt.lines() {
if let Ok(mut user) = User::from_str(line) {
user.auth_delay = config.auth_delay;
passwd_entries.push(user);
}
}
if config.auth_enabled {
let shadow_cntnt = read_locked_file(config.in_scheme(SHADOW_FILE))?;
let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect();
for entry in shadow_entries.iter() {
let mut entry = entry.split(';');
let name = entry.next().ok_or(parse_error(
"error parsing shadowfile: expected username"
))?;
let hash = entry.next().ok_or(parse_error(
"error parsing shadowfile: expected hash"
))?;
passwd_entries
.iter_mut()
.find(|user| user.user == name)
.ok_or(parse_error(
"error parsing shadowfile: unkown user"
))?
.populate_hash(hash)?;
}
}
Ok(AllUsers {
users: passwd_entries,
config
})
}
pub fn add_user(
&mut self,
login: &str,
uid: usize,
gid: usize,
name: &str,
home: &str,
shell: &str
) -> Result<()> {
if self.iter()
.any(|user| user.user == login || user.uid == uid)
{
return Err(From::from(UsersError::AlreadyExists))
}
if !self.config.auth_enabled {
panic!("Attempt to create user without access to the shadowfile");
}
self.users.push(User {
user: login.into(),
hash: Some(("!".into(), None)),
uid,
gid,
name: name.into(),
home: home.into(),
shell: shell.into(),
auth_delay: self.config.auth_delay
});
Ok(())
}
pub fn save(&self) -> Result<()> {
let mut userstring = String::new();
let mut shadowstring = String::new();
for user in &self.users {
userstring.push_str(&format!("{}\n", user.to_string().as_str()));
if self.config.auth_enabled {
shadowstring.push_str(&format!("{}\n", user.shadowstring()));
}
}
write_locked_file(self.config.in_scheme(PASSWD_FILE), userstring)?;
if self.config.auth_enabled {
write_locked_file(self.config.in_scheme(SHADOW_FILE), shadowstring)?;
}
Ok(())
}
}
impl AllInner for AllUsers {
type Gruser = User;
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 All for AllUsers {}
impl Debug for AllUsers {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "AllUsers {{\nusers: {:?}\n}}", self.users)
}
}
pub struct AllGroups {
groups: Vec<Group>,
config: Config,
}
impl AllGroups {
pub fn new(config: Config) -> Result<AllGroups> {
let group_cntnt = read_locked_file(config.in_scheme(GROUP_FILE))?;
let mut entries: Vec<Group> = Vec::new();
for line in group_cntnt.lines() {
if let Ok(group) = Group::from_str(line) {
entries.push(group);
}
}
Ok(AllGroups {
groups: entries,
config,
})
}
pub fn add_group(
&mut self,
name: &str,
gid: usize,
users: &[&str]
) -> Result<()> {
if self.iter()
.any(|group| group.group == name || group.gid == gid)
{
return Err(From::from(UsersError::AlreadyExists))
}
self.groups.push(Group {
group: name.into(),
gid,
users: users
.iter()
.map(|user| user.to_string())
.collect()
});
Ok(())
}
pub fn save(&self) -> Result<()> {
let mut groupstring = String::new();
for group in &self.groups {
groupstring.push_str(&format!("{}\n", group.to_string().as_str()));
}
write_locked_file(self.config.in_scheme(GROUP_FILE), groupstring)
}
}
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 = concat!(env!("CARGO_MANIFEST_DIR"), "/tests");
fn test_prefix(filename: &str) -> String {
let mut complete = String::from(TEST_PREFIX);
complete.push_str(filename);
complete
}
fn test_cfg() -> Config {
Config::default()
.scheme(TEST_PREFIX.to_string())
}
fn test_auth_cfg() -> Config {
test_cfg().auth(true)
}
#[test]
#[should_panic(expected = "Hash not populated!")]
fn wrong_attempt_set_password() {
let mut users = AllUsers::new(test_cfg()).unwrap();
let user = users.get_mut_by_id(1000).unwrap();
user.set_passwd("").unwrap();
}
#[test]
#[should_panic(expected = "Hash not populated!")]
fn wrong_attempt_unset_password() {
let mut users = AllUsers::new(test_cfg()).unwrap();
let user = users.get_mut_by_id(1000).unwrap();
user.unset_passwd();
}
#[test]
#[should_panic(expected = "Hash not populated!")]
fn wrong_attempt_verify_password() {
let mut users = AllUsers::new(test_cfg()).unwrap();
let user = users.get_mut_by_id(1000).unwrap();
user.verify_passwd("hi folks");
}
#[test]
#[should_panic(expected = "Hash not populated!")]
fn wrong_attempt_is_password_blank() {
let mut users = AllUsers::new(test_cfg()).unwrap();
let user = users.get_mut_by_id(1000).unwrap();
user.is_passwd_blank();
}
#[test]
#[should_panic(expected = "Hash not populated!")]
fn wrong_attempt_is_password_unset() {
let mut users = AllUsers::new(test_cfg()).unwrap();
let user = users.get_mut_by_id(1000).unwrap();
user.is_passwd_unset();
}
#[test]
fn attempt_user_api() {
let mut users = AllUsers::new(test_auth_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);
}
#[test]
fn get_user() {
let users = AllUsers::new(test_auth_cfg()).unwrap();
let root = users.get_by_id(0).expect("'root' user missing");
assert_eq!(root.user, "root".to_string());
let &(ref hashstring, ref encoded) = root.hash.as_ref().expect("'root' hash is None");
assert_eq!(hashstring,
&"$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk".to_string());
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());
match encoded {
&Some(_) => (),
&None => panic!("Expected encoded argon hash!")
}
let user = users.get_by_name("user").expect("'user' user missing");
assert_eq!(user.user, "user".to_string());
let &(ref hashstring, ref encoded) = user.hash.as_ref().expect("'user' hash is None");
assert_eq!(hashstring, &"".to_string());
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());
match encoded {
&Some(_) => panic!("Should not be an argon hash!"),
&None => ()
}
println!("{:?}", users);
let li = users.get_by_name("li").expect("'li' user missing");
println!("got li");
assert_eq!(li.user, "li");
let &(ref hashstring, ref encoded) = li.hash.as_ref().expect("'li' hash is None");
assert_eq!(hashstring, &"!".to_string());
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());
match encoded {
&Some(_) => panic!("Should not be an argon hash!"),
&None => ()
}
}
#[test]
fn manip_user() {
let mut users = AllUsers::new(test_auth_cfg()).unwrap();
let id = 7099;
users
.add_user("fb", id, id, "FooBar", "/home/foob", "/bin/zsh")
.expect("failed to add user 'fb'");
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",
"li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
"fb;7099;7099;FooBar;/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",
"li;!\n",
"fb;!\n"
));
{
println!("{:?}", users);
let fb = users.get_mut_by_name("fb").expect("'fb' 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",
"li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
"fb;7099;7099;FooBar;/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",
"li;!\n",
"fb;\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",
"li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n"
)
);
}
#[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 mut groups = AllGroups::new(test_cfg()).unwrap();
let id = 7099;
groups.add_group("fb", id, &["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",
"li;1007;li\n",
"fb;7099;fb\n"
)
);
{
let fb = groups.get_mut_by_name("fb").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",
"li;1007;li\n",
"fb;7099;fb,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",
"li;1007;li\n"
)
);
}
#[test]
fn users_get_unused_ids() {
let users = AllUsers::new(test_cfg()).unwrap_or_else(|err| panic!(err));
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!");
}
}
}