umanux/src/userlib/mod.rs
2020-11-09 09:56:24 +01:00

493 lines
17 KiB
Rust

#![allow(clippy::non_ascii_literal)]
pub mod files;
pub mod hashes;
use crate::api::UserRead;
use crate::{api::GroupRead, 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 struct UserDBLocal {
source_files: files::Files,
source_hashes: hashes::Hashes, // to detect changes
pub users: HashMap<String, crate::User>,
pub groups: Vec<crate::Group>,
}
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<crate::Shadow> = string_to(shadow_content);
let mut users = user_vec_to_hashmap(string_to(passwd_content));
let groups = string_to(group_content);
shadow_to_users(&mut users, shadow_entries);
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<Self, crate::UserLibError> {
// 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<crate::Shadow> = string_to(&my_shadow_lines);
shadow_to_users(&mut users, passwds);
Ok(Self {
source_files: files,
users,
groups: string_to(&my_group_lines),
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.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 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 get_group_pos_by_id(&self, id: u32) -> Option<(&crate::Group, usize)> {
for (i, group) in self.groups.iter().enumerate() {
if group.get_gid()? == id {
return Some((group, i));
}
}
None
}
}
use crate::api::{DeleteHome, NewUserArgs, UserDBRead, UserDBWrite};
impl UserDBWrite for UserDBLocal {
fn delete_user(&mut self, args: NewUserArgs) -> Result<crate::User, UserLibError> {
// 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)?;
}
let group = self.get_group_pos_by_id(user.get_gid());
match group {
Some((group, id)) => {
if group
.get_member_names()
.expect("groups have to have members")
.len()
== 1
{
UserDBLocal::delete_from_group(
group,
&group_file_content,
&mut locked_g,
)?;
let _gres = self.groups.remove(id);
} else {
warn!(
"The primary group {} was not empty and is thus not removed.",
group.get_groupname().unwrap()
);
}
}
None => warn!(
"The users primary group could not be found {}",
user.get_gid()
),
}
// 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, /*
username: String,
enc_password: String,
uid: u32,
gid: u32,
full_name: String,
room: String,
phone_work: String,
phone_home: String,
other: Option<Vec<String>>,
home_dir: String,
shell_path: String,*/
) -> Result<&crate::User, crate::UserLibError> {
/*if self.users.contains_key(&username) {
Err(format!(
"The username {} already exists! Aborting!",
username
)
.into())
} else {
let pwd = if self.source_files.shadow.is_none(){
crate::Password::Encrypted(crate::EncryptedPassword{});
}
else{
crate::Password::Shadow(crate::Shadow{})
}
self.users.insert(
username,
crate::User {
username: crate::Username { username },
password:,
uid: crate::Uid{uid},
gid:crate::Gid{gid},
gecos: crate::Gecos{},
home_dir:crate::HomeDir{dir: home_dir},
shell_path: crate::ShellPath{shell: shell_path},
},
)
}*/
todo!()
}
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<&crate::Group> {
self.groups.iter().collect()
}
fn get_group_by_name(&self, name: &str) -> Option<&crate::Group> {
for group in &self.groups {
if group.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.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.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.get_groupname().unwrap() != name);
valid && free
}
}
/// Parse a file to a string
fn file_to_string(file: &File) -> Result<String, crate::UserLibError> {
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()),
}
}
/// Merge the Shadow passwords into the users
fn shadow_to_users(
users: &mut HashMap<String, crate::User>,
shadow: Vec<crate::Shadow>,
) -> &mut HashMap<String, crate::User> {
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<crate::User>` to a `HashMap<String, crate::User>` where the username is used as key
fn user_vec_to_hashmap(users: Vec<crate::User>) -> HashMap<String, crate::User> {
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<Self, crate::UserLibError>
where
Self: Sized;
}
/// A generic function that parses a string line by line and creates the appropriate `Vec<T>` requested by the type system.
fn string_to<T>(source: &str) -> Vec<T>
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:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test", "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::", "teste:x:1002:test,test");
assert_eq!(
data.users.get("test").unwrap().get_username().unwrap(),
"test"
)
}
#[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().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().get_gid().unwrap(),
0
);
assert_eq!(
data.get_group_by_id(0).unwrap().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::NewUserArgs;
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$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::", "teste:x:1002:test,test");
let user = "test";
assert_eq!(data.get_all_users().len(), 1);
assert!(data
.delete_user(NewUserArgs::builder().username(user).build().unwrap())
.is_ok());
assert!(data
.delete_user(NewUserArgs::builder().username(user).build().unwrap())
.is_err());
assert_eq!(data.get_all_users().len(), 0);
}