temp #2

Merged
dietrich merged 6 commits from temp into master 2020-10-07 17:11:11 +02:00
10 changed files with 1085 additions and 674 deletions

35
src/api.rs Normal file
View File

@ -0,0 +1,35 @@
trait UserDBRead {
fn get_all_users(&self) -> Vec<crate::User>;
fn get_user_by_name(&self, name: &str) -> Option<crate::User>;
fn get_user_by_id(&self, uid: u64) -> Option<crate::User>;
fn get_all_groups(&self) -> Vec<crate::Group>;
fn get_group_by_name(&self) -> Option<crate::Group>;
fn get_group_by_id(&self) -> Option<crate::Group>;
}
trait UserDBValidation {
fn is_uid_valid_and_free(&self) -> bool;
fn is_username_valid_and_free(&self) -> bool;
fn is_gid_valid_and_free(&self) -> bool;
fn is_groupname_valid_and_free(&self) -> bool;
}
trait UserDBWrite {
fn set_user(&self) -> Option<crate::User>;
fn new_user(&self) -> Option<crate::User>;
fn set_group(&self) -> Option<crate::Group>;
fn new_group(&self) -> Option<crate::Group>;
}
trait UserRead {
fn get_username(&self) -> Option<crate::User>;
fn get_uid(&self) -> Option<crate::User>;
fn get_gid(&self) -> Option<crate::User>;
// …
}
trait UserWrite {}
trait GroupRead {}
trait GroupWrite {}

View File

@ -1,7 +1,8 @@
extern crate adduser;
use adduser::passwd::Passwd;
use adduser::shadow::Shadow;
use adduser::NewFromString;
use adduser::Shadow;
use adduser::User;
use std::fs::File;
use std::io::{prelude::*, BufReader};
@ -17,16 +18,18 @@ fn main() {
for line in reader.lines() {
let line = line.unwrap();
println!("{}", line);
println!("{}", Passwd::new_from_string(&line).unwrap());
println!("{}", User::new_from_string(line).unwrap());
}
let line = "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::";
assert_eq!(format!("{}", Shadow::new_from_string(line).unwrap()), line);
let line = "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::".to_string();
assert_eq!(
format!("{}", Shadow::new_from_string(line.clone()).unwrap()),
line
);
// let pwd = Passwd::default();
// let pwd = User::default();
// let pwd2 =
// Passwd::new_from_string("howdy:notencrypted:1001:1001:not done:/home/test:/bin/bash");
// User::new_from_string("howdy:notencrypted:1001:1001:not done:/home/test:/bin/bash");
// println!("Test struct: {}", pwd);
// assert_eq!(pwd, pwd2.unwrap())

151
src/group/mod.rs Normal file
View File

@ -0,0 +1,151 @@
#![warn(
clippy::all,
/* clippy::restriction,*/
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::non_ascii_literal)]
use crate::userlib::NewFromString;
use log::warn;
use regex::Regex;
use crate::userlib_error::UserLibError;
use std::cmp::Eq;
use std::convert::TryFrom;
use std::fmt::{self, Debug, Display};
#[derive(Debug, PartialEq, Eq)]
pub struct Groupname {
groupname: String,
}
impl Display for Groupname {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.groupname,)
}
}
impl TryFrom<String> for Groupname {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
lazy_static! {
static ref USERVALIDATION: Regex =
Regex::new("^[a-z_]([a-z0-9_\\-]{0,31}|[a-z0-9_\\-]{0,30}\\$)$").unwrap();
}
if USERVALIDATION.is_match(&source) {
Ok(Self { groupname: source })
} else if source == "Debian-exim" {
warn!("username {} is not a valid username. This might cause problems. (It is default in Debian and Ubuntu)", source);
Ok(Self { groupname: source })
} else {
Err(UserLibError::Message(format!(
"Invalid groupname -{}-",
source
)))
}
}
}
/// A record(line) in the user database `/etc/shadow` found in most linux systems.
#[derive(Debug, PartialEq, Eq)]
pub struct Group {
groupname: Groupname, /* Username. */
pub(crate) password: crate::Password, /* Usually not used (disabled with x) */
gid: crate::Gid, /* Group ID. */
members: Vec<crate::Username>, /* Real name. */
}
impl Group {
#[must_use]
pub fn get_groupname(&self) -> &str {
&self.groupname.groupname
}
#[must_use]
pub const fn get_members(&self) -> &Vec<crate::Username> {
&self.members
}
}
impl Display for Group {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
"{}:{}:{}:{}",
self.groupname,
self.password,
self.gid,
self.members
.iter()
.map(|mem| format!("{}", mem))
.collect::<Vec<String>>()
.join(",")
)
}
}
impl NewFromString for Group {
/// Parse a line formatted like one in `/etc/shadow` and construct a matching `Shadow` instance
///
/// # Example
/// ```
/// /*let shad = adduser::shadow::Shadow::new_from_string(
/// "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::"
/// ).unwrap();
/// assert_eq!(shad.get_username(), "test");*/
/// ```
///
/// # Errors
/// When parsing fails this function returns a `UserLibError::Message` containing some information as to why the function failed.
fn new_from_string(line: String) -> Result<Self, UserLibError> {
println!("{}", &line);
let elements: Vec<String> = line.split(':').map(ToString::to_string).collect();
if elements.len() == 4 {
Ok(Group {
groupname: Groupname::try_from(elements.get(0).unwrap().to_string())?,
password: crate::Password::Disabled,
gid: crate::Gid::try_from(elements.get(2).unwrap().to_string())?,
members: parse_members_list(elements.get(3).unwrap().to_string()),
})
} else {
Err(UserLibError::Message(format!(
"Failed to parse: not enough elements ({}): {:?}",
elements.len(),
elements
)))
}
}
}
fn parse_members_list(source: String) -> Vec<crate::Username> {
let mut res = vec![];
for mem in source
.split(',')
.filter(|x| !x.is_empty())
.map(ToString::to_string)
{
res.push(crate::Username::try_from(mem).expect("failed to parse username"));
}
res
}
#[test]
fn test_parse_and_back_identity() {
let line = "teste:x:1002:test,teste";
let line2 = Group::new_from_string(line.to_owned()).unwrap();
assert_eq!(format!("{}", line2), line);
}
#[test]
fn test_groupname() {
let line = "teste:x:1002:test,teste";
let line2 = Group::new_from_string(line.to_owned()).unwrap();
assert_eq!(line2.get_groupname(), "teste");
}
#[test]
fn test_root_group() {
let line = "root:x:0:";
let line2 = Group::new_from_string(line.to_owned()).unwrap();
assert_eq!(line2.get_groupname(), "root");
}

