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 
30 use std::convert::From;
31 use std::error::Error;
32 use std::fmt::{self, Debug};
33 use std::fs::{File, OpenOptions};
34 use std::io::{Read, Seek, SeekFrom, Write};
35 use std::marker::PhantomData;
36 #[cfg(target_os = "redox")]
37 use std::os::unix::fs::OpenOptionsExt;
38 #[cfg(not(target_os = "redox"))]
39 use std::os::unix::io::AsRawFd;
40 use std::os::unix::process::CommandExt;
41 use std::path::{Path, PathBuf};
42 use std::process::Command;
43 use std::slice::{Iter, IterMut};
44 #[cfg(not(test))]
45 #[cfg(feature = "auth")]
46 use std::thread;
47 use std::time::Duration;
48 
49 //#[cfg(not(target_os = "redox"))]
50 //use nix::fcntl::{flock, FlockArg};
51 
52 #[cfg(target_os = "redox")]
53 use syscall::flag::{O_EXLOCK, O_SHLOCK};
54 use syscall::Error as SyscallError;
55 
56 const PASSWD_FILE: &'static str = "/etc/passwd";
57 const GROUP_FILE: &'static str = "/etc/group";
58 #[cfg(feature = "auth")]
59 const SHADOW_FILE: &'static str = "/etc/shadow";
60 
61 #[cfg(target_os = "redox")]
62 const DEFAULT_SCHEME: &'static str = "file:";
63 #[cfg(not(target_os = "redox"))]
64 const DEFAULT_SCHEME: &'static str = "";
65 
66 const MIN_ID: usize = 1000;
67 const MAX_ID: usize = 6000;
68 const DEFAULT_TIMEOUT: u64 = 3;
69 
70 #[cfg(feature = "auth")]
71 const USER_AUTH_FULL_EXPECTED_HASH: &str = "A User<auth::Full> had no hash";
72 
73 pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
74 
75 /// Errors that might happen while using this crate
76 #[derive(Debug, PartialEq)]
77 pub enum UsersError {
78     Os { reason: String },
79     Parsing { reason: String, line: usize },
80     NotFound,
81     AlreadyExists
82 }
83 
84 impl fmt::Display for UsersError {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result85     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
86         match self {
87             UsersError::Os { reason } => write!(f, "os error: code {}", reason),
88             UsersError::Parsing { reason, line } => {
89                 write!(f, "parse error line {}: {}", line, reason)
90             },
91             UsersError::NotFound => write!(f, "user/group not found"),
92             UsersError::AlreadyExists => write!(f, "user/group already exists")
93         }
94     }
95 }
96 
97 impl Error for UsersError {
description(&self) -> &str98     fn description(&self) -> &str { "UsersError" }
99 
cause(&self) -> Option<&dyn Error>100     fn cause(&self) -> Option<&dyn Error> { None }
101 }
102 
103 #[inline]
parse_error(line: usize, reason: &str) -> UsersError104 fn parse_error(line: usize, reason: &str) -> UsersError {
105     UsersError::Parsing {
106         reason: reason.into(),
107         line,
108     }
109 }
110 
111 #[inline]
os_error(reason: &str) -> UsersError112 fn os_error(reason: &str) -> UsersError {
113     UsersError::Os {
114         reason: reason.into()
115     }
116 }
117 
118 impl From<SyscallError> for UsersError {
from(syscall_error: SyscallError) -> UsersError119     fn from(syscall_error: SyscallError) -> UsersError {
120         UsersError::Os {
121             reason: format!("{}", syscall_error)
122         }
123     }
124 }
125 
126 #[derive(Clone, Copy)]
127 #[allow(dead_code)]
128 enum Lock {
129     Shared,
130     Exclusive,
131 }
132 
133 impl Lock {
134     #[cfg(target_os = "redox")]
as_olock(self) -> i32135     fn as_olock(self) -> i32 {
136         (match self {
137             Lock::Shared => O_SHLOCK,
138             Lock::Exclusive => O_EXLOCK,
139         }) as i32
140     }
141 
142     /*#[cfg(not(target_os = "redox"))]
143     fn as_flock(self) -> FlockArg {
144         match self {
145             Lock::Shared => FlockArg::LockShared,
146             Lock::Exclusive => FlockArg::LockExclusive,
147         }
148     }*/
149 }
150 
151 /// Naive semi-cross platform file locking (need to support linux for tests).
152 #[allow(dead_code)]
locked_file(file: impl AsRef<Path>, _lock: Lock) -> Result<File>153 fn locked_file(file: impl AsRef<Path>, _lock: Lock) -> Result<File> {
154     #[cfg(test)]
155     println!("Open file: {}", file.as_ref().display());
156 
157     #[cfg(target_os = "redox")]
158     {
159         Ok(OpenOptions::new()
160             .read(true)
161             .write(true)
162             .custom_flags(_lock.as_olock())
163             .open(file)?)
164     }
165     #[cfg(not(target_os = "redox"))]
166     #[cfg_attr(rustfmt, rustfmt_skip)]
167     {
168         let file = OpenOptions::new()
169             .read(true)
170             .write(true)
171             .open(file)?;
172         let fd = file.as_raw_fd();
173         eprintln!("Fd: {}", fd);
174         //flock(fd, _lock.as_flock())?;
175         Ok(file)
176     }
177 }
178 
179 /// Reset a file for rewriting (user/group dbs must be erased before write-out)
reset_file(fd: &mut File) -> Result<()>180 fn reset_file(fd: &mut File) -> Result<()> {
181     fd.set_len(0)?;
182     fd.seek(SeekFrom::Start(0))?;
183     Ok(())
184 }
185 
186 /// Marker types for [`User`] and [`AllUsers`].
187 pub mod auth {
188     /// Marker type indicating that a `User` only has access to world-readable
189     /// user information, and cannot authenticate.
190     #[derive(Debug)]
191     pub struct Basic {}
192 
193     /// Marker type indicating that a `User` has access to all user
194     /// information, including password hashes.
195     #[cfg(feature = "auth")]
196     #[derive(Debug)]
197     pub struct Full {}
198 }
199 
200 /// A struct representing a Redox user.
201 /// Currently maps to an entry in the `/etc/passwd` file.
202 ///
203 /// `A` should be a type from [`crate::auth`].
204 ///
205 /// # Unset vs. Blank Passwords
206 /// A note on unset passwords vs. blank passwords. A blank password
207 /// is a hash field that is completely blank (aka, `""`). According
208 /// to this crate, successful login is only allowed if the input
209 /// password is blank as well.
210 ///
211 /// An unset password is one whose hash is not empty (`""`), but
212 /// also not a valid serialized argon2rs hashing session. This
213 /// hash always returns `false` upon attempted verification. The
214 /// most commonly used hash for an unset password is `"!"`, but
215 /// this crate makes no distinction. The most common way to unset
216 /// the password is to use [`User::unset_passwd`].
217 pub struct User<A> {
218     /// Username (login name)
219     pub user: String,
220     /// User id
221     pub uid: usize,
222     /// Group id
223     pub gid: usize,
224     /// Real name (human readable, can contain spaces)
225     pub name: String,
226     /// Home directory path
227     pub home: String,
228     /// Shell path
229     pub shell: String,
230 
231     // Stored password hash text and an indicator to determine if the text is a
232     // hash.
233     #[cfg(feature = "auth")]
234     hash: Option<(String, bool)>,
235     // Failed login delay duration
236     auth_delay: Duration,
237     auth: PhantomData<A>,
238 }
239 
240 impl<A> User<A> {
241     /// Get a Command to run the user's default shell (see [`User::login_cmd`]
242     /// for more docs).
shell_cmd(&self) -> Command243     pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) }
244 
245     /// Provide a login command for the user, which is any entry point for
246     /// starting a user's session, whether a shell (use [`User::shell_cmd`]
247     /// instead) or a graphical init.
248     ///
249     /// The `Command` will use the user's `uid` and `gid`, its `current_dir`
250     /// will be set to the user's home directory, and the follwing enviroment
251     /// variables will be populated:
252     ///
253     ///    - `USER` set to the user's `user` field.
254     ///    - `UID` set to the user's `uid` field.
255     ///    - `GROUPS` set the user's `gid` field.
256     ///    - `HOME` set to the user's `home` field.
257     ///    - `SHELL` set to the user's `shell` field.
login_cmd<T>(&self, cmd: T) -> Command where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str>258     pub fn login_cmd<T>(&self, cmd: T) -> Command
259         where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str>
260     {
261         let mut command = Command::new(cmd);
262         command
263             .uid(self.uid as u32)
264             .gid(self.gid as u32)
265             .current_dir(&self.home)
266             .env("USER", &self.user)
267             .env("UID", format!("{}", self.uid))
268             .env("GROUPS", format!("{}", self.gid))
269             .env("HOME", &self.home)
270             .env("SHELL", &self.shell);
271         command
272     }
273 
from_passwd_entry(s: &str, line: usize) -> Result<Self>274     fn from_passwd_entry(s: &str, line: usize) -> Result<Self> {
275         let mut parts = s.split(';');
276 
277         let user = parts
278             .next()
279             .ok_or(parse_error(line, "expected user"))?;
280         let uid = parts
281             .next()
282             .ok_or(parse_error(line, "expected uid"))?
283             .parse::<usize>()?;
284         let gid = parts
285             .next()
286             .ok_or(parse_error(line, "expected uid"))?
287             .parse::<usize>()?;
288         let name = parts
289             .next()
290             .ok_or(parse_error(line, "expected real name"))?;
291         let home = parts
292             .next()
293             .ok_or(parse_error(line, "expected home dir path"))?;
294         let shell = parts
295             .next()
296             .ok_or(parse_error(line, "expected shell path"))?;
297 
298         Ok(User::<A> {
299             user: user.into(),
300             uid,
301             gid,
302             name: name.into(),
303             home: home.into(),
304             shell: shell.into(),
305             #[cfg(feature = "auth")]
306             hash: None,
307             auth: PhantomData,
308             auth_delay: Duration::default(),
309         })
310     }
311 
312     /// Format this user as an entry in `/etc/passwd`.
passwd_entry(&self) -> String313     fn passwd_entry(&self) -> String {
314         #[cfg_attr(rustfmt, rustfmt_skip)]
315         format!("{};{};{};{};{};{}\n",
316             self.user, self.uid, self.gid, self.name, self.home, self.shell
317         )
318     }
319 }
320 
321 /// Additional methods for if this `User` is authenticatable.
322 #[cfg(feature = "auth")]
323 impl User<auth::Full> {
324     /// Set the password for a user. Make **sure** that `password`
325     /// is actually what the user wants as their password (this doesn't).
326     ///
327     /// To set the password blank, pass `""` as `password`.
set_passwd(&mut self, password: impl AsRef<str>) -> Result<()>328     pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<()> {
329         let password = password.as_ref();
330 
331         self.hash = if password != "" {
332             let mut buf = [0u8; 8];
333             getrandom::getrandom(&mut buf)?;
334             let salt = format!("{:X}", u64::from_ne_bytes(buf));
335             let config = argon2::Config::default();
336             let hash = argon2::hash_encoded(
337                 password.as_bytes(),
338                 salt.as_bytes(),
339                 &config
340             )?;
341             Some((hash, true))
342         } else {
343             Some(("".into(), false))
344         };
345         Ok(())
346     }
347 
348     /// Unset the password ([`User::verify_passwd`] always returns `false`).
unset_passwd(&mut self)349     pub fn unset_passwd(&mut self) {
350         self.hash = Some(("!".into(), false));
351     }
352 
353     /// Verify the password. If the hash is empty, this only returns `true` if
354     /// `password` is also empty.
355     ///
356     /// Note that this is a blocking operation if the password is incorrect.
357     /// See [`Config::auth_delay`] to set the wait time. Default is 3 seconds.
verify_passwd(&self, password: impl AsRef<str>) -> bool358     pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool {
359         let &(ref hash, ref encoded) = self.hash.as_ref()
360             .expect(USER_AUTH_FULL_EXPECTED_HASH);
361         let password = password.as_ref();
362 
363         let verified = if *encoded {
364             argon2::verify_encoded(&hash, password.as_bytes()).unwrap()
365         } else {
366             hash == "" && password == ""
367         };
368 
369         if !verified {
370             #[cfg(not(test))] // Make tests run faster
371             thread::sleep(self.auth_delay);
372         }
373         verified
374     }
375 
376     /// Determine if the hash for the password is blank ([`User::verify_passwd`]
377     /// returns `true` *only* when the password is blank).
is_passwd_blank(&self) -> bool378     pub fn is_passwd_blank(&self) -> bool {
379         let &(ref hash, ref encoded) = self.hash.as_ref()
380             .expect(USER_AUTH_FULL_EXPECTED_HASH);
381         hash == "" && ! encoded
382     }
383 
384     /// Determine if the hash for the password is unset
385     /// ([`User::verify_passwd`] returns `false` regardless of input).
is_passwd_unset(&self) -> bool386     pub fn is_passwd_unset(&self) -> bool {
387         let &(ref hash, ref encoded) = self.hash.as_ref()
388             .expect(USER_AUTH_FULL_EXPECTED_HASH);
389         hash != "" && ! encoded
390     }
391 
shadow_entry(&self) -> String392     fn shadow_entry(&self) -> String {
393         let hashstring = match self.hash {
394             Some((ref hash, _)) => hash,
395             None => panic!(USER_AUTH_FULL_EXPECTED_HASH)
396         };
397         format!("{};{}\n", self.user, hashstring)
398     }
399 
400     /// Give this a hash string (not a shadowfile entry!!!)
populate_hash(&mut self, hash: &str) -> Result<()>401     fn populate_hash(&mut self, hash: &str) -> Result<()> {
402         let encoded = match hash {
403             "" => false,
404             "!" => false,
405             _ => true,
406         };
407         self.hash = Some((hash.to_string(), encoded));
408         Ok(())
409     }
410 }
411 
412 impl<A> Name for User<A> {
name(&self) -> &str413     fn name(&self) -> &str {
414         &self.user
415     }
416 }
417 
418 impl<A> Id for User<A> {
id(&self) -> usize419     fn id(&self) -> usize {
420         self.uid
421     }
422 }
423 
424 impl<A> Debug for User<A> {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result425     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
426         f.debug_struct("User")
427             .field("user", &self.user)
428             .field("uid", &self.uid)
429             .field("gid", &self.gid)
430             .field("name", &self.name)
431             .field("home", &self.home)
432             .field("shell", &self.shell)
433             .field("auth_delay", &self.auth_delay)
434             .finish()
435     }
436 }
437 
438 /// A struct representing a Redox user group.
439 /// Currently maps to an `/etc/group` file entry.
440 #[derive(Debug)]
441 pub struct Group {
442     /// Group name
443     pub group: String,
444     /// Unique group id
445     pub gid: usize,
446     /// Group members' usernames
447     pub users: Vec<String>,
448 }
449 
450 impl Group {
from_group_entry(s: &str, line: usize) -> Result<Self>451     fn from_group_entry(s: &str, line: usize) -> Result<Self> {
452         let mut parts = s.trim()
453             .split(';');
454 
455         let group = parts
456             .next()
457             .ok_or(parse_error(line, "expected group"))?;
458         let gid = parts
459             .next()
460             .ok_or(parse_error(line, "expected gid"))?
461             .parse::<usize>()?;
462         let users_str = parts.next()
463             .unwrap_or("");
464         let users = users_str.split(',')
465             .filter_map(|u| if u == "" {
466                 None
467             } else {
468                 Some(u.into())
469             })
470             .collect();
471 
472         Ok(Group {
473             group: group.into(),
474             gid,
475             users,
476         })
477     }
478 
479     /// Format this group as an entry in `/etc/group`. This
480     /// is an implementation detail, do NOT rely on this trait
481     /// being implemented in future.
group_entry(&self) -> String482     fn group_entry(&self) -> String {
483         #[cfg_attr(rustfmt, rustfmt_skip)]
484         format!("{};{};{}\n",
485             self.group,
486             self.gid,
487             self.users.join(",").trim_matches(',')
488         )
489     }
490 }
491 
492 impl Name for Group {
name(&self) -> &str493     fn name(&self) -> &str {
494         &self.group
495     }
496 }
497 
498 impl Id for Group {
id(&self) -> usize499     fn id(&self) -> usize {
500         self.gid
501     }
502 }
503 
504 /// Gets the current process effective user ID.
505 ///
506 /// This function issues the `geteuid` system call returning the process effective
507 /// user id.
508 ///
509 /// # Examples
510 ///
511 /// Basic usage:
512 ///
513 /// ```no_run
514 /// # use redox_users::get_euid;
515 /// let euid = get_euid().unwrap();
516 /// ```
get_euid() -> Result<usize>517 pub fn get_euid() -> Result<usize> {
518     match syscall::geteuid() {
519         Ok(euid) => Ok(euid),
520         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
521     }
522 }
523 
524 /// Gets the current process real user ID.
525 ///
526 /// This function issues the `getuid` system call returning the process real
527 /// user id.
528 ///
529 /// # Examples
530 ///
531 /// Basic usage:
532 ///
533 /// ```no_run
534 /// # use redox_users::get_uid;
535 /// let uid = get_uid().unwrap();
536 /// ```
get_uid() -> Result<usize>537 pub fn get_uid() -> Result<usize> {
538     match syscall::getuid() {
539         Ok(uid) => Ok(uid),
540         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
541     }
542 }
543 
544 /// Gets the current process effective group ID.
545 ///
546 /// This function issues the `getegid` system call returning the process effective
547 /// group id.
548 ///
549 /// # Examples
550 ///
551 /// Basic usage:
552 ///
553 /// ```no_run
554 /// # use redox_users::get_egid;
555 /// let egid = get_egid().unwrap();
556 /// ```
get_egid() -> Result<usize>557 pub fn get_egid() -> Result<usize> {
558     match syscall::getegid() {
559         Ok(egid) => Ok(egid),
560         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
561     }
562 }
563 
564 /// Gets the current process real group ID.
565 ///
566 /// This function issues the `getegid` system call returning the process real
567 /// group id.
568 ///
569 /// # Examples
570 ///
571 /// Basic usage:
572 ///
573 /// ```no_run
574 /// # use redox_users::get_gid;
575 /// let gid = get_gid().unwrap();
576 /// ```
get_gid() -> Result<usize>577 pub fn get_gid() -> Result<usize> {
578     match syscall::getgid() {
579         Ok(gid) => Ok(gid),
580         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
581     }
582 }
583 
584 /// A generic configuration that allows fine control of an [`AllUsers`] or
585 /// [`AllGroups`].
586 ///
587 /// `auth_delay` is not used by [`AllGroups`]
588 ///
589 /// In most situations, [`Config::default`](struct.Config.html#impl-Default)
590 /// will work just fine. The other fields are for finer control if it is
591 /// required.
592 ///
593 /// # Example
594 /// ```
595 /// # use redox_users::Config;
596 /// use std::time::Duration;
597 ///
598 /// let cfg = Config::default()
599 ///     .min_id(500)
600 ///     .max_id(1000)
601 ///     .auth_delay(Duration::from_secs(5));
602 /// ```
603 #[derive(Clone, Debug)]
604 pub struct Config {
605     scheme: String,
606     auth_delay: Duration,
607     min_id: usize,
608     max_id: usize,
609 }
610 
611 impl Config {
612     /// Set the delay for a failed authentication. Default is 3 seconds.
auth_delay(mut self, delay: Duration) -> Config613     pub fn auth_delay(mut self, delay: Duration) -> Config {
614         self.auth_delay = delay;
615         self
616     }
617 
618     /// Set the smallest ID possible to use when finding an unused ID.
min_id(mut self, id: usize) -> Config619     pub fn min_id(mut self, id: usize) -> Config {
620         self.min_id = id;
621         self
622     }
623 
624     /// Set the largest possible ID to use when finding an unused ID.
max_id(mut self, id: usize) -> Config625     pub fn max_id(mut self, id: usize) -> Config {
626         self.max_id = id;
627         self
628     }
629 
630     /// Set the scheme relative to which the [`AllUsers`] or [`AllGroups`]
631     /// should be looking for its data files. This is a compromise between
632     /// exposing implementation details and providing fine enough
633     /// control over the behavior of this API.
scheme(mut self, scheme: String) -> Config634     pub fn scheme(mut self, scheme: String) -> Config {
635         self.scheme = scheme;
636         self
637     }
638 
639     // Prepend a path with the scheme in this Config
in_scheme(&self, path: impl AsRef<Path>) -> PathBuf640     fn in_scheme(&self, path: impl AsRef<Path>) -> PathBuf {
641         let mut canonical_path = PathBuf::from(&self.scheme);
642         // Should be a little careful here, not sure I want this behavior
643         if path.as_ref().is_absolute() {
644             // This is nasty
645             canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string());
646         } else {
647             canonical_path.push(path);
648         }
649         canonical_path
650     }
651 }
652 
653 impl Default for Config {
654     /// The default base scheme is `file:`.
655     ///
656     /// The default auth delay is 3 seconds.
657     ///
658     /// The default min and max ids are 1000 and 6000.
default() -> Config659     fn default() -> Config {
660         Config {
661             scheme: String::from(DEFAULT_SCHEME),
662             auth_delay: Duration::new(DEFAULT_TIMEOUT, 0),
663             min_id: MIN_ID,
664             max_id: MAX_ID,
665         }
666     }
667 }
668 
669 // Nasty hack to prevent the compiler complaining about
670 // "leaking" `AllInner`
671 mod sealed {
672     use crate::Config;
673 
674     pub trait Name {
name(&self) -> &str675         fn name(&self) -> &str;
676     }
677 
678     pub trait Id {
id(&self) -> usize679         fn id(&self) -> usize;
680     }
681 
682     pub trait AllInner {
683         // Group+User, thanks Dad
684         type Gruser: Name + Id;
685 
686         /// These functions grab internal elements so that the other
687         /// methods of `All` can manipulate them.
list(&self) -> &Vec<Self::Gruser>688         fn list(&self) -> &Vec<Self::Gruser>;
list_mut(&mut self) -> &mut Vec<Self::Gruser>689         fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
config(&self) -> &Config690         fn config(&self) -> &Config;
691     }
692 }
693 
694 use sealed::{AllInner, Id, Name};
695 
696 /// This trait is used to remove repetitive API items from
697 /// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait
698 /// so that the implementations of functions can be implemented
699 /// at the trait level. Do not try to implement this trait.
700 pub trait All: AllInner {
701     /// Get an iterator borrowing all [`User`]s or [`Group`]s on the system.
iter(&self) -> Iter<<Self as AllInner>::Gruser>702     fn iter(&self) -> Iter<<Self as AllInner>::Gruser> {
703         self.list().iter()
704     }
705 
706     /// Get an iterator mutably borrowing all [`User`]s or [`Group`]s on the
707     /// system.
iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser>708     fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> {
709         self.list_mut().iter_mut()
710     }
711 
712     /// Borrow the [`User`] or [`Group`] with a given name.
713     ///
714     /// # Examples
715     ///
716     /// Basic usage:
717     ///
718     /// ```no_run
719     /// # use redox_users::{All, AllUsers, Config};
720     /// let users = AllUsers::basic(Config::default()).unwrap();
721     /// let user = users.get_by_name("root").unwrap();
722     /// ```
get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser>723     fn get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser> {
724         self.iter()
725             .find(|gruser| gruser.name() == name.as_ref() )
726     }
727 
728     /// Mutable version of [`All::get_by_name`].
get_mut_by_name(&mut self, name: impl AsRef<str>) -> Option<&mut <Self as AllInner>::Gruser>729     fn get_mut_by_name(&mut self, name: impl AsRef<str>) -> Option<&mut <Self as AllInner>::Gruser> {
730         self.iter_mut()
731             .find(|gruser| gruser.name() == name.as_ref() )
732     }
733 
734     /// Borrow the [`User`] or [`Group`] with the given ID.
735     ///
736     /// # Examples
737     ///
738     /// Basic usage:
739     ///
740     /// ```no_run
741     /// # use redox_users::{All, AllUsers, Config};
742     /// let users = AllUsers::basic(Config::default()).unwrap();
743     /// let user = users.get_by_id(0).unwrap();
744     /// ```
get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser>745     fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> {
746         self.iter()
747             .find(|gruser| gruser.id() == id )
748     }
749 
750     /// Mutable version of [`All::get_by_id`].
get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser>751     fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> {
752         self.iter_mut()
753             .find(|gruser| gruser.id() == id )
754     }
755 
756     /// Provides an unused id based on the min and max values in the [`Config`]
757     /// passed to the `All`'s constructor.
758     ///
759     /// # Examples
760     ///
761     /// ```no_run
762     /// # use redox_users::{All, AllUsers, Config};
763     /// let users = AllUsers::basic(Config::default()).unwrap();
764     /// let uid = users.get_unique_id().expect("no available uid");
765     /// ```
get_unique_id(&self) -> Option<usize>766     fn get_unique_id(&self) -> Option<usize> {
767         for id in self.config().min_id..self.config().max_id {
768             if !self.iter().any(|gruser| gruser.id() == id ) {
769                 return Some(id)
770             }
771         }
772         None
773     }
774 
775     /// Remove a [`User`] or [`Group`] from this `All` given it's name. If the
776     /// Gruser was removed return `true`, else return `false`. This ensures
777     /// that the Gruser no longer exists.
remove_by_name(&mut self, name: impl AsRef<str>) -> bool778     fn remove_by_name(&mut self, name: impl AsRef<str>) -> bool {
779         let list = self.list_mut();
780         let indx = list.iter()
781             .enumerate()
782             .find_map(|(indx, gruser)| if gruser.name() == name.as_ref() {
783                     Some(indx)
784                 } else {
785                     None
786                 });
787         if let Some(indx) = indx {
788             list.remove(indx);
789             true
790         } else {
791             false
792         }
793     }
794 
795     /// Id version of [`All::remove_by_name`].
remove_by_id(&mut self, id: usize) -> bool796     fn remove_by_id(&mut self, id: usize) -> bool {
797         let list = self.list_mut();
798         let indx = list.iter()
799             .enumerate()
800             .find_map(|(indx, gruser)| if gruser.id() == id {
801                     Some(indx)
802                 } else {
803                     None
804                 });
805         if let Some(indx) = indx {
806             list.remove(indx);
807             true
808         } else {
809             false
810         }
811     }
812 }
813 
814 /// `AllUsers` provides (borrowed) access to all the users on the system.
815 /// Note that this struct implements [`All`] for all of its access functions.
816 ///
817 /// # Notes
818 /// Note that everything in this section also applies to [`AllGroups`].
819 ///
820 /// * If you mutate anything owned by an `AllUsers`, you must call the
821 ///   [`AllUsers::save`] in order for those changes to be applied to the system.
822 /// * The API here is kept small. Most mutating actions can be accomplished via
823 ///   the [`All::get_mut_by_id`] and [`All::get_mut_by_name`]
824 ///   functions.
825 #[derive(Debug)]
826 pub struct AllUsers<A> {
827     users: Vec<User<A>>,
828     config: Config,
829 
830     // Hold on to the locked fds to prevent race conditions
831     passwd_fd: File,
832     shadow_fd: Option<File>,
833 }
834 
835 impl<A> AllUsers<A> {
new(config: Config) -> Result<AllUsers<A>>836     fn new(config: Config) -> Result<AllUsers<A>> {
837         let mut passwd_fd = locked_file(config.in_scheme(PASSWD_FILE), Lock::Exclusive)?;
838         let mut passwd_cntnt = String::new();
839         passwd_fd.read_to_string(&mut passwd_cntnt)?;
840 
841         let mut passwd_entries = Vec::new();
842         for (indx, line) in passwd_cntnt.lines().enumerate() {
843             let mut user = User::from_passwd_entry(line, indx)?;
844             user.auth_delay = config.auth_delay;
845             passwd_entries.push(user);
846         }
847 
848         Ok(AllUsers::<A> {
849             users: passwd_entries,
850             config,
851             passwd_fd,
852             shadow_fd: None,
853         })
854     }
855 }
856 
857 impl AllUsers<auth::Basic> {
858     /// Provide access to all user information on the system except
859     /// authentication. This is adequate for almost all uses of `AllUsers`.
basic(config: Config) -> Result<AllUsers<auth::Basic>>860     pub fn basic(config: Config) -> Result<AllUsers<auth::Basic>> {
861         Self::new(config)
862     }
863 }
864 
865 #[cfg(feature = "auth")]
866 impl AllUsers<auth::Full> {
867     /// If access to password related methods for the [`User`]s yielded by this
868     /// `AllUsers` is required, use this constructor.
authenticator(config: Config) -> Result<AllUsers<auth::Full>>869     pub fn authenticator(config: Config) -> Result<AllUsers<auth::Full>> {
870         let mut shadow_fd = locked_file(config.in_scheme(SHADOW_FILE), Lock::Exclusive)?;
871         let mut shadow_cntnt = String::new();
872         shadow_fd.read_to_string(&mut shadow_cntnt)?;
873         let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect();
874 
875         let mut new = Self::new(config)?;
876         new.shadow_fd = Some(shadow_fd);
877 
878         for (indx, entry) in shadow_entries.iter().enumerate() {
879             let mut entry = entry.split(';');
880             let name = entry.next().ok_or(parse_error(indx,
881                 "error parsing shadowfile: expected username"
882             ))?;
883             let hash = entry.next().ok_or(parse_error(indx,
884                 "error parsing shadowfile: expected hash"
885             ))?;
886             new.users
887                 .iter_mut()
888                 .find(|user| user.user == name)
889                 .ok_or(parse_error(indx,
890                     "error parsing shadowfile: unkown user"
891                 ))?
892                 .populate_hash(hash)?;
893         }
894 
895         Ok(new)
896     }
897 
898     /// Adds a user with the specified attributes to the `AllUsers`
899     /// instance. Note that the user's password is set unset (see
900     /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords))
901     /// during this call.
902     ///
903     /// Make sure to call [`AllUsers::save`] in order for the new user to be
904     /// applied to the system.
905     //TODO: Take uid/gid as Option<usize> and if none, find an unused ID.
add_user( &mut self, login: &str, uid: usize, gid: usize, name: &str, home: &str, shell: &str ) -> Result<()>906     pub fn add_user(
907         &mut self,
908         login: &str,
909         uid: usize,
910         gid: usize,
911         name: &str,
912         home: &str,
913         shell: &str
914     ) -> Result<()> {
915         if self.iter()
916             .any(|user| user.user == login || user.uid == uid)
917         {
918             return Err(From::from(UsersError::AlreadyExists))
919         }
920 
921         self.users.push(User {
922             user: login.into(),
923             uid,
924             gid,
925             name: name.into(),
926             home: home.into(),
927             shell: shell.into(),
928             hash: Some(("!".into(), false)),
929             auth: PhantomData,
930             auth_delay: self.config.auth_delay
931         });
932         Ok(())
933     }
934 
935     /// Syncs the data stored in the `AllUsers` instance to the filesystem.
936     /// To apply changes to the system from an `AllUsers`, you MUST call this
937     /// function!
save(&mut self) -> Result<()>938     pub fn save(&mut self) -> Result<()> {
939         let mut userstring = String::new();
940         let mut shadowstring = String::new();
941         for user in &self.users {
942             userstring.push_str(&user.passwd_entry());
943             shadowstring.push_str(&user.shadow_entry());
944         }
945 
946         let mut shadow_fd = self.shadow_fd.as_mut()
947             .expect("shadow_fd should exist for AllUsers<auth::Full>");
948 
949         reset_file(&mut self.passwd_fd)?;
950         self.passwd_fd.write_all(userstring.as_bytes())?;
951 
952         reset_file(&mut shadow_fd)?;
953         shadow_fd.write_all(shadowstring.as_bytes())?;
954         Ok(())
955     }
956 }
957 
958 impl<A> AllInner for AllUsers<A> {
959     type Gruser = User<A>;
960 
list(&self) -> &Vec<Self::Gruser>961     fn list(&self) -> &Vec<Self::Gruser> {
962         &self.users
963     }
964 
list_mut(&mut self) -> &mut Vec<Self::Gruser>965     fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
966         &mut self.users
967     }
968 
config(&self) -> &Config969     fn config(&self) -> &Config {
970         &self.config
971     }
972 }
973 
974 impl<A> All for AllUsers<A> {}
975 /*
976 #[cfg(not(target_os = "redox"))]
977 impl<A> Drop for AllUsers<A> {
978     fn drop(&mut self) {
979         eprintln!("Dropping AllUsers");
980         let _ = flock(self.passwd_fd.as_raw_fd(), FlockArg::Unlock);
981         if let Some(fd) = self.shadow_fd.as_ref() {
982             eprintln!("Shadow");
983             let _ = flock(fd.as_raw_fd(), FlockArg::Unlock);
984         }
985     }
986 }
987 */
988 /// `AllGroups` provides (borrowed) access to all groups on the system. Note
989 /// that this struct implements [`All`] for all of its access functions.
990 ///
991 /// General notes that also apply to this struct may be found with
992 /// [`AllUsers`].
993 #[derive(Debug)]
994 pub struct AllGroups {
995     groups: Vec<Group>,
996     config: Config,
997 
998     group_fd: File,
999 }
1000 
1001 impl AllGroups {
1002     /// Create a new `AllGroups`.
new(config: Config) -> Result<AllGroups>1003     pub fn new(config: Config) -> Result<AllGroups> {
1004         let mut group_fd = locked_file(config.in_scheme(GROUP_FILE), Lock::Exclusive)?;
1005         let mut group_cntnt = String::new();
1006         group_fd.read_to_string(&mut group_cntnt)?;
1007 
1008         let mut entries: Vec<Group> = Vec::new();
1009         for (indx, line) in group_cntnt.lines().enumerate() {
1010             let group = Group::from_group_entry(line, indx)?;
1011             entries.push(group);
1012         }
1013 
1014         Ok(AllGroups {
1015             groups: entries,
1016             config,
1017             group_fd,
1018         })
1019     }
1020 
1021     /// Adds a group with the specified attributes to this `AllGroups`.
1022     ///
1023     /// Make sure to call [`AllGroups::save`] in order for the new group to be
1024     /// applied to the system.
1025     //TODO: Take Option<usize> for gid and find unused ID if None
add_group( &mut self, name: &str, gid: usize, users: &[&str] ) -> Result<()>1026     pub fn add_group(
1027         &mut self,
1028         name: &str,
1029         gid: usize,
1030         users: &[&str]
1031     ) -> Result<()> {
1032         if self.iter()
1033             .any(|group| group.group == name || group.gid == gid)
1034         {
1035             return Err(From::from(UsersError::AlreadyExists))
1036         }
1037 
1038         //Might be cleaner... Also breaks...
1039         //users: users.iter().map(String::to_string).collect()
1040         self.groups.push(Group {
1041             group: name.into(),
1042             gid,
1043             users: users
1044                 .iter()
1045                 .map(|user| user.to_string())
1046                 .collect()
1047         });
1048 
1049         Ok(())
1050     }
1051 
1052     /// Syncs the data stored in this `AllGroups` instance to the filesystem.
1053     /// To apply changes from an `AllGroups`, you MUST call this function!
save(&mut self) -> Result<()>1054     pub fn save(&mut self) -> Result<()> {
1055         let mut groupstring = String::new();
1056         for group in &self.groups {
1057             groupstring.push_str(&group.group_entry());
1058         }
1059 
1060         reset_file(&mut self.group_fd)?;
1061         self.group_fd.write_all(groupstring.as_bytes())?;
1062         Ok(())
1063     }
1064 }
1065 
1066 impl AllInner for AllGroups {
1067     type Gruser = Group;
1068 
list(&self) -> &Vec<Self::Gruser>1069     fn list(&self) -> &Vec<Self::Gruser> {
1070         &self.groups
1071     }
1072 
list_mut(&mut self) -> &mut Vec<Self::Gruser>1073     fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1074         &mut self.groups
1075     }
1076 
config(&self) -> &Config1077     fn config(&self) -> &Config {
1078         &self.config
1079     }
1080 }
1081 
1082 impl All for AllGroups {}
1083 /*
1084 #[cfg(not(target_os = "redox"))]
1085 impl Drop for AllGroups {
1086     fn drop(&mut self) {
1087         eprintln!("Dropping AllGroups");
1088         let _ = flock(self.group_fd.as_raw_fd(), FlockArg::Unlock);
1089     }
1090 }*/
1091 
1092 #[cfg(test)]
1093 mod test {
1094     use super::*;
1095 
1096     const TEST_PREFIX: &'static str = "tests";
1097 
1098     /// Needed for the file checks, this is done by the library
test_prefix(filename: &str) -> String1099     fn test_prefix(filename: &str) -> String {
1100         let mut complete = String::from(TEST_PREFIX);
1101         complete.push_str(filename);
1102         complete
1103     }
1104 
test_cfg() -> Config1105     fn test_cfg() -> Config {
1106         Config::default()
1107             // Since all this really does is prepend `sheme` to the consts
1108             .scheme(TEST_PREFIX.to_string())
1109     }
1110 
read_locked_file(file: impl AsRef<Path>) -> Result<String>1111     fn read_locked_file(file: impl AsRef<Path>) -> Result<String> {
1112         let mut fd = locked_file(file, Lock::Exclusive)?;
1113         let mut cntnt = String::new();
1114         fd.read_to_string(&mut cntnt)?;
1115         Ok(cntnt)
1116     }
1117 
write_locked_file(file: impl AsRef<Path>, cntnt: impl AsRef<[u8]>) -> Result<()>1118     fn write_locked_file(file: impl AsRef<Path>, cntnt: impl AsRef<[u8]>) -> Result<()> {
1119         locked_file(file, Lock::Exclusive)?
1120             .write_all(cntnt.as_ref())?;
1121         Ok(())
1122     }
1123 
1124     // *** struct.User ***
1125     #[cfg(feature = "auth")]
1126     #[test]
attempt_user_api()1127     fn attempt_user_api() {
1128         let mut users = AllUsers::authenticator(test_cfg()).unwrap();
1129         let user = users.get_mut_by_id(1000).unwrap();
1130 
1131         assert_eq!(user.is_passwd_blank(), true);
1132         assert_eq!(user.is_passwd_unset(), false);
1133         assert_eq!(user.verify_passwd(""), true);
1134         assert_eq!(user.verify_passwd("Something"), false);
1135 
1136         user.set_passwd("hi,i_am_passwd").unwrap();
1137 
1138         assert_eq!(user.is_passwd_blank(), false);
1139         assert_eq!(user.is_passwd_unset(), false);
1140         assert_eq!(user.verify_passwd(""), false);
1141         assert_eq!(user.verify_passwd("Something"), false);
1142         assert_eq!(user.verify_passwd("hi,i_am_passwd"), true);
1143 
1144         user.unset_passwd();
1145 
1146         assert_eq!(user.is_passwd_blank(), false);
1147         assert_eq!(user.is_passwd_unset(), true);
1148         assert_eq!(user.verify_passwd(""), false);
1149         assert_eq!(user.verify_passwd("Something"), false);
1150         assert_eq!(user.verify_passwd("hi,i_am_passwd"), false);
1151 
1152         user.set_passwd("").unwrap();
1153 
1154         assert_eq!(user.is_passwd_blank(), true);
1155         assert_eq!(user.is_passwd_unset(), false);
1156         assert_eq!(user.verify_passwd(""), true);
1157         assert_eq!(user.verify_passwd("Something"), false);
1158     }
1159 
1160     // *** struct.AllUsers ***
1161     #[cfg(feature = "auth")]
1162     #[test]
get_user()1163     fn get_user() {
1164         let users = AllUsers::authenticator(test_cfg()).unwrap();
1165 
1166         let root = users.get_by_id(0).expect("'root' user missing");
1167         assert_eq!(root.user, "root".to_string());
1168         let &(ref hashstring, ref encoded) = root.hash.as_ref().expect("'root' hash is None");
1169         assert_eq!(hashstring,
1170             &"$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk".to_string());
1171         assert_eq!(root.uid, 0);
1172         assert_eq!(root.gid, 0);
1173         assert_eq!(root.name, "root".to_string());
1174         assert_eq!(root.home, "file:/root".to_string());
1175         assert_eq!(root.shell, "file:/bin/ion".to_string());
1176         match encoded {
1177             true => (),
1178             false => panic!("Expected encoded argon hash!")
1179         }
1180 
1181         let user = users.get_by_name("user").expect("'user' user missing");
1182         assert_eq!(user.user, "user".to_string());
1183         let &(ref hashstring, ref encoded) = user.hash.as_ref().expect("'user' hash is None");
1184         assert_eq!(hashstring, &"".to_string());
1185         assert_eq!(user.uid, 1000);
1186         assert_eq!(user.gid, 1000);
1187         assert_eq!(user.name, "user".to_string());
1188         assert_eq!(user.home, "file:/home/user".to_string());
1189         assert_eq!(user.shell, "file:/bin/ion".to_string());
1190         match encoded {
1191             true => panic!("Should not be an argon hash!"),
1192             false => ()
1193         }
1194         println!("{:?}", users);
1195 
1196         let li = users.get_by_name("li").expect("'li' user missing");
1197         println!("got li");
1198         assert_eq!(li.user, "li");
1199         let &(ref hashstring, ref encoded) = li.hash.as_ref().expect("'li' hash is None");
1200         assert_eq!(hashstring, &"!".to_string());
1201         assert_eq!(li.uid, 1007);
1202         assert_eq!(li.gid, 1007);
1203         assert_eq!(li.name, "Lorem".to_string());
1204         assert_eq!(li.home, "file:/home/lorem".to_string());
1205         assert_eq!(li.shell, "file:/bin/ion".to_string());
1206         match encoded {
1207             true => panic!("Should not be an argon hash!"),
1208             false => ()
1209         }
1210     }
1211 
1212     #[cfg(feature = "auth")]
1213     #[test]
manip_user()1214     fn manip_user() {
1215         let mut users = AllUsers::authenticator(test_cfg()).unwrap();
1216         // NOT testing `get_unique_id`
1217         let id = 7099;
1218         users
1219             .add_user("fb", id, id, "Foo Bar", "/home/foob", "/bin/zsh")
1220             .expect("failed to add user 'fb'");
1221         //                                            weirdo ^^^^^^^^ :P
1222         users.save().unwrap();
1223         let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1224         assert_eq!(
1225             p_file_content,
1226             concat!(
1227                 "root;0;0;root;file:/root;file:/bin/ion\n",
1228                 "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1229                 "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
1230                 "fb;7099;7099;Foo Bar;/home/foob;/bin/zsh\n"
1231             )
1232         );
1233         let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap();
1234         assert_eq!(s_file_content, concat!(
1235             "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n",
1236             "user;\n",
1237             "li;!\n",
1238             "fb;!\n"
1239         ));
1240 
1241         {
1242             println!("{:?}", users);
1243             let fb = users.get_mut_by_name("fb").expect("'fb' user missing");
1244             fb.shell = "/bin/fish".to_string(); // That's better
1245             fb.set_passwd("").unwrap();
1246         }
1247         users.save().unwrap();
1248         let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1249         assert_eq!(
1250             p_file_content,
1251             concat!(
1252                 "root;0;0;root;file:/root;file:/bin/ion\n",
1253                 "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1254                 "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
1255                 "fb;7099;7099;Foo Bar;/home/foob;/bin/fish\n"
1256             )
1257         );
1258         let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap();
1259         assert_eq!(s_file_content, concat!(
1260             "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n",
1261             "user;\n",
1262             "li;!\n",
1263             "fb;\n"
1264         ));
1265 
1266         users.remove_by_id(id);
1267         users.save().unwrap();
1268         let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1269         assert_eq!(
1270             file_content,
1271             concat!(
1272                 "root;0;0;root;file:/root;file:/bin/ion\n",
1273                 "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1274                 "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n"
1275             )
1276         );
1277     }
1278 
1279     /* struct.Group */
1280     #[test]
empty_groups()1281     fn empty_groups() {
1282         let group_trailing = Group::from_group_entry("nobody;2066; ", 0).unwrap();
1283         assert_eq!(group_trailing.users.len(), 0);
1284 
1285         let group_no_trailing = Group::from_group_entry("nobody;2066;", 0).unwrap();
1286         assert_eq!(group_no_trailing.users.len(), 0);
1287 
1288         assert_eq!(group_trailing.group, group_no_trailing.group);
1289         assert_eq!(group_trailing.gid, group_no_trailing.gid);
1290         assert_eq!(group_trailing.users, group_no_trailing.users);
1291     }
1292 
1293     /* struct.AllGroups */
1294     #[test]
get_group()1295     fn get_group() {
1296         let groups = AllGroups::new(test_cfg()).unwrap();
1297         let user = groups.get_by_name("user").unwrap();
1298         assert_eq!(user.group, "user");
1299         assert_eq!(user.gid, 1000);
1300         assert_eq!(user.users, vec!["user"]);
1301 
1302         let wheel = groups.get_by_id(1).unwrap();
1303         assert_eq!(wheel.group, "wheel");
1304         assert_eq!(wheel.gid, 1);
1305         assert_eq!(wheel.users, vec!["user", "root"]);
1306     }
1307 
1308     #[test]
manip_group()1309     fn manip_group() {
1310         let mut groups = AllGroups::new(test_cfg()).unwrap();
1311         // NOT testing `get_unique_id`
1312         let id = 7099;
1313 
1314         groups.add_group("fb", id, &["fb"]).unwrap();
1315         groups.save().unwrap();
1316         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1317         assert_eq!(
1318             file_content,
1319             concat!(
1320                 "root;0;root\n",
1321                 "user;1000;user\n",
1322                 "wheel;1;user,root\n",
1323                 "li;1007;li\n",
1324                 "fb;7099;fb\n"
1325             )
1326         );
1327 
1328         {
1329             let fb = groups.get_mut_by_name("fb").unwrap();
1330             fb.users.push("user".to_string());
1331         }
1332         groups.save().unwrap();
1333         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1334         assert_eq!(
1335             file_content,
1336             concat!(
1337                 "root;0;root\n",
1338                 "user;1000;user\n",
1339                 "wheel;1;user,root\n",
1340                 "li;1007;li\n",
1341                 "fb;7099;fb,user\n"
1342             )
1343         );
1344 
1345         groups.remove_by_id(id);
1346         groups.save().unwrap();
1347         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1348         assert_eq!(
1349             file_content,
1350             concat!(
1351                 "root;0;root\n",
1352                 "user;1000;user\n",
1353                 "wheel;1;user,root\n",
1354                 "li;1007;li\n"
1355             )
1356         );
1357     }
1358 
1359     #[test]
empty_group()1360     fn empty_group() {
1361         let mut groups = AllGroups::new(test_cfg()).unwrap();
1362 
1363         groups.add_group("nobody", 2260, &[]).unwrap();
1364         groups.save().unwrap();
1365         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1366         assert_eq!(
1367             file_content,
1368             concat!(
1369                 "root;0;root\n",
1370                 "user;1000;user\n",
1371                 "wheel;1;user,root\n",
1372                 "li;1007;li\n",
1373                 "nobody;2260;\n",
1374             )
1375         );
1376 
1377         drop(groups);
1378         let mut groups = AllGroups::new(test_cfg()).unwrap();
1379 
1380         groups.remove_by_name("nobody");
1381         groups.save().unwrap();
1382 
1383         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1384         assert_eq!(
1385             file_content,
1386             concat!(
1387                 "root;0;root\n",
1388                 "user;1000;user\n",
1389                 "wheel;1;user,root\n",
1390                 "li;1007;li\n"
1391             )
1392         );
1393     }
1394 
1395     // *** Misc ***
1396     #[test]
users_get_unused_ids()1397     fn users_get_unused_ids() {
1398         let users = AllUsers::basic(test_cfg()).unwrap();
1399         let id = users.get_unique_id().unwrap();
1400         if id < users.config.min_id || id > users.config.max_id {
1401             panic!("User ID is not between allowed margins")
1402         } else if let Some(_) = users.get_by_id(id) {
1403             panic!("User ID is used!");
1404         }
1405     }
1406 
1407     #[test]
groups_get_unused_ids()1408     fn groups_get_unused_ids() {
1409         let groups = AllGroups::new(test_cfg()).unwrap();
1410         let id = groups.get_unique_id().unwrap();
1411         if id < groups.config.min_id || id > groups.config.max_id {
1412             panic!("Group ID is not between allowed margins")
1413         } else if let Some(_) = groups.get_by_id(id) {
1414             panic!("Group ID is used!");
1415         }
1416     }
1417 }
1418