]>
Commit | Line | Data |
---|---|---|
f20569fa XL |
1 | //! `redox-users` is designed to be a small, low-ish level interface |
2 | //! to system user and group information, as well as user password | |
3 | //! authentication. It is OS-specific and will break horribly on platforms | |
4 | //! that are not [Redox-OS](https://redox-os.org). | |
5 | //! | |
6 | //! # Permissions | |
7 | //! Because this is a system level tool dealing with password | |
8 | //! authentication, programs are often required to run with | |
9 | //! escalated priveleges. The implementation of the crate is | |
10 | //! privelege unaware. The only privelege requirements are those | |
11 | //! laid down by the system administrator over these files: | |
12 | //! - `/etc/group` | |
13 | //! - Read: Required to access group information | |
14 | //! - Write: Required to change group information | |
15 | //! - `/etc/passwd` | |
16 | //! - Read: Required to access user information | |
17 | //! - Write: Required to change user information | |
18 | //! - `/etc/shadow` | |
19 | //! - Read: Required to authenticate users | |
20 | //! - Write: Required to set user passwords | |
21 | //! | |
22 | //! # Reimplementation | |
23 | //! This crate is designed to be as small as possible without | |
24 | //! sacrificing critical functionality. The idea is that a small | |
25 | //! enough redox-users will allow easy re-implementation based on | |
26 | //! the same flexible API. This would allow more complicated authentication | |
27 | //! schemes for redox in future without breakage of existing | |
28 | //! software. | |
29 | ||
30 | use std::convert::From; | |
31 | use std::error::Error; | |
32 | use std::fmt::{self, Debug}; | |
33 | use std::fs::{File, OpenOptions}; | |
34 | use std::io::{Read, Seek, SeekFrom, Write}; | |
35 | use std::marker::PhantomData; | |
36 | #[cfg(target_os = "redox")] | |
37 | use std::os::unix::fs::OpenOptionsExt; | |
38 | #[cfg(not(target_os = "redox"))] | |
39 | use std::os::unix::io::AsRawFd; | |
40 | use std::os::unix::process::CommandExt; | |
41 | use std::path::{Path, PathBuf}; | |
42 | use std::process::Command; | |
43 | use std::slice::{Iter, IterMut}; | |
44 | #[cfg(not(test))] | |
45 | #[cfg(feature = "auth")] | |
46 | use std::thread; | |
47 | use std::time::Duration; | |
48 | ||
49 | //#[cfg(not(target_os = "redox"))] | |
50 | //use nix::fcntl::{flock, FlockArg}; | |
51 | ||
52 | #[cfg(target_os = "redox")] | |
53 | use syscall::flag::{O_EXLOCK, O_SHLOCK}; | |
54 | use syscall::Error as SyscallError; | |
55 | ||
56 | const PASSWD_FILE: &'static str = "/etc/passwd"; | |
57 | const GROUP_FILE: &'static str = "/etc/group"; | |
58 | #[cfg(feature = "auth")] | |
59 | const SHADOW_FILE: &'static str = "/etc/shadow"; | |
60 | ||
61 | #[cfg(target_os = "redox")] | |
62 | const DEFAULT_SCHEME: &'static str = "file:"; | |
63 | #[cfg(not(target_os = "redox"))] | |
64 | const DEFAULT_SCHEME: &'static str = ""; | |
65 | ||
66 | const MIN_ID: usize = 1000; | |
67 | const MAX_ID: usize = 6000; | |
68 | const DEFAULT_TIMEOUT: u64 = 3; | |
69 | ||
70 | #[cfg(feature = "auth")] | |
71 | const USER_AUTH_FULL_EXPECTED_HASH: &str = "A User<auth::Full> had no hash"; | |
72 | ||
73 | pub type Result<T> = std::result::Result<T, Box<dyn Error + Send + Sync>>; | |
74 | ||
75 | /// Errors that might happen while using this crate | |
76 | #[derive(Debug, PartialEq)] | |
77 | pub enum UsersError { | |
78 | Os { reason: String }, | |
79 | Parsing { reason: String, line: usize }, | |
80 | NotFound, | |
81 | AlreadyExists | |
82 | } | |
83 | ||
84 | impl fmt::Display for UsersError { | |
85 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
86 | match self { | |
87 | UsersError::Os { reason } => write!(f, "os error: code {}", reason), | |
88 | UsersError::Parsing { reason, line } => { | |
89 | write!(f, "parse error line {}: {}", line, reason) | |
90 | }, | |
91 | UsersError::NotFound => write!(f, "user/group not found"), | |
92 | UsersError::AlreadyExists => write!(f, "user/group already exists") | |
93 | } | |
94 | } | |
95 | } | |
96 | ||
97 | impl Error for UsersError { | |
98 | fn description(&self) -> &str { "UsersError" } | |
99 | ||
100 | fn cause(&self) -> Option<&dyn Error> { None } | |
101 | } | |
102 | ||
103 | #[inline] | |
104 | fn parse_error(line: usize, reason: &str) -> UsersError { | |
105 | UsersError::Parsing { | |
106 | reason: reason.into(), | |
107 | line, | |
108 | } | |
109 | } | |
110 | ||
111 | #[inline] | |
112 | fn os_error(reason: &str) -> UsersError { | |
113 | UsersError::Os { | |
114 | reason: reason.into() | |
115 | } | |
116 | } | |
117 | ||
118 | impl From<SyscallError> for UsersError { | |
119 | fn from(syscall_error: SyscallError) -> UsersError { | |
120 | UsersError::Os { | |
121 | reason: format!("{}", syscall_error) | |
122 | } | |
123 | } | |
124 | } | |
125 | ||
126 | #[derive(Clone, Copy)] | |
127 | #[allow(dead_code)] | |
128 | enum Lock { | |
129 | Shared, | |
130 | Exclusive, | |
131 | } | |
132 | ||
133 | impl Lock { | |
134 | #[cfg(target_os = "redox")] | |
135 | fn as_olock(self) -> i32 { | |
136 | (match self { | |
137 | Lock::Shared => O_SHLOCK, | |
138 | Lock::Exclusive => O_EXLOCK, | |
139 | }) as i32 | |
140 | } | |
141 | ||
142 | /*#[cfg(not(target_os = "redox"))] | |
143 | fn as_flock(self) -> FlockArg { | |
144 | match self { | |
145 | Lock::Shared => FlockArg::LockShared, | |
146 | Lock::Exclusive => FlockArg::LockExclusive, | |
147 | } | |
148 | }*/ | |
149 | } | |
150 | ||
151 | /// Naive semi-cross platform file locking (need to support linux for tests). | |
152 | #[allow(dead_code)] | |
153 | fn locked_file(file: impl AsRef<Path>, _lock: Lock) -> Result<File> { | |
154 | #[cfg(test)] | |
155 | println!("Open file: {}", file.as_ref().display()); | |
156 | ||
157 | #[cfg(target_os = "redox")] | |
158 | { | |
159 | Ok(OpenOptions::new() | |
160 | .read(true) | |
161 | .write(true) | |
162 | .custom_flags(_lock.as_olock()) | |
163 | .open(file)?) | |
164 | } | |
165 | #[cfg(not(target_os = "redox"))] | |
166 | #[cfg_attr(rustfmt, rustfmt_skip)] | |
167 | { | |
168 | let file = OpenOptions::new() | |
169 | .read(true) | |
170 | .write(true) | |
171 | .open(file)?; | |
172 | let fd = file.as_raw_fd(); | |
173 | eprintln!("Fd: {}", fd); | |
174 | //flock(fd, _lock.as_flock())?; | |
175 | Ok(file) | |
176 | } | |
177 | } | |
178 | ||
179 | /// Reset a file for rewriting (user/group dbs must be erased before write-out) | |
180 | fn reset_file(fd: &mut File) -> Result<()> { | |
181 | fd.set_len(0)?; | |
182 | fd.seek(SeekFrom::Start(0))?; | |
183 | Ok(()) | |
184 | } | |
185 | ||
186 | /// Marker types for [`User`] and [`AllUsers`]. | |
187 | pub mod auth { | |
188 | /// Marker type indicating that a `User` only has access to world-readable | |
189 | /// user information, and cannot authenticate. | |
190 | #[derive(Debug)] | |
191 | pub struct Basic {} | |
192 | ||
193 | /// Marker type indicating that a `User` has access to all user | |
194 | /// information, including password hashes. | |
195 | #[cfg(feature = "auth")] | |
196 | #[derive(Debug)] | |
197 | pub struct Full {} | |
198 | } | |
199 | ||
200 | /// A struct representing a Redox user. | |
201 | /// Currently maps to an entry in the `/etc/passwd` file. | |
202 | /// | |
203 | /// `A` should be a type from [`crate::auth`]. | |
204 | /// | |
205 | /// # Unset vs. Blank Passwords | |
206 | /// A note on unset passwords vs. blank passwords. A blank password | |
207 | /// is a hash field that is completely blank (aka, `""`). According | |
208 | /// to this crate, successful login is only allowed if the input | |
209 | /// password is blank as well. | |
210 | /// | |
211 | /// An unset password is one whose hash is not empty (`""`), but | |
212 | /// also not a valid serialized argon2rs hashing session. This | |
213 | /// hash always returns `false` upon attempted verification. The | |
214 | /// most commonly used hash for an unset password is `"!"`, but | |
215 | /// this crate makes no distinction. The most common way to unset | |
216 | /// the password is to use [`User::unset_passwd`]. | |
217 | pub struct User<A> { | |
218 | /// Username (login name) | |
219 | pub user: String, | |
220 | /// User id | |
221 | pub uid: usize, | |
222 | /// Group id | |
223 | pub gid: usize, | |
224 | /// Real name (human readable, can contain spaces) | |
225 | pub name: String, | |
226 | /// Home directory path | |
227 | pub home: String, | |
228 | /// Shell path | |
229 | pub shell: String, | |
230 | ||
231 | // Stored password hash text and an indicator to determine if the text is a | |
232 | // hash. | |
233 | #[cfg(feature = "auth")] | |
234 | hash: Option<(String, bool)>, | |
235 | // Failed login delay duration | |
236 | auth_delay: Duration, | |
237 | auth: PhantomData<A>, | |
238 | } | |
239 | ||
240 | impl<A> User<A> { | |
241 | /// Get a Command to run the user's default shell (see [`User::login_cmd`] | |
242 | /// for more docs). | |
243 | pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) } | |
244 | ||
245 | /// Provide a login command for the user, which is any entry point for | |
246 | /// starting a user's session, whether a shell (use [`User::shell_cmd`] | |
247 | /// instead) or a graphical init. | |
248 | /// | |
249 | /// The `Command` will use the user's `uid` and `gid`, its `current_dir` | |
250 | /// will be set to the user's home directory, and the follwing enviroment | |
251 | /// variables will be populated: | |
252 | /// | |
253 | /// - `USER` set to the user's `user` field. | |
254 | /// - `UID` set to the user's `uid` field. | |
255 | /// - `GROUPS` set the user's `gid` field. | |
256 | /// - `HOME` set to the user's `home` field. | |
257 | /// - `SHELL` set to the user's `shell` field. | |
258 | pub fn login_cmd<T>(&self, cmd: T) -> Command | |
259 | where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str> | |
260 | { | |
261 | let mut command = Command::new(cmd); | |
262 | command | |
263 | .uid(self.uid as u32) | |
264 | .gid(self.gid as u32) | |
265 | .current_dir(&self.home) | |
266 | .env("USER", &self.user) | |
267 | .env("UID", format!("{}", self.uid)) | |
268 | .env("GROUPS", format!("{}", self.gid)) | |
269 | .env("HOME", &self.home) | |
270 | .env("SHELL", &self.shell); | |
271 | command | |
272 | } | |
273 | ||
274 | fn from_passwd_entry(s: &str, line: usize) -> Result<Self> { | |
275 | let mut parts = s.split(';'); | |
276 | ||
277 | let user = parts | |
278 | .next() | |
279 | .ok_or(parse_error(line, "expected user"))?; | |
280 | let uid = parts | |
281 | .next() | |
282 | .ok_or(parse_error(line, "expected uid"))? | |
283 | .parse::<usize>()?; | |
284 | let gid = parts | |
285 | .next() | |
286 | .ok_or(parse_error(line, "expected uid"))? | |
287 | .parse::<usize>()?; | |
288 | let name = parts | |
289 | .next() | |
290 | .ok_or(parse_error(line, "expected real name"))?; | |
291 | let home = parts | |
292 | .next() | |
293 | .ok_or(parse_error(line, "expected home dir path"))?; | |
294 | let shell = parts | |
295 | .next() | |
296 | .ok_or(parse_error(line, "expected shell path"))?; | |
297 | ||
298 | Ok(User::<A> { | |
299 | user: user.into(), | |
300 | uid, | |
301 | gid, | |
302 | name: name.into(), | |
303 | home: home.into(), | |
304 | shell: shell.into(), | |
305 | #[cfg(feature = "auth")] | |
306 | hash: None, | |
307 | auth: PhantomData, | |
308 | auth_delay: Duration::default(), | |
309 | }) | |
310 | } | |
311 | ||
312 | /// Format this user as an entry in `/etc/passwd`. | |
313 | fn passwd_entry(&self) -> String { | |
314 | #[cfg_attr(rustfmt, rustfmt_skip)] | |
315 | format!("{};{};{};{};{};{}\n", | |
316 | self.user, self.uid, self.gid, self.name, self.home, self.shell | |
317 | ) | |
318 | } | |
319 | } | |
320 | ||
321 | /// Additional methods for if this `User` is authenticatable. | |
322 | #[cfg(feature = "auth")] | |
323 | impl User<auth::Full> { | |
324 | /// Set the password for a user. Make **sure** that `password` | |
325 | /// is actually what the user wants as their password (this doesn't). | |
326 | /// | |
327 | /// To set the password blank, pass `""` as `password`. | |
328 | pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<()> { | |
329 | let password = password.as_ref(); | |
330 | ||
331 | self.hash = if password != "" { | |
332 | let mut buf = [0u8; 8]; | |
333 | getrandom::getrandom(&mut buf)?; | |
334 | let salt = format!("{:X}", u64::from_ne_bytes(buf)); | |
335 | let config = argon2::Config::default(); | |
336 | let hash = argon2::hash_encoded( | |
337 | password.as_bytes(), | |
338 | salt.as_bytes(), | |
339 | &config | |
340 | )?; | |
341 | Some((hash, true)) | |
342 | } else { | |
343 | Some(("".into(), false)) | |
344 | }; | |
345 | Ok(()) | |
346 | } | |
347 | ||
348 | /// Unset the password ([`User::verify_passwd`] always returns `false`). | |
349 | pub fn unset_passwd(&mut self) { | |
350 | self.hash = Some(("!".into(), false)); | |
351 | } | |
352 | ||
353 | /// Verify the password. If the hash is empty, this only returns `true` if | |
354 | /// `password` is also empty. | |
355 | /// | |
356 | /// Note that this is a blocking operation if the password is incorrect. | |
357 | /// See [`Config::auth_delay`] to set the wait time. Default is 3 seconds. | |
358 | pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool { | |
359 | let &(ref hash, ref encoded) = self.hash.as_ref() | |
360 | .expect(USER_AUTH_FULL_EXPECTED_HASH); | |
361 | let password = password.as_ref(); | |
362 | ||
363 | let verified = if *encoded { | |
364 | argon2::verify_encoded(&hash, password.as_bytes()).unwrap() | |
365 | } else { | |
366 | hash == "" && password == "" | |
367 | }; | |
368 | ||
369 | if !verified { | |
370 | #[cfg(not(test))] // Make tests run faster | |
371 | thread::sleep(self.auth_delay); | |
372 | } | |
373 | verified | |
374 | } | |
375 | ||
376 | /// Determine if the hash for the password is blank ([`User::verify_passwd`] | |
377 | /// returns `true` *only* when the password is blank). | |
378 | pub fn is_passwd_blank(&self) -> bool { | |
379 | let &(ref hash, ref encoded) = self.hash.as_ref() | |
380 | .expect(USER_AUTH_FULL_EXPECTED_HASH); | |
381 | hash == "" && ! encoded | |
382 | } | |
383 | ||
384 | /// Determine if the hash for the password is unset | |
385 | /// ([`User::verify_passwd`] returns `false` regardless of input). | |
386 | pub fn is_passwd_unset(&self) -> bool { | |
387 | let &(ref hash, ref encoded) = self.hash.as_ref() | |
388 | .expect(USER_AUTH_FULL_EXPECTED_HASH); | |
389 | hash != "" && ! encoded | |
390 | } | |
391 | ||
392 | fn shadow_entry(&self) -> String { | |
393 | let hashstring = match self.hash { | |
394 | Some((ref hash, _)) => hash, | |
395 | None => panic!(USER_AUTH_FULL_EXPECTED_HASH) | |
396 | }; | |
397 | format!("{};{}\n", self.user, hashstring) | |
398 | } | |
399 | ||
400 | /// Give this a hash string (not a shadowfile entry!!!) | |
401 | fn populate_hash(&mut self, hash: &str) -> Result<()> { | |
402 | let encoded = match hash { | |
403 | "" => false, | |
404 | "!" => false, | |
405 | _ => true, | |
406 | }; | |
407 | self.hash = Some((hash.to_string(), encoded)); | |
408 | Ok(()) | |
409 | } | |
410 | } | |
411 | ||
412 | impl<A> Name for User<A> { | |
413 | fn name(&self) -> &str { | |
414 | &self.user | |
415 | } | |
416 | } | |
417 | ||
418 | impl<A> Id for User<A> { | |
419 | fn id(&self) -> usize { | |
420 | self.uid | |
421 | } | |
422 | } | |
423 | ||
424 | impl<A> Debug for User<A> { | |
425 | fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { | |
426 | f.debug_struct("User") | |
427 | .field("user", &self.user) | |
428 | .field("uid", &self.uid) | |
429 | .field("gid", &self.gid) | |
430 | .field("name", &self.name) | |
431 | .field("home", &self.home) | |
432 | .field("shell", &self.shell) | |
433 | .field("auth_delay", &self.auth_delay) | |
434 | .finish() | |
435 | } | |
436 | } | |
437 | ||
438 | /// A struct representing a Redox user group. | |
439 | /// Currently maps to an `/etc/group` file entry. | |
440 | #[derive(Debug)] | |
441 | pub struct Group { | |
442 | /// Group name | |
443 | pub group: String, | |
444 | /// Unique group id | |
445 | pub gid: usize, | |
446 | /// Group members' usernames | |
447 | pub users: Vec<String>, | |
448 | } | |
449 | ||
450 | impl Group { | |
451 | fn from_group_entry(s: &str, line: usize) -> Result<Self> { | |
452 | let mut parts = s.trim() | |
453 | .split(';'); | |
454 | ||
455 | let group = parts | |
456 | .next() | |
457 | .ok_or(parse_error(line, "expected group"))?; | |
458 | let gid = parts | |
459 | .next() | |
460 | .ok_or(parse_error(line, "expected gid"))? | |
461 | .parse::<usize>()?; | |
462 | let users_str = parts.next() | |
463 | .unwrap_or(""); | |
464 | let users = users_str.split(',') | |
465 | .filter_map(|u| if u == "" { | |
466 | None | |
467 | } else { | |
468 | Some(u.into()) | |
469 | }) | |
470 | .collect(); | |
471 | ||
472 | Ok(Group { | |
473 | group: group.into(), | |
474 | gid, | |
475 | users, | |
476 | }) | |
477 | } | |
478 | ||
479 | /// Format this group as an entry in `/etc/group`. This | |
480 | /// is an implementation detail, do NOT rely on this trait | |
481 | /// being implemented in future. | |
482 | fn group_entry(&self) -> String { | |
483 | #[cfg_attr(rustfmt, rustfmt_skip)] | |
484 | format!("{};{};{}\n", | |
485 | self.group, | |
486 | self.gid, | |
487 | self.users.join(",").trim_matches(',') | |
488 | ) | |
489 | } | |
490 | } | |
491 | ||
492 | impl Name for Group { | |
493 | fn name(&self) -> &str { | |
494 | &self.group | |
495 | } | |
496 | } | |
497 | ||
498 | impl Id for Group { | |
499 | fn id(&self) -> usize { | |
500 | self.gid | |
501 | } | |
502 | } | |
503 | ||
504 | /// Gets the current process effective user ID. | |
505 | /// | |
506 | /// This function issues the `geteuid` system call returning the process effective | |
507 | /// user id. | |
508 | /// | |
509 | /// # Examples | |
510 | /// | |
511 | /// Basic usage: | |
512 | /// | |
513 | /// ```no_run | |
514 | /// # use redox_users::get_euid; | |
515 | /// let euid = get_euid().unwrap(); | |
516 | /// ``` | |
517 | pub fn get_euid() -> Result<usize> { | |
518 | match syscall::geteuid() { | |
519 | Ok(euid) => Ok(euid), | |
520 | Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) | |
521 | } | |
522 | } | |
523 | ||
524 | /// Gets the current process real user ID. | |
525 | /// | |
526 | /// This function issues the `getuid` system call returning the process real | |
527 | /// user id. | |
528 | /// | |
529 | /// # Examples | |
530 | /// | |
531 | /// Basic usage: | |
532 | /// | |
533 | /// ```no_run | |
534 | /// # use redox_users::get_uid; | |
535 | /// let uid = get_uid().unwrap(); | |
536 | /// ``` | |
537 | pub fn get_uid() -> Result<usize> { | |
538 | match syscall::getuid() { | |
539 | Ok(uid) => Ok(uid), | |
540 | Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) | |
541 | } | |
542 | } | |
543 | ||
544 | /// Gets the current process effective group ID. | |
545 | /// | |
546 | /// This function issues the `getegid` system call returning the process effective | |
547 | /// group id. | |
548 | /// | |
549 | /// # Examples | |
550 | /// | |
551 | /// Basic usage: | |
552 | /// | |
553 | /// ```no_run | |
554 | /// # use redox_users::get_egid; | |
555 | /// let egid = get_egid().unwrap(); | |
556 | /// ``` | |
557 | pub fn get_egid() -> Result<usize> { | |
558 | match syscall::getegid() { | |
559 | Ok(egid) => Ok(egid), | |
560 | Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) | |
561 | } | |
562 | } | |
563 | ||
564 | /// Gets the current process real group ID. | |
565 | /// | |
566 | /// This function issues the `getegid` system call returning the process real | |
567 | /// group id. | |
568 | /// | |
569 | /// # Examples | |
570 | /// | |
571 | /// Basic usage: | |
572 | /// | |
573 | /// ```no_run | |
574 | /// # use redox_users::get_gid; | |
575 | /// let gid = get_gid().unwrap(); | |
576 | /// ``` | |
577 | pub fn get_gid() -> Result<usize> { | |
578 | match syscall::getgid() { | |
579 | Ok(gid) => Ok(gid), | |
580 | Err(syscall_error) => Err(From::from(os_error(syscall_error.text()))) | |
581 | } | |
582 | } | |
583 | ||
584 | /// A generic configuration that allows fine control of an [`AllUsers`] or | |
585 | /// [`AllGroups`]. | |
586 | /// | |
587 | /// `auth_delay` is not used by [`AllGroups`] | |
588 | /// | |
589 | /// In most situations, [`Config::default`](struct.Config.html#impl-Default) | |
590 | /// will work just fine. The other fields are for finer control if it is | |
591 | /// required. | |
592 | /// | |
593 | /// # Example | |
594 | /// ``` | |
595 | /// # use redox_users::Config; | |
596 | /// use std::time::Duration; | |
597 | /// | |
598 | /// let cfg = Config::default() | |
599 | /// .min_id(500) | |
600 | /// .max_id(1000) | |
601 | /// .auth_delay(Duration::from_secs(5)); | |
602 | /// ``` | |
603 | #[derive(Clone, Debug)] | |
604 | pub struct Config { | |
605 | scheme: String, | |
606 | auth_delay: Duration, | |
607 | min_id: usize, | |
608 | max_id: usize, | |
609 | } | |
610 | ||
611 | impl Config { | |
612 | /// Set the delay for a failed authentication. Default is 3 seconds. | |
613 | pub fn auth_delay(mut self, delay: Duration) -> Config { | |
614 | self.auth_delay = delay; | |
615 | self | |
616 | } | |
617 | ||
618 | /// Set the smallest ID possible to use when finding an unused ID. | |
619 | pub fn min_id(mut self, id: usize) -> Config { | |
620 | self.min_id = id; | |
621 | self | |
622 | } | |
623 | ||
624 | /// Set the largest possible ID to use when finding an unused ID. | |
625 | pub fn max_id(mut self, id: usize) -> Config { | |
626 | self.max_id = id; | |
627 | self | |
628 | } | |
629 | ||
630 | /// Set the scheme relative to which the [`AllUsers`] or [`AllGroups`] | |
631 | /// should be looking for its data files. This is a compromise between | |
632 | /// exposing implementation details and providing fine enough | |
633 | /// control over the behavior of this API. | |
634 | pub fn scheme(mut self, scheme: String) -> Config { | |
635 | self.scheme = scheme; | |
636 | self | |
637 | } | |
638 | ||
639 | // Prepend a path with the scheme in this Config | |
640 | fn in_scheme(&self, path: impl AsRef<Path>) -> PathBuf { | |
641 | let mut canonical_path = PathBuf::from(&self.scheme); | |
642 | // Should be a little careful here, not sure I want this behavior | |
643 | if path.as_ref().is_absolute() { | |
644 | // This is nasty | |
645 | canonical_path.push(path.as_ref().to_string_lossy()[1..].to_string()); | |
646 | } else { | |
647 | canonical_path.push(path); | |
648 | } | |
649 | canonical_path | |
650 | } | |
651 | } | |
652 | ||
653 | impl Default for Config { | |
654 | /// The default base scheme is `file:`. | |
655 | /// | |
656 | /// The default auth delay is 3 seconds. | |
657 | /// | |
658 | /// The default min and max ids are 1000 and 6000. | |
659 | fn default() -> Config { | |
660 | Config { | |
661 | scheme: String::from(DEFAULT_SCHEME), | |
662 | auth_delay: Duration::new(DEFAULT_TIMEOUT, 0), | |
663 | min_id: MIN_ID, | |
664 | max_id: MAX_ID, | |
665 | } | |
666 | } | |
667 | } | |
668 | ||
669 | // Nasty hack to prevent the compiler complaining about | |
670 | // "leaking" `AllInner` | |
671 | mod sealed { | |
672 | use crate::Config; | |
673 | ||
674 | pub trait Name { | |
675 | fn name(&self) -> &str; | |
676 | } | |
677 | ||
678 | pub trait Id { | |
679 | fn id(&self) -> usize; | |
680 | } | |
681 | ||
682 | pub trait AllInner { | |
683 | // Group+User, thanks Dad | |
684 | type Gruser: Name + Id; | |
685 | ||
686 | /// These functions grab internal elements so that the other | |
687 | /// methods of `All` can manipulate them. | |
688 | fn list(&self) -> &Vec<Self::Gruser>; | |
689 | fn list_mut(&mut self) -> &mut Vec<Self::Gruser>; | |
690 | fn config(&self) -> &Config; | |
691 | } | |
692 | } | |
693 | ||
694 | use sealed::{AllInner, Id, Name}; | |
695 | ||
696 | /// This trait is used to remove repetitive API items from | |
697 | /// [`AllGroups`] and [`AllUsers`]. It uses a hidden trait | |
698 | /// so that the implementations of functions can be implemented | |
699 | /// at the trait level. Do not try to implement this trait. | |
700 | pub trait All: AllInner { | |
701 | /// Get an iterator borrowing all [`User`]s or [`Group`]s on the system. | |
702 | fn iter(&self) -> Iter<<Self as AllInner>::Gruser> { | |
703 | self.list().iter() | |
704 | } | |
705 | ||
706 | /// Get an iterator mutably borrowing all [`User`]s or [`Group`]s on the | |
707 | /// system. | |
708 | fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> { | |
709 | self.list_mut().iter_mut() | |
710 | } | |
711 | ||
712 | /// Borrow the [`User`] or [`Group`] with a given name. | |
713 | /// | |
714 | /// # Examples | |
715 | /// | |
716 | /// Basic usage: | |
717 | /// | |
718 | /// ```no_run | |
719 | /// # use redox_users::{All, AllUsers, Config}; | |
720 | /// let users = AllUsers::basic(Config::default()).unwrap(); | |
721 | /// let user = users.get_by_name("root").unwrap(); | |
722 | /// ``` | |
723 | fn get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser> { | |
724 | self.iter() | |
725 | .find(|gruser| gruser.name() == name.as_ref() ) | |
726 | } | |
727 | ||
728 | /// Mutable version of [`All::get_by_name`]. | |
729 | fn get_mut_by_name(&mut self, name: impl AsRef<str>) -> Option<&mut <Self as AllInner>::Gruser> { | |
730 | self.iter_mut() | |
731 | .find(|gruser| gruser.name() == name.as_ref() ) | |
732 | } | |
733 | ||
734 | /// Borrow the [`User`] or [`Group`] with the given ID. | |
735 | /// | |
736 | /// # Examples | |
737 | /// | |
738 | /// Basic usage: | |
739 | /// | |
740 | /// ```no_run | |
741 | /// # use redox_users::{All, AllUsers, Config}; | |
742 | /// let users = AllUsers::basic(Config::default()).unwrap(); | |
743 | /// let user = users.get_by_id(0).unwrap(); | |
744 | /// ``` | |
745 | fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> { | |
746 | self.iter() | |
747 | .find(|gruser| gruser.id() == id ) | |
748 | } | |
749 | ||
750 | /// Mutable version of [`All::get_by_id`]. | |
751 | fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> { | |
752 | self.iter_mut() | |
753 | .find(|gruser| gruser.id() == id ) | |
754 | } | |
755 | ||
756 | /// Provides an unused id based on the min and max values in the [`Config`] | |
757 | /// passed to the `All`'s constructor. | |
758 | /// | |
759 | /// # Examples | |
760 | /// | |
761 | /// ```no_run | |
762 | /// # use redox_users::{All, AllUsers, Config}; | |
763 | /// let users = AllUsers::basic(Config::default()).unwrap(); | |
764 | /// let uid = users.get_unique_id().expect("no available uid"); | |
765 | /// ``` | |
766 | fn get_unique_id(&self) -> Option<usize> { | |
767 | for id in self.config().min_id..self.config().max_id { | |
768 | if !self.iter().any(|gruser| gruser.id() == id ) { | |
769 | return Some(id) | |
770 | } | |
771 | } | |
772 | None | |
773 | } | |
774 | ||
775 | /// Remove a [`User`] or [`Group`] from this `All` given it's name. If the | |
776 | /// Gruser was removed return `true`, else return `false`. This ensures | |
777 | /// that the Gruser no longer exists. | |
778 | fn remove_by_name(&mut self, name: impl AsRef<str>) -> bool { | |
779 | let list = self.list_mut(); | |
780 | let indx = list.iter() | |
781 | .enumerate() | |
782 | .find_map(|(indx, gruser)| if gruser.name() == name.as_ref() { | |
783 | Some(indx) | |
784 | } else { | |
785 | None | |
786 | }); | |
787 | if let Some(indx) = indx { | |
788 | list.remove(indx); | |
789 | true | |
790 | } else { | |
791 | false | |
792 | } | |
793 | } | |
794 | ||
795 | /// Id version of [`All::remove_by_name`]. | |
796 | fn remove_by_id(&mut self, id: usize) -> bool { | |
797 | let list = self.list_mut(); | |
798 | let indx = list.iter() | |
799 | .enumerate() | |
800 | .find_map(|(indx, gruser)| if gruser.id() == id { | |
801 | Some(indx) | |
802 | } else { | |
803 | None | |
804 | }); | |
805 | if let Some(indx) = indx { | |
806 | list.remove(indx); | |
807 | true | |
808 | } else { | |
809 | false | |
810 | } | |
811 | } | |
812 | } | |
813 | ||
814 | /// `AllUsers` provides (borrowed) access to all the users on the system. | |
815 | /// Note that this struct implements [`All`] for all of its access functions. | |
816 | /// | |
817 | /// # Notes | |
818 | /// Note that everything in this section also applies to [`AllGroups`]. | |
819 | /// | |
820 | /// * If you mutate anything owned by an `AllUsers`, you must call the | |
821 | /// [`AllUsers::save`] in order for those changes to be applied to the system. | |
822 | /// * The API here is kept small. Most mutating actions can be accomplished via | |
823 | /// the [`All::get_mut_by_id`] and [`All::get_mut_by_name`] | |
824 | /// functions. | |
825 | #[derive(Debug)] | |
826 | pub struct AllUsers<A> { | |
827 | users: Vec<User<A>>, | |
828 | config: Config, | |
829 | ||
830 | // Hold on to the locked fds to prevent race conditions | |
831 | passwd_fd: File, | |
832 | shadow_fd: Option<File>, | |
833 | } | |
834 | ||
835 | impl<A> AllUsers<A> { | |
836 | fn new(config: Config) -> Result<AllUsers<A>> { | |
837 | let mut passwd_fd = locked_file(config.in_scheme(PASSWD_FILE), Lock::Exclusive)?; | |
838 | let mut passwd_cntnt = String::new(); | |
839 | passwd_fd.read_to_string(&mut passwd_cntnt)?; | |
840 | ||
841 | let mut passwd_entries = Vec::new(); | |
842 | for (indx, line) in passwd_cntnt.lines().enumerate() { | |
843 | let mut user = User::from_passwd_entry(line, indx)?; | |
844 | user.auth_delay = config.auth_delay; | |
845 | passwd_entries.push(user); | |
846 | } | |
847 | ||
848 | Ok(AllUsers::<A> { | |
849 | users: passwd_entries, | |
850 | config, | |
851 | passwd_fd, | |
852 | shadow_fd: None, | |
853 | }) | |
854 | } | |
855 | } | |
856 | ||
857 | impl AllUsers<auth::Basic> { | |
858 | /// Provide access to all user information on the system except | |
859 | /// authentication. This is adequate for almost all uses of `AllUsers`. | |
860 | pub fn basic(config: Config) -> Result<AllUsers<auth::Basic>> { | |
861 | Self::new(config) | |
862 | } | |
863 | } | |
864 | ||
865 | #[cfg(feature = "auth")] | |
866 | impl AllUsers<auth::Full> { | |
867 | /// If access to password related methods for the [`User`]s yielded by this | |
868 | /// `AllUsers` is required, use this constructor. | |
869 | pub fn authenticator(config: Config) -> Result<AllUsers<auth::Full>> { | |
870 | let mut shadow_fd = locked_file(config.in_scheme(SHADOW_FILE), Lock::Exclusive)?; | |
871 | let mut shadow_cntnt = String::new(); | |
872 | shadow_fd.read_to_string(&mut shadow_cntnt)?; | |
873 | let shadow_entries: Vec<&str> = shadow_cntnt.lines().collect(); | |
874 | ||
875 | let mut new = Self::new(config)?; | |
876 | new.shadow_fd = Some(shadow_fd); | |
877 | ||
878 | for (indx, entry) in shadow_entries.iter().enumerate() { | |
879 | let mut entry = entry.split(';'); | |
880 | let name = entry.next().ok_or(parse_error(indx, | |
881 | "error parsing shadowfile: expected username" | |
882 | ))?; | |
883 | let hash = entry.next().ok_or(parse_error(indx, | |
884 | "error parsing shadowfile: expected hash" | |
885 | ))?; | |
886 | new.users | |
887 | .iter_mut() | |
888 | .find(|user| user.user == name) | |
889 | .ok_or(parse_error(indx, | |
890 | "error parsing shadowfile: unkown user" | |
891 | ))? | |
892 | .populate_hash(hash)?; | |
893 | } | |
894 | ||
895 | Ok(new) | |
896 | } | |
897 | ||
898 | /// Adds a user with the specified attributes to the `AllUsers` | |
899 | /// instance. Note that the user's password is set unset (see | |
900 | /// [Unset vs Blank Passwords](struct.User.html#unset-vs-blank-passwords)) | |
901 | /// during this call. | |
902 | /// | |
903 | /// Make sure to call [`AllUsers::save`] in order for the new user to be | |
904 | /// applied to the system. | |
905 | //TODO: Take uid/gid as Option<usize> and if none, find an unused ID. | |
906 | pub fn add_user( | |
907 | &mut self, | |
908 | login: &str, | |
909 | uid: usize, | |
910 | gid: usize, | |
911 | name: &str, | |
912 | home: &str, | |
913 | shell: &str | |
914 | ) -> Result<()> { | |
915 | if self.iter() | |
916 | .any(|user| user.user == login || user.uid == uid) | |
917 | { | |
918 | return Err(From::from(UsersError::AlreadyExists)) | |
919 | } | |
920 | ||
921 | self.users.push(User { | |
922 | user: login.into(), | |
923 | uid, | |
924 | gid, | |
925 | name: name.into(), | |
926 | home: home.into(), | |
927 | shell: shell.into(), | |
928 | hash: Some(("!".into(), false)), | |
929 | auth: PhantomData, | |
930 | auth_delay: self.config.auth_delay | |
931 | }); | |
932 | Ok(()) | |
933 | } | |
934 | ||
935 | /// Syncs the data stored in the `AllUsers` instance to the filesystem. | |
936 | /// To apply changes to the system from an `AllUsers`, you MUST call this | |
937 | /// function! | |
938 | pub fn save(&mut self) -> Result<()> { | |
939 | let mut userstring = String::new(); | |
940 | let mut shadowstring = String::new(); | |
941 | for user in &self.users { | |
942 | userstring.push_str(&user.passwd_entry()); | |
943 | shadowstring.push_str(&user.shadow_entry()); | |
944 | } | |
945 | ||
946 | let mut shadow_fd = self.shadow_fd.as_mut() | |
947 | .expect("shadow_fd should exist for AllUsers<auth::Full>"); | |
948 | ||
949 | reset_file(&mut self.passwd_fd)?; | |
950 | self.passwd_fd.write_all(userstring.as_bytes())?; | |
951 | ||
952 | reset_file(&mut shadow_fd)?; | |
953 | shadow_fd.write_all(shadowstring.as_bytes())?; | |
954 | Ok(()) | |
955 | } | |
956 | } | |
957 | ||
958 | impl<A> AllInner for AllUsers<A> { | |
959 | type Gruser = User<A>; | |
960 | ||
961 | fn list(&self) -> &Vec<Self::Gruser> { | |
962 | &self.users | |
963 | } | |
964 | ||
965 | fn list_mut(&mut self) -> &mut Vec<Self::Gruser> { | |
966 | &mut self.users | |
967 | } | |
968 | ||
969 | fn config(&self) -> &Config { | |
970 | &self.config | |
971 | } | |
972 | } | |
973 | ||
974 | impl<A> All for AllUsers<A> {} | |
975 | /* | |
976 | #[cfg(not(target_os = "redox"))] | |
977 | impl<A> Drop for AllUsers<A> { | |
978 | fn drop(&mut self) { | |
979 | eprintln!("Dropping AllUsers"); | |
980 | let _ = flock(self.passwd_fd.as_raw_fd(), FlockArg::Unlock); | |
981 | if let Some(fd) = self.shadow_fd.as_ref() { | |
982 | eprintln!("Shadow"); | |
983 | let _ = flock(fd.as_raw_fd(), FlockArg::Unlock); | |
984 | } | |
985 | } | |
986 | } | |
987 | */ | |
988 | /// `AllGroups` provides (borrowed) access to all groups on the system. Note | |
989 | /// that this struct implements [`All`] for all of its access functions. | |
990 | /// | |
991 | /// General notes that also apply to this struct may be found with | |
992 | /// [`AllUsers`]. | |
993 | #[derive(Debug)] | |
994 | pub struct AllGroups { | |
995 | groups: Vec<Group>, | |
996 | config: Config, | |
997 | ||
998 | group_fd: File, | |
999 | } | |
1000 | ||
1001 | impl AllGroups { | |
1002 | /// Create a new `AllGroups`. | |
1003 | pub fn new(config: Config) -> Result<AllGroups> { | |
1004 | let mut group_fd = locked_file(config.in_scheme(GROUP_FILE), Lock::Exclusive)?; | |
1005 | let mut group_cntnt = String::new(); | |
1006 | group_fd.read_to_string(&mut group_cntnt)?; | |
1007 | ||
1008 | let mut entries: Vec<Group> = Vec::new(); | |
1009 | for (indx, line) in group_cntnt.lines().enumerate() { | |
1010 | let group = Group::from_group_entry(line, indx)?; | |
1011 | entries.push(group); | |
1012 | } | |
1013 | ||
1014 | Ok(AllGroups { | |
1015 | groups: entries, | |
1016 | config, | |
1017 | group_fd, | |
1018 | }) | |
1019 | } | |
1020 | ||
1021 | /// Adds a group with the specified attributes to this `AllGroups`. | |
1022 | /// | |
1023 | /// Make sure to call [`AllGroups::save`] in order for the new group to be | |
1024 | /// applied to the system. | |
1025 | //TODO: Take Option<usize> for gid and find unused ID if None | |
1026 | pub fn add_group( | |
1027 | &mut self, | |
1028 | name: &str, | |
1029 | gid: usize, | |
1030 | users: &[&str] | |
1031 | ) -> Result<()> { | |
1032 | if self.iter() | |
1033 | .any(|group| group.group == name || group.gid == gid) | |
1034 | { | |
1035 | return Err(From::from(UsersError::AlreadyExists)) | |
1036 | } | |
1037 | ||
1038 | //Might be cleaner... Also breaks... | |
1039 | //users: users.iter().map(String::to_string).collect() | |
1040 | self.groups.push(Group { | |
1041 | group: name.into(), | |
1042 | gid, | |
1043 | users: users | |
1044 | .iter() | |
1045 | .map(|user| user.to_string()) | |
1046 | .collect() | |
1047 | }); | |
1048 | ||
1049 | Ok(()) | |
1050 | } | |
1051 | ||
1052 | /// Syncs the data stored in this `AllGroups` instance to the filesystem. | |
1053 | /// To apply changes from an `AllGroups`, you MUST call this function! | |
1054 | pub fn save(&mut self) -> Result<()> { | |
1055 | let mut groupstring = String::new(); | |
1056 | for group in &self.groups { | |
1057 | groupstring.push_str(&group.group_entry()); | |
1058 | } | |
1059 | ||
1060 | reset_file(&mut self.group_fd)?; | |
1061 | self.group_fd.write_all(groupstring.as_bytes())?; | |
1062 | Ok(()) | |
1063 | } | |
1064 | } | |
1065 | ||
1066 | impl AllInner for AllGroups { | |
1067 | type Gruser = Group; | |
1068 | ||
1069 | fn list(&self) -> &Vec<Self::Gruser> { | |
1070 | &self.groups | |
1071 | } | |
1072 | ||
1073 | fn list_mut(&mut self) -> &mut Vec<Self::Gruser> { | |
1074 | &mut self.groups | |
1075 | } | |
1076 | ||
1077 | fn config(&self) -> &Config { | |
1078 | &self.config | |
1079 | } | |
1080 | } | |
1081 | ||
1082 | impl All for AllGroups {} | |
1083 | /* | |
1084 | #[cfg(not(target_os = "redox"))] | |
1085 | impl Drop for AllGroups { | |
1086 | fn drop(&mut self) { | |
1087 | eprintln!("Dropping AllGroups"); | |
1088 | let _ = flock(self.group_fd.as_raw_fd(), FlockArg::Unlock); | |
1089 | } | |
1090 | }*/ | |
1091 | ||
1092 | #[cfg(test)] | |
1093 | mod test { | |
1094 | use super::*; | |
1095 | ||
1096 | const TEST_PREFIX: &'static str = "tests"; | |
1097 | ||
1098 | /// Needed for the file checks, this is done by the library | |
1099 | fn test_prefix(filename: &str) -> String { | |
1100 | let mut complete = String::from(TEST_PREFIX); | |
1101 | complete.push_str(filename); | |
1102 | complete | |
1103 | } | |
1104 | ||
1105 | fn test_cfg() -> Config { | |
1106 | Config::default() | |
1107 | // Since all this really does is prepend `sheme` to the consts | |
1108 | .scheme(TEST_PREFIX.to_string()) | |
1109 | } | |
1110 | ||
1111 | fn read_locked_file(file: impl AsRef<Path>) -> Result<String> { | |
1112 | let mut fd = locked_file(file, Lock::Exclusive)?; | |
1113 | let mut cntnt = String::new(); | |
1114 | fd.read_to_string(&mut cntnt)?; | |
1115 | Ok(cntnt) | |
1116 | } | |
1117 | ||
1118 | fn write_locked_file(file: impl AsRef<Path>, cntnt: impl AsRef<[u8]>) -> Result<()> { | |
1119 | locked_file(file, Lock::Exclusive)? | |
1120 | .write_all(cntnt.as_ref())?; | |
1121 | Ok(()) | |
1122 | } | |
1123 | ||
1124 | // *** struct.User *** | |
1125 | #[cfg(feature = "auth")] | |
1126 | #[test] | |
1127 | fn attempt_user_api() { | |
1128 | let mut users = AllUsers::authenticator(test_cfg()).unwrap(); | |
1129 | let user = users.get_mut_by_id(1000).unwrap(); | |
1130 | ||
1131 | assert_eq!(user.is_passwd_blank(), true); | |
1132 | assert_eq!(user.is_passwd_unset(), false); | |
1133 | assert_eq!(user.verify_passwd(""), true); | |
1134 | assert_eq!(user.verify_passwd("Something"), false); | |
1135 | ||
1136 | user.set_passwd("hi,i_am_passwd").unwrap(); | |
1137 | ||
1138 | assert_eq!(user.is_passwd_blank(), false); | |
1139 | assert_eq!(user.is_passwd_unset(), false); | |
1140 | assert_eq!(user.verify_passwd(""), false); | |
1141 | assert_eq!(user.verify_passwd("Something"), false); | |
1142 | assert_eq!(user.verify_passwd("hi,i_am_passwd"), true); | |
1143 | ||
1144 | user.unset_passwd(); | |
1145 | ||
1146 | assert_eq!(user.is_passwd_blank(), false); | |
1147 | assert_eq!(user.is_passwd_unset(), true); | |
1148 | assert_eq!(user.verify_passwd(""), false); | |
1149 | assert_eq!(user.verify_passwd("Something"), false); | |
1150 | assert_eq!(user.verify_passwd("hi,i_am_passwd"), false); | |
1151 | ||
1152 | user.set_passwd("").unwrap(); | |
1153 | ||
1154 | assert_eq!(user.is_passwd_blank(), true); | |
1155 | assert_eq!(user.is_passwd_unset(), false); | |
1156 | assert_eq!(user.verify_passwd(""), true); | |
1157 | assert_eq!(user.verify_passwd("Something"), false); | |
1158 | } | |
1159 | ||
1160 | // *** struct.AllUsers *** | |
1161 | #[cfg(feature = "auth")] | |
1162 | #[test] | |
1163 | fn get_user() { | |
1164 | let users = AllUsers::authenticator(test_cfg()).unwrap(); | |
1165 | ||
1166 | let root = users.get_by_id(0).expect("'root' user missing"); | |
1167 | assert_eq!(root.user, "root".to_string()); | |
1168 | let &(ref hashstring, ref encoded) = root.hash.as_ref().expect("'root' hash is None"); | |
1169 | assert_eq!(hashstring, | |
1170 | &"$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk".to_string()); | |
1171 | assert_eq!(root.uid, 0); | |
1172 | assert_eq!(root.gid, 0); | |
1173 | assert_eq!(root.name, "root".to_string()); | |
1174 | assert_eq!(root.home, "file:/root".to_string()); | |
1175 | assert_eq!(root.shell, "file:/bin/ion".to_string()); | |
1176 | match encoded { | |
1177 | true => (), | |
1178 | false => panic!("Expected encoded argon hash!") | |
1179 | } | |
1180 | ||
1181 | let user = users.get_by_name("user").expect("'user' user missing"); | |
1182 | assert_eq!(user.user, "user".to_string()); | |
1183 | let &(ref hashstring, ref encoded) = user.hash.as_ref().expect("'user' hash is None"); | |
1184 | assert_eq!(hashstring, &"".to_string()); | |
1185 | assert_eq!(user.uid, 1000); | |
1186 | assert_eq!(user.gid, 1000); | |
1187 | assert_eq!(user.name, "user".to_string()); | |
1188 | assert_eq!(user.home, "file:/home/user".to_string()); | |
1189 | assert_eq!(user.shell, "file:/bin/ion".to_string()); | |
1190 | match encoded { | |
1191 | true => panic!("Should not be an argon hash!"), | |
1192 | false => () | |
1193 | } | |
1194 | println!("{:?}", users); | |
1195 | ||
1196 | let li = users.get_by_name("li").expect("'li' user missing"); | |
1197 | println!("got li"); | |
1198 | assert_eq!(li.user, "li"); | |
1199 | let &(ref hashstring, ref encoded) = li.hash.as_ref().expect("'li' hash is None"); | |
1200 | assert_eq!(hashstring, &"!".to_string()); | |
1201 | assert_eq!(li.uid, 1007); | |
1202 | assert_eq!(li.gid, 1007); | |
1203 | assert_eq!(li.name, "Lorem".to_string()); | |
1204 | assert_eq!(li.home, "file:/home/lorem".to_string()); | |
1205 | assert_eq!(li.shell, "file:/bin/ion".to_string()); | |
1206 | match encoded { | |
1207 | true => panic!("Should not be an argon hash!"), | |
1208 | false => () | |
1209 | } | |
1210 | } | |
1211 | ||
1212 | #[cfg(feature = "auth")] | |
1213 | #[test] | |
1214 | fn manip_user() { | |
1215 | let mut users = AllUsers::authenticator(test_cfg()).unwrap(); | |
1216 | // NOT testing `get_unique_id` | |
1217 | let id = 7099; | |
1218 | users | |
1219 | .add_user("fb", id, id, "Foo Bar", "/home/foob", "/bin/zsh") | |
1220 | .expect("failed to add user 'fb'"); | |
1221 | // weirdo ^^^^^^^^ :P | |
1222 | users.save().unwrap(); | |
1223 | let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); | |
1224 | assert_eq!( | |
1225 | p_file_content, | |
1226 | concat!( | |
1227 | "root;0;0;root;file:/root;file:/bin/ion\n", | |
1228 | "user;1000;1000;user;file:/home/user;file:/bin/ion\n", | |
1229 | "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", | |
1230 | "fb;7099;7099;Foo Bar;/home/foob;/bin/zsh\n" | |
1231 | ) | |
1232 | ); | |
1233 | let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); | |
1234 | assert_eq!(s_file_content, concat!( | |
1235 | "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", | |
1236 | "user;\n", | |
1237 | "li;!\n", | |
1238 | "fb;!\n" | |
1239 | )); | |
1240 | ||
1241 | { | |
1242 | println!("{:?}", users); | |
1243 | let fb = users.get_mut_by_name("fb").expect("'fb' user missing"); | |
1244 | fb.shell = "/bin/fish".to_string(); // That's better | |
1245 | fb.set_passwd("").unwrap(); | |
1246 | } | |
1247 | users.save().unwrap(); | |
1248 | let p_file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); | |
1249 | assert_eq!( | |
1250 | p_file_content, | |
1251 | concat!( | |
1252 | "root;0;0;root;file:/root;file:/bin/ion\n", | |
1253 | "user;1000;1000;user;file:/home/user;file:/bin/ion\n", | |
1254 | "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n", | |
1255 | "fb;7099;7099;Foo Bar;/home/foob;/bin/fish\n" | |
1256 | ) | |
1257 | ); | |
1258 | let s_file_content = read_locked_file(test_prefix(SHADOW_FILE)).unwrap(); | |
1259 | assert_eq!(s_file_content, concat!( | |
1260 | "root;$argon2i$m=4096,t=10,p=1$Tnc4UVV0N00$ML9LIOujd3nmAfkAwEcSTMPqakWUF0OUiLWrIy0nGLk\n", | |
1261 | "user;\n", | |
1262 | "li;!\n", | |
1263 | "fb;\n" | |
1264 | )); | |
1265 | ||
1266 | users.remove_by_id(id); | |
1267 | users.save().unwrap(); | |
1268 | let file_content = read_locked_file(test_prefix(PASSWD_FILE)).unwrap(); | |
1269 | assert_eq!( | |
1270 | file_content, | |
1271 | concat!( | |
1272 | "root;0;0;root;file:/root;file:/bin/ion\n", | |
1273 | "user;1000;1000;user;file:/home/user;file:/bin/ion\n", | |
1274 | "li;1007;1007;Lorem;file:/home/lorem;file:/bin/ion\n" | |
1275 | ) | |
1276 | ); | |
1277 | } | |
1278 | ||
1279 | /* struct.Group */ | |
1280 | #[test] | |
1281 | fn empty_groups() { | |
1282 | let group_trailing = Group::from_group_entry("nobody;2066; ", 0).unwrap(); | |
1283 | assert_eq!(group_trailing.users.len(), 0); | |
1284 | ||
1285 | let group_no_trailing = Group::from_group_entry("nobody;2066;", 0).unwrap(); | |
1286 | assert_eq!(group_no_trailing.users.len(), 0); | |
1287 | ||
1288 | assert_eq!(group_trailing.group, group_no_trailing.group); | |
1289 | assert_eq!(group_trailing.gid, group_no_trailing.gid); | |
1290 | assert_eq!(group_trailing.users, group_no_trailing.users); | |
1291 | } | |
1292 | ||
1293 | /* struct.AllGroups */ | |
1294 | #[test] | |
1295 | fn get_group() { | |
1296 | let groups = AllGroups::new(test_cfg()).unwrap(); | |
1297 | let user = groups.get_by_name("user").unwrap(); | |
1298 | assert_eq!(user.group, "user"); | |
1299 | assert_eq!(user.gid, 1000); | |
1300 | assert_eq!(user.users, vec!["user"]); | |
1301 | ||
1302 | let wheel = groups.get_by_id(1).unwrap(); | |
1303 | assert_eq!(wheel.group, "wheel"); | |
1304 | assert_eq!(wheel.gid, 1); | |
1305 | assert_eq!(wheel.users, vec!["user", "root"]); | |
1306 | } | |
1307 | ||
1308 | #[test] | |
1309 | fn manip_group() { | |
1310 | let mut groups = AllGroups::new(test_cfg()).unwrap(); | |
1311 | // NOT testing `get_unique_id` | |
1312 | let id = 7099; | |
1313 | ||
1314 | groups.add_group("fb", id, &["fb"]).unwrap(); | |
1315 | groups.save().unwrap(); | |
1316 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); | |
1317 | assert_eq!( | |
1318 | file_content, | |
1319 | concat!( | |
1320 | "root;0;root\n", | |
1321 | "user;1000;user\n", | |
1322 | "wheel;1;user,root\n", | |
1323 | "li;1007;li\n", | |
1324 | "fb;7099;fb\n" | |
1325 | ) | |
1326 | ); | |
1327 | ||
1328 | { | |
1329 | let fb = groups.get_mut_by_name("fb").unwrap(); | |
1330 | fb.users.push("user".to_string()); | |
1331 | } | |
1332 | groups.save().unwrap(); | |
1333 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); | |
1334 | assert_eq!( | |
1335 | file_content, | |
1336 | concat!( | |
1337 | "root;0;root\n", | |
1338 | "user;1000;user\n", | |
1339 | "wheel;1;user,root\n", | |
1340 | "li;1007;li\n", | |
1341 | "fb;7099;fb,user\n" | |
1342 | ) | |
1343 | ); | |
1344 | ||
1345 | groups.remove_by_id(id); | |
1346 | groups.save().unwrap(); | |
1347 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); | |
1348 | assert_eq!( | |
1349 | file_content, | |
1350 | concat!( | |
1351 | "root;0;root\n", | |
1352 | "user;1000;user\n", | |
1353 | "wheel;1;user,root\n", | |
1354 | "li;1007;li\n" | |
1355 | ) | |
1356 | ); | |
1357 | } | |
1358 | ||
1359 | #[test] | |
1360 | fn empty_group() { | |
1361 | let mut groups = AllGroups::new(test_cfg()).unwrap(); | |
1362 | ||
1363 | groups.add_group("nobody", 2260, &[]).unwrap(); | |
1364 | groups.save().unwrap(); | |
1365 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); | |
1366 | assert_eq!( | |
1367 | file_content, | |
1368 | concat!( | |
1369 | "root;0;root\n", | |
1370 | "user;1000;user\n", | |
1371 | "wheel;1;user,root\n", | |
1372 | "li;1007;li\n", | |
1373 | "nobody;2260;\n", | |
1374 | ) | |
1375 | ); | |
1376 | ||
1377 | drop(groups); | |
1378 | let mut groups = AllGroups::new(test_cfg()).unwrap(); | |
1379 | ||
1380 | groups.remove_by_name("nobody"); | |
1381 | groups.save().unwrap(); | |
1382 | ||
1383 | let file_content = read_locked_file(test_prefix(GROUP_FILE)).unwrap(); | |
1384 | assert_eq!( | |
1385 | file_content, | |
1386 | concat!( | |
1387 | "root;0;root\n", | |
1388 | "user;1000;user\n", | |
1389 | "wheel;1;user,root\n", | |
1390 | "li;1007;li\n" | |
1391 | ) | |
1392 | ); | |
1393 | } | |
1394 | ||
1395 | // *** Misc *** | |
1396 | #[test] | |
1397 | fn users_get_unused_ids() { | |
1398 | let users = AllUsers::basic(test_cfg()).unwrap(); | |
1399 | let id = users.get_unique_id().unwrap(); | |
1400 | if id < users.config.min_id || id > users.config.max_id { | |
1401 | panic!("User ID is not between allowed margins") | |
1402 | } else if let Some(_) = users.get_by_id(id) { | |
1403 | panic!("User ID is used!"); | |
1404 | } | |
1405 | } | |
1406 | ||
1407 | #[test] | |
1408 | fn groups_get_unused_ids() { | |
1409 | let groups = AllGroups::new(test_cfg()).unwrap(); | |
1410 | let id = groups.get_unique_id().unwrap(); | |
1411 | if id < groups.config.min_id || id > groups.config.max_id { | |
1412 | panic!("Group ID is not between allowed margins") | |
1413 | } else if let Some(_) = groups.get_by_id(id) { | |
1414 | panic!("Group ID is used!"); | |
1415 | } | |
1416 | } | |
1417 | } |