#![allow(clippy::non_ascii_literal)] pub mod files; pub mod hashes; use crate::{ api::{ CreateUserArgs, DeleteHome, DeleteUserArgs, GroupRead, UserDBRead, UserDBWrite, UserRead, }, group::MembershipKind, UserLibError, }; #[allow(unused_imports)] use log::{debug, error, info, trace, warn}; use std::collections::HashMap; use std::fs::File; use std::io::{BufReader, Read}; pub type UserList = HashMap; pub struct UserDBLocal { source_files: files::Files, source_hashes: hashes::Hashes, // to detect changes pub users: UserList, pub groups: Vec, } impl UserDBLocal { /// Import the database from strings #[must_use] pub fn import_from_strings( passwd_content: &str, shadow_content: &str, group_content: &str, ) -> Self { let shadow_entries: Vec = string_to(shadow_content); let mut users = user_vec_to_hashmap(string_to(passwd_content)); let mut groups = string_to(group_content); shadow_to_users(&mut users, shadow_entries); groups_to_users(&mut users, &mut groups); Self { source_files: files::Files { passwd: None, group: None, shadow: None, }, users, groups, source_hashes: hashes::Hashes::new(passwd_content, shadow_content, group_content), } } /// Import the database from a [`Files`] struct pub fn load_files(files: files::Files) -> Result { // Get the Strings for the files use an inner block to drop references after read. let (my_passwd_lines, my_shadow_lines, my_group_lines) = { let opened = files.lock_all_get(); let (locked_p, locked_s, locked_g) = opened.expect("failed to lock files!"); // read the files to strings let p = file_to_string(&locked_p.file)?; let s = file_to_string(&locked_s.file)?; let g = file_to_string(&locked_g.file)?; // return the strings to the outer scope and release the lock... (p, s, g) }; let mut users = user_vec_to_hashmap(string_to(&my_passwd_lines)); let passwds: Vec = string_to(&my_shadow_lines); let mut groups: Vec = string_to(&my_group_lines); shadow_to_users(&mut users, passwds); groups_to_users(&mut users, &mut groups); Ok(Self { source_files: files, users, groups, source_hashes: hashes::Hashes::new(&my_passwd_lines, &my_shadow_lines, &my_group_lines), }) } fn delete_from_passwd( user: &crate::User, passwd_file_content: &str, locked_p: &mut files::LockedFileGuard, ) -> Result<(), UserLibError> { let modified_p = user.remove_in(passwd_file_content); // write the new content to the file. let ncont = locked_p.replace_contents(modified_p); match ncont { Ok(_) => Ok(()), Err(e) => Err(format!("Failed to write the passwd database: {}", e).into()), } } fn delete_from_shadow( user: &crate::User, shadow_file_content: &str, locked_s: &mut files::LockedFileGuard, ) -> Result<(), UserLibError> { let shad = user.get_shadow(); match shad { Some(shadow) => { let modified_s = shadow.remove_in(shadow_file_content); let ncont = locked_s.replace_contents(modified_s); match ncont { Ok(_) => Ok(()), Err(e) => Err(format!( "Error during write to the database. \ Please doublecheck as the shadowdatabase could be corrupted: {}", e, ) .into()), } } None => Ok(()), } } fn delete_from_group( group: &crate::Group, group_file_content: &str, locked_g: &mut files::LockedFileGuard, ) -> Result<(), UserLibError> { let modified_g = group.borrow().remove_in(group_file_content); let replace_result = locked_g.replace_contents(modified_g); match replace_result { Ok(_) => Ok(()), Err(e) => Err(format!( "Error during write to the database. \ Please doublecheck as the groupdatabase could be corrupted: {}", e, ) .into()), } } fn write_groups(&self, locked_g: &mut files::LockedFileGuard) -> Result<(), UserLibError> { let content = self .groups .iter() .map(|g| (g.borrow().to_string())) .collect::>() .join("\n"); let replace_result = locked_g.replace_contents(content); match replace_result { Ok(_) => Ok(()), Err(e) => Err(format!( "Error during write to the database. \ Please doublecheck as the groupdatabase could be corrupted: {}", e, ) .into()), } } fn delete_home(user: &crate::User) -> std::io::Result<()> { if let Some(dir) = user.get_home_dir() { std::fs::remove_dir_all(dir) } else { let error_msg = "Failed to remove the home directory! As the user did not have one."; error!("{}", error_msg); Err(std::io::Error::new( std::io::ErrorKind::InvalidInput, error_msg, )) } } fn delete_group_by_id(&mut self, gid: u32) { self.groups .retain(|g| g.borrow().get_gid().expect("groups have to have a gid") != gid); } } impl UserDBWrite for UserDBLocal { fn delete_user(&mut self, args: DeleteUserArgs) -> Result { // try to get the user from the database let user_opt = self.get_user_by_name(args.username); let user = match user_opt { Some(user) => user, None => { return Err(UserLibError::NotFound); } }; if self.source_files.is_virtual() { warn!("There are no associated files working in dummy mode!"); let res = self.users.remove(args.username); match res { Some(u) => Ok(u), None => Err(UserLibError::NotFound), // should not happen anymore as existence is checked. } } else { let opened = self.source_files.lock_all_get(); let (mut locked_p, mut locked_s, mut locked_g) = opened.expect("failed to lock files!"); // read the files to strings let passwd_file_content = file_to_string(&locked_p.file)?; let shadow_file_content = file_to_string(&locked_s.file)?; let group_file_content = file_to_string(&locked_g.file)?; let src = &self.source_hashes; if src.passwd.has_changed(&passwd_file_content) | src.shadow.has_changed(&shadow_file_content) { error!("The source files have changed. Deleting the user could corrupt the userdatabase. Aborting!"); Err(format!("The userdatabase has been changed {}", args.username).into()) } else { Self::delete_from_passwd(user, &passwd_file_content, &mut locked_p)?; Self::delete_from_shadow(user, &shadow_file_content, &mut locked_s)?; if args.delete_home == DeleteHome::Delete { Self::delete_home(user)?; } println!("The users groups: {:#?}", user.get_groups()); // Iterate over the GIDs to avoid borrowing issues let users_groups: Vec<(MembershipKind, u32)> = user .get_groups() .iter() .map(|(k, g)| (*k, g.borrow().get_gid().unwrap())) .collect(); for (kind, group) in users_groups { println!("Woring on group: {:?} - {}", kind, group); match kind { crate::group::MembershipKind::Primary => { if self .get_group_by_id(group) .expect("The group does not exist") .borrow() .get_member_names() .expect("this group allways has a member") .len() == 1 { println!( "Deleting group as the user to be deleted is the only member {:?}", self .get_group_by_id(group) .expect("The group does not exist") .borrow() .get_member_names() ); Self::delete_from_group( self.get_group_by_id(group) .expect("The group does not exist"), &group_file_content, &mut locked_g, )?; let _gres = self.delete_group_by_id(group); } else { println!("Do not delete the group as the user to be deleted is not the only member"); // remove the from the group instead of deleting the group if he was not the only user in its primary group. if let Some(group) = self.get_group_by_id(group) { group .borrow_mut() .remove_member(MembershipKind::Primary, args.username) }; self.write_groups(&mut locked_g)?; warn!( "The primary group (GID: {}) was not empty and is thus not removed. Only the membership has been removed", group ); } } crate::group::MembershipKind::Member => { println!("delete the membership in the group"); if let Some(group) = self.get_group_by_id(group) { group .borrow_mut() .remove_member(MembershipKind::Member, args.username) }; self.write_groups(&mut locked_g)?; } } } // Remove the user from the memory database(HashMap) let res = self.users.remove(args.username); match res { Some(u) => Ok(u), None => Err("Failed to remove the user from the internal HashMap".into()), } } } } fn new_user(&mut self, args: CreateUserArgs) -> Result<&crate::User, crate::UserLibError> { if self.users.contains_key(args.username) { Err(format!("The username {} already exists! Aborting!", args.username).into()) } else { let mut new_user = crate::User::default(); new_user.username(args.username.to_owned()); if self.users.contains_key(args.username) { Err("Failed to create the user. A user with the same Name already exists".into()) } else { let opened = self.source_files.lock_all_get(); let (mut locked_p, mut locked_s, mut _locked_g) = opened.expect("failed to lock files!"); //dbg!(&locked_p); locked_p.append(format!("{}", new_user))?; if let Some(shadow) = new_user.get_shadow() { info!("Adding shadow entry {}", shadow); locked_s.append(format!("{}", shadow))?; } else { warn!("Omitting shadow entry!") } assert!(self .users .insert(args.username.to_owned(), new_user) .is_none()); self.users .get(args.username) .map_or_else(|| Err("User was not successfully added!".into()), Ok) } } } fn delete_group(&mut self, _group: &crate::Group) -> Result<(), crate::UserLibError> { todo!() } fn new_group(&mut self) -> Result<&crate::Group, crate::UserLibError> { todo!() } } impl UserDBRead for UserDBLocal { fn get_all_users(&self) -> Vec<&crate::User> { let mut res: Vec<&crate::User> = self.users.iter().map(|(_, x)| x).collect(); res.sort(); res } fn get_user_by_name(&self, name: &str) -> Option<&crate::User> { self.users.get(name) } fn get_user_by_id(&self, uid: u32) -> Option<&crate::User> { // could probably be more efficient - on the other hand its no problem to loop a thousand users. for user in self.users.values() { if user.get_uid() == uid { return Some(user); } } None } fn get_all_groups(&self) -> Vec { self.groups.iter().map(std::clone::Clone::clone).collect() } fn get_group_by_name(&self, name: &str) -> Option<&crate::Group> { for group in &self.groups { if group.borrow().get_groupname()? == name { return Some(group); } } None } fn get_group_by_id(&self, id: u32) -> Option<&crate::Group> { for group in &self.groups { if group.borrow().get_gid()? == id { return Some(group); } } None } } use crate::api::UserDBValidation; impl UserDBValidation for UserDBLocal { fn is_uid_valid_and_free(&self, uid: u32) -> bool { warn!("No valid check, only free check"); let free = self.users.iter().all(|(_, u)| u.get_uid() != uid); free } fn is_username_valid_and_free(&self, name: &str) -> bool { let valid = crate::user::passwd_fields::is_username_valid(name); let free = self.get_user_by_name(name).is_none(); valid && free } fn is_gid_valid_and_free(&self, gid: u32) -> bool { warn!("No valid check, only free check"); self.groups .iter() .all(|x| x.borrow().get_gid().unwrap() != gid) } fn is_groupname_valid_and_free(&self, name: &str) -> bool { let valid = crate::group::is_groupname_valid(name); let free = self .groups .iter() .all(|x| x.borrow().get_groupname().unwrap() != name); valid && free } } /// Parse a file to a string fn file_to_string(file: &File) -> Result { let mut reader = BufReader::new(file); let mut lines = String::new(); let res = reader.read_to_string(&mut lines); match res { Ok(_) => Ok(lines), Err(e) => Err(format!("failed to read the file: {:?}", e).into()), } } fn groups_to_users<'a>( users: &'a mut UserList, groups: &'a mut [crate::Group], ) -> &'a mut UserList { // Populate the regular groups for group in groups.iter() { match group.borrow().get_member_names() { Some(usernames) => { for username in usernames { if let Some(user) = users.get_mut(username) { user.add_group(crate::group::MembershipKind::Member, group.clone()); } } } None => continue, } } // Populate the primary membership for user in users.values_mut() { let gid = user.get_gid(); let grouplist: Vec<&crate::Group> = groups .iter() .filter(|g| g.borrow().get_gid().unwrap() == gid) .collect(); if grouplist.len() == 1 { let group = *grouplist.first().unwrap(); group.borrow_mut().append_user( user.get_username() .expect("Users without username are not supported"), ); user.add_group(crate::group::MembershipKind::Primary, group.clone()); } else { error!( "Somehow the group with gid {} was found {} times", gid, grouplist.len() ); } } users } /// Merge the Shadow passwords into the users fn shadow_to_users(users: &mut UserList, shadow: Vec) -> &mut UserList { for pass in shadow { let user = users .get_mut(pass.get_username()) .unwrap_or_else(|| panic!("the user {} does not exist", pass.get_username())); user.password = crate::Password::Shadow(pass); } users } /// Convert a `Vec` to a `UserList` (`HashMap`) where the username is used as key fn user_vec_to_hashmap(users: Vec) -> UserList { users .into_iter() .map(|x| { ( x.get_username() .expect("An empty username is not supported") .to_owned(), x, ) }) .collect() } /// Try to parse a String into some Object. /// /// # Errors /// if the parsing failed a [`UserLibError::Message`](crate::userlib_error::UserLibError::Message) is returned containing a more detailed error message. pub trait NewFromString { fn new_from_string(line: String, position: u32) -> Result where Self: Sized; } /// A generic function that parses a string line by line and creates the appropriate `Vec` requested by the type system. fn string_to(source: &str) -> Vec where T: NewFromString, { use std::convert::TryInto; source .lines() .enumerate() .filter_map(|(n, line)| { if line.len() > 5 { Some( T::new_from_string( line.to_owned(), n.try_into() .unwrap_or_else(|e| panic!("Failed to convert usize to u32 {}", e)), ) .expect("failed to read lines"), ) } else { None } }) .collect() } #[test] fn test_creator_user_db_local() { let data = UserDBLocal::import_from_strings("test:x:1002:1002:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test", "test:$6$u0Hh.9WKRF1Aeu4g$XqoDyL6Re/4ZLNQCGAXlNacxCxbdigexEqzFzkOVPV5Z1H23hlenjW8ZLgq6GQtFURYwenIFpo1c.r4aW9l5S/:18260:0:99999:7:::", "teste:x:1002:\nanother:x:1003:test"); assert_eq!( data.users.get("test").unwrap().get_username().unwrap(), "test" ); for user in data.users.values() { dbg!(user.get_groups()); let (member_group1, group1) = user.get_groups().first().unwrap(); let (member_group2, group2) = user.get_groups().get(1).unwrap(); assert_eq!(*member_group1, crate::group::MembershipKind::Member); assert_eq!(group1.borrow().get_groupname(), Some("another")); assert_eq!(*member_group2, crate::group::MembershipKind::Primary); assert_eq!(group2.borrow().get_groupname(), Some("teste")); } } #[test] fn test_parsing_local_database() { use std::path::PathBuf; // Parse the worldreadable user database ignore the shadow database as this would require root privileges. let pwdfile = File::open(PathBuf::from("/etc/passwd")).unwrap(); let grpfile = File::open(PathBuf::from("/etc/group")).unwrap(); let my_passwd_lines = file_to_string(&pwdfile).unwrap(); let my_group_lines = file_to_string(&grpfile).unwrap(); let data = UserDBLocal::import_from_strings(&my_passwd_lines, "", &my_group_lines); assert_eq!( data.groups .get(0) .unwrap() .borrow() .get_groupname() .unwrap(), "root" ); } #[test] fn test_user_db_read_implementation() { use std::path::PathBuf; let pwdfile = File::open(PathBuf::from("/etc/passwd")).unwrap(); let grpfile = File::open(PathBuf::from("/etc/group")).unwrap(); let pass = file_to_string(&pwdfile).unwrap(); let group = file_to_string(&grpfile).unwrap(); let data = UserDBLocal::import_from_strings(&pass, "", &group); // Usually there are more than 10 users assert!(data.get_all_users().len() > 10); assert!(data.get_user_by_name("root").is_some()); assert_eq!(data.get_user_by_name("root").unwrap().get_uid(), 0); assert_eq!( data.get_user_by_id(0).unwrap().get_username().unwrap(), "root" ); assert!(data.get_all_groups().len() > 10); assert!(data.get_group_by_name("root").is_some()); assert_eq!( data.get_group_by_name("root") .unwrap() .borrow() .get_gid() .unwrap(), 0 ); assert_eq!( data.get_group_by_id(0) .unwrap() .borrow() .get_groupname() .unwrap(), "root" ); assert!(data.get_user_by_name("norealnameforsure").is_none()); assert!(data.get_group_by_name("norealgroupforsure").is_none()); } #[test] fn test_user_db_write_implementation() { use crate::api::DeleteUserArgs; let mut data = UserDBLocal::import_from_strings("test:x:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test", "test:$6$u0Hh.9WKRF1Aeu4g$XqoDyL6Re/4ZLNQCGAXlNacxCxbdigexEqzFzkOVPV5Z1H23hlenjW8ZLgq6GQtFURYwenIFpo1c.r4aW9l5S/:18260:0:99999:7:::", "teste:x:1002:test,test"); let user = "test"; assert_eq!(data.get_all_users().len(), 1); assert!(data .delete_user(DeleteUserArgs::builder().username(user).build().unwrap()) .is_ok()); assert!(data .delete_user(DeleteUserArgs::builder().username(user).build().unwrap()) .is_err()); assert_eq!(data.get_all_users().len(), 0); }