├── .gitignore ├── .gitlab-ci.yml ├── .rustfmt.toml ├── .travis.yml ├── Cargo.lock ├── Cargo.toml ├── LICENSE ├── README.md ├── src └── lib.rs └── tests └── etc ├── group ├── passwd └── shadow /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | /target/ 3 | **/*.rs.bk 4 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | image: "redoxos/redoxer" 2 | 3 | stages: 4 | - build 5 | - test 6 | 7 | cache: 8 | paths: 9 | - target/ 10 | 11 | build:linux: 12 | stage: build 13 | script: cargo +nightly build --verbose 14 | 15 | build:redox: 16 | stage: build 17 | script: redoxer build --verbose 18 | 19 | test:linux: 20 | stage: test 21 | dependencies: 22 | - build:linux 23 | script: 24 | - cargo +nightly test --verbose -- --test-threads=1 25 | - cargo +nightly test --verbose --no-default-features -- --test-threads=1 26 | 27 | test:redox: 28 | stage: test 29 | dependencies: 30 | - build:redox 31 | script: redoxer test --verbose 32 | 33 | -------------------------------------------------------------------------------- /.rustfmt.toml: -------------------------------------------------------------------------------- 1 | trailing_comma = "Never" 2 | fn_single_line = true 3 | where_single_line = true 4 | match_block_trailing_comma = true 5 | reorder_imported_names = true 6 | reorder_imports = true 7 | reorder_imports_in_group = true 8 | trailing_semicolon = false 9 | use_field_init_shorthand = true 10 | color = "Always" 11 | struct_lit_single_line = true 12 | max_width = 80 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: rust 2 | rust: 3 | - nightly 4 | script: 5 | - cargo build --verbose 6 | - cargo test --verbose -- --test-threads=1 7 | -------------------------------------------------------------------------------- /Cargo.lock: -------------------------------------------------------------------------------- 1 | # This file is automatically @generated by Cargo. 2 | # It is not intended for manual editing. 3 | version = 3 4 | 5 | [[package]] 6 | name = "arrayref" 7 | version = "0.3.9" 8 | source = "registry+https://github.com/rust-lang/crates.io-index" 9 | checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" 10 | 11 | [[package]] 12 | name = "arrayvec" 13 | version = "0.5.2" 14 | source = "registry+https://github.com/rust-lang/crates.io-index" 15 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" 16 | 17 | [[package]] 18 | name = "base64" 19 | version = "0.13.1" 20 | source = "registry+https://github.com/rust-lang/crates.io-index" 21 | checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" 22 | 23 | [[package]] 24 | name = "bitflags" 25 | version = "2.6.0" 26 | source = "registry+https://github.com/rust-lang/crates.io-index" 27 | checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" 28 | 29 | [[package]] 30 | name = "blake2b_simd" 31 | version = "0.5.11" 32 | source = "registry+https://github.com/rust-lang/crates.io-index" 33 | checksum = "afa748e348ad3be8263be728124b24a24f268266f6f5d58af9d75f6a40b5c587" 34 | dependencies = [ 35 | "arrayref", 36 | "arrayvec", 37 | "constant_time_eq", 38 | ] 39 | 40 | [[package]] 41 | name = "cfg-if" 42 | version = "1.0.0" 43 | source = "registry+https://github.com/rust-lang/crates.io-index" 44 | checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" 45 | 46 | [[package]] 47 | name = "constant_time_eq" 48 | version = "0.1.5" 49 | source = "registry+https://github.com/rust-lang/crates.io-index" 50 | checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" 51 | 52 | [[package]] 53 | name = "crossbeam-utils" 54 | version = "0.8.21" 55 | source = "registry+https://github.com/rust-lang/crates.io-index" 56 | checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" 57 | 58 | [[package]] 59 | name = "getrandom" 60 | version = "0.2.15" 61 | source = "registry+https://github.com/rust-lang/crates.io-index" 62 | checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" 63 | dependencies = [ 64 | "cfg-if", 65 | "libc", 66 | "wasi", 67 | ] 68 | 69 | [[package]] 70 | name = "libc" 71 | version = "0.2.169" 72 | source = "registry+https://github.com/rust-lang/crates.io-index" 73 | checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" 74 | 75 | [[package]] 76 | name = "libredox" 77 | version = "0.1.3" 78 | source = "registry+https://github.com/rust-lang/crates.io-index" 79 | checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" 80 | dependencies = [ 81 | "bitflags", 82 | "libc", 83 | ] 84 | 85 | [[package]] 86 | name = "proc-macro2" 87 | version = "1.0.92" 88 | source = "registry+https://github.com/rust-lang/crates.io-index" 89 | checksum = "37d3544b3f2748c54e147655edb5025752e2303145b5aefb3c3ea2c78b973bb0" 90 | dependencies = [ 91 | "unicode-ident", 92 | ] 93 | 94 | [[package]] 95 | name = "quote" 96 | version = "1.0.38" 97 | source = "registry+https://github.com/rust-lang/crates.io-index" 98 | checksum = "0e4dccaaaf89514f546c693ddc140f729f958c247918a13380cccc6078391acc" 99 | dependencies = [ 100 | "proc-macro2", 101 | ] 102 | 103 | [[package]] 104 | name = "redox_users" 105 | version = "0.5.0" 106 | dependencies = [ 107 | "getrandom", 108 | "libredox", 109 | "rust-argon2", 110 | "thiserror", 111 | "zeroize", 112 | ] 113 | 114 | [[package]] 115 | name = "rust-argon2" 116 | version = "0.8.3" 117 | source = "registry+https://github.com/rust-lang/crates.io-index" 118 | checksum = "4b18820d944b33caa75a71378964ac46f58517c92b6ae5f762636247c09e78fb" 119 | dependencies = [ 120 | "base64", 121 | "blake2b_simd", 122 | "constant_time_eq", 123 | "crossbeam-utils", 124 | ] 125 | 126 | [[package]] 127 | name = "syn" 128 | version = "2.0.94" 129 | source = "registry+https://github.com/rust-lang/crates.io-index" 130 | checksum = "987bc0be1cdea8b10216bd06e2ca407d40b9543468fafd3ddfb02f36e77f71f3" 131 | dependencies = [ 132 | "proc-macro2", 133 | "quote", 134 | "unicode-ident", 135 | ] 136 | 137 | [[package]] 138 | name = "thiserror" 139 | version = "2.0.9" 140 | source = "registry+https://github.com/rust-lang/crates.io-index" 141 | checksum = "f072643fd0190df67a8bab670c20ef5d8737177d6ac6b2e9a236cb096206b2cc" 142 | dependencies = [ 143 | "thiserror-impl", 144 | ] 145 | 146 | [[package]] 147 | name = "thiserror-impl" 148 | version = "2.0.9" 149 | source = "registry+https://github.com/rust-lang/crates.io-index" 150 | checksum = "7b50fa271071aae2e6ee85f842e2e28ba8cd2c5fb67f11fcb1fd70b276f9e7d4" 151 | dependencies = [ 152 | "proc-macro2", 153 | "quote", 154 | "syn", 155 | ] 156 | 157 | [[package]] 158 | name = "unicode-ident" 159 | version = "1.0.14" 160 | source = "registry+https://github.com/rust-lang/crates.io-index" 161 | checksum = "adb9e6ca4f869e1180728b7950e35922a7fc6397f7b641499e8f3ef06e50dc83" 162 | 163 | [[package]] 164 | name = "wasi" 165 | version = "0.11.0+wasi-snapshot-preview1" 166 | source = "registry+https://github.com/rust-lang/crates.io-index" 167 | checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" 168 | 169 | [[package]] 170 | name = "zeroize" 171 | version = "1.8.1" 172 | source = "registry+https://github.com/rust-lang/crates.io-index" 173 | checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" 174 | dependencies = [ 175 | "zeroize_derive", 176 | ] 177 | 178 | [[package]] 179 | name = "zeroize_derive" 180 | version = "1.4.2" 181 | source = "registry+https://github.com/rust-lang/crates.io-index" 182 | checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" 183 | dependencies = [ 184 | "proc-macro2", 185 | "quote", 186 | "syn", 187 | ] 188 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "redox_users" 3 | version = "0.5.0" 4 | authors = ["Jose Narvaez ", "Wesley Hershberger "] 5 | description = "A Rust library to access Redox users and groups functionality" 6 | license = "MIT" 7 | repository = "https://gitlab.redox-os.org/redox-os/users" 8 | documentation = "https://docs.rs/redox_users" 9 | readme = "README.md" 10 | keywords = ["redox", "auth"] 11 | edition = "2021" 12 | 13 | [dependencies] 14 | getrandom = { version = "0.2", features = ["std"] } 15 | libredox = { version = "0.1.3", default-features = false, features = ["std", "call"] } 16 | rust-argon2 = { version = "0.8", optional = true } 17 | thiserror = "2.0" 18 | zeroize = { version = "1.4", features = ["zeroize_derive"], optional = true } 19 | 20 | #[target.'cfg(not(target_os = "redox"))'.dependencies] 21 | #nix = "0.19" 22 | 23 | [features] 24 | default = ["auth"] 25 | 26 | # Enable password authentication using argon2. Some API may not be available 27 | # without default features. 28 | auth = ["rust-argon2", "zeroize"] 29 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2017 Jose Narvaez 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # redox_users 2 | 3 | Redox OS APIs for accessing users and groups information. [Documentation](https://docs.rs/redox_users/0.1.0/redox_users/) 4 | 5 | High level APIs for: 6 | 7 | - Getting the current process effective user ID. 8 | - Getting the current process user ID. 9 | - Getting the current process effective group ID. 10 | - Getting the current process group ID. 11 | - Manipulating User and Group information (including adding, removing, and modifying groups and users, in addition to other functionality, see docs) 12 | 13 | We recommend to use these APIs instead of directly manipulating the 14 | `/etc/group` and `/etc/passwd` as this is an implementation detail and 15 | might change in the future. 16 | 17 | 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. 18 | 19 | ## Hashing 20 | redox_users uses the Argon2 hashing algorithm. The default hashing parameters are as follows: 21 | ```Rust 22 | Argon2::new(10, 1, 4096, Variant::Argon2i) 23 | ``` 24 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! `redox-users` is designed to be a small, low-ish level interface 2 | //! to system user and group information, as well as user password 3 | //! authentication. It is OS-specific and will break horribly on platforms 4 | //! that are not [Redox-OS](https://redox-os.org). 5 | //! 6 | //! # Permissions 7 | //! Because this is a system level tool dealing with password 8 | //! authentication, programs are often required to run with 9 | //! escalated priveleges. The implementation of the crate is 10 | //! privelege unaware. The only privelege requirements are those 11 | //! laid down by the system administrator over these files: 12 | //! - `/etc/group` 13 | //! - Read: Required to access group information 14 | //! - Write: Required to change group information 15 | //! - `/etc/passwd` 16 | //! - Read: Required to access user information 17 | //! - Write: Required to change user information 18 | //! - `/etc/shadow` 19 | //! - Read: Required to authenticate users 20 | //! - Write: Required to set user passwords 21 | //! 22 | //! # Reimplementation 23 | //! This crate is designed to be as small as possible without 24 | //! sacrificing critical functionality. The idea is that a small 25 | //! enough redox-users will allow easy re-implementation based on 26 | //! the same flexible API. This would allow more complicated authentication 27 | //! schemes for redox in future without breakage of existing 28 | //! software. 29 | 30 | use std::fmt::Debug; 31 | use std::fs::{File, OpenOptions}; 32 | use std::io::{Read, Seek, SeekFrom, Write}; 33 | #[cfg(target_os = "redox")] 34 | use std::os::unix::fs::OpenOptionsExt; 35 | #[cfg(not(target_os = "redox"))] 36 | use std::os::unix::io::AsRawFd; 37 | use std::os::unix::process::CommandExt; 38 | use std::path::{Path, PathBuf}; 39 | use std::process::Command; 40 | use std::slice::{Iter, IterMut}; 41 | #[cfg(not(test))] 42 | #[cfg(feature = "auth")] 43 | use std::thread; 44 | use std::time::Duration; 45 | 46 | use thiserror::Error; 47 | #[cfg(feature = "auth")] 48 | use zeroize::Zeroize; 49 | 50 | //#[cfg(not(target_os = "redox"))] 51 | //use nix::fcntl::{flock, FlockArg}; 52 | 53 | #[cfg(target_os = "redox")] 54 | use libredox::flag::{O_EXLOCK, O_SHLOCK}; 55 | 56 | const PASSWD_FILE: &'static str = "/etc/passwd"; 57 | const GROUP_FILE: &'static str = "/etc/group"; 58 | #[cfg(feature = "auth")] 59 | const SHADOW_FILE: &'static str = "/etc/shadow"; 60 | 61 | const MIN_ID: usize = 1000; 62 | const MAX_ID: usize = 6000; 63 | const DEFAULT_TIMEOUT: u64 = 3; 64 | 65 | const USERNAME_LEN_MIN: usize = 3; 66 | const USERNAME_LEN_MAX: usize = 32; 67 | 68 | /// Errors that might happen while using this crate 69 | #[derive(Debug, Error)] 70 | #[non_exhaustive] 71 | pub enum Error { 72 | #[error("os error: {reason}")] 73 | Os { reason: &'static str }, 74 | 75 | #[error(transparent)] 76 | Io(#[from] std::io::Error), 77 | 78 | #[error("failed to generate seed: {0}")] 79 | Getrandom(#[from] getrandom::Error), 80 | 81 | #[cfg(feature = "auth")] 82 | #[error("")] 83 | Argon(#[from] argon2::Error), 84 | 85 | #[error("parse error line {line}: {reason}")] 86 | Parsing { reason: String, line: usize }, 87 | 88 | #[error(transparent)] 89 | ParseInt(#[from] std::num::ParseIntError), 90 | 91 | #[error("user not found")] 92 | UserNotFound, 93 | 94 | #[error("group not found")] 95 | GroupNotFound, 96 | 97 | #[error("user already exists")] 98 | UserAlreadyExists, 99 | 100 | #[error("group already exists")] 101 | GroupAlreadyExists, 102 | 103 | #[error("invalid name '{name}'")] 104 | InvalidName { name: String }, 105 | 106 | /// Used for invalid string field values of [`User`] 107 | #[error("invalid entry element '{data}'")] 108 | InvalidData { data: String }, 109 | } 110 | pub type Result = core::result::Result; 111 | 112 | #[inline] 113 | fn parse_error(line: usize, reason: &str) -> Error { 114 | Error::Parsing { 115 | reason: reason.into(), 116 | line, 117 | } 118 | } 119 | 120 | impl From for Error { 121 | fn from(syscall_error: libredox::error::Error) -> Error { 122 | Error::Io(std::io::Error::from(syscall_error)) 123 | } 124 | } 125 | 126 | #[derive(Clone, Copy, Debug)] 127 | enum Lock { 128 | Shared, 129 | Exclusive, 130 | } 131 | 132 | impl Lock { 133 | fn can_write(&self) -> bool { 134 | match self { 135 | Lock::Shared => false, 136 | Lock::Exclusive => true, 137 | } 138 | } 139 | 140 | #[cfg(target_os = "redox")] 141 | fn as_olock(self) -> i32 { 142 | (match self { 143 | Lock::Shared => O_SHLOCK, 144 | Lock::Exclusive => O_EXLOCK, 145 | }) as i32 146 | } 147 | 148 | /*#[cfg(not(target_os = "redox"))] 149 | fn as_flock(self) -> FlockArg { 150 | match self { 151 | Lock::Shared => FlockArg::LockShared, 152 | Lock::Exclusive => FlockArg::LockExclusive, 153 | } 154 | }*/ 155 | } 156 | 157 | /// Naive semi-cross platform file locking (need to support linux for tests). 158 | #[allow(dead_code)] 159 | fn locked_file(file: impl AsRef, lock: Lock) -> Result { 160 | #[cfg(test)] 161 | println!("Open file: {}", file.as_ref().display()); 162 | 163 | #[cfg(target_os = "redox")] 164 | { 165 | Ok(OpenOptions::new() 166 | .read(true) 167 | .write(lock.can_write()) 168 | .custom_flags(lock.as_olock()) 169 | .open(file)?) 170 | } 171 | #[cfg(not(target_os = "redox"))] 172 | #[cfg_attr(rustfmt, rustfmt_skip)] 173 | { 174 | let file = OpenOptions::new() 175 | .read(true) 176 | .write(lock.can_write()) 177 | .open(file)?; 178 | let fd = file.as_raw_fd(); 179 | eprintln!("Fd: {}", fd); 180 | //flock(fd, lock.as_flock())?; 181 | Ok(file) 182 | } 183 | } 184 | 185 | /// Reset a file for rewriting (user/group dbs must be erased before write-out) 186 | fn reset_file(fd: &mut File) -> Result<(), Error> { 187 | fd.set_len(0)?; 188 | fd.seek(SeekFrom::Start(0))?; 189 | Ok(()) 190 | } 191 | 192 | /// Is a string safe to write to `/etc/group` or `/etc/passwd`? 193 | fn is_safe_string(s: &str) -> bool { 194 | !s.contains(';') 195 | } 196 | 197 | const PORTABLE_FILE_NAME_CHARS: &str = 198 | "0123456789._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"; 199 | 200 | /// This function is used by [`UserBuilder`] and [`GroupBuilder`] to determine 201 | /// if a name for a user/group is valid. It is provided for convenience. 202 | /// 203 | /// Usernames must match the [POSIX standard 204 | /// for usernames](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_437) 205 | /// . The "portable filename character set" is defined as `A-Z`, `a-z`, `0-9`, 206 | /// and `._-` (see [here](https://pubs.opengroup.org/onlinepubs/9699919799/basedefs/V1_chap03.html#tag_03_282)). 207 | /// 208 | /// Usernames may not be more than 32 or less than 3 characters in length. 209 | pub fn is_valid_name(name: &str) -> bool { 210 | if name.len() < USERNAME_LEN_MIN || name.len() > USERNAME_LEN_MAX { 211 | false 212 | } else if let Some(first) = name.chars().next() { 213 | first != '-' && 214 | name.chars().all(|c| { 215 | PORTABLE_FILE_NAME_CHARS.contains(c) 216 | }) 217 | } else { 218 | false 219 | } 220 | } 221 | 222 | /// Marker types for [`User`] and [`AllUsers`]. 223 | pub mod auth { 224 | #[cfg(feature = "auth")] 225 | use std::fmt; 226 | 227 | #[cfg(feature = "auth")] 228 | use zeroize::Zeroize; 229 | 230 | #[cfg(feature = "auth")] 231 | use crate::Error; 232 | 233 | /// Marker type indicating that a `User` only has access to world-readable 234 | /// user information, and cannot authenticate. 235 | #[derive(Debug, Default)] 236 | pub struct Basic {} 237 | 238 | /// Marker type indicating that a `User` has access to all user 239 | /// information, including password hashes. 240 | #[cfg(feature = "auth")] 241 | #[derive(Default, Zeroize)] 242 | #[zeroize(drop)] 243 | pub struct Full { 244 | pub(crate) hash: String, 245 | } 246 | 247 | #[cfg(feature = "auth")] 248 | impl Full { 249 | pub(crate) fn empty() -> Full { 250 | Full { hash: "".into() } 251 | } 252 | 253 | pub(crate) fn is_empty(&self) -> bool { 254 | &self.hash == "" 255 | } 256 | 257 | pub(crate) fn unset() -> Full { 258 | Full { hash: "!".into() } 259 | } 260 | 261 | pub(crate) fn is_unset(&self) -> bool { 262 | &self.hash == "!" 263 | } 264 | 265 | pub(crate) fn passwd(pw: &str) -> Result { 266 | Ok(if pw != "" { 267 | let mut buf = [0u8; 8]; 268 | getrandom::getrandom(&mut buf)?; 269 | let mut salt = format!("{:X}", u64::from_ne_bytes(buf)); 270 | 271 | let config = argon2::Config::default(); 272 | let hash: String = argon2::hash_encoded( 273 | pw.as_bytes(), 274 | salt.as_bytes(), 275 | &config 276 | )?; 277 | 278 | buf.zeroize(); 279 | salt.zeroize(); 280 | Full { hash } // note that move == shallow copy in Rust 281 | } else { 282 | Full::empty() 283 | }) 284 | } 285 | 286 | pub(crate) fn verify(&self, pw: &str) -> bool { 287 | match self.hash.as_str() { 288 | "" => pw == "", 289 | "!" => false, 290 | //TODO: When does this panic? Should this function return 291 | // Result? Or does it need to simply fail to verify if 292 | // verify_encoded() fails? 293 | hash => argon2::verify_encoded(&hash, pw.as_bytes()) 294 | .expect("failed to verify hash"), 295 | } 296 | } 297 | } 298 | 299 | #[cfg(feature = "auth")] 300 | impl fmt::Debug for Full { 301 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { 302 | f.debug_struct("Full") 303 | .finish() 304 | } 305 | } 306 | } 307 | 308 | /// A builder pattern for adding [`User`]s to [`AllUsers`]. Fields are verified 309 | /// when the group is built via [`AllUsers::add_user`]. See the documentation 310 | /// of that function for default values. 311 | /// 312 | /// Note that this builder is not available when the `auth` feature of the 313 | /// crate is disabled. 314 | /// 315 | /// # Example 316 | /// ```no_run 317 | /// # use redox_users::{AllGroups, Config, GroupBuilder, UserBuilder}; 318 | /// let mut allgs = AllGroups::new(Config::default()).unwrap(); 319 | /// 320 | /// let g = GroupBuilder::new("foobar") 321 | /// .user("foobar"); 322 | /// let foobar_g = allgs.add_group(g).unwrap(); 323 | /// 324 | /// let u = UserBuilder::new("foobar") 325 | /// .gid(foobar_g.gid) 326 | /// .name("Foo Bar") 327 | /// // Note that this directory will not be created 328 | /// .home("file:/home/foobar"); 329 | /// ``` 330 | #[cfg(feature = "auth")] 331 | pub struct UserBuilder { 332 | user: String, 333 | uid: Option, 334 | gid: Option, 335 | name: Option, 336 | home: Option, 337 | shell: Option, 338 | } 339 | 340 | #[cfg(feature = "auth")] 341 | impl UserBuilder { 342 | /// Create a new `UserBuilder` with the login name for the new user. 343 | pub fn new(user: impl AsRef) -> UserBuilder { 344 | UserBuilder { 345 | user: user.as_ref().to_string(), 346 | uid: None, 347 | gid: None, 348 | name: None, 349 | home: None, 350 | shell: None, 351 | } 352 | } 353 | 354 | /// Set the user id for this user. 355 | pub fn uid(mut self, uid: usize) -> UserBuilder { 356 | self.uid = Some(uid); 357 | self 358 | } 359 | 360 | /// Set the primary group id for this user. 361 | pub fn gid(mut self, gid: usize) -> UserBuilder { 362 | self.gid = Some(gid); 363 | self 364 | } 365 | 366 | /// Set the GECOS field for this user. 367 | pub fn name(mut self, name: impl AsRef) -> UserBuilder { 368 | self.name = Some(name.as_ref().to_string()); 369 | self 370 | } 371 | 372 | /// Set the home directory for this user. 373 | pub fn home(mut self, home: impl AsRef) -> UserBuilder { 374 | self.home = Some(home.as_ref().to_string()); 375 | self 376 | } 377 | 378 | /// Set the login shell for this user. 379 | pub fn shell(mut self, shell: impl AsRef) -> UserBuilder { 380 | self.shell = Some(shell.as_ref().to_string()); 381 | self 382 | } 383 | } 384 | 385 | /// A struct representing a Redox user. 386 | /// Currently maps to an entry in the `/etc/passwd` file. 387 | /// 388 | /// `A` should be a type from [`crate::auth`]. 389 | /// 390 | /// # Unset vs. Blank Passwords 391 | /// A note on unset passwords vs. blank passwords. A blank password 392 | /// is a hash field that is completely blank (aka, `""`). According 393 | /// to this crate, successful login is only allowed if the input 394 | /// password is blank as well. 395 | /// 396 | /// An unset password is one whose hash is not empty (`""`), but 397 | /// also not a valid serialized argon2rs hashing session. This 398 | /// hash always returns `false` upon attempted verification. The 399 | /// most commonly used hash for an unset password is `"!"`, but 400 | /// this crate makes no distinction. The most common way to unset 401 | /// the password is to use [`User::unset_passwd`]. 402 | #[derive(Debug)] 403 | pub struct User { 404 | /// Username (login name) 405 | pub user: String, 406 | /// User id 407 | pub uid: usize, 408 | /// Group id 409 | pub gid: usize, 410 | /// Real name (human readable, can contain spaces) 411 | pub name: String, 412 | /// Home directory path 413 | pub home: String, 414 | /// Shell path 415 | pub shell: String, 416 | 417 | // Failed login delay duration 418 | auth_delay: Duration, 419 | 420 | #[allow(dead_code)] 421 | auth: A, 422 | } 423 | 424 | impl User { 425 | /// Get a Command to run the user's default shell (see [`User::login_cmd`] 426 | /// for more docs). 427 | pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) } 428 | 429 | /// Provide a login command for the user, which is any entry point for 430 | /// starting a user's session, whether a shell (use [`User::shell_cmd`] 431 | /// instead) or a graphical init. 432 | /// 433 | /// The `Command` will use the user's `uid` and `gid`, its `current_dir` 434 | /// will be set to the user's home directory, and the follwing enviroment 435 | /// variables will be populated: 436 | /// 437 | /// - `USER` set to the user's `user` field. 438 | /// - `UID` set to the user's `uid` field. 439 | /// - `GROUPS` set the user's `gid` field. 440 | /// - `HOME` set to the user's `home` field. 441 | /// - `SHELL` set to the user's `shell` field. 442 | pub fn login_cmd(&self, cmd: T) -> Command 443 | where T: std::convert::AsRef + AsRef 444 | { 445 | let mut command = Command::new(cmd); 446 | command 447 | .uid(self.uid as u32) 448 | .gid(self.gid as u32) 449 | .current_dir(&self.home) 450 | .env("USER", &self.user) 451 | .env("UID", format!("{}", self.uid)) 452 | .env("GROUPS", format!("{}", self.gid)) 453 | .env("HOME", &self.home) 454 | .env("SHELL", &self.shell); 455 | command 456 | } 457 | 458 | fn from_passwd_entry(s: &str, line: usize) -> Result, Error> { 459 | let mut parts = s.split(';'); 460 | 461 | let user = parts 462 | .next() 463 | .ok_or(parse_error(line, "expected user"))?; 464 | let uid = parts 465 | .next() 466 | .ok_or(parse_error(line, "expected uid"))? 467 | .parse::()?; 468 | let gid = parts 469 | .next() 470 | .ok_or(parse_error(line, "expected uid"))? 471 | .parse::()?; 472 | let name = parts 473 | .next() 474 | .ok_or(parse_error(line, "expected real name"))?; 475 | let home = parts 476 | .next() 477 | .ok_or(parse_error(line, "expected home dir path"))?; 478 | let shell = parts 479 | .next() 480 | .ok_or(parse_error(line, "expected shell path"))?; 481 | 482 | Ok(User:: { 483 | user: user.into(), 484 | uid, 485 | gid, 486 | name: name.into(), 487 | home: home.into(), 488 | shell: shell.into(), 489 | auth: A::default(), 490 | auth_delay: Duration::default(), 491 | }) 492 | } 493 | } 494 | 495 | #[cfg(feature = "auth")] 496 | impl User { 497 | /// Set the password for a user. Make **sure** that `password` 498 | /// is actually what the user wants as their password (this doesn't). 499 | /// 500 | /// To set the password blank, pass `""` as `password`. 501 | /// 502 | /// Note that `password` is taken as a reference, so it is up to the caller 503 | /// to properly zero sensitive memory (see `zeroize` on crates.io). 504 | pub fn set_passwd(&mut self, password: impl AsRef) -> Result<(), Error> { 505 | self.auth = auth::Full::passwd(password.as_ref())?; 506 | Ok(()) 507 | } 508 | 509 | /// Unset the password ([`User::verify_passwd`] always returns `false`). 510 | pub fn unset_passwd(&mut self) { 511 | self.auth = auth::Full::unset(); 512 | } 513 | 514 | /// Verify the password. If the hash is empty, this only returns `true` if 515 | /// `password` is also empty. 516 | /// 517 | /// Note that this is a blocking operation if the password is incorrect. 518 | /// See [`Config::auth_delay`] to set the wait time. Default is 3 seconds. 519 | /// 520 | /// Note that `password` is taken as a reference, so it is up to the caller 521 | /// to properly zero sensitive memory (see `zeroize` on crates.io). 522 | pub fn verify_passwd(&self, password: impl AsRef) -> bool { 523 | let verified = self.auth.verify(password.as_ref()); 524 | if !verified { 525 | #[cfg(not(test))] // Make tests run faster 526 | thread::sleep(self.auth_delay); 527 | } 528 | verified 529 | } 530 | 531 | /// Determine if the hash for the password is blank ([`User::verify_passwd`] 532 | /// returns `true` *only* when the password is blank). 533 | pub fn is_passwd_blank(&self) -> bool { 534 | self.auth.is_empty() 535 | } 536 | 537 | /// Determine if the hash for the password is unset 538 | /// ([`User::verify_passwd`] returns `false` regardless of input). 539 | pub fn is_passwd_unset(&self) -> bool { 540 | self.auth.is_unset() 541 | } 542 | 543 | /// Format this user as an entry in `/etc/passwd`. 544 | fn passwd_entry(&self) -> Result { 545 | if !is_safe_string(&self.user) { 546 | Err(Error::InvalidName { name: self.user.to_string() }) 547 | } else if !is_safe_string(&self.name) { 548 | Err(Error::InvalidData { data: self.name.to_string() }) 549 | } else if !is_safe_string(&self.home) { 550 | Err(Error::InvalidData { data: self.home.to_string() }) 551 | } else if !is_safe_string(&self.shell) { 552 | Err(Error::InvalidData { data: self.shell.to_string() }) 553 | } else { 554 | #[cfg_attr(rustfmt, rustfmt_skip)] 555 | Ok(format!("{};{};{};{};{};{}\n", 556 | self.user, self.uid, self.gid, self.name, self.home, self.shell 557 | )) 558 | } 559 | } 560 | 561 | fn shadow_entry(&self) -> Result { 562 | if !is_safe_string(&self.user) { 563 | Err(Error::InvalidName { name: self.user.to_string() }) 564 | } else { 565 | Ok(format!("{};{}\n", self.user, self.auth.hash)) 566 | } 567 | } 568 | } 569 | 570 | impl Name for User { 571 | fn name(&self) -> &str { 572 | &self.user 573 | } 574 | } 575 | 576 | impl Id for User { 577 | fn id(&self) -> usize { 578 | self.uid 579 | } 580 | } 581 | 582 | /// A builder pattern for adding [`Group`]s to [`AllGroups`]. Fields are 583 | /// verified when the `Group` is built, via [`AllGroups::add_group`]. 584 | /// 585 | /// # Example 586 | /// ``` 587 | /// # use redox_users::GroupBuilder; 588 | /// // When added, this group will use the first available group id 589 | /// let mygroup = GroupBuilder::new("group_name"); 590 | /// 591 | /// // A little more stuff: 592 | /// let other = GroupBuilder::new("special") 593 | /// .gid(9055) 594 | /// .user("some_username"); 595 | /// ``` 596 | pub struct GroupBuilder { 597 | // Group name 598 | group: String, 599 | 600 | gid: Option, 601 | 602 | users: Vec, 603 | } 604 | 605 | impl GroupBuilder { 606 | /// Create a new `GroupBuilder` with the given group name. 607 | pub fn new(group: impl AsRef) -> GroupBuilder { 608 | GroupBuilder { 609 | group: group.as_ref().to_string(), 610 | gid: None, 611 | users: vec![], 612 | } 613 | } 614 | 615 | /// Set the group id of this group. 616 | pub fn gid(mut self, gid: usize) -> GroupBuilder { 617 | self.gid = Some(gid); 618 | self 619 | } 620 | 621 | /// Add a user to this group. Call this function multiple times to add more 622 | /// users. 623 | pub fn user(mut self, user: impl AsRef) -> GroupBuilder { 624 | self.users.push(user.as_ref().to_string()); 625 | self 626 | } 627 | } 628 | 629 | /// A struct representing a Redox user group. 630 | /// Currently maps to an `/etc/group` file entry. 631 | #[derive(Debug)] 632 | pub struct Group { 633 | /// Group name 634 | pub group: String, 635 | /// Password (unused, usually "x") 636 | pub password: String, 637 | /// Unique group id 638 | pub gid: usize, 639 | /// Group members' usernames 640 | pub users: Vec, 641 | } 642 | 643 | impl Group { 644 | fn from_group_entry(s: &str, line: usize) -> Result { 645 | let mut parts = s.trim() 646 | .split(';'); 647 | 648 | let group = parts 649 | .next() 650 | .ok_or(parse_error(line, "expected group"))?; 651 | let password = parts 652 | .next() 653 | .ok_or(parse_error(line, "expected password"))?; 654 | let gid = parts 655 | .next() 656 | .ok_or(parse_error(line, "expected gid"))? 657 | .parse::()?; 658 | let users_str = parts.next() 659 | .unwrap_or(""); 660 | let users = users_str.split(',') 661 | .filter_map(|u| if u == "" { 662 | None 663 | } else { 664 | Some(u.into()) 665 | }) 666 | .collect(); 667 | 668 | Ok(Group { 669 | group: group.into(), 670 | password: password.into(), 671 | gid, 672 | users, 673 | }) 674 | } 675 | 676 | fn group_entry(&self) -> Result { 677 | if !is_safe_string(&self.group) { 678 | Err(Error::InvalidName { name: self.group.to_string() }) 679 | } else { 680 | for username in self.users.iter() { 681 | if !is_safe_string(&username) { 682 | return Err(Error::InvalidData { data: username.to_string() }); 683 | } 684 | } 685 | 686 | #[cfg_attr(rustfmt, rustfmt_skip)] 687 | Ok(format!("{};{};{};{}\n", 688 | self.group, 689 | self.password, 690 | self.gid, 691 | self.users.join(",").trim_matches(',') 692 | )) 693 | } 694 | } 695 | } 696 | 697 | impl Name for Group { 698 | fn name(&self) -> &str { 699 | &self.group 700 | } 701 | } 702 | 703 | impl Id for Group { 704 | fn id(&self) -> usize { 705 | self.gid 706 | } 707 | } 708 | 709 | /// Gets the current process effective user ID. 710 | /// 711 | /// This function issues the `geteuid` system call returning the process effective 712 | /// user id. 713 | /// 714 | /// # Examples 715 | /// 716 | /// Basic usage: 717 | /// 718 | /// ```no_run 719 | /// # use redox_users::get_euid; 720 | /// let euid = get_euid().unwrap(); 721 | /// ``` 722 | pub fn get_euid() -> Result { 723 | libredox::call::geteuid() 724 | .map_err(From::from) 725 | } 726 | 727 | /// Gets the current process real user ID. 728 | /// 729 | /// This function issues the `getuid` system call returning the process real 730 | /// user id. 731 | /// 732 | /// # Examples 733 | /// 734 | /// Basic usage: 735 | /// 736 | /// ```no_run 737 | /// # use redox_users::get_uid; 738 | /// let uid = get_uid().unwrap(); 739 | /// ``` 740 | pub fn get_uid() -> Result { 741 | libredox::call::getruid() 742 | .map_err(From::from) 743 | } 744 | 745 | /// Gets the current process effective group ID. 746 | /// 747 | /// This function issues the `getegid` system call returning the process effective 748 | /// group id. 749 | /// 750 | /// # Examples 751 | /// 752 | /// Basic usage: 753 | /// 754 | /// ```no_run 755 | /// # use redox_users::get_egid; 756 | /// let egid = get_egid().unwrap(); 757 | /// ``` 758 | pub fn get_egid() -> Result { 759 | libredox::call::getegid() 760 | .map_err(From::from) 761 | } 762 | 763 | /// Gets the current process real group ID. 764 | /// 765 | /// This function issues the `getegid` system call returning the process real 766 | /// group id. 767 | /// 768 | /// # Examples 769 | /// 770 | /// Basic usage: 771 | /// 772 | /// ```no_run 773 | /// # use redox_users::get_gid; 774 | /// let gid = get_gid().unwrap(); 775 | /// ``` 776 | pub fn get_gid() -> Result { 777 | libredox::call::getrgid() 778 | .map_err(From::from) 779 | } 780 | 781 | /// A generic configuration that allows fine control of an [`AllUsers`] or 782 | /// [`AllGroups`]. 783 | /// 784 | /// `auth_delay` is not used by [`AllGroups`] 785 | /// 786 | /// In most situations, [`Config::default`](struct.Config.html#impl-Default) 787 | /// will work just fine. The other fields are for finer control if it is 788 | /// required. 789 | /// 790 | /// # Example 791 | /// ``` 792 | /// # use redox_users::Config; 793 | /// use std::time::Duration; 794 | /// 795 | /// let cfg = Config::default() 796 | /// .min_id(500) 797 | /// .max_id(1000) 798 | /// .auth_delay(Duration::from_secs(5)); 799 | /// ``` 800 | #[derive(Clone, Debug)] 801 | pub struct Config { 802 | root_fs: PathBuf, 803 | auth_delay: Duration, 804 | min_id: usize, 805 | max_id: usize, 806 | lock: Lock, 807 | } 808 | 809 | impl Config { 810 | /// Set the delay for a failed authentication. Default is 3 seconds. 811 | pub fn auth_delay(mut self, delay: Duration) -> Config { 812 | self.auth_delay = delay; 813 | self 814 | } 815 | 816 | /// Set the smallest ID possible to use when finding an unused ID. 817 | pub fn min_id(mut self, id: usize) -> Config { 818 | self.min_id = id; 819 | self 820 | } 821 | 822 | /// Set the largest possible ID to use when finding an unused ID. 823 | pub fn max_id(mut self, id: usize) -> Config { 824 | self.max_id = id; 825 | self 826 | } 827 | 828 | /// Set the scheme relative to which the [`AllUsers`] or [`AllGroups`] 829 | /// should be looking for its data files. This is a compromise between 830 | /// exposing implementation details and providing fine enough 831 | /// control over the behavior of this API. 832 | // FIXME rename to root_fs the next time we release a breaking change 833 | pub fn scheme(mut self, scheme: String) -> Config { 834 | self.root_fs = PathBuf::from(scheme); 835 | self 836 | } 837 | 838 | /// Allow writes to group, passwd, and shadow files 839 | pub fn writeable(mut self, writeable: bool) -> Config { 840 | self.lock = if writeable { 841 | Lock::Exclusive 842 | } else { 843 | Lock::Shared 844 | }; 845 | self 846 | } 847 | 848 | // Prepend a path with the scheme in this Config 849 | fn in_root_fs(&self, path: impl AsRef) -> PathBuf { 850 | let mut canonical_path = self.root_fs.clone(); 851 | // Should be a little careful here, not sure I want this behavior 852 | if path.as_ref().is_absolute() { 853 | // This is nasty 854 | canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string()); 855 | } else { 856 | canonical_path.push(path); 857 | } 858 | canonical_path 859 | } 860 | } 861 | 862 | impl Default for Config { 863 | /// The default root filesystem is `/`. 864 | /// 865 | /// The default auth delay is 3 seconds. 866 | /// 867 | /// The default min and max ids are 1000 and 6000. 868 | fn default() -> Config { 869 | Config { 870 | root_fs: PathBuf::from("/"), 871 | auth_delay: Duration::new(DEFAULT_TIMEOUT, 0), 872 | min_id: MIN_ID, 873 | max_id: MAX_ID, 874 | lock: Lock::Shared, 875 | } 876 | } 877 | } 878 | 879 | // Nasty hack to prevent the compiler complaining about 880 | // "leaking" `AllInner` 881 | mod sealed { 882 | use crate::Config; 883 | 884 | pub trait Name { 885 | fn name(&self) -> &str; 886 | } 887 | 888 | pub trait Id { 889 | fn id(&self) -> usize; 890 | } 891 | 892 | pub trait AllInner { 893 | // Group+User, thanks Dad 894 | type Gruser: Name + Id; 895 | 896 | /// These functions grab internal elements so that the other 897 | /// methods of `All` can manipulate them. 898 | fn list(&self) -> &Vec; 899 | fn list_mut(&mut self) -> &mut Vec; 900 | fn config(&self) -> &Config; 901 | } 902 | } 903 | 904 | use sealed::{AllInner, Id, Name}; 905 | 906 | /// This trait is used to remove repetitive API items from 907 | /// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait 908 | /// so that the implementations of functions can be implemented 909 | /// at the trait level. Do not try to implement this trait. 910 | pub trait All: AllInner { 911 | /// Get an iterator borrowing all [`User`]s or [`Group`]s on the system. 912 | fn iter(&self) -> Iter<::Gruser> { 913 | self.list().iter() 914 | } 915 | 916 | /// Get an iterator mutably borrowing all [`User`]s or [`Group`]s on the 917 | /// system. 918 | fn iter_mut(&mut self) -> IterMut<::Gruser> { 919 | self.list_mut().iter_mut() 920 | } 921 | 922 | /// Borrow the [`User`] or [`Group`] with a given name. 923 | /// 924 | /// # Examples 925 | /// 926 | /// Basic usage: 927 | /// 928 | /// ```no_run 929 | /// # use redox_users::{All, AllUsers, Config}; 930 | /// let users = AllUsers::basic(Config::default()).unwrap(); 931 | /// let user = users.get_by_name("root").unwrap(); 932 | /// ``` 933 | fn get_by_name(&self, name: impl AsRef) -> Option<&::Gruser> { 934 | self.iter() 935 | .find(|gruser| gruser.name() == name.as_ref() ) 936 | } 937 | 938 | /// Mutable version of [`All::get_by_name`]. 939 | fn get_mut_by_name(&mut self, name: impl AsRef) -> Option<&mut ::Gruser> { 940 | self.iter_mut() 941 | .find(|gruser| gruser.name() == name.as_ref() ) 942 | } 943 | 944 | /// Borrow the [`User`] or [`Group`] with the given ID. 945 | /// 946 | /// # Examples 947 | /// 948 | /// Basic usage: 949 | /// 950 | /// ```no_run 951 | /// # use redox_users::{All, AllUsers, Config}; 952 | /// let users = AllUsers::basic(Config::default()).unwrap(); 953 | /// let user = users.get_by_id(0).unwrap(); 954 | /// ``` 955 | fn get_by_id(&self, id: usize) -> Option<&::Gruser> { 956 | self.iter() 957 | .find(|gruser| gruser.id() == id ) 958 | } 959 | 960 | /// Mutable version of [`All::get_by_id`]. 961 | fn get_mut_by_id(&mut self, id: usize) -> Option<&mut ::Gruser> { 962 | self.iter_mut() 963 | .find(|gruser| gruser.id() == id ) 964 | } 965 | 966 | /// Provides an unused id based on the min and max values in the [`Config`] 967 | /// passed to the `All`'s constructor. 968 | /// 969 | /// # Examples 970 | /// 971 | /// ```no_run 972 | /// # use redox_users::{All, AllUsers, Config}; 973 | /// let users = AllUsers::basic(Config::default()).unwrap(); 974 | /// let uid = users.get_unique_id().expect("no available uid"); 975 | /// ``` 976 | fn get_unique_id(&self) -> Option { 977 | for id in self.config().min_id..self.config().max_id { 978 | if !self.iter().any(|gruser| gruser.id() == id ) { 979 | return Some(id) 980 | } 981 | } 982 | None 983 | } 984 | 985 | /// Remove a [`User`] or [`Group`] from this `All` given it's name. If the 986 | /// Gruser was removed return `true`, else return `false`. This ensures 987 | /// that the Gruser no longer exists. 988 | fn remove_by_name(&mut self, name: impl AsRef) -> bool { 989 | let list = self.list_mut(); 990 | let indx = list.iter() 991 | .enumerate() 992 | .find_map(|(indx, gruser)| if gruser.name() == name.as_ref() { 993 | Some(indx) 994 | } else { 995 | None 996 | }); 997 | if let Some(indx) = indx { 998 | list.remove(indx); 999 | true 1000 | } else { 1001 | false 1002 | } 1003 | } 1004 | 1005 | /// Id version of [`All::remove_by_name`]. 1006 | fn remove_by_id(&mut self, id: usize) -> bool { 1007 | let list = self.list_mut(); 1008 | let indx = list.iter() 1009 | .enumerate() 1010 | .find_map(|(indx, gruser)| if gruser.id() == id { 1011 | Some(indx) 1012 | } else { 1013 | None 1014 | }); 1015 | if let Some(indx) = indx { 1016 | list.remove(indx); 1017 | true 1018 | } else { 1019 | false 1020 | } 1021 | } 1022 | } 1023 | 1024 | /// `AllUsers` provides (borrowed) access to all the users on the system. 1025 | /// Note that this struct implements [`All`] for all of its access functions. 1026 | /// 1027 | /// # Notes 1028 | /// Note that everything in this section also applies to [`AllGroups`]. 1029 | /// 1030 | /// * If you mutate anything owned by an `AllUsers`, you must call the 1031 | /// [`AllUsers::save`] in order for those changes to be applied to the system. 1032 | /// * The API here is kept small. Most mutating actions can be accomplished via 1033 | /// the [`All::get_mut_by_id`] and [`All::get_mut_by_name`] 1034 | /// functions. 1035 | #[derive(Debug)] 1036 | pub struct AllUsers { 1037 | users: Vec>, 1038 | config: Config, 1039 | 1040 | // Hold on to the locked fds to prevent race conditions 1041 | #[allow(dead_code)] 1042 | passwd_fd: File, 1043 | #[allow(dead_code)] 1044 | shadow_fd: Option, 1045 | } 1046 | 1047 | impl AllUsers { 1048 | pub fn new(config: Config) -> Result, Error> { 1049 | let mut passwd_fd = locked_file(config.in_root_fs(PASSWD_FILE), config.lock)?; 1050 | let mut passwd_cntnt = String::new(); 1051 | passwd_fd.read_to_string(&mut passwd_cntnt)?; 1052 | 1053 | let mut passwd_entries = Vec::new(); 1054 | for (indx, line) in passwd_cntnt.lines().enumerate() { 1055 | let mut user = User::from_passwd_entry(line, indx)?; 1056 | user.auth_delay = config.auth_delay; 1057 | passwd_entries.push(user); 1058 | } 1059 | 1060 | Ok(AllUsers:: { 1061 | users: passwd_entries, 1062 | config, 1063 | passwd_fd, 1064 | shadow_fd: None, 1065 | }) 1066 | } 1067 | } 1068 | 1069 | impl AllUsers { 1070 | /// Provide access to all user information on the system except 1071 | /// authentication. This is adequate for almost all uses of `AllUsers`. 1072 | pub fn basic(config: Config) -> Result, Error> { 1073 | Self::new(config) 1074 | } 1075 | } 1076 | 1077 | #[cfg(feature = "auth")] 1078 | impl AllUsers { 1079 | /// If access to password related methods for the [`User`]s yielded by this 1080 | /// `AllUsers` is required, use this constructor. 1081 | pub fn authenticator(config: Config) -> Result, Error> { 1082 | let mut shadow_fd = locked_file(config.in_root_fs(SHADOW_FILE), config.lock)?; 1083 | let mut shadow_cntnt = String::new(); 1084 | shadow_fd.read_to_string(&mut shadow_cntnt)?; 1085 | let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect(); 1086 | 1087 | let mut new = Self::new(config)?; 1088 | new.shadow_fd = Some(shadow_fd); 1089 | 1090 | for (indx, entry) in shadow_entries.iter().enumerate() { 1091 | let mut entry = entry.split(';'); 1092 | let name = entry.next().ok_or(parse_error(indx, 1093 | "error parsing shadowfile: expected username" 1094 | ))?; 1095 | let hash = entry.next().ok_or(parse_error(indx, 1096 | "error parsing shadowfile: expected hash" 1097 | ))?; 1098 | new.users 1099 | .iter_mut() 1100 | .find(|user| user.user == name) 1101 | .ok_or(parse_error(indx, 1102 | "error parsing shadowfile: unkown user" 1103 | ))?.auth.hash = hash.to_string(); 1104 | } 1105 | 1106 | shadow_cntnt.zeroize(); 1107 | Ok(new) 1108 | } 1109 | 1110 | /// Consumes a builder, adding a new user to this `AllUsers`. Returns a 1111 | /// reference to the created user. 1112 | /// 1113 | /// Make sure to call [`AllUsers::save`] in order for the new user to be 1114 | /// applied to the system. 1115 | /// 1116 | /// Note that the user's password is set unset (see 1117 | /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords)) 1118 | /// during this call. 1119 | /// 1120 | /// Also note that the user is not added to any groups when this builder is 1121 | /// consumed. In order to keep the system in a consistent state, it is 1122 | /// reccomended to also use an instance of [`AllGroups`] to update group 1123 | /// information when creating new users. 1124 | /// 1125 | /// # Defaults 1126 | /// Fields not passed to the builder before calling this function are as 1127 | /// follows: 1128 | /// - `uid`: [`AllUsers::get_unique_id`] is called on self to get the next 1129 | /// available id. 1130 | /// - `gid`: `99`. This is the default UID for the group `nobody`. Note 1131 | /// that the user is NOT added to this group in `/etc/groups`. 1132 | /// - `name`: The login name passed to [`UserBuilder::new`]. 1133 | /// - `home`: `"/"` 1134 | /// - `shell`: `file:/bin/ion` 1135 | pub fn add_user(&mut self, builder: UserBuilder) -> Result<&User, Error> { 1136 | if !is_valid_name(&builder.user) { 1137 | return Err(Error::InvalidName { name: builder.user }); 1138 | } 1139 | 1140 | let uid = builder.uid.unwrap_or_else(|| 1141 | self.get_unique_id() 1142 | .expect("no remaining unused user ids") 1143 | ); 1144 | 1145 | if self.iter().any(|user| user.user == builder.user || user.uid == uid) { 1146 | Err(Error::UserAlreadyExists) 1147 | } else { 1148 | self.users.push(User { 1149 | user: builder.user.clone(), 1150 | uid, 1151 | gid: builder.gid.unwrap_or(99), 1152 | name: builder.name.unwrap_or(builder.user), 1153 | home: builder.home.unwrap_or("/".to_string()), 1154 | shell: builder.shell.unwrap_or("file:/bin/ion".to_string()), 1155 | auth: auth::Full::unset(), 1156 | auth_delay: self.config.auth_delay 1157 | }); 1158 | Ok(&self.users[self.users.len() - 1]) 1159 | } 1160 | } 1161 | 1162 | /// Syncs the data stored in the `AllUsers` instance to the filesystem. 1163 | /// To apply changes to the system from an `AllUsers`, you MUST call this 1164 | /// function! 1165 | pub fn save(&mut self) -> Result<(), Error> { 1166 | let mut userstring = String::new(); 1167 | 1168 | // Need to be careful to prevent allocations here so that 1169 | // shadowstring can be zeroed when this process is complete. 1170 | // 1 is suppossedly parallelism, not sure exactly what this means. 1171 | // 16 is the max length of a u64, which is used as the salt. 1172 | // 2 accounts for the semicolon separator and newline 1173 | let acfg = argon2::Config::default(); 1174 | let argon_len = argon2::encoded_len( 1175 | acfg.variant, acfg.mem_cost, acfg.time_cost, 1176 | 1, 16, acfg.hash_length) as usize; 1177 | let mut shadowstring = String::with_capacity( 1178 | self.users.len() * (USERNAME_LEN_MAX + argon_len + 2) 1179 | ); 1180 | 1181 | for user in &self.users { 1182 | userstring.push_str(&user.passwd_entry()?); 1183 | 1184 | let mut shadow_entry = user.shadow_entry()?; 1185 | shadowstring.push_str(&shadow_entry); 1186 | 1187 | shadow_entry.zeroize(); 1188 | } 1189 | 1190 | let mut shadow_fd = self.shadow_fd.as_mut() 1191 | .expect("shadow_fd should exist for AllUsers"); 1192 | 1193 | reset_file(&mut self.passwd_fd)?; 1194 | self.passwd_fd.write_all(userstring.as_bytes())?; 1195 | 1196 | reset_file(&mut shadow_fd)?; 1197 | shadow_fd.write_all(shadowstring.as_bytes())?; 1198 | 1199 | shadowstring.zeroize(); 1200 | Ok(()) 1201 | } 1202 | } 1203 | 1204 | impl AllInner for AllUsers { 1205 | type Gruser = User; 1206 | 1207 | fn list(&self) -> &Vec { 1208 | &self.users 1209 | } 1210 | 1211 | fn list_mut(&mut self) -> &mut Vec { 1212 | &mut self.users 1213 | } 1214 | 1215 | fn config(&self) -> &Config { 1216 | &self.config 1217 | } 1218 | } 1219 | 1220 | impl All for AllUsers {} 1221 | /* 1222 | #[cfg(not(target_os = "redox"))] 1223 | impl Drop for AllUsers { 1224 | fn drop(&mut self) { 1225 | eprintln!("Dropping AllUsers"); 1226 | let _ = flock(self.passwd_fd.as_raw_fd(), FlockArg::Unlock); 1227 | if let Some(fd) = self.shadow_fd.as_ref() { 1228 | eprintln!("Shadow"); 1229 | let _ = flock(fd.as_raw_fd(), FlockArg::Unlock); 1230 | } 1231 | } 1232 | } 1233 | */ 1234 | /// `AllGroups` provides (borrowed) access to all groups on the system. Note 1235 | /// that this struct implements [`All`] for all of its access functions. 1236 | /// 1237 | /// General notes that also apply to this struct may be found with 1238 | /// [`AllUsers`]. 1239 | #[derive(Debug)] 1240 | pub struct AllGroups { 1241 | groups: Vec, 1242 | config: Config, 1243 | 1244 | group_fd: File, 1245 | } 1246 | 1247 | impl AllGroups { 1248 | /// Create a new `AllGroups`. 1249 | pub fn new(config: Config) -> Result { 1250 | let mut group_fd = locked_file(config.in_root_fs(GROUP_FILE), config.lock)?; 1251 | let mut group_cntnt = String::new(); 1252 | group_fd.read_to_string(&mut group_cntnt)?; 1253 | 1254 | let mut entries: Vec = Vec::new(); 1255 | for (indx, line) in group_cntnt.lines().enumerate() { 1256 | let group = Group::from_group_entry(line, indx)?; 1257 | entries.push(group); 1258 | } 1259 | 1260 | Ok(AllGroups { 1261 | groups: entries, 1262 | config, 1263 | group_fd, 1264 | }) 1265 | } 1266 | 1267 | /// Consumes a builder, adding a new group to this `AllGroups`. Returns a 1268 | /// reference to the created `Group`. 1269 | /// 1270 | /// Make sure to call [`AllGroups::save`] in order for the new group to be 1271 | /// applied to the system. 1272 | /// 1273 | /// # Defaults 1274 | /// If a builder is not passed a group id ([`GroupBuilder::gid`]) before 1275 | /// being passed to this function, [`AllGroups::get_unique_id`] is used. 1276 | /// 1277 | /// If the builder is not passed any users ([`GroupBuilder::user`]), the 1278 | /// group will still be created. 1279 | pub fn add_group(&mut self, builder: GroupBuilder) -> Result<&Group, Error> { 1280 | let group_exists = self.iter() 1281 | .any(|group| { 1282 | let gid_taken = if let Some(gid) = builder.gid { 1283 | group.gid == gid 1284 | } else { 1285 | false 1286 | }; 1287 | group.group == builder.group || gid_taken 1288 | }); 1289 | 1290 | if group_exists { 1291 | Err(Error::GroupAlreadyExists) 1292 | } else if !is_valid_name(&builder.group) { 1293 | Err(Error::InvalidName { name: builder.group }) 1294 | } else { 1295 | for username in builder.users.iter() { 1296 | if !is_valid_name(username) { 1297 | return Err(Error::InvalidName { name: username.to_string() }); 1298 | } 1299 | } 1300 | 1301 | self.groups.push(Group { 1302 | group: builder.group, 1303 | password: "x".into(), 1304 | gid: builder.gid.unwrap_or_else(|| 1305 | self.get_unique_id() 1306 | .expect("no remaining unused group IDs") 1307 | ), 1308 | users: builder.users, 1309 | }); 1310 | Ok(&self.groups[self.groups.len() - 1]) 1311 | } 1312 | } 1313 | 1314 | /// Syncs the data stored in this `AllGroups` instance to the filesystem. 1315 | /// To apply changes from an `AllGroups`, you MUST call this function! 1316 | pub fn save(&mut self) -> Result<(), Error> { 1317 | let mut groupstring = String::new(); 1318 | for group in &self.groups { 1319 | groupstring.push_str(&group.group_entry()?); 1320 | } 1321 | 1322 | reset_file(&mut self.group_fd)?; 1323 | self.group_fd.write_all(groupstring.as_bytes())?; 1324 | Ok(()) 1325 | } 1326 | } 1327 | 1328 | impl AllInner for AllGroups { 1329 | type Gruser = Group; 1330 | 1331 | fn list(&self) -> &Vec { 1332 | &self.groups 1333 | } 1334 | 1335 | fn list_mut(&mut self) -> &mut Vec { 1336 | &mut self.groups 1337 | } 1338 | 1339 | fn config(&self) -> &Config { 1340 | &self.config 1341 | } 1342 | } 1343 | 1344 | impl All for AllGroups {} 1345 | /* 1346 | #[cfg(not(target_os = "redox"))] 1347 | impl Drop for AllGroups { 1348 | fn drop(&mut self) { 1349 | eprintln!("Dropping AllGroups"); 1350 | let _ = flock(self.group_fd.as_raw_fd(), FlockArg::Unlock); 1351 | } 1352 | }*/ 1353 | 1354 | #[cfg(test)] 1355 | mod test { 1356 | use super::*; 1357 | 1358 | const TEST_PREFIX: &'static str = "tests"; 1359 | 1360 | /// Needed for the file checks, this is done by the library 1361 | fn test_prefix(filename: &str) -> String { 1362 | let mut complete = String::from(TEST_PREFIX); 1363 | complete.push_str(filename); 1364 | complete 1365 | } 1366 | 1367 | #[test] 1368 | fn test_safe_string() { 1369 | assert!(is_safe_string("Hello\\$!")); 1370 | assert!(!is_safe_string("semicolons are awesome; yeah!")); 1371 | } 1372 | 1373 | #[test] 1374 | fn test_portable_filename() { 1375 | let valid = |s| { 1376 | assert!(is_valid_name(s)); 1377 | }; 1378 | let invld = |s| { 1379 | assert!(!is_valid_name(s)); 1380 | }; 1381 | valid("valid"); 1382 | valid("vld.io"); 1383 | valid("hyphen-ated"); 1384 | valid("under_scores"); 1385 | valid("1334"); 1386 | 1387 | invld("-no_flgs"); 1388 | invld("invalid!"); 1389 | invld("also:invalid"); 1390 | invld("coolie-o?"); 1391 | invld("sh"); 1392 | invld("avery_very_very_very_loooooooonnggg-username"); 1393 | } 1394 | 1395 | fn test_cfg() -> Config { 1396 | Config::default() 1397 | // Since all this really does is prepend `sheme` to the consts 1398 | .scheme(TEST_PREFIX.to_string()) 1399 | .writeable(true) 1400 | } 1401 | 1402 | fn read_locked_file(file: impl AsRef) -> Result { 1403 | let mut fd = locked_file(file, Lock::Shared)?; 1404 | let mut cntnt = String::new(); 1405 | fd.read_to_string(&mut cntnt)?; 1406 | Ok(cntnt) 1407 | } 1408 | 1409 | // *** struct.User *** 1410 | #[cfg(feature = "auth")] 1411 | #[test] 1412 | fn attempt_user_api() { 1413 | let mut users = AllUsers::authenticator(test_cfg()).unwrap(); 1414 | let user = users.get_mut_by_id(1000).unwrap(); 1415 | 1416 | assert_eq!(user.is_passwd_blank(), true); 1417 | assert_eq!(user.is_passwd_unset(), false); 1418 | assert_eq!(user.verify_passwd(""), true); 1419 | assert_eq!(user.verify_passwd("Something"), false); 1420 | 1421 | user.set_passwd("hi,i_am_passwd").unwrap(); 1422 | 1423 | assert_eq!(user.is_passwd_blank(), false); 1424 | assert_eq!(user.is_passwd_unset(), false); 1425 | assert_eq!(user.verify_passwd(""), false); 1426 | assert_eq!(user.verify_passwd("Something"), false); 1427 | assert_eq!(user.verify_passwd("hi,i_am_passwd"), true); 1428 | 1429 | user.unset_passwd(); 1430 | 1431 | assert_eq!(user.is_passwd_blank(), false); 1432 | assert_eq!(user.is_passwd_unset(), true); 1433 | assert_eq!(user.verify_passwd(""), false); 1434 | assert_eq!(user.verify_passwd("Something"), false); 1435 | assert_eq!(user.verify_passwd("hi,i_am_passwd"), false); 1436 | 1437 | user.set_passwd("").unwrap(); 1438 | 1439 | assert_eq!(user.is_passwd_blank(), true); 1440 | assert_eq!(user.is_passwd_unset(), false); 1441 | assert_eq!(user.verify_passwd(""), true); 1442 | assert_eq!(user.verify_passwd("Something"), false); 1443 | } 1444 | 1445 | // *** struct.AllUsers *** 1446 | #[cfg(feature = "auth")] 1447 | #[test] 1448 | fn get_user() { 1449 | let users = AllUsers::authenticator(test_cfg()).unwrap(); 1450 | 1451 | let root = users.get_by_id(0).expect("'root' user missing"); 1452 | assert_eq!(root.user, "root".to_string()); 1453 | assert_eq!(root.auth.hash.as_str(), 1454 | "$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk"); 1455 | assert_eq!(root.uid, 0); 1456 | assert_eq!(root.gid, 0); 1457 | assert_eq!(root.name, "root".to_string()); 1458 | assert_eq!(root.home, "file:/root".to_string()); 1459 | assert_eq!(root.shell, "file:/bin/ion".to_string()); 1460 | 1461 | let user = users.get_by_name("user").expect("'user' user missing"); 1462 | assert_eq!(user.user, "user".to_string()); 1463 | assert_eq!(user.auth.hash.as_str(), ""); 1464 | assert_eq!(user.uid, 1000); 1465 | assert_eq!(user.gid, 1000); 1466 | assert_eq!(user.name, "user".to_string()); 1467 | assert_eq!(user.home, "file:/home/user".to_string()); 1468 | assert_eq!(user.shell, "file:/bin/ion".to_string()); 1469 | println!("{:?}", users); 1470 | 1471 | let li = users.get_by_name("loip").expect("'loip' user missing"); 1472 | println!("got loip"); 1473 | assert_eq!(li.user, "loip"); 1474 | assert_eq!(li.auth.hash.as_str(), "!"); 1475 | assert_eq!(li.uid, 1007); 1476 | assert_eq!(li.gid, 1007); 1477 | assert_eq!(li.name, "Lorem".to_string()); 1478 | assert_eq!(li.home, "file:/home/lorem".to_string()); 1479 | assert_eq!(li.shell, "file:/bin/ion".to_string()); 1480 | } 1481 | 1482 | #[cfg(feature = "auth")] 1483 | #[test] 1484 | fn manip_user() { 1485 | let mut users = AllUsers::authenticator(test_cfg()).unwrap(); 1486 | // NOT testing `get_unique_id` 1487 | let id = 7099; 1488 | 1489 | let fb = UserBuilder::new("fbar") 1490 | .uid(id) 1491 | .gid(id) 1492 | .name("Foo Bar") 1493 | .home("/home/foob") 1494 | .shell("/bin/zsh"); 1495 | 1496 | users 1497 | .add_user(fb) 1498 | .expect("failed to add user 'fbar'"); 1499 | // weirdo ^^^^^^^^ :P 1500 | users.save().unwrap(); 1501 | let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); 1502 | assert_eq!( 1503 | p_file_content, 1504 | concat!( 1505 | "root;0;0;root;file:/root;file:/bin/ion\n", 1506 | "user;1000;1000;user;file:/home/user;file:/bin/ion\n", 1507 | "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", 1508 | "fbar;7099;7099;Foo Bar;/home/foob;/bin/zsh\n" 1509 | ) 1510 | ); 1511 | let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); 1512 | assert_eq!(s_file_content, concat!( 1513 | "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", 1514 | "user;\n", 1515 | "loip;!\n", 1516 | "fbar;!\n" 1517 | )); 1518 | 1519 | { 1520 | println!("{:?}", users); 1521 | let fb = users.get_mut_by_name("fbar") 1522 | .expect("'fbar' user missing"); 1523 | fb.shell = "/bin/fish".to_string(); // That's better 1524 | fb.set_passwd("").unwrap(); 1525 | } 1526 | users.save().unwrap(); 1527 | let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); 1528 | assert_eq!( 1529 | p_file_content, 1530 | concat!( 1531 | "root;0;0;root;file:/root;file:/bin/ion\n", 1532 | "user;1000;1000;user;file:/home/user;file:/bin/ion\n", 1533 | "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", 1534 | "fbar;7099;7099;Foo Bar;/home/foob;/bin/fish\n" 1535 | ) 1536 | ); 1537 | let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); 1538 | assert_eq!(s_file_content, concat!( 1539 | "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", 1540 | "user;\n", 1541 | "loip;!\n", 1542 | "fbar;\n" 1543 | )); 1544 | 1545 | users.remove_by_id(id); 1546 | users.save().unwrap(); 1547 | let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); 1548 | assert_eq!( 1549 | file_content, 1550 | concat!( 1551 | "root;0;0;root;file:/root;file:/bin/ion\n", 1552 | "user;1000;1000;user;file:/home/user;file:/bin/ion\n", 1553 | "loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n" 1554 | ) 1555 | ); 1556 | } 1557 | 1558 | /* struct.Group */ 1559 | #[test] 1560 | fn empty_groups() { 1561 | let group_trailing = Group::from_group_entry("nobody;x;2066; ", 0).unwrap(); 1562 | assert_eq!(group_trailing.users.len(), 0); 1563 | 1564 | let group_no_trailing = Group::from_group_entry("nobody;x;2066;", 0).unwrap(); 1565 | assert_eq!(group_no_trailing.users.len(), 0); 1566 | 1567 | assert_eq!(group_trailing.group, group_no_trailing.group); 1568 | assert_eq!(group_trailing.gid, group_no_trailing.gid); 1569 | assert_eq!(group_trailing.users, group_no_trailing.users); 1570 | } 1571 | 1572 | /* struct.AllGroups */ 1573 | #[test] 1574 | fn get_group() { 1575 | let groups = AllGroups::new(test_cfg()).unwrap(); 1576 | let user = groups.get_by_name("user").unwrap(); 1577 | assert_eq!(user.group, "user"); 1578 | assert_eq!(user.gid, 1000); 1579 | assert_eq!(user.users, vec!["user"]); 1580 | 1581 | let wheel = groups.get_by_id(1).unwrap(); 1582 | assert_eq!(wheel.group, "wheel"); 1583 | assert_eq!(wheel.gid, 1); 1584 | assert_eq!(wheel.users, vec!["user", "root"]); 1585 | } 1586 | 1587 | #[test] 1588 | fn manip_group() { 1589 | let id = 7099; 1590 | let mut groups = AllGroups::new(test_cfg()).unwrap(); 1591 | 1592 | let fb = GroupBuilder::new("fbar") 1593 | // NOT testing `get_unique_id` 1594 | .gid(id) 1595 | .user("fbar"); 1596 | 1597 | groups.add_group(fb).unwrap(); 1598 | groups.save().unwrap(); 1599 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); 1600 | assert_eq!( 1601 | file_content, 1602 | concat!( 1603 | "root;x;0;root\n", 1604 | "user;x;1000;user\n", 1605 | "wheel;x;1;user,root\n", 1606 | "loip;x;1007;loip\n", 1607 | "fbar;x;7099;fbar\n" 1608 | ) 1609 | ); 1610 | 1611 | { 1612 | let fb = groups.get_mut_by_name("fbar").unwrap(); 1613 | fb.users.push("user".to_string()); 1614 | } 1615 | groups.save().unwrap(); 1616 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); 1617 | assert_eq!( 1618 | file_content, 1619 | concat!( 1620 | "root;x;0;root\n", 1621 | "user;x;1000;user\n", 1622 | "wheel;x;1;user,root\n", 1623 | "loip;x;1007;loip\n", 1624 | "fbar;x;7099;fbar,user\n" 1625 | ) 1626 | ); 1627 | 1628 | groups.remove_by_id(id); 1629 | groups.save().unwrap(); 1630 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); 1631 | assert_eq!( 1632 | file_content, 1633 | concat!( 1634 | "root;x;0;root\n", 1635 | "user;x;1000;user\n", 1636 | "wheel;x;1;user,root\n", 1637 | "loip;x;1007;loip\n" 1638 | ) 1639 | ); 1640 | } 1641 | 1642 | #[test] 1643 | fn empty_group() { 1644 | let mut groups = AllGroups::new(test_cfg()).unwrap(); 1645 | let nobody = GroupBuilder::new("nobody") 1646 | .gid(2260); 1647 | 1648 | groups.add_group(nobody).unwrap(); 1649 | groups.save().unwrap(); 1650 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); 1651 | assert_eq!( 1652 | file_content, 1653 | concat!( 1654 | "root;x;0;root\n", 1655 | "user;x;1000;user\n", 1656 | "wheel;x;1;user,root\n", 1657 | "loip;x;1007;loip\n", 1658 | "nobody;x;2260;\n", 1659 | ) 1660 | ); 1661 | 1662 | drop(groups); 1663 | let mut groups = AllGroups::new(test_cfg()).unwrap(); 1664 | 1665 | groups.remove_by_name("nobody"); 1666 | groups.save().unwrap(); 1667 | 1668 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); 1669 | assert_eq!( 1670 | file_content, 1671 | concat!( 1672 | "root;x;0;root\n", 1673 | "user;x;1000;user\n", 1674 | "wheel;x;1;user,root\n", 1675 | "loip;x;1007;loip\n" 1676 | ) 1677 | ); 1678 | } 1679 | 1680 | // *** Misc *** 1681 | #[test] 1682 | fn users_get_unused_ids() { 1683 | let users = AllUsers::basic(test_cfg()).unwrap(); 1684 | let id = users.get_unique_id().unwrap(); 1685 | if id < users.config.min_id || id > users.config.max_id { 1686 | panic!("User ID is not between allowed margins") 1687 | } else if let Some(_) = users.get_by_id(id) { 1688 | panic!("User ID is used!"); 1689 | } 1690 | } 1691 | 1692 | #[test] 1693 | fn groups_get_unused_ids() { 1694 | let groups = AllGroups::new(test_cfg()).unwrap(); 1695 | let id = groups.get_unique_id().unwrap(); 1696 | if id < groups.config.min_id || id > groups.config.max_id { 1697 | panic!("Group ID is not between allowed margins") 1698 | } else if let Some(_) = groups.get_by_id(id) { 1699 | panic!("Group ID is used!"); 1700 | } 1701 | } 1702 | } 1703 | -------------------------------------------------------------------------------- /tests/etc/group: -------------------------------------------------------------------------------- 1 | root;x;0;root 2 | user;x;1000;user 3 | wheel;x;1;user,root 4 | loip;x;1007;loip 5 | -------------------------------------------------------------------------------- /tests/etc/passwd: -------------------------------------------------------------------------------- 1 | root;0;0;root;file:/root;file:/bin/ion 2 | user;1000;1000;user;file:/home/user;file:/bin/ion 3 | loip;1007;1007;Lorem;file:/home/lorem;file:/bin/ion 4 | -------------------------------------------------------------------------------- /tests/etc/shadow: -------------------------------------------------------------------------------- 1 | root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk 2 | user; 3 | loip;! 4 | --------------------------------------------------------------------------------