]> git.proxmox.com Git - rustc.git/blame - vendor/redox_users/src/lib.rs
Merge tag 'debian/1.52.1+dfsg1-1_exp2' into proxmox/buster
[rustc.git] / vendor / redox_users / src / lib.rs
CommitLineData
f20569fa
XL
1//! `redox-users` is designed to be a small, low-ish level interface
2//! to system user and group information, as well as user password
3//! authentication. It is OS-specific and will break horribly on platforms
4//! that are not [Redox-OS](https://redox-os.org).
5//!
6//! # Permissions
7//! Because this is a system level tool dealing with password
8//! authentication, programs are often required to run with
9//! escalated priveleges. The implementation of the crate is
10//! privelege unaware. The only privelege requirements are those
11//! laid down by the system administrator over these files:
12//! - `/etc/group`
13//! - Read: Required to access group information
14//! - Write: Required to change group information
15//! - `/etc/passwd`
16//! - Read: Required to access user information
17//! - Write: Required to change user information
18//! - `/etc/shadow`
19//! - Read: Required to authenticate users
20//! - Write: Required to set user passwords
21//!
22//! # Reimplementation
23//! This crate is designed to be as small as possible without
24//! sacrificing critical functionality. The idea is that a small
25//! enough redox-users will allow easy re-implementation based on
26//! the same flexible API. This would allow more complicated authentication
27//! schemes for redox in future without breakage of existing
28//! software.
29
30use std::convert::From;
31use std::error::Error;
32use std::fmt::{self, Debug};
33use std::fs::{File, OpenOptions};
34use std::io::{Read, Seek, SeekFrom, Write};
35use std::marker::PhantomData;
36#[cfg(target_os = "redox")]
37use std::os::unix::fs::OpenOptionsExt;
38#[cfg(not(target_os = "redox"))]
39use std::os::unix::io::AsRawFd;
40use std::os::unix::process::CommandExt;
41use std::path::{Path, PathBuf};
42use std::process::Command;
43use std::slice::{Iter, IterMut};
44#[cfg(not(test))]
45#[cfg(feature = "auth")]
46use std::thread;
47use std::time::Duration;
48
49//#[cfg(not(target_os = "redox"))]
50//use nix::fcntl::{flock, FlockArg};
51
52#[cfg(target_os = "redox")]
53use syscall::flag::{O_EXLOCK, O_SHLOCK};
54use syscall::Error as SyscallError;
55
56const PASSWD_FILE: &'static str = "/etc/passwd";
57const GROUP_FILE: &'static str = "/etc/group";
58#[cfg(feature = "auth")]
59const SHADOW_FILE: &'static str = "/etc/shadow";
60
61#[cfg(target_os = "redox")]
62const DEFAULT_SCHEME: &'static str = "file:";
63#[cfg(not(target_os = "redox"))]
64const DEFAULT_SCHEME: &'static str = "";
65
66const MIN_ID: usize = 1000;
67const MAX_ID: usize = 6000;
68const DEFAULT_TIMEOUT: u64 = 3;
69
70#[cfg(feature = "auth")]
71const USER_AUTH_FULL_EXPECTED_HASH: &str = "A User<auth::Full> had no hash";
72
73pub 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)]
77pub enum UsersError {
78 Os { reason: String },
79 Parsing { reason: String, line: usize },
80 NotFound,
81 AlreadyExists
82}
83
84impl fmt::Display for UsersError {
85 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
97impl Error for UsersError {
98 fn description(&self) -> &str { "UsersError" }
99
100 fn cause(&self) -> Option<&dyn Error> { None }
101}
102
103#[inline]
104fn parse_error(line: usize, reason: &str) -> UsersError {
105 UsersError::Parsing {
106 reason: reason.into(),
107 line,
108 }
109}
110
111#[inline]
112fn os_error(reason: &str) -> UsersError {
113 UsersError::Os {
114 reason: reason.into()
115 }
116}
117
118impl From<SyscallError> for UsersError {
119 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)]
128enum Lock {
129 Shared,
130 Exclusive,
131}
132
133impl Lock {
134 #[cfg(target_os = "redox")]
135 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)]
153fn 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)
180fn 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`].
187pub 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`].
217pub 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
240impl<A> User<A> {
241 /// Get a Command to run the user's default shell (see [`User::login_cmd`]
242 /// for more docs).
243 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.
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
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`.
313 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")]
323impl 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`.
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`).
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.
358 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).
378 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).
386 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
392 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!!!)
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
412impl<A> Name for User<A> {
413 fn name(&self) -> &str {
414 &self.user
415 }
416}
417
418impl<A> Id for User<A> {
419 fn id(&self) -> usize {
420 self.uid
421 }
422}
423
424impl<A> Debug for User<A> {
425 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)]
441pub 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
450impl Group {
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.
482 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
492impl Name for Group {
493 fn name(&self) -> &str {
494 &self.group
495 }
496}
497
498impl Id for Group {
499 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/// ```
517pub 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/// ```
537pub 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/// ```
557pub 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/// ```
577pub 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)]
604pub struct Config {
605 scheme: String,
606 auth_delay: Duration,
607 min_id: usize,
608 max_id: usize,
609}
610
611impl Config {
612 /// Set the delay for a failed authentication. Default is 3 seconds.
613 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.
619 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.
625 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.
634 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
640 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
653impl 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.
659 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`
671mod sealed {
672 use crate::Config;
673
674 pub trait Name {
675 fn name(&self) -> &str;
676 }
677
678 pub trait Id {
679 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.
688 fn list(&self) -> &Vec<Self::Gruser>;
689 fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
690 fn config(&self) -> &Config;
691 }
692}
693
694use 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.
700pub trait All: AllInner {
701 /// Get an iterator borrowing all [`User`]s or [`Group`]s on the system.
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.
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 /// ```
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`].
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 /// ```
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`].
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 /// ```
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.
778 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`].
796 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)]
826pub 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
835impl<A> 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
857impl 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`.
860 pub fn basic(config: Config) -> Result<AllUsers<auth::Basic>> {
861 Self::new(config)
862 }
863}
864
865#[cfg(feature = "auth")]
866impl AllUsers<auth::Full> {
867 /// If access to password related methods for the [`User`]s yielded by this
868 /// `AllUsers` is required, use this constructor.
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.
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!
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
958impl<A> AllInner for AllUsers<A> {
959 type Gruser = User<A>;
960
961 fn list(&self) -> &Vec<Self::Gruser> {
962 &self.users
963 }
964
965 fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
966 &mut self.users
967 }
968
969 fn config(&self) -> &Config {
970 &self.config
971 }
972}
973
974impl<A> All for AllUsers<A> {}
975/*
976#[cfg(not(target_os = "redox"))]
977impl<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)]
994pub struct AllGroups {
995 groups: Vec<Group>,
996 config: Config,
997
998 group_fd: File,
999}
1000
1001impl AllGroups {
1002 /// Create a new `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
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!
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
1066impl AllInner for AllGroups {
1067 type Gruser = Group;
1068
1069 fn list(&self) -> &Vec<Self::Gruser> {
1070 &self.groups
1071 }
1072
1073 fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1074 &mut self.groups
1075 }
1076
1077 fn config(&self) -> &Config {
1078 &self.config
1079 }
1080}
1081
1082impl All for AllGroups {}
1083/*
1084#[cfg(not(target_os = "redox"))]
1085impl 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)]
1093mod 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
1099 fn test_prefix(filename: &str) -> String {
1100 let mut complete = String::from(TEST_PREFIX);
1101 complete.push_str(filename);
1102 complete
1103 }
1104
1105 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
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
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]
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]
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]
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]
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]
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]
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]
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]
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]
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}