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