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