View File

@ -3,7 +3,17 @@ extern crate lazy_static;
extern crate log;
pub mod passwd;
pub mod shadow;
pub mod api;
pub mod group;
pub mod user;
pub mod userlib;
pub mod userlib_error;
pub use passwd::{Gecos, Gid, HomeDir, Passwd, Password, ShellPath, Uid, Username};
pub use group::Group;
pub use user::gecos_fields::Gecos;
pub use user::passwd_fields::{
EncryptedPassword, Gid, HomeDir, Password, ShellPath, Uid, Username,
};
pub use user::shadow_fields::Shadow;
pub use user::User;
pub use userlib::NewFromString;
pub use userlib_error::UserLibError;

View File

@ -1,637 +0,0 @@
#![warn(
clippy::all,
/* clippy::restriction,*/
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::non_ascii_literal)]
use log::warn;
use regex::Regex;
use crate::userlib_error::UserLibError;
use std::cmp::Eq;
use std::convert::TryFrom;
use std::fmt::{self, Display};
/// The username of the current user
///
/// When done the validity will automatically be checked in the `trait TryFrom`.
///
/// In the future some extra fields might be added.
#[derive(Debug, PartialEq, Eq)]
pub struct Username<'a> {
/// The username value
pub(crate) username: &'a str,
}
impl Display for Username<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.username,)
}
}
impl<'a> TryFrom<&'a str> for Username<'a> {
type Error = UserLibError;
fn try_from(source: &'a str) -> std::result::Result<Self, Self::Error> {
lazy_static! {
static ref USERVALIDATION: Regex =
Regex::new("^[a-z_]([a-z0-9_\\-]{0,31}|[a-z0-9_\\-]{0,30}\\$)$").unwrap();
}
if USERVALIDATION.is_match(source) {
Ok(Self { username: source })
} else if source == "Debian-exim" {
warn!("username {} is not a valid username. This might cause problems. (It is default in Debian and Ubuntu)", source);
Ok(Self { username: source })
} else {
Err(UserLibError::Message(format!(
"Invalid username {}",
source
)))
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Password<'a> {
Encrypted(EncryptedPassword<'a>),
Shadow(crate::shadow::Shadow<'a>),
}
impl Display for Password<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Password::Encrypted(EncryptedPassword { password }) => write!(f, "{}", password,),
Password::Shadow(_) => write!(f, "x"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct EncryptedPassword<'a> {
pub(crate) password: &'a str,
}
impl Display for EncryptedPassword<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.password,)
}
}
impl<'a> TryFrom<&'a str> for EncryptedPassword<'a> {
type Error = UserLibError;
fn try_from(source: &'a str) -> std::result::Result<Self, Self::Error> {
if source == "x" {
warn!("password from shadow not loaded!")
} else {
warn!("Password field has an unexpected value")
};
Ok(Self { password: source })
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Uid {
uid: u32,
}
impl Display for Uid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.uid,)
}
}
impl TryFrom<&str> for Uid {
type Error = UserLibError;
fn try_from(source: &str) -> std::result::Result<Self, Self::Error> {
Ok(Self {
uid: source.parse::<u32>().unwrap(),
})
}
}
impl Uid {
#[must_use]
pub const fn is_system_uid(&self) -> bool {
// since it is a u32 it cannot be smaller than 0
self.uid < 1000
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Gid {
gid: u32,
}
impl Display for Gid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.gid,)
}
}
impl TryFrom<&str> for Gid {
type Error = UserLibError;
fn try_from(source: &str) -> std::result::Result<Self, Self::Error> {
Ok(Self {
gid: source.parse::<u32>().unwrap(),
})
}
}
impl Gid {
#[must_use]
pub const fn is_system_gid(&self) -> bool {
// since it is a u32 it cannot be smaller than 0
self.gid < 1000
}
}
/// The gecos field of a user.
///
/// In the `/etc/passwd` file this field is a `,` sepparated list of items.
/// The first 4 values are more or less standardised to be full name, room, phone at work and phone at home. After that there can be some extra fields often containing the emailadress and even additional information.
///
/// This enum represents the first 4 values by name and adds the other values to a list of strings [`Gecos::Detail`]. If only one field is found and no `,` at all this value is used as a human readable comment [`Gecos::Simple`].
#[derive(Debug, PartialEq, Eq)]
pub enum Gecos<'a> {
Detail {
full_name: &'a str,
room: &'a str,
phone_work: &'a str,
phone_home: &'a str,
other: Option<Vec<&'a str>>,
},
Simple {
comment: &'a str,
},
}
impl<'a> Gecos<'a> {
#[must_use]
pub const fn get_comment(&'a self) -> Option<&'a str> {
match *self {
Gecos::Simple { comment, .. } => Some(comment),
Gecos::Detail { .. } => None,
}
}
#[must_use]
pub const fn get_full_name(&'a self) -> Option<&'a str> {
match *self {
Gecos::Simple { .. } => None,
Gecos::Detail { full_name, .. } => {
if full_name.is_empty() {
None
} else {
Some(full_name)
}
}
}
}
#[must_use]
pub const fn get_room(&'a self) -> Option<&'a str> {
match *self {
Gecos::Simple { .. } => None,
Gecos::Detail { room, .. } => {
if room.is_empty() {
None
} else {
Some(room)
}
}
}
}
#[must_use]
pub const fn get_phone_work(&'a self) -> Option<&'a str> {
match *self {
Gecos::Simple { .. } => None,
Gecos::Detail { phone_work, .. } => {
if phone_work.is_empty() {
None
} else {
Some(phone_work)
}
}
}
}
#[must_use]
pub const fn get_phone_home(&'a self) -> Option<&'a str> {
match *self {
Gecos::Simple { .. } => None,
Gecos::Detail { phone_home, .. } => {
if phone_home.is_empty() {
None
} else {
Some(phone_home)
}
}
}
}
#[must_use]
pub const fn get_other(&'a self) -> Option<&Vec<&'a str>> {
match self {
Gecos::Simple { .. } => None,
Gecos::Detail { other, .. } => match other {
None => None,
Some(comments) => Some(comments),
},
}
}
}
impl Display for Gecos<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Gecos::Simple { comment } => write!(f, "{}", comment),
Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => write!(
f,
"{},{},{},{}{}",
full_name,
room,
phone_work,
phone_home,
match other {
None => "".to_string(),
Some(cont) => format!(",{}", cont.join(",")),
}
),
}
}
}
impl<'a> TryFrom<&'a str> for Gecos<'a> {
type Error = UserLibError;
fn try_from(source: &'a str) -> std::result::Result<Self, Self::Error> {
let vals: Vec<&str> = source.split(',').collect();
if vals.len() > 3 {
Ok(Gecos::Detail {
full_name: vals[0],
room: vals[1],
phone_work: vals[2],
phone_home: vals[3],
other: if vals.len() == 4 {
None
} else {
Some(vals[4..].to_vec())
},
})
} else if vals.len() == 1 {
Ok(Gecos::Simple {
comment: vals.get(0).unwrap(),
})
} else {
panic!(format!("Could not parse this string: {}", source))
}
}
}
/// The home directory of a user
#[derive(Debug, PartialEq, Eq)]
pub struct HomeDir<'a> {
dir: &'a str,
}
impl Display for HomeDir<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.dir,)
}
}
impl<'a> TryFrom<&'a str> for HomeDir<'a> {
type Error = UserLibError;
fn try_from(source: &'a str) -> std::result::Result<Self, Self::Error> {
Ok(Self { dir: source })
}
}
/// The path to the Shell binary
#[derive(Debug, PartialEq, Eq)]
pub struct ShellPath<'a> {
shell: &'a str,
}
impl Display for ShellPath<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.shell,)
}
}
impl<'a> TryFrom<&'a str> for ShellPath<'a> {
type Error = UserLibError;
fn try_from(source: &'a str) -> std::result::Result<Self, Self::Error> {
Ok(ShellPath { shell: source })
}
}
/// A record(line) in the user database `/etc/passwd` found in most linux systems.
#[derive(Debug, PartialEq, Eq)]
pub struct Passwd<'a> {
username: Username<'a>, /* Username. */
password: Password<'a>, /* Hashed passphrase, if shadow database not in use (see shadow.h). */
uid: Uid, /* User ID. */
gid: Gid, /* Group ID. */
gecos: Gecos<'a>, /* Real name. */
home_dir: HomeDir<'a>, /* Home directory. */
shell_path: ShellPath<'a>, /* Shell program. */
}
impl<'a> Passwd<'a> {
/// Parse a line formatted like one in `/etc/passwd` and construct a matching `Passwd` instance
///
/// # Example
/// ```
/// let pwd = adduser::passwd::Passwd::new_from_string(
/// "testuser:testpassword:1001:1001:full Name,,,,:/home/test:/bin/test"
/// ).unwrap();
/// assert_eq!(pwd.get_username(), "testuser");
/// ```
///
/// # Errors
/// When parsing fails this function returns a `UserLibError::Message` containing some information as to why the function failed.
pub fn new_from_string(line: &'a str) -> Result<Self, UserLibError> {
let elements: Vec<&str> = line.split(':').collect();
if elements.len() == 7 {
Ok(Passwd {
username: Username::try_from(*elements.get(0).unwrap())?,
password: Password::Encrypted(EncryptedPassword::try_from(
*elements.get(1).unwrap(),
)?),
uid: Uid::try_from(*elements.get(2).unwrap())?,
gid: Gid::try_from(*elements.get(3).unwrap())?,
gecos: Gecos::try_from(*elements.get(4).unwrap())?,
home_dir: HomeDir::try_from(*elements.get(5).unwrap())?,
shell_path: ShellPath::try_from(*elements.get(6).unwrap())?,
})
} else {
Err("Failed to parse: not enough elements".into())
}
}
#[must_use]
pub const fn get_username(&self) -> &'a str {
self.username.username
}
#[must_use]
pub const fn get_password(&self) -> &'a str {
match self.password {
Password::Encrypted(EncryptedPassword { password }) => password,
Password::Shadow(crate::shadow::Shadow { ref password, .. }) => password.password,
}
}
#[must_use]
pub const fn get_uid(&self) -> u32 {
self.uid.uid
}
#[must_use]
pub const fn get_gid(&self) -> u32 {
self.gid.gid
}
#[must_use]
pub const fn get_comment(&self) -> &Gecos {
&self.gecos
}
#[must_use]
pub const fn get_home_dir(&self) -> &'a str {
self.home_dir.dir
}
#[must_use]
pub const fn get_shell_path(&self) -> &'a str {
self.shell_path.shell
}
}
impl Default for Passwd<'_> {
fn default() -> Self {
Passwd {
username: Username {
username: "defaultuser",
},
password: Password::Encrypted(EncryptedPassword {
password: "notencrypted",
}),
uid: Uid { uid: 1001 },
gid: Gid { gid: 1001 },
gecos: Gecos::Simple {
comment: "gecos default comment",
},
home_dir: HomeDir {
dir: "/home/default",
},
shell_path: ShellPath { shell: "/bin/bash" },
}
}
}
impl Display for Passwd<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{}:{}:{}:{}:{}",
self.username,
self.password,
self.uid,
self.gid,
self.gecos,
self.home_dir,
self.shell_path
)
}
}
// Tests ----------------------------------------------------------------------
#[test]
fn test_username_validation() {
// Failing tests
let umlauts = Username::try_from("täst"); // umlauts
assert_eq!(
Err(UserLibError::Message("Invalid username täst".into())),
umlauts
);
let number_first = Username::try_from("11elf"); // numbers first
assert_eq!(
Err(UserLibError::Message("Invalid username 11elf".into())),
number_first
);
let slashes = Username::try_from("test/name"); // slashes in the name
assert_eq!(
Err(UserLibError::Message("Invalid username test/name".into())),
slashes
);
let long = Username::try_from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"); // maximum size 32 letters
assert_eq!(
Err(UserLibError::Message(
"Invalid username aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".into()
)),
long
);
// Working tests
let ubuntu_exception = Username::try_from("Debian-exim"); // for some reason ubuntu and debian have a capital user.
assert_eq!(ubuntu_exception.unwrap().username, "Debian-exim");
let single = Username::try_from("t"); // single characters are ok
assert_eq!(single.unwrap().username, "t");
let normal = Username::try_from("superman"); // regular username
assert_eq!(normal.unwrap().username, "superman");
let normal = Username::try_from("anna3pete"); // regular username containing a number
assert_eq!(normal.unwrap().username, "anna3pete");
let normal = Username::try_from("enya$"); // regular username ending in a $
assert_eq!(normal.unwrap().username, "enya$");
}
#[test]
fn test_default_user() {
// Check if a user can be created.
let pwd = Passwd::default();
assert_eq!(pwd.username.username, "defaultuser");
assert_eq!(pwd.home_dir.dir, "/home/default");
assert_eq!(pwd.uid.uid, 1001);
}
#[test]
fn test_guid_system_user() {
// Check uids of system users.
let values = vec![("999", true), ("0", true), ("1000", false)];
for val in values {
assert_eq!(Uid::try_from(val.0).unwrap().is_system_uid(), val.1);
assert_eq!(Gid::try_from(val.0).unwrap().is_system_gid(), val.1);
}
}
#[test]
fn test_parse_gecos() {
// test if the Gecos field can be parsed and the resulting struct is populated correctly.
let gcdetail = "Full Name,504,11345342,ä1-2312,myemail@test.com";
let gcsimple = "A böring comment →";
let gc_no_other: &str = "systemd Network Management,,,";
let res_detail = Gecos::try_from(gcdetail).unwrap();
let res_simple = Gecos::try_from(gcsimple).unwrap();
let res_no_other = Gecos::try_from(gc_no_other).unwrap();
match res_simple {
Gecos::Simple { comment } => assert_eq!(comment, "A böring comment →"),
_ => unreachable!(),
}
match res_detail {
Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => {
assert_eq!(full_name, "Full Name");
assert_eq!(room, "504");
assert_eq!(phone_work, "11345342");
assert_eq!(phone_home, "ä1-2312");
assert_eq!(other.unwrap()[0], "myemail@test.com");
}
_ => unreachable!(),
}
match res_no_other {
Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => {
assert_eq!(full_name, "systemd Network Management");
assert_eq!(room, "");
assert_eq!(phone_work, "");
assert_eq!(phone_home, "");
assert_eq!(other, None);
}
_ => unreachable!(),
}
}
#[test]
fn test_gecos_getters() {
// test if the Gecos field can be parsed and the resulting struct is populated correctly.
let gcdetail = "Full Name,504,11345342,ä1-2312,myemail@test.com";
let gcsimple = "A böring comment →";
let gc_no_other: &str = "systemd Network Management,,,";
let res_detail = Gecos::try_from(gcdetail).unwrap();
let res_simple = Gecos::try_from(gcsimple).unwrap();
let res_no_other = Gecos::try_from(gc_no_other).unwrap();
assert_eq!(res_simple.get_comment(), Some("A böring comment →"));
assert_eq!(res_detail.get_comment(), None);
println!("{:?}", res_detail);
assert_eq!(res_detail.get_full_name(), Some("Full Name"));
assert_eq!(res_detail.get_room(), Some("504"));
assert_eq!(res_detail.get_phone_work(), Some("11345342"));
assert_eq!(res_detail.get_phone_home(), Some("ä1-2312"));
assert_eq!(res_detail.get_other(), Some(&vec!["myemail@test.com"]));
assert_eq!(
res_no_other.get_full_name(),
Some("systemd Network Management")
);
assert_eq!(res_no_other.get_room(), None);
assert_eq!(res_no_other.get_phone_work(), None);
assert_eq!(res_no_other.get_phone_home(), None);
assert_eq!(res_no_other.get_other(), None);
}
#[test]
fn test_new_from_string() {
// Test if a single line can be parsed and if the resulting struct is populated correctly.
let fail = Passwd::new_from_string("").err().unwrap();
assert_eq!(
fail,
UserLibError::Message("Failed to parse: not enough elements".into())
);
let pwd =
Passwd::new_from_string("testuser:testpassword:1001:1001:testcomment:/home/test:/bin/test")
.unwrap();
let pwd2 =
Passwd::new_from_string("testuser:testpassword:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test")
.unwrap();
assert_eq!(pwd.username.username, "testuser");
assert_eq!(pwd.home_dir.dir, "/home/test");
assert_eq!(pwd.uid.uid, 1001);
match pwd.gecos {
Gecos::Simple { comment } => assert_eq!(comment, "testcomment"),
_ => unreachable!(),
}
match pwd2.gecos {
Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => {
assert_eq!(full_name, "full Name");
assert_eq!(room, "004");
assert_eq!(phone_work, "000342");
assert_eq!(phone_home, "001-2312");
assert_eq!(other.unwrap()[0], "myemail@test.com");
}
_ => unreachable!(),
}
}
#[test]
fn test_parse_passwd() {
// Test wether the passwd file can be parsed and recreated without throwing an exception
use std::fs::File;
use std::io::{prelude::*, BufReader};
let file = File::open("/etc/passwd").unwrap();
let reader = BufReader::new(file);
for line in reader.lines() {
let lineorig: String = line.unwrap();
let linecopy = lineorig.clone();
let pass_struc = Passwd::new_from_string(&linecopy).unwrap();
assert_eq!(
// ignoring the numbers of `,` since the implementation does not (yet) reproduce a missing comment field.
lineorig,
format!("{}", pass_struc)
);
}
}

