diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-28 14:29:10 +0000 |
commit | 2aa4a82499d4becd2284cdb482213d541b8804dd (patch) | |
tree | b80bf8bf13c3766139fbacc530efd0dd9d54394c /third_party/rust/redox_users | |
parent | Initial commit. (diff) | |
download | firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.tar.xz firefox-2aa4a82499d4becd2284cdb482213d541b8804dd.zip |
Adding upstream version 86.0.1.upstream/86.0.1upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'third_party/rust/redox_users')
-rw-r--r-- | third_party/rust/redox_users/.cargo-checksum.json | 1 | ||||
-rw-r--r-- | third_party/rust/redox_users/Cargo.toml | 30 | ||||
-rw-r--r-- | third_party/rust/redox_users/LICENSE | 22 | ||||
-rw-r--r-- | third_party/rust/redox_users/README.md | 23 | ||||
-rw-r--r-- | third_party/rust/redox_users/src/lib.rs | 1349 | ||||
-rw-r--r-- | third_party/rust/redox_users/tests/etc/group | 4 | ||||
-rw-r--r-- | third_party/rust/redox_users/tests/etc/passwd | 3 | ||||
-rw-r--r-- | third_party/rust/redox_users/tests/etc/shadow | 3 |
8 files changed, 1435 insertions, 0 deletions
diff --git a/third_party/rust/redox_users/.cargo-checksum.json b/third_party/rust/redox_users/.cargo-checksum.json new file mode 100644 index 0000000000..bac668a6d0 --- /dev/null +++ b/third_party/rust/redox_users/.cargo-checksum.json @@ -0,0 +1 @@ +{"files":{"Cargo.toml":"da161acc152b9cdd6c483c8665b49efbefd1a55749ee1ef800385d2c8ddb3919","LICENSE":"e6a8ae2d796083783efc94b1e66271aa2929dc4dfb231d34239aa9c7db8396db","README.md":"9c3a6fd2a798bd1e105c5ee72f7b475471e3a9abec89b5be2b5263d654085d22","src/lib.rs":"3af9f7cc0c9da8afb6c7b9c7f4d36a7650d478e9b6bdfb899a968dea4b33e122","tests/etc/group":"175d89e8d03e2976d3f37110050372163fcafd07a20c899ade3a9690b2cb4526","tests/etc/passwd":"aee0d4bd2abf55846683cc2e5daaa03641636a262519623d59b1ab8e1eb1db32","tests/etc/shadow":"ca7c1a6f96eaef3bd26da4faeae55e78cad04822c191e3263b539ee687de4d0a"},"package":"24c700446b49d2003eaff378ab580c16aa2b06e12ca485a30d319a3b03fbb557"}
\ No newline at end of file diff --git a/third_party/rust/redox_users/Cargo.toml b/third_party/rust/redox_users/Cargo.toml new file mode 100644 index 0000000000..fc469a8d98 --- /dev/null +++ b/third_party/rust/redox_users/Cargo.toml @@ -0,0 +1,30 @@ +# THIS FILE IS AUTOMATICALLY GENERATED BY CARGO +# +# When uploading crates to the registry Cargo will automatically +# "normalize" Cargo.toml files for maximal compatibility +# with all versions of Cargo and also rewrite `path` dependencies +# to registry (e.g., crates.io) dependencies +# +# If you believe there's an error in this file please file an +# issue against the rust-lang/cargo repository. If you're +# editing this file be aware that the upstream Cargo.toml +# will likely look very different (and much more reasonable) + +[package] +name = "redox_users" +version = "0.3.2" +authors = ["Jose Narvaez <goyox86@gmail.com>", "Wesley Hershberger <mggmugginsmc@gmail.com>"] +description = "A Rust library to access Redox users and groups functionality" +documentation = "https://docs.rs/redox_users" +readme = "README.md" +keywords = ["redox", "auth"] +license = "MIT" +repository = "https://gitlab.redox-os.org/redox-os/users" +[dependencies.getrandom] +version = "0.1" + +[dependencies.redox_syscall] +version = "0.1" + +[dependencies.rust-argon2] +version = "0.5" diff --git a/third_party/rust/redox_users/LICENSE b/third_party/rust/redox_users/LICENSE new file mode 100644 index 0000000000..643ad0518e --- /dev/null +++ b/third_party/rust/redox_users/LICENSE @@ -0,0 +1,22 @@ +The MIT License (MIT) + +Copyright (c) 2017 Jose Narvaez + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. + diff --git a/third_party/rust/redox_users/README.md b/third_party/rust/redox_users/README.md new file mode 100644 index 0000000000..ccb5d7965c --- /dev/null +++ b/third_party/rust/redox_users/README.md @@ -0,0 +1,23 @@ +# redox_users <a href="https://crates.io/crates/redox_users"><img src="https://img.shields.io/crates/v/redox_users.svg"></a> + +Redox OS APIs for accessing users and groups information. [Documentation](https://docs.rs/redox_users/0.1.0/redox_users/) + +High level APIs for: + +- Getting the current process effective user ID. +- Getting the current process user ID. +- Getting the current process effective group ID. +- Getting the current process group ID. +- Manipulating User and Group information (including adding, removing, and modifying groups and users, in addition to other functionality, see docs) + +We recommend to use these APIs instead of directly manipulating the +`/etc/group` and `/etc/passwd` as this is an implementation detail and +might change in the future. + +Note that redox_users is an API designed only for use on Redox. It compiles on other platforms (for testing), but it will not work and might produce unexpected behavior. + +## Hashing +redox_users uses the Argon2 hashing algorithm. The default hashing parameters are as follows: +```Rust +Argon2::new(10, 1, 4096, Variant::Argon2i) +``` diff --git a/third_party/rust/redox_users/src/lib.rs b/third_party/rust/redox_users/src/lib.rs new file mode 100644 index 0000000000..6c77f127df --- /dev/null +++ b/third_party/rust/redox_users/src/lib.rs @@ -0,0 +1,1349 @@ +//! `redox-users` is designed to be a small, low-ish level interface +//! to system user and group information, as well as user password +//! authentication. +//! +//! # Permissions +//! Because this is a system level tool dealing with password +//! authentication, programs are often required to run with +//! escalated priveleges. The implementation of the crate is +//! privelege unaware. The only privelege requirements are those +//! laid down by the system administrator over these files: +//! - `/etc/group` +//! - Read: Required to access group information +//! - Write: Required to change group information +//! - `/etc/passwd` +//! - Read: Required to access user information +//! - Write: Required to change user information +//! - `/etc/shadow` +//! - Read: Required to authenticate users +//! - Write: Required to set user passwords +//! +//! # Reimplementation +//! This crate is designed to be as small as possible without +//! sacrificing critical functionality. The idea is that a small +//! enough redox-users will allow easy re-implementation based on +//! the same flexible API. This would allow more complicated authentication +//! schemes for redox in future without breakage of existing +//! software. + +extern crate argon2; +extern crate getrandom; +extern crate syscall; + +use std::convert::From; +use std::error::Error; +use std::fmt::{self, Debug, Display}; +use std::fs::OpenOptions; +use std::io::{Read, Write}; +#[cfg(target_os = "redox")] +use std::os::unix::fs::OpenOptionsExt; +use std::os::unix::process::CommandExt; +use std::path::{Path, PathBuf}; +use std::process::Command; +use std::slice::{Iter, IterMut}; +use std::str::FromStr; +#[cfg(not(test))] +use std::thread; +use std::time::Duration; + +#[cfg(target_os = "redox")] +use syscall::flag::{O_EXLOCK, O_SHLOCK}; +use syscall::Error as SyscallError; + +const PASSWD_FILE: &'static str = "/etc/passwd"; +const GROUP_FILE: &'static str = "/etc/group"; +const SHADOW_FILE: &'static str = "/etc/shadow"; + +#[cfg(target_os = "redox")] +const DEFAULT_SCHEME: &'static str = "file:"; +#[cfg(not(target_os = "redox"))] +const DEFAULT_SCHEME: &'static str = ""; + +const MIN_ID: usize = 1000; +const MAX_ID: usize = 6000; +const DEFAULT_TIMEOUT: u64 = 3; + +pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>; + +/// Errors that might happen while using this crate +#[derive(Debug, PartialEq)] +pub enum UsersError { + Os { reason: String }, + Parsing { reason: String }, + NotFound, + AlreadyExists +} + +impl fmt::Display for UsersError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + UsersError::Os { reason } => write!(f, "os error: code {}", reason), + UsersError::Parsing { reason } => { + write!(f, "parse error: {}", reason) + }, + UsersError::NotFound => write!(f, "user/group not found"), + UsersError::AlreadyExists => write!(f, "user/group already exists") + } + } +} + +impl Error for UsersError { + fn description(&self) -> &str { "UsersError" } + + fn cause(&self) -> Option<&dyn Error> { None } +} + +#[inline] +fn parse_error(reason: &str) -> UsersError { + UsersError::Parsing { + reason: reason.into() + } +} + +#[inline] +fn os_error(reason: &str) -> UsersError { + UsersError::Os { + reason: reason.into() + } +} + +impl From<SyscallError> for UsersError { + fn from(syscall_error: SyscallError) -> UsersError { + UsersError::Os { + reason: format!("{}", syscall_error) + } + } +} + +fn read_locked_file(file: impl AsRef<Path>) -> Result<String> { + #[cfg(test)] + println!("Reading file: {}", file.as_ref().display()); + + #[cfg(target_os = "redox")] + let mut file = OpenOptions::new() + .read(true) + .custom_flags(O_SHLOCK as i32) + .open(file)?; + #[cfg(not(target_os = "redox"))] + #[cfg_attr(rustfmt, rustfmt_skip)] + let mut file = OpenOptions::new() + .read(true) + .open(file)?; + + let len = file.metadata()?.len(); + let mut file_data = String::with_capacity(len as usize); + file.read_to_string(&mut file_data)?; + Ok(file_data) +} + +fn write_locked_file(file: impl AsRef<Path>, data: String) -> Result<()> { + #[cfg(test)] + println!("Writing file: {}", file.as_ref().display()); + + #[cfg(target_os = "redox")] + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .custom_flags(O_EXLOCK as i32) + .open(file)?; + #[cfg(not(target_os = "redox"))] + #[cfg_attr(rustfmt, rustfmt_skip)] + let mut file = OpenOptions::new() + .write(true) + .truncate(true) + .open(file)?; + + file.write(data.as_bytes())?; + Ok(()) +} + +/// A struct representing a Redox user. +/// Currently maps to an entry in the `/etc/passwd` file. +/// +/// # Unset vs. Blank Passwords +/// A note on unset passwords vs. blank passwords. A blank password +/// is a hash field that is completely blank (aka, `""`). According +/// to this crate, successful login is only allowed if the input +/// password is blank as well. +/// +/// An unset password is one whose hash is not empty (`""`), but +/// also not a valid serialized argon2rs hashing session. This +/// hash always returns `false` upon attempted verification. The +/// most commonly used hash for an unset password is `"!"`, but +/// this crate makes no distinction. The most common way to unset +/// the password is to use [`unset_passwd`](struct.User.html#method.unset_passwd). +pub struct User { + /// Username (login name) + pub user: String, + // Hashed password and Argon2 indicator, stored to simplify API + hash: Option<(String, bool)>, + /// User id + pub uid: usize, + /// Group id + pub gid: usize, + /// Real name (GECOS field) + pub name: String, + /// Home directory path + pub home: String, + /// Shell path + pub shell: String, + /// Failed login delay duration + auth_delay: Duration +} + +impl User { + /// Set the password for a user. Make sure the password you have + /// received is actually what the user wants as their password (this doesn't). + /// + /// To set the password blank, use `""` as the password parameter. + /// + /// # Panics + /// If the User's hash fields are unpopulated, this function will `panic!` + /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info). + pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<()> { + self.panic_if_unpopulated(); + let password = password.as_ref(); + + self.hash = if password != "" { + let mut buf = [0u8; 8]; + getrandom::getrandom(&mut buf)?; + let salt = format!("{:X}", u64::from_ne_bytes(buf)); + let config = argon2::Config::default(); + let hash = argon2::hash_encoded( + password.as_bytes(), + salt.as_bytes(), + &config + )?; + Some((hash, true)) + } else { + Some(("".into(), false)) + }; + Ok(()) + } + + /// Unset the password (do not allow logins). + /// + /// # Panics + /// If the User's hash fields are unpopulated, this function will `panic!` + /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info). + pub fn unset_passwd(&mut self) { + self.panic_if_unpopulated(); + self.hash = Some(("!".into(), false)); + } + + /// Verify the password. If the hash is empty, this only + /// returns `true` if the password field is also empty. + /// Note that this is a blocking operation if the password + /// is incorrect. See [`Config::auth_delay`](struct.Config.html#method.auth_delay) + /// to set the wait time. Default is 3 seconds. + /// + /// # Panics + /// If the User's hash fields are unpopulated, this function will `panic!` + /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info). + pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool { + self.panic_if_unpopulated(); + // Safe because it will have panicked already if self.hash.is_none() + let &(ref hash, ref encoded) = self.hash.as_ref().unwrap(); + let password = password.as_ref(); + + let verified = if *encoded { + argon2::verify_encoded(&hash, password.as_bytes()).unwrap() + } else { + hash == "" && password == "" + }; + + if !verified { + #[cfg(not(test))] // Make tests run faster + thread::sleep(self.auth_delay); + } + verified + } + + /// Determine if the hash for the password is blank + /// (any user can log in as this user with no password). + /// + /// # Panics + /// If the User's hash fields are unpopulated, this function will `panic!` + /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info). + pub fn is_passwd_blank(&self) -> bool { + self.panic_if_unpopulated(); + let &(ref hash, ref encoded) = self.hash.as_ref().unwrap(); + hash == "" && ! encoded + } + + /// Determine if the hash for the password is unset + /// ([`verify_passwd`](struct.User.html#method.verify_passwd) + /// returns `false` regardless of input). + /// + /// # Panics + /// If the User's hash fields are unpopulated, this function will `panic!` + /// (see [`AllUsers`](struct.AllUsers.html#shadowfile-handling) for more info). + pub fn is_passwd_unset(&self) -> bool { + self.panic_if_unpopulated(); + let &(ref hash, ref encoded) = self.hash.as_ref().unwrap(); + hash != "" && ! encoded + } + + /// Get a Command to run the user's default shell + /// (see [`login_cmd`](struct.User.html#method.login_cmd) for more docs). + pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) } + + /// Provide a login command for the user, which is any + /// entry point for starting a user's session, whether + /// a shell (use [`shell_cmd`](struct.User.html#method.shell_cmd) instead) or a graphical init. + /// + /// The `Command` will use the user's `uid` and `gid`, its `current_dir` will be + /// set to the user's home directory, and the follwing enviroment variables will + /// be populated: + /// + /// - `USER` set to the user's `user` field. + /// - `UID` set to the user's `uid` field. + /// - `GROUPS` set the user's `gid` field. + /// - `HOME` set to the user's `home` field. + /// - `SHELL` set to the user's `shell` field. + pub fn login_cmd<T>(&self, cmd: T) -> Command + where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str> + { + let mut command = Command::new(cmd); + command + .uid(self.uid as u32) + .gid(self.gid as u32) + .current_dir(&self.home) + .env("USER", &self.user) + .env("UID", format!("{}", self.uid)) + .env("GROUPS", format!("{}", self.gid)) + .env("HOME", &self.home) + .env("SHELL", &self.shell); + command + } + + /// This returns an entry for `/etc/shadow` + /// Will panic! + fn shadowstring(&self) -> String { + self.panic_if_unpopulated(); + let hashstring = match self.hash { + Some((ref hash, _)) => hash, + None => panic!("Shadowfile not read!") + }; + format!("{};{}", self.user, hashstring) + } + + /// Give this a hash string (not a shadowfile entry!!!) + fn populate_hash(&mut self, hash: &str) -> Result<()> { + let encoded = match hash { + "" => false, + "!" => false, + _ => true, + }; + self.hash = Some((hash.to_string(), encoded)); + Ok(()) + } + + #[inline] + fn panic_if_unpopulated(&self) { + if self.hash.is_none() { + panic!("Hash not populated!"); + } + } +} + +impl Name for User { + fn name(&self) -> &str { + &self.user + } +} + +impl Id for User { + fn id(&self) -> usize { + self.uid + } +} + +impl Debug for User { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, + "User {{\n\tuser: {:?}\n\tuid: {:?}\n\tgid: {:?}\n\tname: {:?} + home: {:?}\n\tshell: {:?}\n\tauth_delay: {:?}\n}}", + self.user, self.uid, self.gid, self.name, self.home, self.shell, self.auth_delay + ) + } +} + +impl Display for User { + /// Format this user as an entry in `/etc/passwd`. This + /// is an implementation detail, do NOT rely on this trait + /// being implemented in future. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + #[cfg_attr(rustfmt, rustfmt_skip)] + write!(f, "{};{};{};{};{};{}", + self.user, self.uid, self.gid, self.name, self.home, self.shell + ) + } +} + +impl FromStr for User { + type Err = Box<dyn Error + Send + Sync>; + + /// Parse an entry from `/etc/passwd`. This + /// is an implementation detail, do NOT rely on this trait + /// being implemented in future. + fn from_str(s: &str) -> Result<Self> { + let mut parts = s.split(';'); + + let user = parts + .next() + .ok_or(parse_error("expected user"))?; + let uid = parts + .next() + .ok_or(parse_error("expected uid"))? + .parse::<usize>()?; + let gid = parts + .next() + .ok_or(parse_error("expected uid"))? + .parse::<usize>()?; + let name = parts + .next() + .ok_or(parse_error("expected real name"))?; + let home = parts + .next() + .ok_or(parse_error("expected home dir path"))?; + let shell = parts + .next() + .ok_or(parse_error("expected shell path"))?; + + Ok(User { + user: user.into(), + hash: None, + uid, + gid, + name: name.into(), + home: home.into(), + shell: shell.into(), + auth_delay: Duration::default(), + }) + } +} + +/// A struct representing a Redox user group. +/// Currently maps to an `/etc/group` file entry. +#[derive(Debug)] +pub struct Group { + /// Group name + pub group: String, + /// Unique group id + pub gid: usize, + /// Group members usernames + pub users: Vec<String>, +} + +impl Name for Group { + fn name(&self) -> &str { + &self.group + } +} + +impl Id for Group { + fn id(&self) -> usize { + self.gid + } +} + +impl Display for Group { + /// Format this group as an entry in `/etc/group`. This + /// is an implementation detail, do NOT rely on this trait + /// being implemented in future. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + #[cfg_attr(rustfmt, rustfmt_skip)] + write!(f, "{};{};{}", + self.group, + self.gid, + self.users.join(",").trim_matches(',') + ) + } +} + +impl FromStr for Group { + type Err = Box<dyn Error + Send + Sync>; + + /// Parse an entry from `/etc/group`. This + /// is an implementation detail, do NOT rely on this trait + /// being implemented in future. + fn from_str(s: &str) -> Result<Self> { + let mut parts = s.split(';'); + + let group = parts + .next() + .ok_or(parse_error("expected group"))?; + let gid = parts + .next() + .ok_or(parse_error("expected gid"))? + .parse::<usize>()?; + //Allow for an empty users field. If there is a better way to do this, do it + let users_str = parts.next().unwrap_or(" "); + let users = users_str.split(',').map(|u| u.into()).collect(); + + Ok(Group { + group: group.into(), + gid, + users, + }) + } +} + +/// Gets the current process effective user ID. +/// +/// This function issues the `geteuid` system call returning the process effective +/// user id. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use redox_users::get_euid; +/// let euid = get_euid().unwrap(); +/// ``` +pub fn get_euid() -> Result<usize> { + match syscall::geteuid() { + Ok(euid) => Ok(euid), + Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) + } +} + +/// Gets the current process real user ID. +/// +/// This function issues the `getuid` system call returning the process real +/// user id. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use redox_users::get_uid; +/// let uid = get_uid().unwrap(); +/// ``` +pub fn get_uid() -> Result<usize> { + match syscall::getuid() { + Ok(uid) => Ok(uid), + Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) + } +} + +/// Gets the current process effective group ID. +/// +/// This function issues the `getegid` system call returning the process effective +/// group id. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use redox_users::get_egid; +/// let egid = get_egid().unwrap(); +/// ``` +pub fn get_egid() -> Result<usize> { + match syscall::getegid() { + Ok(egid) => Ok(egid), + Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) + } +} + +/// Gets the current process real group ID. +/// +/// This function issues the `getegid` system call returning the process real +/// group id. +/// +/// # Examples +/// +/// Basic usage: +/// +/// ```no_run +/// # use redox_users::get_gid; +/// let gid = get_gid().unwrap(); +/// ``` +pub fn get_gid() -> Result<usize> { + match syscall::getgid() { + Ok(gid) => Ok(gid), + Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) + } +} + +/// A generic configuration that allows better control of +/// `AllUsers` or `AllGroups` than might otherwise be possible. +/// +/// The use of the fields of this struct is completely optional +/// depending on what constructor it is passed to. For example, +/// `AllGroups` doesn't care if auth is enabled or not, or what +/// the duration is. +/// +/// In most situations, `Config::default()` will work just fine. +/// The other methods on this struct are usually for finer control +/// of an `AllUsers` or `AllGroups` if it is required. +#[derive(Clone)] +pub struct Config { + auth_enabled: bool, + scheme: String, + auth_delay: Duration, + min_id: usize, + max_id: usize, +} + +impl Config { + /// An alternative to the default constructor, this indicates that + /// authentication should be enabled. + pub fn with_auth() -> Config { + Config { + auth_enabled: true, + ..Default::default() + } + } + + /// Builder pattern version of `Self::with_auth`. + pub fn auth(mut self, auth: bool) -> Config { + self.auth_enabled = auth; + self + } + + /// Set the delay for a failed authentication. Default is 3 seconds. + pub fn auth_delay(mut self, delay: Duration) -> Config { + self.auth_delay = delay; + self + } + + /// Set the smallest ID possible to use when finding an unused ID. + pub fn min_id(mut self, id: usize) -> Config { + self.min_id = id; + self + } + + /// Set the largest possible ID to use when finding an unused ID. + pub fn max_id(mut self, id: usize) -> Config { + self.max_id = id; + self + } + + /// Set the scheme relative to which the `AllUsers` or `AllGroups` + /// should be looking for its data files. This is a compromise between + /// exposing implementation details and providing fine enough + /// control over the behavior of this API. + pub fn scheme(mut self, scheme: String) -> Config { + self.scheme = scheme; + self + } + + // Prepend a path with the scheme in this Config + fn in_scheme(&self, path: impl AsRef<Path>) -> PathBuf { + let mut canonical_path = PathBuf::from(&self.scheme); + // Should be a little careful here, not sure I want this behavior + if path.as_ref().is_absolute() { + // This is nasty + canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string()); + } else { + canonical_path.push(path); + } + canonical_path + } +} + +impl Default for Config { + /// Authentication is not enabled; The default base scheme is `file`. + fn default() -> Config { + Config { + auth_enabled: false, + scheme: String::from(DEFAULT_SCHEME), + auth_delay: Duration::new(DEFAULT_TIMEOUT, 0), + min_id: MIN_ID, + max_id: MAX_ID, + } + } +} + +// Nasty hack to prevent the compiler complaining about +// "leaking" `AllInner` +mod sealed { + use Config; + + pub trait Name { + fn name(&self) -> &str; + } + + pub trait Id { + fn id(&self) -> usize; + } + + pub trait AllInner { + // Group+User, thanks Dad + type Gruser: Name + Id; + + /// These functions grab internal elements so that the other + /// methods of `All` can manipulate them. + fn list(&self) -> &Vec<Self::Gruser>; + fn list_mut(&mut self) -> &mut Vec<Self::Gruser>; + fn config(&self) -> &Config; + } +} + +use sealed::{AllInner, Id, Name}; + +/// This trait is used to remove repetitive API items from +/// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait +/// so that the implementations of functions can be implemented +/// at the trait level. Do not try to implement this trait. +pub trait All: AllInner { + /// Get an iterator borrowing all [`User`](struct.User.html)'s + /// or [`Group`](struct.Group.html)'s on the system. + fn iter(&self) -> Iter<<Self as AllInner>::Gruser> { + self.list().iter() + } + + /// Get an iterator mutably borrowing all [`User`](struct.User.html)'s + /// or [`Group`](struct.Group.html)'s on the system. + fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> { + self.list_mut().iter_mut() + } + + /// Borrow the [`User`](struct.User.html) or [`Group`](struct.Group.html) + /// with a given name. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use redox_users::{All, AllUsers, Config}; + /// let users = AllUsers::new(Config::default()).unwrap(); + /// let user = users.get_by_name("root").unwrap(); + /// ``` + fn get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser> { + self.iter() + .find(|gruser| gruser.name() == name.as_ref() ) + } + + /// Mutable version of [`get_by_name`](trait.All.html#method.get_by_name). + fn get_mut_by_name(&mut self, name: impl AsRef<str>) -> Option<&mut <Self as AllInner>::Gruser> { + self.iter_mut() + .find(|gruser| gruser.name() == name.as_ref() ) + } + + /// Borrow the [`User`](struct.User.html) or [`Group`](struct.Group.html) + /// with the given ID. + /// + /// # Examples + /// + /// Basic usage: + /// + /// ```no_run + /// # use redox_users::{All, AllUsers, Config}; + /// let users = AllUsers::new(Config::default()).unwrap(); + /// let user = users.get_by_id(0).unwrap(); + /// ``` + fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> { + self.iter() + .find(|gruser| gruser.id() == id ) + } + + /// Mutable version of [`get_by_id`](trait.All.html#method.get_by_id). + fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> { + self.iter_mut() + .find(|gruser| gruser.id() == id ) + } + + /// Provides an unused id based on the min and max values in + /// the [`Config`](struct.Config.html) passed to the `All`'s constructor. + /// + /// # Examples + /// + /// ```no_run + /// # use redox_users::{All, AllUsers, Config}; + /// let users = AllUsers::new(Config::default()).unwrap(); + /// let uid = users.get_unique_id().expect("no available uid"); + /// ``` + fn get_unique_id(&self) -> Option<usize> { + for id in self.config().min_id..self.config().max_id { + if !self.iter().any(|gruser| gruser.id() == id ) { + return Some(id) + } + } + None + } + + /// Remove a [`User`](struct.User.html) or [`Group`](struct.Group.html) + /// from this `All` given it's name. This won't provide an indication + /// of whether the user was removed or not, but is guaranteed to work + /// if a user with the specified name exists. + fn remove_by_name(&mut self, name: impl AsRef<str>) { + // Significantly more elegant than other possible solutions. + // I wish it could indicate if it removed anything. + self.list_mut() + .retain(|gruser| gruser.name() != name.as_ref() ); + } + + /// Id version of [`remove_by_name`](trait.All.html#method.remove_by_name). + fn remove_by_id(&mut self, id: usize) { + self.list_mut() + .retain(|gruser| gruser.id() != id ); + } +} + +/// [`AllUsers`](struct.AllUsers.html) provides +/// (borrowed) access to all the users on the system. +/// Note that this struct implements [`All`](trait.All.html) for +/// a bunch of convenient access functions. +/// +/// # Notes +/// Note that everything in this section also applies to +/// [`AllGroups`](struct.AllGroups.html) +/// +/// * If you mutate anything owned by an `AllUsers`, +/// you must call the [`save`](struct.AllUsers.html#method.save) +/// method in order for those changes to be applied to the system. +/// * The API here is kept small on purpose in order to reduce the +/// surface area for security exploitation. Most mutating actions +/// can be accomplished via the [`get_mut_by_id`](struct.AllUsers.html#method.get_mut_by_id) +/// and [`get_mut_by_name`](struct.AllUsers.html#method.get_mut_by_name) +/// functions. +/// +/// # Shadowfile handling +/// This implementation of redox-users uses a shadowfile implemented primarily +/// by this struct. `AllUsers` respects the `auth_enabled` status of the `Config` +/// that is was passed. If auth is enabled, it populates the +/// hash fields of each user struct that it parses from `/etc/passwd` with +/// info from `/et/shadow`. If a caller attempts to perform an action that +/// requires this info with an `AllUsers` config that does not have auth enabled, +/// the `User` handling action will panic. +pub struct AllUsers { + users: Vec<User>, + config: Config, +} + +impl AllUsers { + /// See [Shadowfile Handling](struct.AllUsers.html#shadowfile-handling) for + /// configuration information regarding this constructor. + //TODO: Indicate if parsing an individual line failed or not + pub fn new(config: Config) -> Result<AllUsers> { + let passwd_cntnt = read_locked_file(config.in_scheme(PASSWD_FILE))?; + + let mut passwd_entries: Vec<User> = Vec::new(); + for line in passwd_cntnt.lines() { + if let Ok(mut user) = User::from_str(line) { + user.auth_delay = config.auth_delay; + passwd_entries.push(user); + } + } + + if config.auth_enabled { + let shadow_cntnt = read_locked_file(config.in_scheme(SHADOW_FILE))?; + let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect(); + for entry in shadow_entries.iter() { + let mut entry = entry.split(';'); + let name = entry.next().ok_or(parse_error( + "error parsing shadowfile: expected username" + ))?; + let hash = entry.next().ok_or(parse_error( + "error parsing shadowfile: expected hash" + ))?; + passwd_entries + .iter_mut() + .find(|user| user.user == name) + .ok_or(parse_error( + "error parsing shadowfile: unkown user" + ))? + .populate_hash(hash)?; + } + } + + Ok(AllUsers { + users: passwd_entries, + config + }) + } + + /// Adds a user with the specified attributes to the `AllUsers` + /// instance. Note that the user's password is set unset (see + /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords)) + /// during this call. + /// + /// This function is classified as a mutating operation, + /// and users must therefore call [`save`](struct.AllUsers.html#method.save) + /// in order for the new user to be applied to the system. + /// + /// # Panics + /// This function will `panic!` if the [`Config`](struct.Config.html) + /// passed to [`AllUsers::new`](struct.AllUsers.html#method.new) + /// does not have authentication enabled (see + /// [`Shadowfile handling`](struct.AllUsers.html#shadowfile-handling)). + //TODO: Take uid/gid as Option<usize> and if none, find an unused ID. + pub fn add_user( + &mut self, + login: &str, + uid: usize, + gid: usize, + name: &str, + home: &str, + shell: &str + ) -> Result<()> { + if self.iter() + .any(|user| user.user == login || user.uid == uid) + { + return Err(From::from(UsersError::AlreadyExists)) + } + + if !self.config.auth_enabled { + panic!("Attempt to create user without access to the shadowfile"); + } + + self.users.push(User { + user: login.into(), + hash: Some(("!".into(), false)), + uid, + gid, + name: name.into(), + home: home.into(), + shell: shell.into(), + auth_delay: self.config.auth_delay + }); + Ok(()) + } + + /// Syncs the data stored in the `AllUsers` instance to the filesystem. + /// To apply changes to the system from an `AllUsers`, you MUST call this function! + pub fn save(&self) -> Result<()> { + let mut userstring = String::new(); + let mut shadowstring = String::new(); + for user in &self.users { + userstring.push_str(&format!("{}\n", user.to_string().as_str())); + if self.config.auth_enabled { + shadowstring.push_str(&format!("{}\n", user.shadowstring())); + } + } + + write_locked_file(self.config.in_scheme(PASSWD_FILE), userstring)?; + if self.config.auth_enabled { + write_locked_file(self.config.in_scheme(SHADOW_FILE), shadowstring)?; + } + Ok(()) + } +} + +impl AllInner for AllUsers { + type Gruser = User; + + fn list(&self) -> &Vec<Self::Gruser> { + &self.users + } + + fn list_mut(&mut self) -> &mut Vec<Self::Gruser> { + &mut self.users + } + + fn config(&self) -> &Config { + &self.config + } +} + +impl All for AllUsers {} + +impl Debug for AllUsers { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "AllUsers {{\nusers: {:?}\n}}", self.users) + } +} + +/// [`AllGroups`](struct.AllGroups.html) provides +/// (borrowed) access to all groups on the system. Note that this +/// struct implements [`All`](trait.All.html), for a bunch of convenience +/// functions. +/// +/// General notes that also apply to this struct may be found with +/// [`AllUsers`](struct.AllUsers.html). +pub struct AllGroups { + groups: Vec<Group>, + config: Config, +} + +impl AllGroups { + /// Create a new `AllGroups`. + //TODO: Indicate if parsing an individual line failed or not + pub fn new(config: Config) -> Result<AllGroups> { + let group_cntnt = read_locked_file(config.in_scheme(GROUP_FILE))?; + + let mut entries: Vec<Group> = Vec::new(); + for line in group_cntnt.lines() { + if let Ok(group) = Group::from_str(line) { + entries.push(group); + } + } + + Ok(AllGroups { + groups: entries, + config, + }) + } + + /// Adds a group with the specified attributes to this `AllGroups`. + /// + /// This function is classified as a mutating operation, + /// and users must therefore call [`save`](struct.AllGroups.html#method.save) + /// in order for the new group to be applied to the system. + //TODO: Take Option<usize> for gid and find unused ID if None + pub fn add_group( + &mut self, + name: &str, + gid: usize, + users: &[&str] + ) -> Result<()> { + if self.iter() + .any(|group| group.group == name || group.gid == gid) + { + return Err(From::from(UsersError::AlreadyExists)) + } + + //Might be cleaner... Also breaks... + //users: users.iter().map(String::to_string).collect() + self.groups.push(Group { + group: name.into(), + gid, + users: users + .iter() + .map(|user| user.to_string()) + .collect() + }); + + Ok(()) + } + + /// Syncs the data stored in this `AllGroups` instance to the filesystem. + /// To apply changes from an `AllGroups`, you MUST call this function! + pub fn save(&self) -> Result<()> { + let mut groupstring = String::new(); + for group in &self.groups { + groupstring.push_str(&format!("{}\n", group.to_string().as_str())); + } + + write_locked_file(self.config.in_scheme(GROUP_FILE), groupstring) + } +} + +impl AllInner for AllGroups { + type Gruser = Group; + + fn list(&self) -> &Vec<Self::Gruser> { + &self.groups + } + + fn list_mut(&mut self) -> &mut Vec<Self::Gruser> { + &mut self.groups + } + + fn config(&self) -> &Config { + &self.config + } +} + +impl All for AllGroups {} + +#[cfg(test)] +mod test { + use super::*; + + const TEST_PREFIX: &'static str = "tests"; + + /// Needed for the file checks, this is done by the library + fn test_prefix(filename: &str) -> String { + let mut complete = String::from(TEST_PREFIX); + complete.push_str(filename); + complete + } + + fn test_cfg() -> Config { + Config::default() + // Since all this really does is prepend `sheme` to the consts + .scheme(TEST_PREFIX.to_string()) + } + + fn test_auth_cfg() -> Config { + test_cfg().auth(true) + } + + // *** struct.User *** + #[test] + #[should_panic(expected = "Hash not populated!")] + fn wrong_attempt_set_password() { + let mut users = AllUsers::new(test_cfg()).unwrap(); + let user = users.get_mut_by_id(1000).unwrap(); + user.set_passwd("").unwrap(); + } + + #[test] + #[should_panic(expected = "Hash not populated!")] + fn wrong_attempt_unset_password() { + let mut users = AllUsers::new(test_cfg()).unwrap(); + let user = users.get_mut_by_id(1000).unwrap(); + user.unset_passwd(); + } + + #[test] + #[should_panic(expected = "Hash not populated!")] + fn wrong_attempt_verify_password() { + let mut users = AllUsers::new(test_cfg()).unwrap(); + let user = users.get_mut_by_id(1000).unwrap(); + user.verify_passwd("hi folks"); + } + + #[test] + #[should_panic(expected = "Hash not populated!")] + fn wrong_attempt_is_password_blank() { + let mut users = AllUsers::new(test_cfg()).unwrap(); + let user = users.get_mut_by_id(1000).unwrap(); + user.is_passwd_blank(); + } + + #[test] + #[should_panic(expected = "Hash not populated!")] + fn wrong_attempt_is_password_unset() { + let mut users = AllUsers::new(test_cfg()).unwrap(); + let user = users.get_mut_by_id(1000).unwrap(); + user.is_passwd_unset(); + } + + #[test] + fn attempt_user_api() { + let mut users = AllUsers::new(test_auth_cfg()).unwrap(); + let user = users.get_mut_by_id(1000).unwrap(); + + assert_eq!(user.is_passwd_blank(), true); + assert_eq!(user.is_passwd_unset(), false); + assert_eq!(user.verify_passwd(""), true); + assert_eq!(user.verify_passwd("Something"), false); + + user.set_passwd("hi,i_am_passwd").unwrap(); + + assert_eq!(user.is_passwd_blank(), false); + assert_eq!(user.is_passwd_unset(), false); + assert_eq!(user.verify_passwd(""), false); + assert_eq!(user.verify_passwd("Something"), false); + assert_eq!(user.verify_passwd("hi,i_am_passwd"), true); + + user.unset_passwd(); + + assert_eq!(user.is_passwd_blank(), false); + assert_eq!(user.is_passwd_unset(), true); + assert_eq!(user.verify_passwd(""), false); + assert_eq!(user.verify_passwd("Something"), false); + assert_eq!(user.verify_passwd("hi,i_am_passwd"), false); + + user.set_passwd("").unwrap(); + + assert_eq!(user.is_passwd_blank(), true); + assert_eq!(user.is_passwd_unset(), false); + assert_eq!(user.verify_passwd(""), true); + assert_eq!(user.verify_passwd("Something"), false); + } + + // *** struct.AllUsers *** + #[test] + fn get_user() { + let users = AllUsers::new(test_auth_cfg()).unwrap(); + + let root = users.get_by_id(0).expect("'root' user missing"); + assert_eq!(root.user, "root".to_string()); + let &(ref hashstring, ref encoded) = root.hash.as_ref().expect("'root' hash is None"); + assert_eq!(hashstring, + &"$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk".to_string()); + assert_eq!(root.uid, 0); + assert_eq!(root.gid, 0); + assert_eq!(root.name, "root".to_string()); + assert_eq!(root.home, "file:/root".to_string()); + assert_eq!(root.shell, "file:/bin/ion".to_string()); + match encoded { + true => (), + false => panic!("Expected encoded argon hash!") + } + + let user = users.get_by_name("user").expect("'user' user missing"); + assert_eq!(user.user, "user".to_string()); + let &(ref hashstring, ref encoded) = user.hash.as_ref().expect("'user' hash is None"); + assert_eq!(hashstring, &"".to_string()); + assert_eq!(user.uid, 1000); + assert_eq!(user.gid, 1000); + assert_eq!(user.name, "user".to_string()); + assert_eq!(user.home, "file:/home/user".to_string()); + assert_eq!(user.shell, "file:/bin/ion".to_string()); + match encoded { + true => panic!("Should not be an argon hash!"), + false => () + } + println!("{:?}", users); + + let li = users.get_by_name("li").expect("'li' user missing"); + println!("got li"); + assert_eq!(li.user, "li"); + let &(ref hashstring, ref encoded) = li.hash.as_ref().expect("'li' hash is None"); + assert_eq!(hashstring, &"!".to_string()); + assert_eq!(li.uid, 1007); + assert_eq!(li.gid, 1007); + assert_eq!(li.name, "Lorem".to_string()); + assert_eq!(li.home, "file:/home/lorem".to_string()); + assert_eq!(li.shell, "file:/bin/ion".to_string()); + match encoded { + true => panic!("Should not be an argon hash!"), + false => () + } + } + + #[test] + fn manip_user() { + let mut users = AllUsers::new(test_auth_cfg()).unwrap(); + // NOT testing `get_unique_id` + let id = 7099; + users + .add_user("fb", id, id, "FooBar", "/home/foob", "/bin/zsh") + .expect("failed to add user 'fb'"); + // weirdo ^^^^^^^^ :P + users.save().unwrap(); + let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); + assert_eq!( + p_file_content, + concat!( + "root;0;0;root;file:/root;file:/bin/ion\n", + "user;1000;1000;user;file:/home/user;file:/bin/ion\n", + "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", + "fb;7099;7099;FooBar;/home/foob;/bin/zsh\n" + ) + ); + let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); + assert_eq!(s_file_content, concat!( + "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", + "user;\n", + "li;!\n", + "fb;!\n" + )); + + { + println!("{:?}", users); + let fb = users.get_mut_by_name("fb").expect("'fb' user missing"); + fb.shell = "/bin/fish".to_string(); // That's better + fb.set_passwd("").unwrap(); + } + users.save().unwrap(); + let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); + assert_eq!( + p_file_content, + concat!( + "root;0;0;root;file:/root;file:/bin/ion\n", + "user;1000;1000;user;file:/home/user;file:/bin/ion\n", + "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", + "fb;7099;7099;FooBar;/home/foob;/bin/fish\n" + ) + ); + let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); + assert_eq!(s_file_content, concat!( + "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", + "user;\n", + "li;!\n", + "fb;\n" + )); + + users.remove_by_id(id); + users.save().unwrap(); + let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); + assert_eq!( + file_content, + concat!( + "root;0;0;root;file:/root;file:/bin/ion\n", + "user;1000;1000;user;file:/home/user;file:/bin/ion\n", + "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n" + ) + ); + } + + #[test] + fn get_group() { + let groups = AllGroups::new(test_cfg()).unwrap(); + let user = groups.get_by_name("user").unwrap(); + assert_eq!(user.group, "user"); + assert_eq!(user.gid, 1000); + assert_eq!(user.users, vec!["user"]); + + let wheel = groups.get_by_id(1).unwrap(); + assert_eq!(wheel.group, "wheel"); + assert_eq!(wheel.gid, 1); + assert_eq!(wheel.users, vec!["user", "root"]); + } + + #[test] + fn manip_group() { + let mut groups = AllGroups::new(test_cfg()).unwrap(); + // NOT testing `get_unique_id` + let id = 7099; + + groups.add_group("fb", id, &["fb"]).unwrap(); + groups.save().unwrap(); + let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); + assert_eq!( + file_content, + concat!( + "root;0;root\n", + "user;1000;user\n", + "wheel;1;user,root\n", + "li;1007;li\n", + "fb;7099;fb\n" + ) + ); + + { + let fb = groups.get_mut_by_name("fb").unwrap(); + fb.users.push("user".to_string()); + } + groups.save().unwrap(); + let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); + assert_eq!( + file_content, + concat!( + "root;0;root\n", + "user;1000;user\n", + "wheel;1;user,root\n", + "li;1007;li\n", + "fb;7099;fb,user\n" + ) + ); + + groups.remove_by_id(id); + groups.save().unwrap(); + let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); + assert_eq!( + file_content, + concat!( + "root;0;root\n", + "user;1000;user\n", + "wheel;1;user,root\n", + "li;1007;li\n" + ) + ); + } + + // *** Misc *** + #[test] + fn users_get_unused_ids() { + let users = AllUsers::new(test_cfg()).unwrap(); + let id = users.get_unique_id().unwrap(); + if id < users.config.min_id || id > users.config.max_id { + panic!("User ID is not between allowed margins") + } else if let Some(_) = users.get_by_id(id) { + panic!("User ID is used!"); + } + } + + #[test] + fn groups_get_unused_ids() { + let groups = AllGroups::new(test_cfg()).unwrap(); + let id = groups.get_unique_id().unwrap(); + if id < groups.config.min_id || id > groups.config.max_id { + panic!("Group ID is not between allowed margins") + } else if let Some(_) = groups.get_by_id(id) { + panic!("Group ID is used!"); + } + } +} diff --git a/third_party/rust/redox_users/tests/etc/group b/third_party/rust/redox_users/tests/etc/group new file mode 100644 index 0000000000..89cb5e61bb --- /dev/null +++ b/third_party/rust/redox_users/tests/etc/group @@ -0,0 +1,4 @@ +root;0;root +user;1000;user +wheel;1;user,root +li;1007;li diff --git a/third_party/rust/redox_users/tests/etc/passwd b/third_party/rust/redox_users/tests/etc/passwd new file mode 100644 index 0000000000..0679ecfdc1 --- /dev/null +++ b/third_party/rust/redox_users/tests/etc/passwd @@ -0,0 +1,3 @@ +root;0;0;root;file:/root;file:/bin/ion +user;1000;1000;user;file:/home/user;file:/bin/ion +li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion diff --git a/third_party/rust/redox_users/tests/etc/shadow b/third_party/rust/redox_users/tests/etc/shadow new file mode 100644 index 0000000000..e0bab42a8d --- /dev/null +++ b/third_party/rust/redox_users/tests/etc/shadow @@ -0,0 +1,3 @@ +root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk +user; +li;! |