redox_users/
lib.rs

1//! `redox-users` is designed to be a small, low-ish level interface
2//! to system user and group information, as well as user password
3//! authentication. It is OS-specific and will break horribly on platforms
4//! that are not [Redox-OS](https://redox-os.org).
5//!
6//! # Permissions
7//! Because this is a system level tool dealing with password
8//! authentication, programs are often required to run with
9//! escalated priveleges. The implementation of the crate is
10//! privelege unaware. The only privelege requirements are those
11//! laid down by the system administrator over these files:
12//! - `/etc/group`
13//!   - Read: Required to access group information
14//!   - Write: Required to change group information
15//! - `/etc/passwd`
16//!   - Read: Required to access user information
17//!   - Write: Required to change user information
18//! - `/etc/shadow`
19//!   - Read: Required to authenticate users
20//!   - Write: Required to set user passwords
21//!
22//! # Reimplementation
23//! This crate is designed to be as small as possible without
24//! sacrificing critical functionality. The idea is that a small
25//! enough redox-users will allow easy re-implementation based on
26//! the same flexible API. This would allow more complicated authentication
27//! schemes for redox in future without breakage of existing
28//! software.
29
30use std::fmt::Debug;
31use std::fs::{File, OpenOptions};
32use std::io::{Read, Seek, SeekFrom, Write};
33#[cfg(target_os = "redox")]
34use std::os::unix::fs::OpenOptionsExt;
35#[cfg(not(target_os = "redox"))]
36use std::os::unix::io::AsRawFd;
37use std::os::unix::process::CommandExt;
38use std::path::{Path, PathBuf};
39use std::process::Command;
40use std::slice::{Iter, IterMut};
41#[cfg(not(test))]
42#[cfg(feature = "auth")]
43use std::thread;
44use std::time::Duration;
45
46use thiserror::Error;
47#[cfg(feature = "auth")]
48use zeroize::Zeroize;
49
50//#[cfg(not(target_os = "redox"))]
51//use nix::fcntl::{flock, FlockArg};
52
53#[cfg(target_os = "redox")]
54use libredox::flag::{O_EXLOCK, O_SHLOCK};
55
56const PASSWD_FILE: &'static str = "/etc/passwd";
57const GROUP_FILE: &'static str = "/etc/group";
58#[cfg(feature = "auth")]
59const SHADOW_FILE: &'static str = "/etc/shadow";
60
61const MIN_ID: usize = 1000;
62const MAX_ID: usize = 6000;
63const DEFAULT_TIMEOUT: u64 = 3;
64
65const USERNAME_LEN_MIN: usize = 3;
66const USERNAME_LEN_MAX: usize = 32;
67
68/// Errors that might happen while using this crate
69#[derive(Debug, Error)]
70#[non_exhaustive]
71pub enum Error {
72    #[error("os error: {reason}")]
73    Os { reason: &'static str },
74
75    #[error(transparent)]
76    Io(#[from] std::io::Error),
77
78    #[error("failed to generate seed: {0}")]
79    Getrandom(#[from] getrandom::Error),
80
81    #[cfg(feature = "auth")]
82    #[error("")]
83    Argon(#[from] argon2::Error),
84
85    #[error("parse error line {line}: {reason}")]
86    Parsing { reason: String, line: usize },
87
88    #[error(transparent)]
89    ParseInt(#[from] std::num::ParseIntError),
90
91    #[error("user not found")]
92    UserNotFound,
93
94    #[error("group not found")]
95    GroupNotFound,
96
97    #[error("user already exists")]
98    UserAlreadyExists,
99
100    #[error("group already exists")]
101    GroupAlreadyExists,
102
103    #[error("invalid name '{name}'")]
104    InvalidName { name: String },
105
106    /// Used for invalid string field values of [`User`]
107    #[error("invalid entry element '{data}'")]
108    InvalidData { data: String },
109}
110pub type Result<T, E = Error> = core::result::Result<T, E>;
111
112#[inline]
113fn parse_error(line: usize, reason: &str) -> Error {
114    Error::Parsing {
115        reason: reason.into(),
116        line,
117    }
118}
119
120impl From<libredox::error::Error> for Error {
121    fn from(syscall_error: libredox::error::Error) -> Error {
122        Error::Io(std::io::Error::from(syscall_error))
123    }
124}
125
126#[derive(Clone, Copy, Debug)]
127enum Lock {
128    Shared,
129    Exclusive,
130}
131
132impl Lock {
133    fn can_write(&self) -> bool {
134        match self {
135            Lock::Shared => false,
136            Lock::Exclusive => true,
137        }
138    }
139
140    #[cfg(target_os = "redox")]
141    fn as_olock(self) -> i32 {
142        (match self {
143            Lock::Shared => O_SHLOCK,
144            Lock::Exclusive => O_EXLOCK,
145        }) as i32
146    }
147
148    /*#[cfg(not(target_os = "redox"))]
149    fn as_flock(self) -> FlockArg {
150        match self {
151            Lock::Shared => FlockArg::LockShared,
152            Lock::Exclusive => FlockArg::LockExclusive,
153        }
154    }*/
155}
156
157/// Naive semi-cross platform file locking (need to support linux for tests).
158#[allow(dead_code)]
159fn locked_file(file: impl AsRef<Path>, lock: Lock) -> Result<File, Error> {
160    #[cfg(test)]
161    println!("Open file: {}", file.as_ref().display());
162
163    #[cfg(target_os = "redox")]
164    {
165        Ok(OpenOptions::new()
166            .read(true)
167            .write(lock.can_write())
168            .custom_flags(lock.as_olock())
169            .open(file)?)
170    }
171    #[cfg(not(target_os = "redox"))]
172    #[cfg_attr(rustfmt, rustfmt_skip)]
173    {
174        let file = OpenOptions::new()
175            .read(true)
176            .write(lock.can_write())
177            .open(file)?;
178        let fd = file.as_raw_fd();
179        eprintln!("Fd: {}", fd);
180        //flock(fd, lock.as_flock())?;
181        Ok(file)
182    }
183}
184
185/// Reset a file for rewriting (user/group dbs must be erased before write-out)
186fn reset_file(fd: &mut File) -> Result<(), Error> {
187    fd.set_len(0)?;
188    fd.seek(SeekFrom::Start(0))?;
189    Ok(())
190}
191
192/// Is a string safe to write to `/etc/group` or `/etc/passwd`?
193fn is_safe_string(s: &str) -> bool {
194    !s.contains(';')
195}
196
197const PORTABLE_FILE_NAME_CHARS: &str =
198    "0123456789._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
199
200/// This function is used by [`UserBuilder`] and [`GroupBuilder`] to determine
201/// if a name for a user/group is valid. It is provided for convenience.
202///
203/// Usernames must match the [POSIX standard
204/// for usernames](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_437)
205/// . The "portable filename character set" is defined as `A-Z`, `a-z`, `0-9`,
206/// and `._-` (see [here](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282)).
207///
208/// Usernames may not be more than 32 or less than 3 characters in length.
209pub fn is_valid_name(name: &str) -> bool {
210    if name.len() < USERNAME_LEN_MIN || name.len() > USERNAME_LEN_MAX {
211        false
212    } else if let Some(first) = name.chars().next() {
213        first != '-' &&
214            name.chars().all(|c| {
215                PORTABLE_FILE_NAME_CHARS.contains(c)
216            })
217    } else {
218        false
219    }
220}
221
222/// Marker types for [`User`] and [`AllUsers`].
223pub mod auth {
224    #[cfg(feature = "auth")]
225    use std::fmt;
226
227    #[cfg(feature = "auth")]
228    use zeroize::Zeroize;
229
230    #[cfg(feature = "auth")]
231    use crate::Error;
232
233    /// Marker type indicating that a `User` only has access to world-readable
234    /// user information, and cannot authenticate.
235    #[derive(Debug, Default)]
236    pub struct Basic {}
237
238    /// Marker type indicating that a `User` has access to all user
239    /// information, including password hashes.
240    #[cfg(feature = "auth")]
241    #[derive(Default, Zeroize)]
242    #[zeroize(drop)]
243    pub struct Full {
244        pub(crate) hash: String,
245    }
246
247    #[cfg(feature = "auth")]
248    impl Full {
249        pub(crate) fn empty() -> Full {
250            Full { hash: "".into() }
251        }
252
253        pub(crate) fn is_empty(&self) -> bool {
254            &self.hash == ""
255        }
256
257        pub(crate) fn unset() -> Full {
258            Full { hash: "!".into() }
259        }
260
261        pub(crate) fn is_unset(&self) -> bool {
262            &self.hash == "!"
263        }
264
265        pub(crate) fn passwd(pw: &str) -> Result<Full, Error> {
266            Ok(if pw != "" {
267                let mut buf = [0u8; 8];
268                getrandom::getrandom(&mut buf)?;
269                let mut salt = format!("{:X}", u64::from_ne_bytes(buf));
270
271                let config = argon2::Config::default();
272                let hash: String = argon2::hash_encoded(
273                    pw.as_bytes(),
274                    salt.as_bytes(),
275                    &config
276                )?;
277
278                buf.zeroize();
279                salt.zeroize();
280                Full { hash } // note that move == shallow copy in Rust
281            } else {
282                Full::empty()
283            })
284        }
285
286        pub(crate) fn verify(&self, pw: &str) -> bool {
287            match self.hash.as_str() {
288                "" => pw == "",
289                "!" => false,
290                //TODO: When does this panic? Should this function return
291                // Result? Or does it need to simply fail to verify if
292                // verify_encoded() fails?
293                hash => argon2::verify_encoded(&hash, pw.as_bytes())
294                    .expect("failed to verify hash"),
295            }
296        }
297    }
298
299    #[cfg(feature = "auth")]
300    impl fmt::Debug for Full {
301        fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
302            f.debug_struct("Full")
303                .finish()
304        }
305    }
306}
307
308/// A builder pattern for adding [`User`]s to [`AllUsers`]. Fields are verified
309/// when the group is built via [`AllUsers::add_user`]. See the documentation
310/// of that function for default values.
311///
312/// Note that this builder is not available when the `auth` feature of the
313/// crate is disabled.
314///
315/// # Example
316/// ```no_run
317/// # use redox_users::{AllGroups, Config, GroupBuilder, UserBuilder};
318/// let mut allgs = AllGroups::new(Config::default()).unwrap();
319///
320/// let g = GroupBuilder::new("foobar")
321///     .user("foobar");
322/// let foobar_g = allgs.add_group(g).unwrap();
323///
324/// let u = UserBuilder::new("foobar")
325///     .gid(foobar_g.gid)
326///     .name("Foo Bar")
327///     // Note that this directory will not be created
328///     .home("file:/home/foobar");
329/// ```
330#[cfg(feature = "auth")]
331pub struct UserBuilder {
332    user: String,
333    uid: Option<usize>,
334    gid: Option<usize>,
335    name: Option<String>,
336    home: Option<String>,
337    shell: Option<String>,
338}
339
340#[cfg(feature = "auth")]
341impl UserBuilder {
342    /// Create a new `UserBuilder` with the login name for the new user.
343    pub fn new(user: impl AsRef<str>) -> UserBuilder {
344        UserBuilder {
345            user: user.as_ref().to_string(),
346            uid: None,
347            gid: None,
348            name: None,
349            home: None,
350            shell: None,
351        }
352    }
353
354    /// Set the user id for this user.
355    pub fn uid(mut self, uid: usize) -> UserBuilder {
356        self.uid = Some(uid);
357        self
358    }
359
360    /// Set the primary group id for this user.
361    pub fn gid(mut self, gid: usize) -> UserBuilder {
362        self.gid = Some(gid);
363        self
364    }
365
366    /// Set the GECOS field for this user.
367    pub fn name(mut self, name: impl AsRef<str>) -> UserBuilder {
368        self.name = Some(name.as_ref().to_string());
369        self
370    }
371
372    /// Set the home directory for this user.
373    pub fn home(mut self, home: impl AsRef<str>) -> UserBuilder {
374        self.home = Some(home.as_ref().to_string());
375        self
376    }
377
378    /// Set the login shell for this user.
379    pub fn shell(mut self, shell: impl AsRef<str>) -> UserBuilder {
380        self.shell = Some(shell.as_ref().to_string());
381        self
382    }
383}
384
385/// A struct representing a Redox user.
386/// Currently maps to an entry in the `/etc/passwd` file.
387///
388/// `A` should be a type from [`crate::auth`].
389///
390/// # Unset vs. Blank Passwords
391/// A note on unset passwords vs. blank passwords. A blank password
392/// is a hash field that is completely blank (aka, `""`). According
393/// to this crate, successful login is only allowed if the input
394/// password is blank as well.
395///
396/// An unset password is one whose hash is not empty (`""`), but
397/// also not a valid serialized argon2rs hashing session. This
398/// hash always returns `false` upon attempted verification. The
399/// most commonly used hash for an unset password is `"!"`, but
400/// this crate makes no distinction. The most common way to unset
401/// the password is to use [`User::unset_passwd`].
402#[derive(Debug)]
403pub struct User<A> {
404    /// Username (login name)
405    pub user: String,
406    /// User id
407    pub uid: usize,
408    /// Group id
409    pub gid: usize,
410    /// Real name (human readable, can contain spaces)
411    pub name: String,
412    /// Home directory path
413    pub home: String,
414    /// Shell path
415    pub shell: String,
416
417    // Failed login delay duration
418    auth_delay: Duration,
419
420    #[allow(dead_code)]
421    auth: A,
422}
423
424impl<A: Default> User<A> {
425    /// Get a Command to run the user's default shell (see [`User::login_cmd`]
426    /// for more docs).
427    pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) }
428
429    /// Provide a login command for the user, which is any entry point for
430    /// starting a user's session, whether a shell (use [`User::shell_cmd`]
431    /// instead) or a graphical init.
432    ///
433    /// The `Command` will use the user's `uid` and `gid`, its `current_dir`
434    /// will be set to the user's home directory, and the follwing enviroment
435    /// variables will be populated:
436    ///
437    ///    - `USER` set to the user's `user` field.
438    ///    - `UID` set to the user's `uid` field.
439    ///    - `GROUPS` set the user's `gid` field.
440    ///    - `HOME` set to the user's `home` field.
441    ///    - `SHELL` set to the user's `shell` field.
442    pub fn login_cmd<T>(&self, cmd: T) -> Command
443        where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str>
444    {
445        let mut command = Command::new(cmd);
446        command
447            .uid(self.uid as u32)
448            .gid(self.gid as u32)
449            .current_dir(&self.home)
450            .env("USER", &self.user)
451            .env("UID", format!("{}", self.uid))
452            .env("GROUPS", format!("{}", self.gid))
453            .env("HOME", &self.home)
454            .env("SHELL", &self.shell);
455        command
456    }
457
458    fn from_passwd_entry(s: &str, line: usize) -> Result<User<A>, Error> {
459        let mut parts = s.split(';');
460
461        let user = parts
462            .next()
463            .ok_or(parse_error(line, "expected user"))?;
464        let uid = parts
465            .next()
466            .ok_or(parse_error(line, "expected uid"))?
467            .parse::<usize>()?;
468        let gid = parts
469            .next()
470            .ok_or(parse_error(line, "expected uid"))?
471            .parse::<usize>()?;
472        let name = parts
473            .next()
474            .ok_or(parse_error(line, "expected real name"))?;
475        let home = parts
476            .next()
477            .ok_or(parse_error(line, "expected home dir path"))?;
478        let shell = parts
479            .next()
480            .ok_or(parse_error(line, "expected shell path"))?;
481
482        Ok(User::<A> {
483            user: user.into(),
484            uid,
485            gid,
486            name: name.into(),
487            home: home.into(),
488            shell: shell.into(),
489            auth: A::default(),
490            auth_delay: Duration::default(),
491        })
492    }
493}
494
495#[cfg(feature = "auth")]
496impl User<auth::Full> {
497    /// Set the password for a user. Make **sure** that `password`
498    /// is actually what the user wants as their password (this doesn't).
499    ///
500    /// To set the password blank, pass `""` as `password`.
501    ///
502    /// Note that `password` is taken as a reference, so it is up to the caller
503    /// to properly zero sensitive memory (see `zeroize` on crates.io).
504    pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<(), Error> {
505        self.auth = auth::Full::passwd(password.as_ref())?;
506        Ok(())
507    }
508
509    /// Unset the password ([`User::verify_passwd`] always returns `false`).
510    pub fn unset_passwd(&mut self) {
511        self.auth = auth::Full::unset();
512    }
513
514    /// Verify the password. If the hash is empty, this only returns `true` if
515    /// `password` is also empty.
516    ///
517    /// Note that this is a blocking operation if the password is incorrect.
518    /// See [`Config::auth_delay`] to set the wait time. Default is 3 seconds.
519    ///
520    /// Note that `password` is taken as a reference, so it is up to the caller
521    /// to properly zero sensitive memory (see `zeroize` on crates.io).
522    pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool {
523        let verified = self.auth.verify(password.as_ref());
524        if !verified {
525            #[cfg(not(test))] // Make tests run faster
526            thread::sleep(self.auth_delay);
527        }
528        verified
529    }
530
531    /// Determine if the hash for the password is blank ([`User::verify_passwd`]
532    /// returns `true` *only* when the password is blank).
533    pub fn is_passwd_blank(&self) -> bool {
534        self.auth.is_empty()
535    }
536
537    /// Determine if the hash for the password is unset
538    /// ([`User::verify_passwd`] returns `false` regardless of input).
539    pub fn is_passwd_unset(&self) -> bool {
540        self.auth.is_unset()
541    }
542
543    /// Format this user as an entry in `/etc/passwd`.
544    fn passwd_entry(&self) -> Result<String, Error> {
545        if !is_safe_string(&self.user) {
546            Err(Error::InvalidName { name: self.user.to_string() })
547        } else if !is_safe_string(&self.name) {
548            Err(Error::InvalidData { data: self.name.to_string() })
549        } else if !is_safe_string(&self.home) {
550            Err(Error::InvalidData { data: self.home.to_string() })
551        } else if !is_safe_string(&self.shell) {
552            Err(Error::InvalidData { data: self.shell.to_string() })
553        } else {
554            #[cfg_attr(rustfmt, rustfmt_skip)]
555            Ok(format!("{};{};{};{};{};{}\n",
556                self.user, self.uid, self.gid, self.name, self.home, self.shell
557            ))
558        }
559    }
560
561    fn shadow_entry(&self) -> Result<String, Error> {
562        if !is_safe_string(&self.user) {
563            Err(Error::InvalidName { name: self.user.to_string() })
564        } else {
565            Ok(format!("{};{}\n", self.user, self.auth.hash))
566        }
567    }
568}
569
570impl<A> Name for User<A> {
571    fn name(&self) -> &str {
572        &self.user
573    }
574}
575
576impl<A> Id for User<A> {
577    fn id(&self) -> usize {
578        self.uid
579    }
580}
581
582/// A builder pattern for adding [`Group`]s to [`AllGroups`]. Fields are
583/// verified when the `Group` is built, via [`AllGroups::add_group`].
584///
585/// # Example
586/// ```
587/// # use redox_users::GroupBuilder;
588/// // When added, this group will use the first available group id
589/// let mygroup = GroupBuilder::new("group_name");
590///
591/// // A little more stuff:
592/// let other = GroupBuilder::new("special")
593///     .gid(9055)
594///     .user("some_username");
595/// ```
596pub struct GroupBuilder {
597    // Group name
598    group: String,
599
600    gid: Option<usize>,
601
602    users: Vec<String>,
603}
604
605impl GroupBuilder {
606    /// Create a new `GroupBuilder` with the given group name.
607    pub fn new(group: impl AsRef<str>) -> GroupBuilder {
608        GroupBuilder {
609            group: group.as_ref().to_string(),
610            gid: None,
611            users: vec![],
612        }
613    }
614
615    /// Set the group id of this group.
616    pub fn gid(mut self, gid: usize) -> GroupBuilder {
617        self.gid = Some(gid);
618        self
619    }
620
621    /// Add a user to this group. Call this function multiple times to add more
622    /// users.
623    pub fn user(mut self, user: impl AsRef<str>) -> GroupBuilder {
624        self.users.push(user.as_ref().to_string());
625        self
626    }
627}
628
629/// A struct representing a Redox user group.
630/// Currently maps to an `/etc/group` file entry.
631#[derive(Debug)]
632pub struct Group {
633    /// Group name
634    pub group: String,
635    /// Password (unused, usually "x")
636    pub password: String,
637    /// Unique group id
638    pub gid: usize,
639    /// Group members' usernames
640    pub users: Vec<String>,
641}
642
643impl Group {
644    fn from_group_entry(s: &str, line: usize) -> Result<Group, Error> {
645        let mut parts = s.trim()
646            .split(';');
647
648        let group = parts
649            .next()
650            .ok_or(parse_error(line, "expected group"))?;
651        let password = parts
652            .next()
653            .ok_or(parse_error(line, "expected password"))?;
654        let gid = parts
655            .next()
656            .ok_or(parse_error(line, "expected gid"))?
657            .parse::<usize>()?;
658        let users_str = parts.next()
659            .unwrap_or("");
660        let users = users_str.split(',')
661            .filter_map(|u| if u == "" {
662                None
663            } else {
664                Some(u.into())
665            })
666            .collect();
667
668        Ok(Group {
669            group: group.into(),
670            password: password.into(),
671            gid,
672            users,
673        })
674    }
675
676    fn group_entry(&self) -> Result<String, Error> {
677        if !is_safe_string(&self.group) {
678            Err(Error::InvalidName { name: self.group.to_string() })
679        } else {
680            for username in self.users.iter() {
681                if !is_safe_string(&username) {
682                    return Err(Error::InvalidData { data: username.to_string() });
683                }
684            }
685
686            #[cfg_attr(rustfmt, rustfmt_skip)]
687            Ok(format!("{};{};{};{}\n",
688                self.group,
689                self.password,
690                self.gid,
691                self.users.join(",").trim_matches(',')
692            ))
693        }
694    }
695}
696
697impl Name for Group {
698    fn name(&self) -> &str {
699        &self.group
700    }
701}
702
703impl Id for Group {
704    fn id(&self) -> usize {
705        self.gid
706    }
707}
708
709/// Gets the current process effective user ID.
710///
711/// This function issues the `geteuid` system call returning the process effective
712/// user id.
713///
714/// # Examples
715///
716/// Basic usage:
717///
718/// ```no_run
719/// # use redox_users::get_euid;
720/// let euid = get_euid().unwrap();
721/// ```
722pub fn get_euid() -> Result<usize, Error> {
723    libredox::call::geteuid()
724        .map_err(From::from)
725}
726
727/// Gets the current process real user ID.
728///
729/// This function issues the `getuid` system call returning the process real
730/// user id.
731///
732/// # Examples
733///
734/// Basic usage:
735///
736/// ```no_run
737/// # use redox_users::get_uid;
738/// let uid = get_uid().unwrap();
739/// ```
740pub fn get_uid() -> Result<usize, Error> {
741    libredox::call::getruid()
742        .map_err(From::from)
743}
744
745/// Gets the current process effective group ID.
746///
747/// This function issues the `getegid` system call returning the process effective
748/// group id.
749///
750/// # Examples
751///
752/// Basic usage:
753///
754/// ```no_run
755/// # use redox_users::get_egid;
756/// let egid = get_egid().unwrap();
757/// ```
758pub fn get_egid() -> Result<usize, Error> {
759    libredox::call::getegid()
760        .map_err(From::from)
761}
762
763/// Gets the current process real group ID.
764///
765/// This function issues the `getegid` system call returning the process real
766/// group id.
767///
768/// # Examples
769///
770/// Basic usage:
771///
772/// ```no_run
773/// # use redox_users::get_gid;
774/// let gid = get_gid().unwrap();
775/// ```
776pub fn get_gid() -> Result<usize, Error> {
777    libredox::call::getrgid()
778        .map_err(From::from)
779}
780
781/// A generic configuration that allows fine control of an [`AllUsers`] or
782/// [`AllGroups`].
783///
784/// `auth_delay` is not used by [`AllGroups`]
785///
786/// In most situations, [`Config::default`](struct.Config.html#impl-Default)
787/// will work just fine. The other fields are for finer control if it is
788/// required.
789///
790/// # Example
791/// ```
792/// # use redox_users::Config;
793/// use std::time::Duration;
794///
795/// let cfg = Config::default()
796///     .min_id(500)
797///     .max_id(1000)
798///     .auth_delay(Duration::from_secs(5));
799/// ```
800#[derive(Clone, Debug)]
801pub struct Config {
802    root_fs: PathBuf,
803    auth_delay: Duration,
804    min_id: usize,
805    max_id: usize,
806    lock: Lock,
807}
808
809impl Config {
810    /// Set the delay for a failed authentication. Default is 3 seconds.
811    pub fn auth_delay(mut self, delay: Duration) -> Config {
812        self.auth_delay = delay;
813        self
814    }
815
816    /// Set the smallest ID possible to use when finding an unused ID.
817    pub fn min_id(mut self, id: usize) -> Config {
818        self.min_id = id;
819        self
820    }
821
822    /// Set the largest possible ID to use when finding an unused ID.
823    pub fn max_id(mut self, id: usize) -> Config {
824        self.max_id = id;
825        self
826    }
827
828    /// Set the scheme relative to which the [`AllUsers`] or [`AllGroups`]
829    /// should be looking for its data files. This is a compromise between
830    /// exposing implementation details and providing fine enough
831    /// control over the behavior of this API.
832    // FIXME rename to root_fs the next time we release a breaking change
833    pub fn scheme(mut self, scheme: String) -> Config {
834        self.root_fs = PathBuf::from(scheme);
835        self
836    }
837
838    /// Allow writes to group, passwd, and shadow files
839    pub fn writeable(mut self, writeable: bool) -> Config {
840        self.lock = if writeable {
841            Lock::Exclusive
842        } else {
843            Lock::Shared
844        };
845        self
846    }
847
848    // Prepend a path with the scheme in this Config
849    fn in_root_fs(&self, path: impl AsRef<Path>) -> PathBuf {
850        let mut canonical_path = self.root_fs.clone();
851        // Should be a little careful here, not sure I want this behavior
852        if path.as_ref().is_absolute() {
853            // This is nasty
854            canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string());
855        } else {
856            canonical_path.push(path);
857        }
858        canonical_path
859    }
860}
861
862impl Default for Config {
863    /// The default root filesystem is `/`.
864    ///
865    /// The default auth delay is 3 seconds.
866    ///
867    /// The default min and max ids are 1000 and 6000.
868    fn default() -> Config {
869        Config {
870            root_fs: PathBuf::from("/"),
871            auth_delay: Duration::new(DEFAULT_TIMEOUT, 0),
872            min_id: MIN_ID,
873            max_id: MAX_ID,
874            lock: Lock::Shared,
875        }
876    }
877}
878
879// Nasty hack to prevent the compiler complaining about
880// "leaking" `AllInner`
881mod sealed {
882    use crate::Config;
883
884    pub trait Name {
885        fn name(&self) -> &str;
886    }
887
888    pub trait Id {
889        fn id(&self) -> usize;
890    }
891
892    pub trait AllInner {
893        // Group+User, thanks Dad
894        type Gruser: Name + Id;
895
896        /// These functions grab internal elements so that the other
897        /// methods of `All` can manipulate them.
898        fn list(&self) -> &Vec<Self::Gruser>;
899        fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
900        fn config(&self) -> &Config;
901    }
902}
903
904use sealed::{AllInner, Id, Name};
905
906/// This trait is used to remove repetitive API items from
907/// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait
908/// so that the implementations of functions can be implemented
909/// at the trait level. Do not try to implement this trait.
910pub trait All: AllInner {
911    /// Get an iterator borrowing all [`User`]s or [`Group`]s on the system.
912    fn iter(&self) -> Iter<<Self as AllInner>::Gruser> {
913        self.list().iter()
914    }
915
916    /// Get an iterator mutably borrowing all [`User`]s or [`Group`]s on the
917    /// system.
918    fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> {
919        self.list_mut().iter_mut()
920    }
921
922    /// Borrow the [`User`] or [`Group`] with a given name.
923    ///
924    /// # Examples
925    ///
926    /// Basic usage:
927    ///
928    /// ```no_run
929    /// # use redox_users::{All, AllUsers, Config};
930    /// let users = AllUsers::basic(Config::default()).unwrap();
931    /// let user = users.get_by_name("root").unwrap();
932    /// ```
933    fn get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser> {
934        self.iter()
935            .find(|gruser| gruser.name() == name.as_ref() )
936    }
937
938    /// Mutable version of [`All::get_by_name`].
939    fn get_mut_by_name(&mut self, name: impl AsRef<str>) -> Option<&mut <Self as AllInner>::Gruser> {
940        self.iter_mut()
941            .find(|gruser| gruser.name() == name.as_ref() )
942    }
943
944    /// Borrow the [`User`] or [`Group`] with the given ID.
945    ///
946    /// # Examples
947    ///
948    /// Basic usage:
949    ///
950    /// ```no_run
951    /// # use redox_users::{All, AllUsers, Config};
952    /// let users = AllUsers::basic(Config::default()).unwrap();
953    /// let user = users.get_by_id(0).unwrap();
954    /// ```
955    fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> {
956        self.iter()
957            .find(|gruser| gruser.id() == id )
958    }
959
960    /// Mutable version of [`All::get_by_id`].
961    fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> {
962        self.iter_mut()
963            .find(|gruser| gruser.id() == id )
964    }
965
966    /// Provides an unused id based on the min and max values in the [`Config`]
967    /// passed to the `All`'s constructor.
968    ///
969    /// # Examples
970    ///
971    /// ```no_run
972    /// # use redox_users::{All, AllUsers, Config};
973    /// let users = AllUsers::basic(Config::default()).unwrap();
974    /// let uid = users.get_unique_id().expect("no available uid");
975    /// ```
976    fn get_unique_id(&self) -> Option<usize> {
977        for id in self.config().min_id..self.config().max_id {
978            if !self.iter().any(|gruser| gruser.id() == id ) {
979                return Some(id)
980            }
981        }
982        None
983    }
984
985    /// Remove a [`User`] or [`Group`] from this `All` given it's name. If the
986    /// Gruser was removed return `true`, else return `false`. This ensures
987    /// that the Gruser no longer exists.
988    fn remove_by_name(&mut self, name: impl AsRef<str>) -> bool {
989        let list = self.list_mut();
990        let indx = list.iter()
991            .enumerate()
992            .find_map(|(indx, gruser)| if gruser.name() == name.as_ref() {
993                    Some(indx)
994                } else {
995                    None
996                });
997        if let Some(indx) = indx {
998            list.remove(indx);
999            true
1000        } else {
1001            false
1002        }
1003    }
1004
1005    /// Id version of [`All::remove_by_name`].
1006    fn remove_by_id(&mut self, id: usize) -> bool {
1007        let list = self.list_mut();
1008        let indx = list.iter()
1009            .enumerate()
1010            .find_map(|(indx, gruser)| if gruser.id() == id {
1011                    Some(indx)
1012                } else {
1013                    None
1014                });
1015        if let Some(indx) = indx {
1016            list.remove(indx);
1017            true
1018        } else {
1019            false
1020        }
1021    }
1022}
1023
1024/// `AllUsers` provides (borrowed) access to all the users on the system.
1025/// Note that this struct implements [`All`] for all of its access functions.
1026///
1027/// # Notes
1028/// Note that everything in this section also applies to [`AllGroups`].
1029///
1030/// * If you mutate anything owned by an `AllUsers`, you must call the
1031///   [`AllUsers::save`] in order for those changes to be applied to the system.
1032/// * The API here is kept small. Most mutating actions can be accomplished via
1033///   the [`All::get_mut_by_id`] and [`All::get_mut_by_name`]
1034///   functions.
1035#[derive(Debug)]
1036pub struct AllUsers<A> {
1037    users: Vec<User<A>>,
1038    config: Config,
1039
1040    // Hold on to the locked fds to prevent race conditions
1041    #[allow(dead_code)]
1042    passwd_fd: File,
1043    #[allow(dead_code)]
1044    shadow_fd: Option<File>,
1045}
1046
1047impl<A: Default> AllUsers<A> {
1048    pub fn new(config: Config) -> Result<AllUsers<A>, Error> {
1049        let mut passwd_fd = locked_file(config.in_root_fs(PASSWD_FILE), config.lock)?;
1050        let mut passwd_cntnt = String::new();
1051        passwd_fd.read_to_string(&mut passwd_cntnt)?;
1052
1053        let mut passwd_entries = Vec::new();
1054        for (indx, line) in passwd_cntnt.lines().enumerate() {
1055            let mut user = User::from_passwd_entry(line, indx)?;
1056            user.auth_delay = config.auth_delay;
1057            passwd_entries.push(user);
1058        }
1059
1060        Ok(AllUsers::<A> {
1061            users: passwd_entries,
1062            config,
1063            passwd_fd,
1064            shadow_fd: None,
1065        })
1066    }
1067}
1068
1069impl AllUsers<auth::Basic> {
1070    /// Provide access to all user information on the system except
1071    /// authentication. This is adequate for almost all uses of `AllUsers`.
1072    pub fn basic(config: Config) -> Result<AllUsers<auth::Basic>, Error> {
1073        Self::new(config)
1074    }
1075}
1076
1077#[cfg(feature = "auth")]
1078impl AllUsers<auth::Full> {
1079    /// If access to password related methods for the [`User`]s yielded by this
1080    /// `AllUsers` is required, use this constructor.
1081    pub fn authenticator(config: Config) -> Result<AllUsers<auth::Full>, Error> {
1082        let mut shadow_fd = locked_file(config.in_root_fs(SHADOW_FILE), config.lock)?;
1083        let mut shadow_cntnt = String::new();
1084        shadow_fd.read_to_string(&mut shadow_cntnt)?;
1085        let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect();
1086
1087        let mut new = Self::new(config)?;
1088        new.shadow_fd = Some(shadow_fd);
1089
1090        for (indx, entry) in shadow_entries.iter().enumerate() {
1091            let mut entry = entry.split(';');
1092            let name = entry.next().ok_or(parse_error(indx,
1093                "error parsing shadowfile: expected username"
1094            ))?;
1095            let hash = entry.next().ok_or(parse_error(indx,
1096                "error parsing shadowfile: expected hash"
1097            ))?;
1098            new.users
1099                .iter_mut()
1100                .find(|user| user.user == name)
1101                .ok_or(parse_error(indx,
1102                    "error parsing shadowfile: unkown user"
1103                ))?.auth.hash = hash.to_string();
1104        }
1105
1106        shadow_cntnt.zeroize();
1107        Ok(new)
1108    }
1109
1110    /// Consumes a builder, adding a new user to this `AllUsers`. Returns a
1111    /// reference to the created user.
1112    ///
1113    /// Make sure to call [`AllUsers::save`] in order for the new user to be
1114    /// applied to the system.
1115    ///
1116    /// Note that the user's password is set unset (see
1117    /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords))
1118    /// during this call.
1119    ///
1120    /// Also note that the user is not added to any groups when this builder is
1121    /// consumed. In order to keep the system in a consistent state, it is
1122    /// reccomended to also use an instance of [`AllGroups`] to update group
1123    /// information when creating new users.
1124    ///
1125    /// # Defaults
1126    /// Fields not passed to the builder before calling this function are as
1127    /// follows:
1128    /// - `uid`: [`AllUsers::get_unique_id`] is called on self to get the next
1129    ///   available id.
1130    /// - `gid`: `99`. This is the default UID for the group `nobody`. Note
1131    ///   that the user is NOT added to this group in `/etc/groups`.
1132    /// - `name`: The login name passed to [`UserBuilder::new`].
1133    /// - `home`: `"/"`
1134    /// - `shell`: `file:/bin/ion`
1135    pub fn add_user(&mut self, builder: UserBuilder) -> Result<&User<auth::Full>, Error> {
1136        if !is_valid_name(&builder.user) {
1137            return Err(Error::InvalidName { name: builder.user });
1138        }
1139
1140        let uid = builder.uid.unwrap_or_else(||
1141            self.get_unique_id()
1142                .expect("no remaining unused user ids")
1143        );
1144
1145        if self.iter().any(|user| user.user == builder.user || user.uid == uid) {
1146            Err(Error::UserAlreadyExists)
1147        } else {
1148            self.users.push(User {
1149                user: builder.user.clone(),
1150                uid,
1151                gid: builder.gid.unwrap_or(99),
1152                name: builder.name.unwrap_or(builder.user),
1153                home: builder.home.unwrap_or("/".to_string()),
1154                shell: builder.shell.unwrap_or("file:/bin/ion".to_string()),
1155                auth: auth::Full::unset(),
1156                auth_delay: self.config.auth_delay
1157            });
1158            Ok(&self.users[self.users.len() - 1])
1159        }
1160    }
1161
1162    /// Syncs the data stored in the `AllUsers` instance to the filesystem.
1163    /// To apply changes to the system from an `AllUsers`, you MUST call this
1164    /// function!
1165    pub fn save(&mut self) -> Result<(), Error> {
1166        let mut userstring = String::new();
1167
1168        // Need to be careful to prevent allocations here so that
1169        // shadowstring can be zeroed when this process is complete.
1170        // 1 is suppossedly parallelism, not sure exactly what this means.
1171        // 16 is the max length of a u64, which is used as the salt.
1172        // 2 accounts for the semicolon separator and newline
1173        let acfg = argon2::Config::default();
1174        let argon_len = argon2::encoded_len(
1175            acfg.variant, acfg.mem_cost, acfg.time_cost,
1176            1, 16, acfg.hash_length) as usize;
1177        let mut shadowstring = String::with_capacity(
1178            self.users.len() * (USERNAME_LEN_MAX + argon_len + 2)
1179        );
1180
1181        for user in &self.users {
1182            userstring.push_str(&user.passwd_entry()?);
1183
1184            let mut shadow_entry = user.shadow_entry()?;
1185            shadowstring.push_str(&shadow_entry);
1186
1187            shadow_entry.zeroize();
1188        }
1189
1190        let mut shadow_fd = self.shadow_fd.as_mut()
1191            .expect("shadow_fd should exist for AllUsers<auth::Full>");
1192
1193        reset_file(&mut self.passwd_fd)?;
1194        self.passwd_fd.write_all(userstring.as_bytes())?;
1195
1196        reset_file(&mut shadow_fd)?;
1197        shadow_fd.write_all(shadowstring.as_bytes())?;
1198
1199        shadowstring.zeroize();
1200        Ok(())
1201    }
1202}
1203
1204impl<A> AllInner for AllUsers<A> {
1205    type Gruser = User<A>;
1206
1207    fn list(&self) -> &Vec<Self::Gruser> {
1208        &self.users
1209    }
1210
1211    fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1212        &mut self.users
1213    }
1214
1215    fn config(&self) -> &Config {
1216        &self.config
1217    }
1218}
1219
1220impl<A> All for AllUsers<A> {}
1221/*
1222#[cfg(not(target_os = "redox"))]
1223impl<A> Drop for AllUsers<A> {
1224    fn drop(&mut self) {
1225        eprintln!("Dropping AllUsers");
1226        let _ = flock(self.passwd_fd.as_raw_fd(), FlockArg::Unlock);
1227        if let Some(fd) = self.shadow_fd.as_ref() {
1228            eprintln!("Shadow");
1229            let _ = flock(fd.as_raw_fd(), FlockArg::Unlock);
1230        }
1231    }
1232}
1233*/
1234/// `AllGroups` provides (borrowed) access to all groups on the system. Note
1235/// that this struct implements [`All`] for all of its access functions.
1236///
1237/// General notes that also apply to this struct may be found with
1238/// [`AllUsers`].
1239#[derive(Debug)]
1240pub struct AllGroups {
1241    groups: Vec<Group>,
1242    config: Config,
1243
1244    group_fd: File,
1245}
1246
1247impl AllGroups {
1248    /// Create a new `AllGroups`.
1249    pub fn new(config: Config) -> Result<AllGroups, Error> {
1250        let mut group_fd = locked_file(config.in_root_fs(GROUP_FILE), config.lock)?;
1251        let mut group_cntnt = String::new();
1252        group_fd.read_to_string(&mut group_cntnt)?;
1253
1254        let mut entries: Vec<Group> = Vec::new();
1255        for (indx, line) in group_cntnt.lines().enumerate() {
1256            let group = Group::from_group_entry(line, indx)?;
1257            entries.push(group);
1258        }
1259
1260        Ok(AllGroups {
1261            groups: entries,
1262            config,
1263            group_fd,
1264        })
1265    }
1266
1267    /// Consumes a builder, adding a new group to this `AllGroups`. Returns a
1268    /// reference to the created `Group`.
1269    ///
1270    /// Make sure to call [`AllGroups::save`] in order for the new group to be
1271    /// applied to the system.
1272    ///
1273    /// # Defaults
1274    /// If a builder is not passed a group id ([`GroupBuilder::gid`]) before
1275    /// being passed to this function, [`AllGroups::get_unique_id`] is used.
1276    ///
1277    /// If the builder is not passed any users ([`GroupBuilder::user`]), the
1278    /// group will still be created.
1279    pub fn add_group(&mut self, builder: GroupBuilder) -> Result<&Group, Error> {
1280        let group_exists = self.iter()
1281            .any(|group| {
1282                let gid_taken = if let Some(gid) = builder.gid {
1283                    group.gid == gid
1284                } else {
1285                    false
1286                };
1287                group.group == builder.group || gid_taken
1288            });
1289
1290        if group_exists {
1291            Err(Error::GroupAlreadyExists)
1292        } else if !is_valid_name(&builder.group) {
1293            Err(Error::InvalidName { name: builder.group })
1294        } else {
1295            for username in builder.users.iter() {
1296                if !is_valid_name(username) {
1297                    return Err(Error::InvalidName { name: username.to_string() });
1298                }
1299            }
1300
1301            self.groups.push(Group {
1302                group: builder.group,
1303                password: "x".into(),
1304                gid: builder.gid.unwrap_or_else(||
1305                    self.get_unique_id()
1306                        .expect("no remaining unused group IDs")
1307                ),
1308                users: builder.users,
1309            });
1310            Ok(&self.groups[self.groups.len() - 1])
1311        }
1312    }
1313
1314    /// Syncs the data stored in this `AllGroups` instance to the filesystem.
1315    /// To apply changes from an `AllGroups`, you MUST call this function!
1316    pub fn save(&mut self) -> Result<(), Error> {
1317        let mut groupstring = String::new();
1318        for group in &self.groups {
1319            groupstring.push_str(&group.group_entry()?);
1320        }
1321
1322        reset_file(&mut self.group_fd)?;
1323        self.group_fd.write_all(groupstring.as_bytes())?;
1324        Ok(())
1325    }
1326}
1327
1328impl AllInner for AllGroups {
1329    type Gruser = Group;
1330
1331    fn list(&self) -> &Vec<Self::Gruser> {
1332        &self.groups
1333    }
1334
1335    fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1336        &mut self.groups
1337    }
1338
1339    fn config(&self) -> &Config {
1340        &self.config
1341    }
1342}
1343
1344impl All for AllGroups {}
1345/*
1346#[cfg(not(target_os = "redox"))]
1347impl Drop for AllGroups {
1348    fn drop(&mut self) {
1349        eprintln!("Dropping AllGroups");
1350        let _ = flock(self.group_fd.as_raw_fd(), FlockArg::Unlock);
1351    }
1352}*/
1353
1354#[cfg(test)]
1355mod test {
1356    use super::*;
1357
1358    const TEST_PREFIX: &'static str = "tests";
1359
1360    /// Needed for the file checks, this is done by the library
1361    fn test_prefix(filename: &str) -> String {
1362        let mut complete = String::from(TEST_PREFIX);
1363        complete.push_str(filename);
1364        complete
1365    }
1366
1367    #[test]
1368    fn test_safe_string() {
1369        assert!(is_safe_string("Hello\\$!"));
1370        assert!(!is_safe_string("semicolons are awesome; yeah!"));
1371    }
1372
1373    #[test]
1374    fn test_portable_filename() {
1375        let valid = |s| {
1376            assert!(is_valid_name(s));
1377        };
1378        let invld = |s| {
1379            assert!(!is_valid_name(s));
1380        };
1381        valid("valid");
1382        valid("vld.io");
1383        valid("hyphen-ated");
1384        valid("under_scores");
1385        valid("1334");
1386
1387        invld("-no_flgs");
1388        invld("invalid!");
1389        invld("also:invalid");
1390        invld("coolie-o?");
1391        invld("sh");
1392        invld("avery_very_very_very_loooooooonnggg-username");
1393    }
1394
1395    fn test_cfg() -> Config {
1396        Config::default()
1397            // Since all this really does is prepend `sheme` to the consts
1398            .scheme(TEST_PREFIX.to_string())
1399            .writeable(true)
1400    }
1401
1402    fn read_locked_file(file: impl AsRef<Path>) -> Result<String, Error> {
1403        let mut fd = locked_file(file, Lock::Shared)?;
1404        let mut cntnt = String::new();
1405        fd.read_to_string(&mut cntnt)?;
1406        Ok(cntnt)
1407    }
1408
1409    // *** struct.User ***
1410    #[cfg(feature = "auth")]
1411    #[test]
1412    fn attempt_user_api() {
1413        let mut users = AllUsers::authenticator(test_cfg()).unwrap();
1414        let user = users.get_mut_by_id(1000).unwrap();
1415
1416        assert_eq!(user.is_passwd_blank(), true);
1417        assert_eq!(user.is_passwd_unset(), false);
1418        assert_eq!(user.verify_passwd(""), true);
1419        assert_eq!(user.verify_passwd("Something"), false);
1420
1421        user.set_passwd("hi,i_am_passwd").unwrap();
1422
1423        assert_eq!(user.is_passwd_blank(), false);
1424        assert_eq!(user.is_passwd_unset(), false);
1425        assert_eq!(user.verify_passwd(""), false);
1426        assert_eq!(user.verify_passwd("Something"), false);
1427        assert_eq!(user.verify_passwd("hi,i_am_passwd"), true);
1428
1429        user.unset_passwd();
1430
1431        assert_eq!(user.is_passwd_blank(), false);
1432        assert_eq!(user.is_passwd_unset(), true);
1433        assert_eq!(user.verify_passwd(""), false);
1434        assert_eq!(user.verify_passwd("Something"), false);
1435        assert_eq!(user.verify_passwd("hi,i_am_passwd"), false);
1436
1437        user.set_passwd("").unwrap();
1438
1439        assert_eq!(user.is_passwd_blank(), true);
1440        assert_eq!(user.is_passwd_unset(), false);
1441        assert_eq!(user.verify_passwd(""), true);
1442        assert_eq!(user.verify_passwd("Something"), false);
1443    }
1444
1445    // *** struct.AllUsers ***
1446    #[cfg(feature = "auth")]
1447    #[test]
1448    fn get_user() {
1449        let users = AllUsers::authenticator(test_cfg()).unwrap();
1450
1451        let root = users.get_by_id(0).expect("'root' user missing");
1452        assert_eq!(root.user, "root".to_string());
1453        assert_eq!(root.auth.hash.as_str(),
1454            "$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk");
1455        assert_eq!(root.uid, 0);
1456        assert_eq!(root.gid, 0);
1457        assert_eq!(root.name, "root".to_string());
1458        assert_eq!(root.home, "file:/root".to_string());
1459        assert_eq!(root.shell, "file:/bin/ion".to_string());
1460
1461        let user = users.get_by_name("user").expect("'user' user missing");
1462        assert_eq!(user.user, "user".to_string());
1463        assert_eq!(user.auth.hash.as_str(), "");
1464        assert_eq!(user.uid, 1000);
1465        assert_eq!(user.gid, 1000);
1466        assert_eq!(user.name, "user".to_string());
1467        assert_eq!(user.home, "file:/home/user".to_string());
1468        assert_eq!(user.shell, "file:/bin/ion".to_string());
1469        println!("{:?}", users);
1470
1471        let li = users.get_by_name("loip").expect("'loip' user missing");
1472        println!("got loip");
1473        assert_eq!(li.user, "loip");
1474        assert_eq!(li.auth.hash.as_str(), "!");
1475        assert_eq!(li.uid, 1007);
1476        assert_eq!(li.gid, 1007);
1477        assert_eq!(li.name, "Lorem".to_string());
1478        assert_eq!(li.home, "file:/home/lorem".to_string());
1479        assert_eq!(li.shell, "file:/bin/ion".to_string());
1480    }
1481
1482    #[cfg(feature = "auth")]
1483    #[test]
1484    fn manip_user() {
1485        let mut users = AllUsers::authenticator(test_cfg()).unwrap();
1486        // NOT testing `get_unique_id`
1487        let id = 7099;
1488
1489        let fb = UserBuilder::new("fbar")
1490            .uid(id)
1491            .gid(id)
1492            .name("Foo Bar")
1493            .home("/home/foob")
1494            .shell("/bin/zsh");
1495
1496        users
1497            .add_user(fb)
1498            .expect("failed to add user 'fbar'");
1499        //                                            weirdo ^^^^^^^^ :P
1500        users.save().unwrap();
1501        let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1502        assert_eq!(
1503            p_file_content,
1504            concat!(
1505                "root;0;0;root;file:/root;file:/bin/ion\n",
1506                "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1507                "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
1508                "fbar;7099;7099;Foo Bar;/home/foob;/bin/zsh\n"
1509            )
1510        );
1511        let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap();
1512        assert_eq!(s_file_content, concat!(
1513            "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n",
1514            "user;\n",
1515            "loip;!\n",
1516            "fbar;!\n"
1517        ));
1518
1519        {
1520            println!("{:?}", users);
1521            let fb = users.get_mut_by_name("fbar")
1522                .expect("'fbar' user missing");
1523            fb.shell = "/bin/fish".to_string(); // That's better
1524            fb.set_passwd("").unwrap();
1525        }
1526        users.save().unwrap();
1527        let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1528        assert_eq!(
1529            p_file_content,
1530            concat!(
1531                "root;0;0;root;file:/root;file:/bin/ion\n",
1532                "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1533                "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
1534                "fbar;7099;7099;Foo Bar;/home/foob;/bin/fish\n"
1535            )
1536        );
1537        let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap();
1538        assert_eq!(s_file_content, concat!(
1539            "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n",
1540            "user;\n",
1541            "loip;!\n",
1542            "fbar;\n"
1543        ));
1544
1545        users.remove_by_id(id);
1546        users.save().unwrap();
1547        let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1548        assert_eq!(
1549            file_content,
1550            concat!(
1551                "root;0;0;root;file:/root;file:/bin/ion\n",
1552                "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1553                "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n"
1554            )
1555        );
1556    }
1557
1558    /* struct.Group */
1559    #[test]
1560    fn empty_groups() {
1561        let group_trailing = Group::from_group_entry("nobody;x;2066; ", 0).unwrap();
1562        assert_eq!(group_trailing.users.len(), 0);
1563
1564        let group_no_trailing = Group::from_group_entry("nobody;x;2066;", 0).unwrap();
1565        assert_eq!(group_no_trailing.users.len(), 0);
1566
1567        assert_eq!(group_trailing.group, group_no_trailing.group);
1568        assert_eq!(group_trailing.gid, group_no_trailing.gid);
1569        assert_eq!(group_trailing.users, group_no_trailing.users);
1570    }
1571
1572    /* struct.AllGroups */
1573    #[test]
1574    fn get_group() {
1575        let groups = AllGroups::new(test_cfg()).unwrap();
1576        let user = groups.get_by_name("user").unwrap();
1577        assert_eq!(user.group, "user");
1578        assert_eq!(user.gid, 1000);
1579        assert_eq!(user.users, vec!["user"]);
1580
1581        let wheel = groups.get_by_id(1).unwrap();
1582        assert_eq!(wheel.group, "wheel");
1583        assert_eq!(wheel.gid, 1);
1584        assert_eq!(wheel.users, vec!["user", "root"]);
1585    }
1586
1587    #[test]
1588    fn manip_group() {
1589        let id = 7099;
1590        let mut groups = AllGroups::new(test_cfg()).unwrap();
1591
1592        let fb = GroupBuilder::new("fbar")
1593            // NOT testing `get_unique_id`
1594            .gid(id)
1595            .user("fbar");
1596
1597        groups.add_group(fb).unwrap();
1598        groups.save().unwrap();
1599        let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1600        assert_eq!(
1601            file_content,
1602            concat!(
1603                "root;x;0;root\n",
1604                "user;x;1000;user\n",
1605                "wheel;x;1;user,root\n",
1606                "loip;x;1007;loip\n",
1607                "fbar;x;7099;fbar\n"
1608            )
1609        );
1610
1611        {
1612            let fb = groups.get_mut_by_name("fbar").unwrap();
1613            fb.users.push("user".to_string());
1614        }
1615        groups.save().unwrap();
1616        let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1617        assert_eq!(
1618            file_content,
1619            concat!(
1620                "root;x;0;root\n",
1621                "user;x;1000;user\n",
1622                "wheel;x;1;user,root\n",
1623                "loip;x;1007;loip\n",
1624                "fbar;x;7099;fbar,user\n"
1625            )
1626        );
1627
1628        groups.remove_by_id(id);
1629        groups.save().unwrap();
1630        let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1631        assert_eq!(
1632            file_content,
1633            concat!(
1634                "root;x;0;root\n",
1635                "user;x;1000;user\n",
1636                "wheel;x;1;user,root\n",
1637                "loip;x;1007;loip\n"
1638            )
1639        );
1640    }
1641
1642    #[test]
1643    fn empty_group() {
1644        let mut groups = AllGroups::new(test_cfg()).unwrap();
1645        let nobody = GroupBuilder::new("nobody")
1646            .gid(2260);
1647
1648        groups.add_group(nobody).unwrap();
1649        groups.save().unwrap();
1650        let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1651        assert_eq!(
1652            file_content,
1653            concat!(
1654                "root;x;0;root\n",
1655                "user;x;1000;user\n",
1656                "wheel;x;1;user,root\n",
1657                "loip;x;1007;loip\n",
1658                "nobody;x;2260;\n",
1659            )
1660        );
1661
1662        drop(groups);
1663        let mut groups = AllGroups::new(test_cfg()).unwrap();
1664
1665        groups.remove_by_name("nobody");
1666        groups.save().unwrap();
1667
1668        let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1669        assert_eq!(
1670            file_content,
1671            concat!(
1672                "root;x;0;root\n",
1673                "user;x;1000;user\n",
1674                "wheel;x;1;user,root\n",
1675                "loip;x;1007;loip\n"
1676            )
1677        );
1678    }
1679
1680    // *** Misc ***
1681    #[test]
1682    fn users_get_unused_ids() {
1683        let users = AllUsers::basic(test_cfg()).unwrap();
1684        let id = users.get_unique_id().unwrap();
1685        if id < users.config.min_id || id > users.config.max_id {
1686            panic!("User ID is not between allowed margins")
1687        } else if let Some(_) = users.get_by_id(id) {
1688            panic!("User ID is used!");
1689        }
1690    }
1691
1692    #[test]
1693    fn groups_get_unused_ids() {
1694        let groups = AllGroups::new(test_cfg()).unwrap();
1695        let id = groups.get_unique_id().unwrap();
1696        if id < groups.config.min_id || id > groups.config.max_id {
1697            panic!("Group ID is not between allowed margins")
1698        } else if let Some(_) = groups.get_by_id(id) {
1699            panic!("Group ID is used!");
1700        }
1701    }
1702}