226
src/user/gecos_fields.rs Normal file
View File

@ -0,0 +1,226 @@
use crate::UserLibError;
use std::cmp::Eq;
use std::convert::TryFrom;
use std::fmt::{self, Display};
/// The gecos field of a user.
///
/// In the `/etc/passwd` file this field is a `,` sepparated list of items.
/// The first 4 values are more or less standardised to be full name, room, phone at work and phone at home. After that there can be some extra fields often containing the emailadress and even additional information.
///
/// This enum represents the first 4 values by name and adds the other values to a list of strings [`Gecos::Detail`]. If only one field is found and no `,` at all this value is used as a human readable comment [`Gecos::Simple`].
#[derive(Debug, PartialEq, Eq)]
pub enum Gecos {
Detail {
full_name: String,
room: String,
phone_work: String,
phone_home: String,
other: Option<Vec<String>>,
},
Simple {
comment: String,
},
}
impl<'a> Gecos {
#[must_use]
pub fn get_comment(&'a self) -> Option<&'a str> {
match &self {
Gecos::Simple { comment, .. } => Some(&comment),
Gecos::Detail { .. } => None,
}
}
#[must_use]
pub fn get_full_name(&self) -> Option<&str> {
match &self {
Gecos::Simple { .. } => None,
Gecos::Detail { full_name, .. } => {
if full_name.is_empty() {
None
} else {
Some(&full_name)
}
}
}
}
#[must_use]
pub fn get_room(&self) -> Option<&str> {
match &self {
Gecos::Simple { .. } => None,
Gecos::Detail { room, .. } => {
if room.is_empty() {
None
} else {
Some(&room)
}
}
}
}
#[must_use]
pub fn get_phone_work(&self) -> Option<&str> {
match &self {
Gecos::Simple { .. } => None,
Gecos::Detail { phone_work, .. } => {
if phone_work.is_empty() {
None
} else {
Some(&phone_work)
}
}
}
}
#[must_use]
pub fn get_phone_home(&'_ self) -> Option<&'_ str> {
match &self {
Gecos::Simple { .. } => None,
Gecos::Detail { phone_home, .. } => {
if phone_home.is_empty() {
None
} else {
Some(&phone_home)
}
}
}
}
#[must_use]
pub const fn get_other(&self) -> Option<&Vec<String>> {
match self {
Gecos::Simple { .. } => None,
Gecos::Detail { other, .. } => match other {
None => None,
Some(comments) => Some(comments),
},
}
}
}
impl Display for Gecos {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self {
Gecos::Simple { comment } => write!(f, "{}", comment),
Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => write!(
f,
"{},{},{},{}{}",
full_name,
room,
phone_work,
phone_home,
match other {
None => "".to_string(),
Some(cont) => format!(",{}", cont.join(",")),
}
),
}
}
}
impl TryFrom<String> for Gecos {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
let vals: Vec<String> = source.split(',').map(ToString::to_string).collect();
if vals.len() > 3 {
Ok(Gecos::Detail {
full_name: vals[0].clone(),
room: vals[1].clone(),
phone_work: vals[2].clone(),
phone_home: vals[3].clone(),
other: if vals.len() == 4 {
None
} else {
Some(vals[4..].to_vec())
},
})
} else if vals.len() == 1 {
Ok(Gecos::Simple {
comment: vals.get(0).unwrap().into(),
})
} else {
panic!(format!("Could not parse this string: {}", source))
}
}
}
#[test]
fn test_parse_gecos() {
// test if the Gecos field can be parsed and the resulting struct is populated correctly.
let gcdetail = "Full Name,504,11345342,ä1-2312,myemail@test.com".to_string();
let gcsimple = "A böring comment →".to_string();
let gc_no_other = "systemd Network Management,,,".to_string();
let res_detail = crate::Gecos::try_from(gcdetail).unwrap();
let res_simple = crate::Gecos::try_from(gcsimple).unwrap();
let res_no_other = crate::Gecos::try_from(gc_no_other).unwrap();
match res_simple {
crate::Gecos::Simple { comment } => assert_eq!(comment, "A böring comment →"),
_ => unreachable!(),
}
match res_detail {
crate::Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => {
assert_eq!(full_name, "Full Name");
assert_eq!(room, "504");
assert_eq!(phone_work, "11345342");
assert_eq!(phone_home, "ä1-2312");
assert_eq!(other.unwrap()[0], "myemail@test.com");
}
_ => unreachable!(),
}
match res_no_other {
crate::Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => {
assert_eq!(full_name, "systemd Network Management");
assert_eq!(room, "");
assert_eq!(phone_work, "");
assert_eq!(phone_home, "");
assert_eq!(other, None);
}
_ => unreachable!(),
}
}
#[test]
fn test_gecos_getters() {
// test if the Gecos field can be parsed and the resulting struct is populated correctly.
let gcdetail = "Full Name,504,11345342,ä1-2312,myemail@test.com".to_string();
let gcsimple = "A böring comment →".to_string();
let gc_no_other = "systemd Network Management,,,".to_string();
let res_detail = crate::Gecos::try_from(gcdetail).unwrap();
let res_simple = crate::Gecos::try_from(gcsimple).unwrap();
let res_no_other = crate::Gecos::try_from(gc_no_other).unwrap();
assert_eq!(res_simple.get_comment(), Some("A böring comment →"));
assert_eq!(res_detail.get_comment(), None);
println!("{:?}", res_detail);
assert_eq!(res_detail.get_full_name(), Some("Full Name"));
assert_eq!(res_detail.get_room(), Some("504"));
assert_eq!(res_detail.get_phone_work(), Some("11345342"));
assert_eq!(res_detail.get_phone_home(), Some("ä1-2312"));
assert_eq!(
res_detail.get_other(),
Some(&vec!["myemail@test.com".to_string()])
);
assert_eq!(
res_no_other.get_full_name(),
Some("systemd Network Management")
);
assert_eq!(res_no_other.get_room(), None);
assert_eq!(res_no_other.get_phone_work(), None);
assert_eq!(res_no_other.get_phone_home(), None);
assert_eq!(res_no_other.get_other(), None);
}

