1use std::fmt::Debug;
31use std::fs::{File, OpenOptions};
32use std::io::{Read, Seek, SeekFrom, Write};
33#[cfg(target_os = "redox")]
34use std::os::unix::fs::OpenOptionsExt;
35#[cfg(not(target_os = "redox"))]
36use std::os::unix::io::AsRawFd;
37use std::os::unix::process::CommandExt;
38use std::path::{Path, PathBuf};
39use std::process::Command;
40use std::slice::{Iter, IterMut};
41#[cfg(not(test))]
42#[cfg(feature = "auth")]
43use std::thread;
44use std::time::Duration;
45
46use thiserror::Error;
47#[cfg(feature = "auth")]
48use zeroize::Zeroize;
49
50#[cfg(target_os = "redox")]
54use libredox::flag::{O_EXLOCK, O_SHLOCK};
55
56const PASSWD_FILE: &'static str = "/etc/passwd";
57const GROUP_FILE: &'static str = "/etc/group";
58#[cfg(feature = "auth")]
59const SHADOW_FILE: &'static str = "/etc/shadow";
60
61const MIN_ID: usize = 1000;
62const MAX_ID: usize = 6000;
63const DEFAULT_TIMEOUT: u64 = 3;
64
65const USERNAME_LEN_MIN: usize = 3;
66const USERNAME_LEN_MAX: usize = 32;
67
68#[derive(Debug, Error)]
70#[non_exhaustive]
71pub 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 #[error("invalid entry element '{data}'")]
108 InvalidData { data: String },
109}
110pub type Result<T, E = Error> = core::result::Result<T, E>;
111
112#[inline]
113fn parse_error(line: usize, reason: &str) -> Error {
114 Error::Parsing {
115 reason: reason.into(),
116 line,
117 }
118}
119
120impl From<libredox::error::Error> 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)]
127enum Lock {
128 Shared,
129 Exclusive,
130}
131
132impl 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 }
156
157#[allow(dead_code)]
159fn locked_file(file: impl AsRef<Path>, lock: Lock) -> Result<File, Error> {
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 Ok(file)
182 }
183}
184
185fn reset_file(fd: &mut File) -> Result<(), Error> {
187 fd.set_len(0)?;
188 fd.seek(SeekFrom::Start(0))?;
189 Ok(())
190}
191
192fn is_safe_string(s: &str) -> bool {
194 !s.contains(';')
195}
196
197const PORTABLE_FILE_NAME_CHARS: &str =
198 "0123456789._-abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
199
200pub 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
222pub 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 #[derive(Debug, Default)]
236 pub struct Basic {}
237
238 #[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<Full, Error> {
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 } } 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 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#[cfg(feature = "auth")]
331pub struct UserBuilder {
332 user: String,
333 uid: Option<usize>,
334 gid: Option<usize>,
335 name: Option<String>,
336 home: Option<String>,
337 shell: Option<String>,
338}
339
340#[cfg(feature = "auth")]
341impl UserBuilder {
342 pub fn new(user: impl AsRef<str>) -> 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 pub fn uid(mut self, uid: usize) -> UserBuilder {
356 self.uid = Some(uid);
357 self
358 }
359
360 pub fn gid(mut self, gid: usize) -> UserBuilder {
362 self.gid = Some(gid);
363 self
364 }
365
366 pub fn name(mut self, name: impl AsRef<str>) -> UserBuilder {
368 self.name = Some(name.as_ref().to_string());
369 self
370 }
371
372 pub fn home(mut self, home: impl AsRef<str>) -> UserBuilder {
374 self.home = Some(home.as_ref().to_string());
375 self
376 }
377
378 pub fn shell(mut self, shell: impl AsRef<str>) -> UserBuilder {
380 self.shell = Some(shell.as_ref().to_string());
381 self
382 }
383}
384
385#[derive(Debug)]
403pub struct User<A> {
404 pub user: String,
406 pub uid: usize,
408 pub gid: usize,
410 pub name: String,
412 pub home: String,
414 pub shell: String,
416
417 auth_delay: Duration,
419
420 #[allow(dead_code)]
421 auth: A,
422}
423
424impl<A: Default> User<A> {
425 pub fn shell_cmd(&self) -> Command { self.login_cmd(&self.shell) }
428
429 pub fn login_cmd<T>(&self, cmd: T) -> Command
443 where T: std::convert::AsRef<std::ffi::OsStr> + AsRef<str>
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<User<A>, 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::<usize>()?;
468 let gid = parts
469 .next()
470 .ok_or(parse_error(line, "expected uid"))?
471 .parse::<usize>()?;
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::<A> {
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")]
496impl User<auth::Full> {
497 pub fn set_passwd(&mut self, password: impl AsRef<str>) -> Result<(), Error> {
505 self.auth = auth::Full::passwd(password.as_ref())?;
506 Ok(())
507 }
508
509 pub fn unset_passwd(&mut self) {
511 self.auth = auth::Full::unset();
512 }
513
514 pub fn verify_passwd(&self, password: impl AsRef<str>) -> bool {
523 let verified = self.auth.verify(password.as_ref());
524 if !verified {
525 #[cfg(not(test))] thread::sleep(self.auth_delay);
527 }
528 verified
529 }
530
531 pub fn is_passwd_blank(&self) -> bool {
534 self.auth.is_empty()
535 }
536
537 pub fn is_passwd_unset(&self) -> bool {
540 self.auth.is_unset()
541 }
542
543 fn passwd_entry(&self) -> Result<String, Error> {
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<String, Error> {
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
570impl<A> Name for User<A> {
571 fn name(&self) -> &str {
572 &self.user
573 }
574}
575
576impl<A> Id for User<A> {
577 fn id(&self) -> usize {
578 self.uid
579 }
580}
581
582pub struct GroupBuilder {
597 group: String,
599
600 gid: Option<usize>,
601
602 users: Vec<String>,
603}
604
605impl GroupBuilder {
606 pub fn new(group: impl AsRef<str>) -> GroupBuilder {
608 GroupBuilder {
609 group: group.as_ref().to_string(),
610 gid: None,
611 users: vec![],
612 }
613 }
614
615 pub fn gid(mut self, gid: usize) -> GroupBuilder {
617 self.gid = Some(gid);
618 self
619 }
620
621 pub fn user(mut self, user: impl AsRef<str>) -> GroupBuilder {
624 self.users.push(user.as_ref().to_string());
625 self
626 }
627}
628
629#[derive(Debug)]
632pub struct Group {
633 pub group: String,
635 pub password: String,
637 pub gid: usize,
639 pub users: Vec<String>,
641}
642
643impl Group {
644 fn from_group_entry(s: &str, line: usize) -> Result<Group, Error> {
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::<usize>()?;
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<String, Error> {
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
697impl Name for Group {
698 fn name(&self) -> &str {
699 &self.group
700 }
701}
702
703impl Id for Group {
704 fn id(&self) -> usize {
705 self.gid
706 }
707}
708
709pub fn get_euid() -> Result<usize, Error> {
723 libredox::call::geteuid()
724 .map_err(From::from)
725}
726
727pub fn get_uid() -> Result<usize, Error> {
741 libredox::call::getruid()
742 .map_err(From::from)
743}
744
745pub fn get_egid() -> Result<usize, Error> {
759 libredox::call::getegid()
760 .map_err(From::from)
761}
762
763pub fn get_gid() -> Result<usize, Error> {
777 libredox::call::getrgid()
778 .map_err(From::from)
779}
780
781#[derive(Clone, Debug)]
801pub struct Config {
802 root_fs: PathBuf,
803 auth_delay: Duration,
804 min_id: usize,
805 max_id: usize,
806 lock: Lock,
807}
808
809impl Config {
810 pub fn auth_delay(mut self, delay: Duration) -> Config {
812 self.auth_delay = delay;
813 self
814 }
815
816 pub fn min_id(mut self, id: usize) -> Config {
818 self.min_id = id;
819 self
820 }
821
822 pub fn max_id(mut self, id: usize) -> Config {
824 self.max_id = id;
825 self
826 }
827
828 pub fn scheme(mut self, scheme: String) -> Config {
834 self.root_fs = PathBuf::from(scheme);
835 self
836 }
837
838 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 fn in_root_fs(&self, path: impl AsRef<Path>) -> PathBuf {
850 let mut canonical_path = self.root_fs.clone();
851 if path.as_ref().is_absolute() {
853 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
862impl Default for Config {
863 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
879mod 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 type Gruser: Name + Id;
895
896 fn list(&self) -> &Vec<Self::Gruser>;
899 fn list_mut(&mut self) -> &mut Vec<Self::Gruser>;
900 fn config(&self) -> &Config;
901 }
902}
903
904use sealed::{AllInner, Id, Name};
905
906pub trait All: AllInner {
911 fn iter(&self) -> Iter<<Self as AllInner>::Gruser> {
913 self.list().iter()
914 }
915
916 fn iter_mut(&mut self) -> IterMut<<Self as AllInner>::Gruser> {
919 self.list_mut().iter_mut()
920 }
921
922 fn get_by_name(&self, name: impl AsRef<str>) -> Option<&<Self as AllInner>::Gruser> {
934 self.iter()
935 .find(|gruser| gruser.name() == name.as_ref() )
936 }
937
938 fn get_mut_by_name(&mut self, name: impl AsRef<str>) -> Option<&mut <Self as AllInner>::Gruser> {
940 self.iter_mut()
941 .find(|gruser| gruser.name() == name.as_ref() )
942 }
943
944 fn get_by_id(&self, id: usize) -> Option<&<Self as AllInner>::Gruser> {
956 self.iter()
957 .find(|gruser| gruser.id() == id )
958 }
959
960 fn get_mut_by_id(&mut self, id: usize) -> Option<&mut <Self as AllInner>::Gruser> {
962 self.iter_mut()
963 .find(|gruser| gruser.id() == id )
964 }
965
966 fn get_unique_id(&self) -> Option<usize> {
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 fn remove_by_name(&mut self, name: impl AsRef<str>) -> 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 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#[derive(Debug)]
1036pub struct AllUsers<A> {
1037 users: Vec<User<A>>,
1038 config: Config,
1039
1040 #[allow(dead_code)]
1042 passwd_fd: File,
1043 #[allow(dead_code)]
1044 shadow_fd: Option<File>,
1045}
1046
1047impl<A: Default> AllUsers<A> {
1048 pub fn new(config: Config) -> Result<AllUsers<A>, 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::<A> {
1061 users: passwd_entries,
1062 config,
1063 passwd_fd,
1064 shadow_fd: None,
1065 })
1066 }
1067}
1068
1069impl AllUsers<auth::Basic> {
1070 pub fn basic(config: Config) -> Result<AllUsers<auth::Basic>, Error> {
1073 Self::new(config)
1074 }
1075}
1076
1077#[cfg(feature = "auth")]
1078impl AllUsers<auth::Full> {
1079 pub fn authenticator(config: Config) -> Result<AllUsers<auth::Full>, 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 pub fn add_user(&mut self, builder: UserBuilder) -> Result<&User<auth::Full>, 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 pub fn save(&mut self) -> Result<(), Error> {
1166 let mut userstring = String::new();
1167
1168 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<auth::Full>");
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
1204impl<A> AllInner for AllUsers<A> {
1205 type Gruser = User<A>;
1206
1207 fn list(&self) -> &Vec<Self::Gruser> {
1208 &self.users
1209 }
1210
1211 fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1212 &mut self.users
1213 }
1214
1215 fn config(&self) -> &Config {
1216 &self.config
1217 }
1218}
1219
1220impl<A> All for AllUsers<A> {}
1221#[derive(Debug)]
1240pub struct AllGroups {
1241 groups: Vec<Group>,
1242 config: Config,
1243
1244 group_fd: File,
1245}
1246
1247impl AllGroups {
1248 pub fn new(config: Config) -> Result<AllGroups, Error> {
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<Group> = 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 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 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
1328impl AllInner for AllGroups {
1329 type Gruser = Group;
1330
1331 fn list(&self) -> &Vec<Self::Gruser> {
1332 &self.groups
1333 }
1334
1335 fn list_mut(&mut self) -> &mut Vec<Self::Gruser> {
1336 &mut self.groups
1337 }
1338
1339 fn config(&self) -> &Config {
1340 &self.config
1341 }
1342}
1343
1344impl All for AllGroups {}
1345#[cfg(test)]
1355mod test {
1356 use super::*;
1357
1358 const TEST_PREFIX: &'static str = "tests";
1359
1360 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 .scheme(TEST_PREFIX.to_string())
1399 .writeable(true)
1400 }
1401
1402 fn read_locked_file(file: impl AsRef<Path>) -> Result<String, Error> {
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 #[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 #[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 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 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(); 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 #[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 #[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 .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 #[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}