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.
4 //!
5 //! # Permissions
6 //! Because this is a system level tool dealing with password
7 //! authentication, programs are often required to run with
8 //! escalated priveleges. The implementation of the crate is
9 //! privelege unaware. The only privelege requirements are those
10 //! laid down by the system administrator over these files:
11 //! - `/etc/group`
12 //!   - Read: Required to access group information
13 //!   - Write: Required to change group information
14 //! - `/etc/passwd`
15 //!   - Read: Required to access user information
16 //!   - Write: Required to change user information
17 //! - `/etc/shadow`
18 //!   - Read: Required to authenticate users
19 //!   - Write: Required to set user passwords
20 //!
21 //! # Reimplementation
22 //! This crate is designed to be as small as possible without
23 //! sacrificing critical functionality. The idea is that a small
24 //! enough redox-users will allow easy re-implementation based on
25 //! the same flexible API. This would allow more complicated authentication
26 //! schemes for redox in future without breakage of existing
27 //! software.
28 
29 #[cfg(feature = "auth")]
30 extern crate argon2;
31 extern crate getrandom;
32 extern crate syscall;
33 
34 use std::convert::From;
35 use std::error::Error;
36 use std::fmt::{self, Debug, Display};
37 use std::fs::OpenOptions;
38 use std::io::{Read, Write};
39 #[cfg(target_os = "redox")]
40 use std::os::unix::fs::OpenOptionsExt;
41 use std::os::unix::process::CommandExt;
42 use std::path::{Path, PathBuf};
43 use std::process::Command;
44 use std::slice::{Iter, IterMut};
45 use std::str::FromStr;
46 #[cfg(not(test))]
47 #[cfg(feature = "auth")]
48 use std::thread;
49 use std::time::Duration;
50 
51 #[cfg(target_os = "redox")]
52 use syscall::flag::{O_EXLOCK, O_SHLOCK};
53 use syscall::Error as SyscallError;
54 
55 const PASSWD_FILE: &'static str = "/etc/passwd";
56 const GROUP_FILE: &'static str = "/etc/group";
57 const SHADOW_FILE: &'static str = "/etc/shadow";
58 
59 #[cfg(target_os = "redox")]
60 const DEFAULT_SCHEME: &'static str = "file:";
61 #[cfg(not(target_os = "redox"))]
62 const DEFAULT_SCHEME: &'static str = "";
63 
64 const MIN_ID: usize = 1000;
65 const MAX_ID: usize = 6000;
66 const DEFAULT_TIMEOUT: u64 = 3;
67 
68 pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>;
69 
70 /// Errors that might happen while using this crate
71 #[derive(Debug, PartialEq)]
72 pub enum UsersError {
73     Os { reason: String },
74     Parsing { reason: String },
75     NotFound,
76     AlreadyExists
77 }
78 
79 impl fmt::Display for UsersError {
fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result80     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
81         match self {
82             UsersError::Os { reason } => write!(f, "os error: code {}", reason),
83             UsersError::Parsing { reason } => {
84                 write!(f, "parse error: {}", reason)
85             },
86             UsersError::NotFound => write!(f, "user/group not found"),
87             UsersError::AlreadyExists => write!(f, "user/group already exists")
88         }
89     }
90 }
91 
92 impl Error for UsersError {
description(&self) -> &str93     fn description(&self) -> &str { "UsersError" }
94 
cause(&self) -> Option<&dyn Error>95     fn cause(&self) -> Option<&dyn Error> { None }
96 }
97 
98 #[inline]
parse_error(reason: &str) -> UsersError99 fn parse_error(reason: &str) -> UsersError {
100     UsersError::Parsing {
101         reason: reason.into()
102     }
103 }
104 
105 #[inline]
os_error(reason: &str) -> UsersError106 fn os_error(reason: &str) -> UsersError {
107     UsersError::Os {
108         reason: reason.into()
109     }
110 }
111 
112 impl From<SyscallError> for UsersError {
from(syscall_error: SyscallError) -> UsersError113     fn from(syscall_error: SyscallError) -> UsersError {
114         UsersError::Os {
115             reason: format!("{}", syscall_error)
116         }
117     }
118 }
119 
read_locked_file(file: impl AsRef<Path>) -> Result<String>120 fn read_locked_file(file: impl AsRef<Path>) -> Result<String> {
121     #[cfg(test)]
122     println!("Reading file: {}", file.as_ref().display());
123 
124     #[cfg(target_os = "redox")]
125     let mut file = OpenOptions::new()
126         .read(true)
127         .custom_flags(O_SHLOCK as i32)
128         .open(file)?;
129     #[cfg(not(target_os = "redox"))]
130     #[cfg_attr(rustfmt, rustfmt_skip)]
131     let mut file = OpenOptions::new()
132         .read(true)
133         .open(file)?;
134 
135     let len = file.metadata()?.len();
136     let mut file_data = String::with_capacity(len as usize);
137     file.read_to_string(&mut file_data)?;
138     Ok(file_data)
139 }
140 
write_locked_file(file: impl AsRef<Path>, data: String) -> Result<()>141 fn write_locked_file(file: impl AsRef<Path>, data: String) -> Result<()> {
142     #[cfg(test)]
143     println!("Writing file: {}", file.as_ref().display());
144 
145     #[cfg(target_os = "redox")]
146     let mut file = OpenOptions::new()
147         .write(true)
148         .truncate(true)
149         .custom_flags(O_EXLOCK as i32)
150         .open(file)?;
151     #[cfg(not(target_os = "redox"))]
152     #[cfg_attr(rustfmt, rustfmt_skip)]
153     let mut file = OpenOptions::new()
154         .write(true)
155         .truncate(true)
156         .open(file)?;
157 
158     file.write(data.as_bytes())?;
159     Ok(())
160 }
161 
162 /// A struct representing a Redox user.
163 /// Currently maps to an entry in the `/etc/passwd` file.
164 ///
165 /// # Unset vs. Blank Passwords
166 /// A note on unset passwords vs. blank passwords. A blank password
167 /// is a hash field that is completely blank (aka, `""`). According
168 /// to this crate, successful login is only allowed if the input
169 /// password is blank as well.
170 ///
171 /// An unset password is one whose hash is not empty (`""`), but
172 /// also not a valid serialized argon2rs hashing session. This
173 /// hash always returns `false` upon attempted verification. The
174 /// most commonly used hash for an unset password is `"!"`, but
175 /// this crate makes no distinction. The most common way to unset
176 /// the password is to use [`unset_passwd`](struct.User.html#method.unset_passwd).
177 pub struct User {
178     /// Username (login name)
179     pub user: String,
180     // Hashed password and Argon2 indicator, stored to simplify API
181     hash: Option<(String, bool)>,
182     /// User id
183     pub uid: usize,
184     /// Group id
185     pub gid: usize,
186     /// Real name (GECOS field)
187     pub name: String,
188     /// Home directory path
189     pub home: String,
190     /// Shell path
191     pub shell: String,
192     /// Failed login delay duration
193     auth_delay: Duration
194 }
195 
196 impl User {
197     /// Set the password for a user. Make sure the password you have
198     /// received is actually what the user wants as their password (this doesn't).
199     ///
200     /// To set the password blank, use `""` as the password parameter.
201     ///
202     /// # Panics
203     /// If the User's hash fields are unpopulated, this function will `panic!`
204     /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info).
205     #[cfg(feature = "auth")]
set_passwd(&mut self, password: impl AsRef<str>) -> Result<()>206     pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<()> {
207         self.panic_if_unpopulated();
208         let password = password.as_ref();
209 
210         self.hash = if password != "" {
211             let mut buf = [0u8; 8];
212             getrandom::getrandom(&mut buf)?;
213             let salt = format!("{:X}", u64::from_ne_bytes(buf));
214             let config = argon2::Config::default();
215             let hash = argon2::hash_encoded(
216                 password.as_bytes(),
217                 salt.as_bytes(),
218                 &config
219             )?;
220             Some((hash, true))
221         } else {
222             Some(("".into(), false))
223         };
224         Ok(())
225     }
226 
227     /// Unset the password (do not allow logins).
228     ///
229     /// # Panics
230     /// If the User's hash fields are unpopulated, this function will `panic!`
231     /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info).
unset_passwd(&mut self)232     pub fn unset_passwd(&mut self) {
233         self.panic_if_unpopulated();
234         self.hash = Some(("!".into(), false));
235     }
236 
237     /// Verify the password. If the hash is empty, this only
238     /// returns `true` if the password field is also empty.
239     /// Note that this is a blocking operation if the password
240     /// is incorrect. See [`Config::auth_delay`](struct.Config.html#method.auth_delay)
241     /// to set the wait time. Default is 3 seconds.
242     ///
243     /// # Panics
244     /// If the User's hash fields are unpopulated, this function will `panic!`
245     /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info).
246     #[cfg(feature = "auth")]
verify_passwd(&self, password: impl AsRef<str>) -> bool247     pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool {
248         self.panic_if_unpopulated();
249         // Safe because it will have panicked already if self.hash.is_none()
250         let &(ref hash, ref encoded) = self.hash.as_ref().unwrap();
251         let password = password.as_ref();
252 
253         let verified = if *encoded {
254             argon2::verify_encoded(&hash, password.as_bytes()).unwrap()
255         } else {
256             hash == "" && password == ""
257         };
258 
259         if !verified {
260             #[cfg(not(test))] // Make tests run faster
261             thread::sleep(self.auth_delay);
262         }
263         verified
264     }
265 
266     /// Determine if the hash for the password is blank
267     /// (any user can log in as this user with no password).
268     ///
269     /// # Panics
270     /// If the User's hash fields are unpopulated, this function will `panic!`
271     /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info).
is_passwd_blank(&self) -> bool272     pub fn is_passwd_blank(&self) -> bool {
273         self.panic_if_unpopulated();
274         let &(ref hash, ref encoded) = self.hash.as_ref().unwrap();
275         hash == "" && ! encoded
276     }
277 
278     /// Determine if the hash for the password is unset
279     /// ([`verify_passwd`](struct.User.html#method.verify_passwd)
280     /// returns `false` regardless of input).
281     ///
282     /// # Panics
283     /// If the User's hash fields are unpopulated, this function will `panic!`
284     /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info).
is_passwd_unset(&self) -> bool285     pub fn is_passwd_unset(&self) -> bool {
286         self.panic_if_unpopulated();
287         let &(ref hash, ref encoded) = self.hash.as_ref().unwrap();
288         hash != "" && ! encoded
289     }
290 
291     /// Get a Command to run the user's default shell
292     /// (see [`login_cmd`](struct.User.html#method.login_cmd) for more docs).
shell_cmd(&self) -> Command293     pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) }
294 
295     /// Provide a login command for the user, which is any
296     /// entry point for starting a user's session, whether
297     /// a shell (use [`shell_cmd`](struct.User.html#method.shell_cmd) instead) or a graphical init.
298     ///
299     /// The `Command` will use the user's `uid` and `gid`, its `current_dir` will be
300     /// set to the user's home directory, and the follwing enviroment variables will
301     /// be populated:
302     ///
303     ///    - `USER` set to the user's `user` field.
304     ///    - `UID` set to the user's `uid` field.
305     ///    - `GROUPS` set the user's `gid` field.
306     ///    - `HOME` set to the user's `home` field.
307     ///    - `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>308     pub fn login_cmd<T>(&self, cmd: T) -> Command
309         where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str>
310     {
311         let mut command = Command::new(cmd);
312         command
313             .uid(self.uid as u32)
314             .gid(self.gid as u32)
315             .current_dir(&self.home)
316             .env("USER", &self.user)
317             .env("UID", format!("{}", self.uid))
318             .env("GROUPS", format!("{}", self.gid))
319             .env("HOME", &self.home)
320             .env("SHELL", &self.shell);
321         command
322     }
323 
324     /// This returns an entry for `/etc/shadow`
325     /// Will panic!
shadowstring(&self) -> String326     fn shadowstring(&self) -> String {
327         self.panic_if_unpopulated();
328         let hashstring = match self.hash {
329             Some((ref hash, _)) => hash,
330             None => panic!("Shadowfile not read!")
331         };
332         format!("{};{}", self.user, hashstring)
333     }
334 
335     /// Give this a hash string (not a shadowfile entry!!!)
populate_hash(&mut self, hash: &str) -> Result<()>336     fn populate_hash(&mut self, hash: &str) -> Result<()> {
337         let encoded = match hash {
338             "" => false,
339             "!" => false,
340             _ => true,
341         };
342         self.hash = Some((hash.to_string(), encoded));
343         Ok(())
344     }
345 
346     #[inline]
panic_if_unpopulated(&self)347     fn panic_if_unpopulated(&self) {
348         if self.hash.is_none() {
349             panic!("Hash not populated!");
350         }
351     }
352 }
353 
354 impl Name for User {
name(&self) -> &str355     fn name(&self) -> &str {
356         &self.user
357     }
358 }
359 
360 impl Id for User {
id(&self) -> usize361     fn id(&self) -> usize {
362         self.uid
363     }
364 }
365 
366 impl Debug for User {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result367     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
368         write!(f,
369             "User {{\n\tuser: {:?}\n\tuid: {:?}\n\tgid: {:?}\n\tname: {:?}
370             home: {:?}\n\tshell: {:?}\n\tauth_delay: {:?}\n}}",
371             self.user, self.uid, self.gid, self.name, self.home, self.shell, self.auth_delay
372         )
373     }
374 }
375 
376 impl Display for User {
377     /// Format this user as an entry in `/etc/passwd`. This
378     /// is an implementation detail, do NOT rely on this trait
379     /// being implemented in future.
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result380     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
381         #[cfg_attr(rustfmt, rustfmt_skip)]
382         write!(f, "{};{};{};{};{};{}",
383             self.user, self.uid, self.gid, self.name, self.home, self.shell
384         )
385     }
386 }
387 
388 impl FromStr for User {
389     type Err = Box<dyn Error + Send + Sync>;
390 
391     /// Parse an entry from `/etc/passwd`. This
392     /// is an implementation detail, do NOT rely on this trait
393     /// being implemented in future.
from_str(s: &str) -> Result<Self>394     fn from_str(s: &str) -> Result<Self> {
395         let mut parts = s.split(';');
396 
397         let user = parts
398             .next()
399             .ok_or(parse_error("expected user"))?;
400         let uid = parts
401             .next()
402             .ok_or(parse_error("expected uid"))?
403             .parse::<usize>()?;
404         let gid = parts
405             .next()
406             .ok_or(parse_error("expected uid"))?
407             .parse::<usize>()?;
408         let name = parts
409             .next()
410             .ok_or(parse_error("expected real name"))?;
411         let home = parts
412             .next()
413             .ok_or(parse_error("expected home dir path"))?;
414         let shell = parts
415             .next()
416             .ok_or(parse_error("expected shell path"))?;
417 
418         Ok(User {
419             user: user.into(),
420             hash: None,
421             uid,
422             gid,
423             name: name.into(),
424             home: home.into(),
425             shell: shell.into(),
426             auth_delay: Duration::default(),
427         })
428     }
429 }
430 
431 /// A struct representing a Redox user group.
432 /// Currently maps to an `/etc/group` file entry.
433 #[derive(Debug)]
434 pub struct Group {
435     /// Group name
436     pub group: String,
437     /// Unique group id
438     pub gid: usize,
439     /// Group members usernames
440     pub users: Vec<String>,
441 }
442 
443 impl Name for Group {
name(&self) -> &str444     fn name(&self) -> &str {
445         &self.group
446     }
447 }
448 
449 impl Id for Group {
id(&self) -> usize450     fn id(&self) -> usize {
451         self.gid
452     }
453 }
454 
455 impl Display for Group {
456     /// Format this group as an entry in `/etc/group`. This
457     /// is an implementation detail, do NOT rely on this trait
458     /// being implemented in future.
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result459     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
460         #[cfg_attr(rustfmt, rustfmt_skip)]
461         write!(f, "{};{};{}",
462             self.group,
463             self.gid,
464             self.users.join(",").trim_matches(',')
465         )
466     }
467 }
468 
469 impl FromStr for Group {
470     type Err = Box<dyn Error + Send + Sync>;
471 
472     /// Parse an entry from `/etc/group`. This
473     /// is an implementation detail, do NOT rely on this trait
474     /// being implemented in future.
from_str(s: &str) -> Result<Self>475     fn from_str(s: &str) -> Result<Self> {
476         let mut parts = s.split(';');
477 
478         let group = parts
479             .next()
480             .ok_or(parse_error("expected group"))?;
481         let gid = parts
482             .next()
483             .ok_or(parse_error("expected gid"))?
484             .parse::<usize>()?;
485         //Allow for an empty users field. If there is a better way to do this, do it
486         let users_str = parts.next().unwrap_or(" ");
487         let users = users_str.split(',').map(|u| u.into()).collect();
488 
489         Ok(Group {
490             group: group.into(),
491             gid,
492             users,
493         })
494     }
495 }
496 
497 /// Gets the current process effective user ID.
498 ///
499 /// This function issues the `geteuid` system call returning the process effective
500 /// user id.
501 ///
502 /// # Examples
503 ///
504 /// Basic usage:
505 ///
506 /// ```no_run
507 /// # use redox_users::get_euid;
508 /// let euid = get_euid().unwrap();
509 /// ```
get_euid() -> Result<usize>510 pub fn get_euid() -> Result<usize> {
511     match syscall::geteuid() {
512         Ok(euid) => Ok(euid),
513         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
514     }
515 }
516 
517 /// Gets the current process real user ID.
518 ///
519 /// This function issues the `getuid` system call returning the process real
520 /// user id.
521 ///
522 /// # Examples
523 ///
524 /// Basic usage:
525 ///
526 /// ```no_run
527 /// # use redox_users::get_uid;
528 /// let uid = get_uid().unwrap();
529 /// ```
get_uid() -> Result<usize>530 pub fn get_uid() -> Result<usize> {
531     match syscall::getuid() {
532         Ok(uid) => Ok(uid),
533         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
534     }
535 }
536 
537 /// Gets the current process effective group ID.
538 ///
539 /// This function issues the `getegid` system call returning the process effective
540 /// group id.
541 ///
542 /// # Examples
543 ///
544 /// Basic usage:
545 ///
546 /// ```no_run
547 /// # use redox_users::get_egid;
548 /// let egid = get_egid().unwrap();
549 /// ```
get_egid() -> Result<usize>550 pub fn get_egid() -> Result<usize> {
551     match syscall::getegid() {
552         Ok(egid) => Ok(egid),
553         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
554     }
555 }
556 
557 /// Gets the current process real group ID.
558 ///
559 /// This function issues the `getegid` system call returning the process real
560 /// group id.
561 ///
562 /// # Examples
563 ///
564 /// Basic usage:
565 ///
566 /// ```no_run
567 /// # use redox_users::get_gid;
568 /// let gid = get_gid().unwrap();
569 /// ```
get_gid() -> Result<usize>570 pub fn get_gid() -> Result<usize> {
571     match syscall::getgid() {
572         Ok(gid) => Ok(gid),
573         Err(syscall_error) => Err(From::from(os_error(syscall_error.text())))
574     }
575 }
576 
577 /// A generic configuration that allows better control of
578 /// `AllUsers` or `AllGroups` than might otherwise be possible.
579 ///
580 /// The use of the fields of this struct is completely optional
581 /// depending on what constructor it is passed to. For example,
582 /// `AllGroups` doesn't care if auth is enabled or not, or what
583 /// the duration is.
584 ///
585 /// In most situations, `Config::default()` will work just fine.
586 /// The other methods on this struct are usually for finer control
587 /// of an `AllUsers` or `AllGroups` if it is required.
588 #[derive(Clone)]
589 pub struct Config {
590     auth_enabled: bool,
591     scheme: String,
592     auth_delay: Duration,
593     min_id: usize,
594     max_id: usize,
595 }
596 
597 impl Config {
598     /// An alternative to the default constructor, this indicates that
599     /// authentication should be enabled.
with_auth() -> Config600     pub fn with_auth() -> Config {
601         Config {
602             auth_enabled: true,
603             ..Default::default()
604         }
605     }
606 
607     /// Builder pattern version of `Self::with_auth`.
auth(mut self, auth: bool) -> Config608     pub fn auth(mut self, auth: bool) -> Config {
609         self.auth_enabled = auth;
610         self
611     }
612 
613     /// Set the delay for a failed authentication. Default is 3 seconds.
auth_delay(mut self, delay: Duration) -> Config614     pub fn auth_delay(mut self, delay: Duration) -> Config {
615         self.auth_delay = delay;
616         self
617     }
618 
619     /// Set the smallest ID possible to use when finding an unused ID.
min_id(mut self, id: usize) -> Config620     pub fn min_id(mut self, id: usize) -> Config {
621         self.min_id = id;
622         self
623     }
624 
625     /// Set the largest possible ID to use when finding an unused ID.
max_id(mut self, id: usize) -> Config626     pub fn max_id(mut self, id: usize) -> Config {
627         self.max_id = id;
628         self
629     }
630 
631     /// Set the scheme relative to which the `AllUsers` or `AllGroups`
632     /// should be looking for its data files. This is a compromise between
633     /// exposing implementation details and providing fine enough
634     /// control over the behavior of this API.
scheme(mut self, scheme: String) -> Config635     pub fn scheme(mut self, scheme: String) -> Config {
636         self.scheme = scheme;
637         self
638     }
639 
640     // Prepend a path with the scheme in this Config
in_scheme(&self, path: impl AsRef<Path>) -> PathBuf641     fn in_scheme(&self, path: impl AsRef<Path>) -> PathBuf {
642         let mut canonical_path = PathBuf::from(&self.scheme);
643         // Should be a little careful here, not sure I want this behavior
644         if path.as_ref().is_absolute() {
645             // This is nasty
646             canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string());
647         } else {
648             canonical_path.push(path);
649         }
650         canonical_path
651     }
652 }
653 
654 impl Default for Config {
655     /// Authentication is not enabled; The default base scheme is `file`.
default() -> Config656     fn default() -> Config {
657         Config {
658             auth_enabled: false,
659             scheme: String::from(DEFAULT_SCHEME),
660             auth_delay: Duration::new(DEFAULT_TIMEOUT, 0),
661             min_id: MIN_ID,
662             max_id: MAX_ID,
663         }
664     }
665 }
666 
667 // Nasty hack to prevent the compiler complaining about
668 // "leaking" `AllInner`
669 mod sealed {
670     use Config;
671 
672     pub trait Name {
name(&self) -> &str673         fn name(&self) -> &str;
674     }
675 
676     pub trait Id {
id(&self) -> usize677         fn id(&self) -> usize;
678     }
679 
680     pub trait AllInner {
681         // Group+User, thanks Dad
682         type Gruser: Name + Id;
683 
684         /// These functions grab internal elements so that the other
685         /// methods of `All` can manipulate them.
list(&self) -> &Vec<Self::Gruser>686         fn list(&self) -> &Vec<Self::Gruser>;
list_mut(&mut self) -> &mut Vec<Self::Gruser>687         fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
config(&self) -> &Config688         fn config(&self) -> &Config;
689     }
690 }
691 
692 use sealed::{AllInner, Id, Name};
693 
694 /// This trait is used to remove repetitive API items from
695 /// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait
696 /// so that the implementations of functions can be implemented
697 /// at the trait level. Do not try to implement this trait.
698 pub trait All: AllInner {
699     /// Get an iterator borrowing all [`User`](struct.User.html)'s
700     /// or [`Group`](struct.Group.html)'s on the system.
iter(&self) -> Iter<<Self as AllInner>::Gruser>701     fn iter(&self) -> Iter<<Self as AllInner>::Gruser> {
702         self.list().iter()
703     }
704 
705     /// Get an iterator mutably borrowing all [`User`](struct.User.html)'s
706     /// or [`Group`](struct.Group.html)'s on the system.
iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser>707     fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> {
708         self.list_mut().iter_mut()
709     }
710 
711     /// Borrow the [`User`](struct.User.html) or [`Group`](struct.Group.html)
712     /// 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::new(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 [`get_by_name`](trait.All.html#method.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`](struct.User.html) or [`Group`](struct.Group.html)
735     /// with the given ID.
736     ///
737     /// # Examples
738     ///
739     /// Basic usage:
740     ///
741     /// ```no_run
742     /// # use redox_users::{All, AllUsers, Config};
743     /// let users = AllUsers::new(Config::default()).unwrap();
744     /// let user = users.get_by_id(0).unwrap();
745     /// ```
get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser>746     fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> {
747         self.iter()
748             .find(|gruser| gruser.id() == id )
749     }
750 
751     /// Mutable version of [`get_by_id`](trait.All.html#method.get_by_id).
get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser>752     fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> {
753         self.iter_mut()
754             .find(|gruser| gruser.id() == id )
755     }
756 
757     /// Provides an unused id based on the min and max values in
758     /// the [`Config`](struct.Config.html) passed to the `All`'s constructor.
759     ///
760     /// # Examples
761     ///
762     /// ```no_run
763     /// # use redox_users::{All, AllUsers, Config};
764     /// let users = AllUsers::new(Config::default()).unwrap();
765     /// let uid = users.get_unique_id().expect("no available uid");
766     /// ```
get_unique_id(&self) -> Option<usize>767     fn get_unique_id(&self) -> Option<usize> {
768         for id in self.config().min_id..self.config().max_id {
769             if !self.iter().any(|gruser| gruser.id() == id ) {
770                 return Some(id)
771             }
772         }
773         None
774     }
775 
776     /// Remove a [`User`](struct.User.html) or [`Group`](struct.Group.html)
777     /// from this `All` given it's name. This won't provide an indication
778     /// of whether the user was removed or not, but is guaranteed to work
779     /// if a user with the specified name exists.
remove_by_name(&mut self, name: impl AsRef<str>)780     fn remove_by_name(&mut self, name: impl AsRef<str>) {
781         // Significantly more elegant than other possible solutions.
782         // I wish it could indicate if it removed anything.
783         self.list_mut()
784             .retain(|gruser| gruser.name() != name.as_ref() );
785     }
786 
787     /// Id version of [`remove_by_name`](trait.All.html#method.remove_by_name).
remove_by_id(&mut self, id: usize)788     fn remove_by_id(&mut self, id: usize) {
789         self.list_mut()
790             .retain(|gruser| gruser.id() != id );
791     }
792 }
793 
794 /// [`AllUsers`](struct.AllUsers.html) provides
795 /// (borrowed) access to all the users on the system.
796 /// Note that this struct implements [`All`](trait.All.html) for
797 /// a bunch of convenient access functions.
798 ///
799 /// # Notes
800 /// Note that everything in this section also applies to
801 /// [`AllGroups`](struct.AllGroups.html)
802 ///
803 /// * If you mutate anything owned by an `AllUsers`,
804 ///   you must call the [`save`](struct.AllUsers.html#method.save)
805 ///   method in order for those changes to be applied to the system.
806 /// * The API here is kept small on purpose in order to reduce the
807 ///   surface area for security exploitation. Most mutating actions
808 ///   can be accomplished via the [`get_mut_by_id`](struct.AllUsers.html#method.get_mut_by_id)
809 ///   and [`get_mut_by_name`](struct.AllUsers.html#method.get_mut_by_name)
810 ///   functions.
811 ///
812 /// # Shadowfile handling
813 /// This implementation of redox-users uses a shadowfile implemented primarily
814 /// by this struct. `AllUsers` respects the `auth_enabled` status of the `Config`
815 /// that is was passed. If auth is enabled, it populates the
816 /// hash fields of each user struct that it parses from `/etc/passwd` with
817 /// info from `/et/shadow`. If a caller attempts to perform an action that
818 /// requires this info with an `AllUsers` config that does not have auth enabled,
819 /// the `User` handling action will panic.
820 pub struct AllUsers {
821     users: Vec<User>,
822     config: Config,
823 }
824 
825 impl AllUsers {
826     /// See [Shadowfile Handling](struct.AllUsers.html#shadowfile-handling) for
827     /// configuration information regarding this constructor.
828     //TODO: Indicate if parsing an individual line failed or not
new(config: Config) -> Result<AllUsers>829     pub fn new(config: Config) -> Result<AllUsers> {
830         let passwd_cntnt = read_locked_file(config.in_scheme(PASSWD_FILE))?;
831 
832         let mut passwd_entries: Vec<User> = Vec::new();
833         for line in passwd_cntnt.lines() {
834             if let Ok(mut user) = User::from_str(line) {
835                 user.auth_delay = config.auth_delay;
836                 passwd_entries.push(user);
837             }
838         }
839 
840         if config.auth_enabled {
841             let shadow_cntnt = read_locked_file(config.in_scheme(SHADOW_FILE))?;
842             let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect();
843             for entry in shadow_entries.iter() {
844                 let mut entry = entry.split(';');
845                 let name = entry.next().ok_or(parse_error(
846                     "error parsing shadowfile: expected username"
847                 ))?;
848                 let hash = entry.next().ok_or(parse_error(
849                     "error parsing shadowfile: expected hash"
850                 ))?;
851                 passwd_entries
852                     .iter_mut()
853                     .find(|user| user.user == name)
854                     .ok_or(parse_error(
855                         "error parsing shadowfile: unkown user"
856                     ))?
857                     .populate_hash(hash)?;
858             }
859         }
860 
861         Ok(AllUsers {
862             users: passwd_entries,
863             config
864         })
865     }
866 
867     /// Adds a user with the specified attributes to the `AllUsers`
868     /// instance. Note that the user's password is set unset (see
869     /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords))
870     /// during this call.
871     ///
872     /// This function is classified as a mutating operation,
873     /// and users must therefore call [`save`](struct.AllUsers.html#method.save)
874     /// in order for the new user to be applied to the system.
875     ///
876     /// # Panics
877     /// This function will `panic!` if the [`Config`](struct.Config.html)
878     /// passed to [`AllUsers::new`](struct.AllUsers.html#method.new)
879     /// does not have authentication enabled (see
880     /// [`Shadowfile handling`](struct.AllUsers.html#shadowfile-handling)).
881     //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<()>882     pub fn add_user(
883         &mut self,
884         login: &str,
885         uid: usize,
886         gid: usize,
887         name: &str,
888         home: &str,
889         shell: &str
890     ) -> Result<()> {
891         if self.iter()
892             .any(|user| user.user == login || user.uid == uid)
893         {
894             return Err(From::from(UsersError::AlreadyExists))
895         }
896 
897         if !self.config.auth_enabled {
898             panic!("Attempt to create user without access to the shadowfile");
899         }
900 
901         self.users.push(User {
902             user: login.into(),
903             hash: Some(("!".into(), false)),
904             uid,
905             gid,
906             name: name.into(),
907             home: home.into(),
908             shell: shell.into(),
909             auth_delay: self.config.auth_delay
910         });
911         Ok(())
912     }
913 
914     /// Syncs the data stored in the `AllUsers` instance to the filesystem.
915     /// To apply changes to the system from an `AllUsers`, you MUST call this function!
save(&self) -> Result<()>916     pub fn save(&self) -> Result<()> {
917         let mut userstring = String::new();
918         let mut shadowstring = String::new();
919         for user in &self.users {
920             userstring.push_str(&format!("{}\n", user.to_string().as_str()));
921             if self.config.auth_enabled {
922                 shadowstring.push_str(&format!("{}\n", user.shadowstring()));
923             }
924         }
925 
926         write_locked_file(self.config.in_scheme(PASSWD_FILE), userstring)?;
927         if self.config.auth_enabled {
928             write_locked_file(self.config.in_scheme(SHADOW_FILE), shadowstring)?;
929         }
930         Ok(())
931     }
932 }
933 
934 impl AllInner for AllUsers {
935     type Gruser = User;
936 
list(&self) -> &Vec<Self::Gruser>937     fn list(&self) -> &Vec<Self::Gruser> {
938         &self.users
939     }
940 
list_mut(&mut self) -> &mut Vec<Self::Gruser>941     fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
942         &mut self.users
943     }
944 
config(&self) -> &Config945     fn config(&self) -> &Config {
946         &self.config
947     }
948 }
949 
950 impl All for AllUsers {}
951 
952 impl Debug for AllUsers {
fmt(&self, f: &mut fmt::Formatter) -> fmt::Result953     fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
954         write!(f, "AllUsers {{\nusers: {:?}\n}}", self.users)
955     }
956 }
957 
958 /// [`AllGroups`](struct.AllGroups.html) provides
959 /// (borrowed) access to all groups on the system. Note that this
960 /// struct implements [`All`](trait.All.html), for a bunch of convenience
961 /// functions.
962 ///
963 /// General notes that also apply to this struct may be found with
964 /// [`AllUsers`](struct.AllUsers.html).
965 pub struct AllGroups {
966     groups: Vec<Group>,
967     config: Config,
968 }
969 
970 impl AllGroups {
971     /// Create a new `AllGroups`.
972     //TODO: Indicate if parsing an individual line failed or not
new(config: Config) -> Result<AllGroups>973     pub fn new(config: Config) -> Result<AllGroups> {
974         let group_cntnt = read_locked_file(config.in_scheme(GROUP_FILE))?;
975 
976         let mut entries: Vec<Group> = Vec::new();
977         for line in group_cntnt.lines() {
978             if let Ok(group) = Group::from_str(line) {
979                 entries.push(group);
980             }
981         }
982 
983         Ok(AllGroups {
984             groups: entries,
985             config,
986         })
987     }
988 
989     /// Adds a group with the specified attributes to this `AllGroups`.
990     ///
991     /// This function is classified as a mutating operation,
992     /// and users must therefore call [`save`](struct.AllGroups.html#method.save)
993     /// in order for the new group to be applied to the system.
994     //TODO: Take Option<usize> for gid and find unused ID if None
add_group( &mut self, name: &str, gid: usize, users: &[&str] ) -> Result<()>995     pub fn add_group(
996         &mut self,
997         name: &str,
998         gid: usize,
999         users: &[&str]
1000     ) -> Result<()> {
1001         if self.iter()
1002             .any(|group| group.group == name || group.gid == gid)
1003         {
1004             return Err(From::from(UsersError::AlreadyExists))
1005         }
1006 
1007         //Might be cleaner... Also breaks...
1008         //users: users.iter().map(String::to_string).collect()
1009         self.groups.push(Group {
1010             group: name.into(),
1011             gid,
1012             users: users
1013                 .iter()
1014                 .map(|user| user.to_string())
1015                 .collect()
1016         });
1017 
1018         Ok(())
1019     }
1020 
1021     /// Syncs the data stored in this `AllGroups` instance to the filesystem.
1022     /// To apply changes from an `AllGroups`, you MUST call this function!
save(&self) -> Result<()>1023     pub fn save(&self) -> Result<()> {
1024         let mut groupstring = String::new();
1025         for group in &self.groups {
1026             groupstring.push_str(&format!("{}\n", group.to_string().as_str()));
1027         }
1028 
1029         write_locked_file(self.config.in_scheme(GROUP_FILE), groupstring)
1030     }
1031 }
1032 
1033 impl AllInner for AllGroups {
1034     type Gruser = Group;
1035 
list(&self) -> &Vec<Self::Gruser>1036     fn list(&self) -> &Vec<Self::Gruser> {
1037         &self.groups
1038     }
1039 
list_mut(&mut self) -> &mut Vec<Self::Gruser>1040     fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1041         &mut self.groups
1042     }
1043 
config(&self) -> &Config1044     fn config(&self) -> &Config {
1045         &self.config
1046     }
1047 }
1048 
1049 impl All for AllGroups {}
1050 
1051 #[cfg(test)]
1052 mod test {
1053     use super::*;
1054 
1055     const TEST_PREFIX: &'static str = "tests";
1056 
1057     /// Needed for the file checks, this is done by the library
test_prefix(filename: &str) -> String1058     fn test_prefix(filename: &str) -> String {
1059         let mut complete = String::from(TEST_PREFIX);
1060         complete.push_str(filename);
1061         complete
1062     }
1063 
test_cfg() -> Config1064     fn test_cfg() -> Config {
1065         Config::default()
1066             // Since all this really does is prepend `sheme` to the consts
1067             .scheme(TEST_PREFIX.to_string())
1068     }
1069 
test_auth_cfg() -> Config1070     fn test_auth_cfg() -> Config {
1071         test_cfg().auth(true)
1072     }
1073 
1074     // *** struct.User ***
1075     #[cfg(feature = "auth")]
1076     #[test]
1077     #[should_panic(expected = "Hash not populated!")]
wrong_attempt_set_password()1078     fn wrong_attempt_set_password() {
1079         let mut users = AllUsers::new(test_cfg()).unwrap();
1080         let user = users.get_mut_by_id(1000).unwrap();
1081         user.set_passwd("").unwrap();
1082     }
1083 
1084     #[test]
1085     #[should_panic(expected = "Hash not populated!")]
wrong_attempt_unset_password()1086     fn wrong_attempt_unset_password() {
1087         let mut users = AllUsers::new(test_cfg()).unwrap();
1088         let user = users.get_mut_by_id(1000).unwrap();
1089         user.unset_passwd();
1090     }
1091 
1092     #[cfg(feature = "auth")]
1093     #[test]
1094     #[should_panic(expected = "Hash not populated!")]
wrong_attempt_verify_password()1095     fn wrong_attempt_verify_password() {
1096         let mut users = AllUsers::new(test_cfg()).unwrap();
1097         let user = users.get_mut_by_id(1000).unwrap();
1098         user.verify_passwd("hi folks");
1099     }
1100 
1101     #[test]
1102     #[should_panic(expected = "Hash not populated!")]
wrong_attempt_is_password_blank()1103     fn wrong_attempt_is_password_blank() {
1104         let mut users = AllUsers::new(test_cfg()).unwrap();
1105         let user = users.get_mut_by_id(1000).unwrap();
1106         user.is_passwd_blank();
1107     }
1108 
1109     #[test]
1110     #[should_panic(expected = "Hash not populated!")]
wrong_attempt_is_password_unset()1111     fn wrong_attempt_is_password_unset() {
1112         let mut users = AllUsers::new(test_cfg()).unwrap();
1113         let user = users.get_mut_by_id(1000).unwrap();
1114         user.is_passwd_unset();
1115     }
1116 
1117     #[cfg(feature = "auth")]
1118     #[test]
attempt_user_api()1119     fn attempt_user_api() {
1120         let mut users = AllUsers::new(test_auth_cfg()).unwrap();
1121         let user = users.get_mut_by_id(1000).unwrap();
1122 
1123         assert_eq!(user.is_passwd_blank(), true);
1124         assert_eq!(user.is_passwd_unset(), false);
1125         assert_eq!(user.verify_passwd(""), true);
1126         assert_eq!(user.verify_passwd("Something"), false);
1127 
1128         user.set_passwd("hi,i_am_passwd").unwrap();
1129 
1130         assert_eq!(user.is_passwd_blank(), false);
1131         assert_eq!(user.is_passwd_unset(), false);
1132         assert_eq!(user.verify_passwd(""), false);
1133         assert_eq!(user.verify_passwd("Something"), false);
1134         assert_eq!(user.verify_passwd("hi,i_am_passwd"), true);
1135 
1136         user.unset_passwd();
1137 
1138         assert_eq!(user.is_passwd_blank(), false);
1139         assert_eq!(user.is_passwd_unset(), true);
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"), false);
1143 
1144         user.set_passwd("").unwrap();
1145 
1146         assert_eq!(user.is_passwd_blank(), true);
1147         assert_eq!(user.is_passwd_unset(), false);
1148         assert_eq!(user.verify_passwd(""), true);
1149         assert_eq!(user.verify_passwd("Something"), false);
1150     }
1151 
1152     // *** struct.AllUsers ***
1153     #[test]
get_user()1154     fn get_user() {
1155         let users = AllUsers::new(test_auth_cfg()).unwrap();
1156 
1157         let root = users.get_by_id(0).expect("'root' user missing");
1158         assert_eq!(root.user, "root".to_string());
1159         let &(ref hashstring, ref encoded) = root.hash.as_ref().expect("'root' hash is None");
1160         assert_eq!(hashstring,
1161             &"$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk".to_string());
1162         assert_eq!(root.uid, 0);
1163         assert_eq!(root.gid, 0);
1164         assert_eq!(root.name, "root".to_string());
1165         assert_eq!(root.home, "file:/root".to_string());
1166         assert_eq!(root.shell, "file:/bin/ion".to_string());
1167         match encoded {
1168             true => (),
1169             false => panic!("Expected encoded argon hash!")
1170         }
1171 
1172         let user = users.get_by_name("user").expect("'user' user missing");
1173         assert_eq!(user.user, "user".to_string());
1174         let &(ref hashstring, ref encoded) = user.hash.as_ref().expect("'user' hash is None");
1175         assert_eq!(hashstring, &"".to_string());
1176         assert_eq!(user.uid, 1000);
1177         assert_eq!(user.gid, 1000);
1178         assert_eq!(user.name, "user".to_string());
1179         assert_eq!(user.home, "file:/home/user".to_string());
1180         assert_eq!(user.shell, "file:/bin/ion".to_string());
1181         match encoded {
1182             true => panic!("Should not be an argon hash!"),
1183             false => ()
1184         }
1185         println!("{:?}", users);
1186 
1187         let li = users.get_by_name("li").expect("'li' user missing");
1188         println!("got li");
1189         assert_eq!(li.user, "li");
1190         let &(ref hashstring, ref encoded) = li.hash.as_ref().expect("'li' hash is None");
1191         assert_eq!(hashstring, &"!".to_string());
1192         assert_eq!(li.uid, 1007);
1193         assert_eq!(li.gid, 1007);
1194         assert_eq!(li.name, "Lorem".to_string());
1195         assert_eq!(li.home, "file:/home/lorem".to_string());
1196         assert_eq!(li.shell, "file:/bin/ion".to_string());
1197         match encoded {
1198             true => panic!("Should not be an argon hash!"),
1199             false => ()
1200         }
1201     }
1202 
1203     #[cfg(feature = "auth")]
1204     #[test]
manip_user()1205     fn manip_user() {
1206         let mut users = AllUsers::new(test_auth_cfg()).unwrap();
1207         // NOT testing `get_unique_id`
1208         let id = 7099;
1209         users
1210             .add_user("fb", id, id, "FooBar", "/home/foob", "/bin/zsh")
1211             .expect("failed to add user 'fb'");
1212         //                                            weirdo ^^^^^^^^ :P
1213         users.save().unwrap();
1214         let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1215         assert_eq!(
1216             p_file_content,
1217             concat!(
1218                 "root;0;0;root;file:/root;file:/bin/ion\n",
1219                 "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1220                 "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
1221                 "fb;7099;7099;FooBar;/home/foob;/bin/zsh\n"
1222             )
1223         );
1224         let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap();
1225         assert_eq!(s_file_content, concat!(
1226             "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n",
1227             "user;\n",
1228             "li;!\n",
1229             "fb;!\n"
1230         ));
1231 
1232         {
1233             println!("{:?}", users);
1234             let fb = users.get_mut_by_name("fb").expect("'fb' user missing");
1235             fb.shell = "/bin/fish".to_string(); // That's better
1236             fb.set_passwd("").unwrap();
1237         }
1238         users.save().unwrap();
1239         let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1240         assert_eq!(
1241             p_file_content,
1242             concat!(
1243                 "root;0;0;root;file:/root;file:/bin/ion\n",
1244                 "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1245                 "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n",
1246                 "fb;7099;7099;FooBar;/home/foob;/bin/fish\n"
1247             )
1248         );
1249         let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap();
1250         assert_eq!(s_file_content, concat!(
1251             "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n",
1252             "user;\n",
1253             "li;!\n",
1254             "fb;\n"
1255         ));
1256 
1257         users.remove_by_id(id);
1258         users.save().unwrap();
1259         let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap();
1260         assert_eq!(
1261             file_content,
1262             concat!(
1263                 "root;0;0;root;file:/root;file:/bin/ion\n",
1264                 "user;1000;1000;user;file:/home/user;file:/bin/ion\n",
1265                 "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n"
1266             )
1267         );
1268     }
1269 
1270     #[test]
get_group()1271     fn get_group() {
1272         let groups = AllGroups::new(test_cfg()).unwrap();
1273         let user = groups.get_by_name("user").unwrap();
1274         assert_eq!(user.group, "user");
1275         assert_eq!(user.gid, 1000);
1276         assert_eq!(user.users, vec!["user"]);
1277 
1278         let wheel = groups.get_by_id(1).unwrap();
1279         assert_eq!(wheel.group, "wheel");
1280         assert_eq!(wheel.gid, 1);
1281         assert_eq!(wheel.users, vec!["user", "root"]);
1282     }
1283 
1284     #[test]
manip_group()1285     fn manip_group() {
1286         let mut groups = AllGroups::new(test_cfg()).unwrap();
1287         // NOT testing `get_unique_id`
1288         let id = 7099;
1289 
1290         groups.add_group("fb", id, &["fb"]).unwrap();
1291         groups.save().unwrap();
1292         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1293         assert_eq!(
1294             file_content,
1295             concat!(
1296                 "root;0;root\n",
1297                 "user;1000;user\n",
1298                 "wheel;1;user,root\n",
1299                 "li;1007;li\n",
1300                 "fb;7099;fb\n"
1301             )
1302         );
1303 
1304         {
1305             let fb = groups.get_mut_by_name("fb").unwrap();
1306             fb.users.push("user".to_string());
1307         }
1308         groups.save().unwrap();
1309         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1310         assert_eq!(
1311             file_content,
1312             concat!(
1313                 "root;0;root\n",
1314                 "user;1000;user\n",
1315                 "wheel;1;user,root\n",
1316                 "li;1007;li\n",
1317                 "fb;7099;fb,user\n"
1318             )
1319         );
1320 
1321         groups.remove_by_id(id);
1322         groups.save().unwrap();
1323         let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap();
1324         assert_eq!(
1325             file_content,
1326             concat!(
1327                 "root;0;root\n",
1328                 "user;1000;user\n",
1329                 "wheel;1;user,root\n",
1330                 "li;1007;li\n"
1331             )
1332         );
1333     }
1334 
1335     // *** Misc ***
1336     #[test]
users_get_unused_ids()1337     fn users_get_unused_ids() {
1338         let users = AllUsers::new(test_cfg()).unwrap();
1339         let id = users.get_unique_id().unwrap();
1340         if id < users.config.min_id || id > users.config.max_id {
1341             panic!("User ID is not between allowed margins")
1342         } else if let Some(_) = users.get_by_id(id) {
1343             panic!("User ID is used!");
1344         }
1345     }
1346 
1347     #[test]
groups_get_unused_ids()1348     fn groups_get_unused_ids() {
1349         let groups = AllGroups::new(test_cfg()).unwrap();
1350         let id = groups.get_unique_id().unwrap();
1351         if id < groups.config.min_id || id > groups.config.max_id {
1352             panic!("Group ID is not between allowed margins")
1353         } else if let Some(_) = groups.get_by_id(id) {
1354             panic!("Group ID is used!");
1355         }
1356     }
1357 }
1358