202
src/user/mod.rs Normal file
View File

@ -0,0 +1,202 @@
pub mod gecos_fields;
pub mod passwd_fields;
pub mod shadow_fields;
use crate::userlib::NewFromString;
use std::convert::TryFrom;
use std::fmt::{self, Display};
/// A record(line) in the user database `/etc/passwd` found in most linux systems.
#[derive(Debug, PartialEq, Eq)]
pub struct User {
source: String,
username: crate::Username, /* Username. */
password: crate::Password, /* Hashed passphrase, if shadow database not in use (see shadow.h). */
uid: crate::Uid, /* User ID. */
gid: crate::Gid, /* Group ID. */
gecos: crate::Gecos, /* Real name. */
home_dir: crate::HomeDir, /* Home directory. */
shell_path: crate::ShellPath, /* Shell program. */
}
impl NewFromString for User {
/// Parse a line formatted like one in `/etc/passwd` and construct a matching [`adduser::User`] instance
///
/// # Example
/// ```
/// use adduser::NewFromString;
/// let pwd = adduser::User::new_from_string(
/// "testuser:testpassword:1001:1001:full Name,,,,:/home/test:/bin/test".to_string()).unwrap();
/// assert_eq!(pwd.get_username(), "testuser");
/// ```
///
/// # Errors
/// When parsing fails this function returns a `UserLibError::Message` containing some information as to why the function failed.
fn new_from_string(line: String) -> Result<Self, crate::UserLibError>
where
Self: Sized,
{
let elements: Vec<String> = line.split(':').map(ToString::to_string).collect();
if elements.len() == 7 {
Ok(Self {
source: line,
username: crate::Username::try_from(elements.get(0).unwrap().to_string())?,
password: crate::Password::Encrypted(crate::EncryptedPassword::try_from(
elements.get(1).unwrap().to_string(),
)?),
uid: crate::Uid::try_from(elements.get(2).unwrap().to_string())?,
gid: crate::Gid::try_from(elements.get(3).unwrap().to_string())?,
gecos: crate::Gecos::try_from(elements.get(4).unwrap().to_string())?,
home_dir: crate::HomeDir::try_from(elements.get(5).unwrap().to_string())?,
shell_path: crate::ShellPath::try_from(elements.get(6).unwrap().to_string())?,
})
} else {
Err("Failed to parse: not enough elements".into())
}
}
}
impl User {
#[must_use]
pub fn get_username(&self) -> &str {
&self.username.username
}
#[must_use]
pub fn get_password(&self) -> &str {
match &self.password {
crate::Password::Encrypted(crate::EncryptedPassword { password }) => &password,
crate::Password::Shadow(crate::Shadow { ref password, .. }) => &password.password,
crate::Password::Disabled => &"x",
}
}
#[must_use]
pub const fn get_uid(&self) -> u32 {
self.uid.uid
}
#[must_use]
pub const fn get_gid(&self) -> u32 {
self.gid.gid
}
#[must_use]
pub const fn get_comment(&self) -> &crate::Gecos {
&self.gecos
}
#[must_use]
pub fn get_home_dir(&self) -> &str {
&self.home_dir.dir
}
#[must_use]
pub fn get_shell_path(&self) -> &str {
&self.shell_path.shell
}
}
impl Default for User {
fn default() -> Self {
Self {
source: "".to_owned(),
username: crate::Username {
username: "defaultuser".to_owned(),
},
password: crate::Password::Encrypted(crate::EncryptedPassword {
password: "notencrypted".to_owned(),
}),
uid: crate::Uid { uid: 1001 },
gid: crate::Gid { gid: 1001 },
gecos: crate::Gecos::Simple {
comment: "gecos default comment".to_string(),
},
home_dir: crate::HomeDir {
dir: "/home/default".to_owned(),
},
shell_path: crate::ShellPath {
shell: "/bin/bash".to_owned(),
},
}
}
}
impl Display for User {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"{}:{}:{}:{}:{}:{}:{}",
self.username,
self.password,
self.uid,
self.gid,
self.gecos,
self.home_dir,
self.shell_path
)
}
}
#[test]
fn test_default_user() {
// Check if a user can be created.
let pwd = User::default();
assert_eq!(pwd.username.username, "defaultuser");
assert_eq!(pwd.home_dir.dir, "/home/default");
assert_eq!(pwd.uid.uid, 1001);
}
#[test]
fn test_new_from_string() {
// Test if a single line can be parsed and if the resulting struct is populated correctly.
let fail = User::new_from_string("".into()).err().unwrap();
assert_eq!(
fail,
crate::UserLibError::Message("Failed to parse: not enough elements".into())
);
let pwd = User::new_from_string(
"testuser:testpassword:1001:1001:testcomment:/home/test:/bin/test".into(),
)
.unwrap();
let pwd2 =
User::new_from_string("testuser:testpassword:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test".into())
.unwrap();
assert_eq!(pwd.username.username, "testuser");
assert_eq!(pwd.home_dir.dir, "/home/test");
assert_eq!(pwd.uid.uid, 1001);
match pwd.gecos {
crate::Gecos::Simple { comment } => assert_eq!(comment, "testcomment"),
_ => unreachable!(),
}
match pwd2.gecos {
crate::Gecos::Detail {
full_name,
room,
phone_work,
phone_home,
other,
} => {
assert_eq!(full_name, "full Name");
assert_eq!(room, "004");
assert_eq!(phone_work, "000342");
assert_eq!(phone_home, "001-2312");
assert_eq!(other.unwrap()[0], "myemail@test.com");
}
_ => unreachable!(),
}
}
#[test]
fn test_parse_passwd() {
// Test wether the passwd file can be parsed and recreated without throwing an exception
use std::fs::File;
use std::io::{prelude::*, BufReader};
let file = File::open("/etc/passwd").unwrap();
let reader = BufReader::new(file);
for line in reader.lines() {
let lineorig: String = line.unwrap();
let linecopy = lineorig.clone();
let pass_struc = User::new_from_string(linecopy).unwrap();
assert_eq!(
// ignoring the numbers of `,` since the implementation does not (yet) reproduce a missing comment field.
lineorig,
format!("{}", pass_struc)
);
}
}

242
src/user/passwd_fields.rs Normal file
View File

@ -0,0 +1,242 @@
#![warn(
clippy::all,
/* clippy::restriction,*/
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::non_ascii_literal)]
use log::warn;
use regex::Regex;
use crate::UserLibError;
use std::cmp::Eq;
use std::convert::TryFrom;
use std::fmt::{self, Display};
/// The username of the current user
///
/// When done the validity will automatically be checked in the `trait TryFrom`.
///
/// In the future some extra fields might be added.
#[derive(Debug, PartialEq, Eq)]
pub struct Username {
/// The username value
pub(in crate::user) username: String,
}
impl Display for Username {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.username,)
}
}
impl TryFrom<String> for Username {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
lazy_static! {
static ref USERVALIDATION: Regex =
Regex::new("^[a-z_]([a-z0-9_\\-]{0,31}|[a-z0-9_\\-]{0,30}\\$)$").unwrap();
}
if USERVALIDATION.is_match(&source) {
Ok(Self { username: source })
} else if source == "Debian-exim" {
warn!("username {} is not a valid username. This might cause problems. (It is default in Debian and Ubuntu)", source);
Ok(Self { username: source })
} else {
Err(UserLibError::Message(format!(
"Invalid username {}",
source
)))
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub enum Password {
Encrypted(crate::EncryptedPassword),
Shadow(crate::Shadow),
Disabled,
}
impl Display for Password {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Password::Encrypted(EncryptedPassword { password }) => write!(f, "{}", password,),
Password::Shadow(_) => write!(f, "x"),
Password::Disabled => write!(f, "x"),
}
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct EncryptedPassword {
pub(in crate::user) password: String,
}
impl Display for EncryptedPassword {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.password,)
}
}
impl TryFrom<String> for EncryptedPassword {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
if source == "x" {
warn!("password from shadow not loaded!")
} else {
warn!("Password field has an unexpected value")
};
Ok(Self { password: source })
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Uid {
pub(in crate::user) uid: u32,
}
impl Display for Uid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.uid,)
}
}
impl TryFrom<String> for Uid {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
Ok(Self {
uid: source.parse::<u32>().unwrap(),
})
}
}
impl Uid {
#[must_use]
pub const fn is_system_uid(&self) -> bool {
// since it is a u32 it cannot be smaller than 0
self.uid < 1000
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct Gid {
pub(in crate::user) gid: u32,
}
impl Display for Gid {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.gid,)
}
}
impl TryFrom<String> for Gid {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
Ok(Self {
gid: source.parse::<u32>().unwrap(),
})
}
}
impl Gid {
#[must_use]
pub const fn is_system_gid(&self) -> bool {
// since it is a u32 it cannot be smaller than 0
self.gid < 1000
}
}
/// The home directory of a user
#[derive(Debug, PartialEq, Eq)]
pub struct HomeDir {
pub(in crate::user) dir: String,
}
impl Display for HomeDir {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.dir,)
}
}
impl TryFrom<String> for HomeDir {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
Ok(Self { dir: source })
}
}
/// The path to the Shell binary
#[derive(Debug, PartialEq, Eq)]
pub struct ShellPath {
pub(in crate::user) shell: String,
}
impl Display for ShellPath {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.shell,)
}
}
impl TryFrom<String> for ShellPath {
type Error = UserLibError;
fn try_from(source: String) -> std::result::Result<Self, Self::Error> {
Ok(ShellPath { shell: source })
}
}
// Tests ----------------------------------------------------------------------
#[test]
fn test_username_validation() {
// Failing tests
let umlauts: Result<Username, UserLibError> = Username::try_from("täst".to_owned()); // umlauts
assert_eq!(
Err(UserLibError::Message("Invalid username täst".into())),
umlauts
);
let number_first = Username::try_from("11elf".to_owned()); // numbers first
assert_eq!(
Err(UserLibError::Message("Invalid username 11elf".into())),
number_first
);
let slashes = Username::try_from("test/name".to_owned()); // slashes in the name
assert_eq!(
Err(UserLibError::Message("Invalid username test/name".into())),
slashes
);
let long = Username::try_from("aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()); // maximum size 32 letters
assert_eq!(
Err(UserLibError::Message(
"Invalid username aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa".to_owned()
)),
long
);
// Working tests
let ubuntu_exception = Username::try_from("Debian-exim".to_owned()); // for some reason ubuntu and debian have a capital user.
assert_eq!(ubuntu_exception.unwrap().username, "Debian-exim");
let single = Username::try_from("t".to_owned()); // single characters are ok
assert_eq!(single.unwrap().username, "t");
let normal = Username::try_from("superman".to_owned()); // regular username
assert_eq!(normal.unwrap().username, "superman");
let normal = Username::try_from("anna3pete".to_owned()); // regular username containing a number
assert_eq!(normal.unwrap().username, "anna3pete");
let normal = Username::try_from("enya$".to_owned()); // regular username ending in a $
assert_eq!(normal.unwrap().username, "enya$");
}
#[test]
fn test_guid_system_user() {
// Check uids of system users.
let values = vec![
("999".to_owned(), true),
("0".to_owned(), true),
("1000".to_owned(), false),
];
for val in values {
assert_eq!(Uid::try_from(val.0.clone()).unwrap().is_system_uid(), val.1);
assert_eq!(Gid::try_from(val.0.clone()).unwrap().is_system_gid(), val.1);
}
}

View File

@ -7,9 +7,9 @@
)]
#![allow(clippy::non_ascii_literal)]
use crate::userlib::NewFromString;
use log::warn;
use crate::passwd;
use crate::userlib_error::UserLibError;
use std::cmp::Eq;
use std::convert::TryFrom;
@ -17,9 +17,9 @@ use std::fmt::{self, Debug, Display};
/// A record(line) in the user database `/etc/shadow` found in most linux systems.
#[derive(Debug, PartialEq, Eq)]
pub struct Shadow<'a> {
username: passwd::Username<'a>, /* Username. */
pub(crate) password: passwd::EncryptedPassword<'a>, /* Hashed passphrase */
pub struct Shadow {
username: crate::Username, /* Username. */
pub(crate) password: crate::EncryptedPassword, /* Hashed passphrase */
last_change: Option<chrono::NaiveDateTime>, /* User ID. */
earliest_change: Option<chrono::NaiveDateTime>, /* Group ID. */
latest_change: Option<chrono::NaiveDateTime>, /* Real name. */
@ -29,18 +29,18 @@ pub struct Shadow<'a> {
extensions: Option<u64>, /* Shell program. */
}
impl<'a> Shadow<'a> {
impl Shadow {
#[must_use]
pub const fn get_username(&self) -> &'a str {
self.username.username
pub fn get_username(&self) -> &str {
&self.username.username
}
#[must_use]
pub const fn get_password(&self) -> &'a str {
self.password.password
pub fn get_password(&self) -> &str {
&self.password.password
}
}
impl<'a> Display for Shadow<'a> {
impl Display for Shadow {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
write!(
f,
@ -78,27 +78,28 @@ fn show_option_duration(input: Option<chrono::Duration>) -> String {
}
}
impl<'a> Shadow<'a> {
impl NewFromString for Shadow {
/// Parse a line formatted like one in `/etc/shadow` and construct a matching `Shadow` instance
///
/// # Example
/// ```
/// let shad = adduser::shadow::Shadow::new_from_string(
/// "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::"
/// use adduser::NewFromString;
/// let shad = adduser::Shadow::new_from_string(
/// "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::".to_string()
/// ).unwrap();
/// assert_eq!(shad.get_username(), "test");
/// ```
///
/// # Errors
/// When parsing fails this function returns a `UserLibError::Message` containing some information as to why the function failed.
pub fn new_from_string(line: &'a str) -> Result<Self, UserLibError> {
fn new_from_string(line: String) -> Result<Self, UserLibError> {
println!("{}", &line);
let elements: Vec<&str> = line.split(':').collect();
let elements: Vec<String> = line.split(':').map(ToString::to_string).collect();
if elements.len() == 9 {
let extra = elements.get(8).unwrap();
Ok(Shadow {
username: passwd::Username::try_from(*elements.get(0).unwrap())?,
password: passwd::EncryptedPassword::try_from(*elements.get(1).unwrap())?,
username: crate::Username::try_from(elements.get(0).unwrap().to_string())?,
password: crate::EncryptedPassword::try_from(elements.get(1).unwrap().to_string())?,
last_change: date_since_epoch(elements.get(2).unwrap()),
earliest_change: date_since_epoch(elements.get(3).unwrap()),
latest_change: date_since_epoch(elements.get(4).unwrap()),
@ -122,6 +123,7 @@ impl<'a> Shadow<'a> {
}
const SECONDS_PER_DAY: i64 = 86400;
fn date_since_epoch(days_since_epoch: &str) -> Option<chrono::NaiveDateTime> {
if days_since_epoch.is_empty() {
None
@ -141,10 +143,10 @@ fn duration_for_days(days_source: &str) -> Option<chrono::Duration> {
}
#[test]
fn test_since_epoch() {
fn test_parse_and_back_identity() {
println!("Test");
let line = "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::";
let line2 = Shadow::new_from_string(line).unwrap();
let line2 = Shadow::new_from_string(line.to_owned()).unwrap();
println!("{:#?}", line2);
assert_eq!(format!("{}", line2), line);
}

177
src/userlib.rs Normal file
View File

@ -0,0 +1,177 @@
#![warn(
clippy::all,
/* clippy::restriction,*/
clippy::pedantic,
clippy::nursery,
clippy::cargo
)]
#![allow(clippy::non_ascii_literal)]
use log::warn;
use std::fs::File;
use std::io::{BufReader, Read};
use std::path::PathBuf;
pub struct UserDBLocal {
source_files: Files,
pub passwd_entries: Vec<crate::User>,
pub shadow_entries: Vec<crate::Shadow>,
pub group_entries: Vec<crate::Group>,
}
pub struct Files {
passwd: Option<PathBuf>,
shadow: Option<PathBuf>,
group: Option<PathBuf>,
}
impl Default for Files {
fn default() -> Self {
Self {
passwd: Some(PathBuf::from("/etc/passwd")),
shadow: Some(PathBuf::from("/etc/shadow")),
group: Some(PathBuf::from("/etc/group")),
}
}
}
impl UserDBLocal {
#[must_use]
pub fn import_from_strings(
passwd_content: String,
shadow_content: String,
group_content: String,
) -> Self {
let res = UserDBLocal {
source_files: Files {
passwd: None,
group: None,
shadow: None,
},
passwd_entries: passwd_content
.lines()
.filter_map(|line| {
if line.len() > 5 {
println!("{}", line);
Some(
crate::User::new_from_string(line.to_owned())
.expect("failed to read lines"),
)
} else {
None
}
})
.collect(),
group_entries: group_content
.lines()
.filter_map(|line| {
if line.len() > 5 {
Some(
crate::Group::new_from_string(line.to_owned()).expect("Parsing failed"),
)
} else {
None
}
})
.collect(),
shadow_entries: shadow_content
.lines()
.filter_map(|line| {
if line.len() > 5 {
Some(
crate::Shadow::new_from_string(line.to_owned())
.expect("Parsing failed"),
)
} else {
None
}
})
.collect(),
};
res
}
#[must_use]
pub fn load_files(files: Files) -> Self {
let passwd_file = File::open(
files
.group
.clone()
.expect("passwd file path cannot be None"),
)
.unwrap();
let mut passwd_reader = BufReader::new(passwd_file);
let mut my_passwd_lines = String::new();
passwd_reader.read_to_string(&mut my_passwd_lines).unwrap();
let group_file =
File::open(files.group.clone().expect("group file path cannot be None")).unwrap();
let mut group_reader = BufReader::new(group_file);
let mut my_group_lines = String::new();
group_reader.read_to_string(&mut my_group_lines).unwrap();
let shadow_file = File::open(
files
.shadow
.clone()
.expect("shadow file path cannot be None"),
)
.expect("Failed to read the shadow file. Most of the time root permissions are needed");
let mut shadow_reader = BufReader::new(shadow_file);
let mut my_shadow_lines = String::new();
shadow_reader.read_to_string(&mut my_shadow_lines).unwrap();
Self {
source_files: files,
passwd_entries: string_to(&my_passwd_lines),
group_entries: string_to(&my_group_lines),
shadow_entries: string_to(&my_shadow_lines),
}
}
}
pub trait NewFromString {
fn new_from_string(line: String) -> Result<Self, crate::UserLibError>
where
Self: Sized;
}
fn string_to<T>(source: &str) -> Vec<T>
where
T: NewFromString,
{
source
.lines()
.filter_map(|line| {
if line.len() > 5 {
println!("{}", line);
Some(T::new_from_string(line.to_owned()).expect("failed to read lines"))
} else {
None
}
})
.collect()
}
#[test]
fn test_creator_user_db_local() {
let data = UserDBLocal::import_from_strings("testuser:x:1001:1001:full Name,004,000342,001-2312,myemail@test.com:/home/test:/bin/test".to_string(), "test:!!$6$/RotIe4VZzzAun4W$7YUONvru1rDnllN5TvrnOMsWUD5wSDUPAD6t6/Xwsr/0QOuWF3HcfAhypRkGa8G1B9qqWV5kZSnCb8GKMN9N61:18260:0:99999:7:::".to_string(), "teste:x:1002:test,teste".to_string());
assert_eq!(
data.passwd_entries.get(0).unwrap().get_username(),
"testuser"
)
}
#[test]
fn test_parsing_local_database() {
use std::fs::File;
use std::io::{BufReader, Read};
let passwd_file = File::open("/etc/passwd").unwrap();
let mut passwd_reader = BufReader::new(passwd_file);
let mut my_passwd_lines = "".to_string();
passwd_reader.read_to_string(&mut my_passwd_lines).unwrap();
let group_file = File::open("/etc/group").unwrap();
let mut group_reader = BufReader::new(group_file);
let mut my_group_lines = "".to_string();
group_reader.read_to_string(&mut my_group_lines).unwrap();
let data = UserDBLocal::import_from_strings(my_passwd_lines, "".to_string(), my_group_lines);
assert_eq!(data.group_entries.get(0).unwrap().get_groupname(), "root");
}