use std::{collections::HashSet, time::Duration};
use chrono::{DateTime, Utc};
use getset::{Getters, Setters};
use typed_builder::TypedBuilder;
use windows::{
Win32::{
NetworkManagement::NetManagement::{AF_OP, USER_INFO_3, USER_INFO_4},
Security::PSID,
},
core::PWSTR,
};
use crate::{
error::WindowsUsersError,
utils::{into_hashset, option_to_wide, psid_to_string, to_wide},
};
pub use self::types::{LogonHours, SidType, UserAccountFlags, UserAuthFlags, UserPrivilege};
pub mod operations;
pub mod types;
const DOMAIN_GROUP_RID_USERS: u32 = 513;
#[derive(Debug, Clone, Getters, Setters, TypedBuilder)]
pub struct User {
#[builder(setter(into))]
#[getset(get = "pub", set = "pub")]
name: String,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
password: Option<String>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
password_age: Option<Duration>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
priv_level: UserPrivilege,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
home_dir: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
comment: Option<String>,
#[builder(default, setter(into))]
#[getset(get = "pub", set = "pub")]
flags: UserAccountFlags,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
script_path: Option<String>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
auth_flags: Option<UserAuthFlags>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
full_name: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
user_comment: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
parms: Option<String>,
#[builder(default, setter(transform = |items: impl IntoIterator<Item = impl Into<String>>| Some(into_hashset(items))))]
#[getset(get = "pub", set = "pub")]
workstations: Option<HashSet<String>>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
last_logon: Option<DateTime<Utc>>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
last_logoff: Option<DateTime<Utc>>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
acct_expires: Option<DateTime<Utc>>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
max_storage: Option<u32>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
units_per_week: Option<u32>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
logon_hours: Option<LogonHours>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
bad_pw_count: Option<u32>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
num_logons: Option<u32>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
logon_server: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
country_code: Option<u32>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
code_page: Option<u32>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
user_id: Option<u32>,
#[builder(default, setter(skip))]
#[getset(get = "pub", set = "pub")]
user_sid: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
primary_group_id: Option<u32>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
profile: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
home_dir_drive: Option<String>,
#[builder(default, setter(strip_option, into))]
#[getset(get = "pub", set = "pub")]
password_expired: Option<bool>,
}
impl TryFrom<&USER_INFO_3> for User {
type Error = WindowsUsersError;
fn try_from(user: &USER_INFO_3) -> Result<Self, WindowsUsersError> {
unsafe {
Ok(Self {
name: user.usri3_name.to_string()?,
password: None, password_age: if user.usri3_password_age != 0 {
Some(Duration::from_secs(user.usri3_password_age.into()))
} else {
None
},
priv_level: user.usri3_priv.try_into()?,
home_dir: user
.usri3_home_dir
.to_string()
.ok()
.filter(|s| !s.is_empty()),
comment: user
.usri3_comment
.to_string()
.ok()
.filter(|s| !s.is_empty()),
flags: user.usri3_flags.try_into()?,
script_path: user
.usri3_script_path
.to_string()
.ok()
.filter(|s| !s.is_empty()),
auth_flags: user
.usri3_auth_flags
.try_into()
.ok()
.filter(|f: &UserAuthFlags| !f.is_empty()),
full_name: user
.usri3_full_name
.to_string()
.ok()
.filter(|s| !s.is_empty()),
user_comment: user
.usri3_usr_comment
.to_string()
.ok()
.filter(|s| !s.is_empty()),
parms: user.usri3_parms.to_string().ok().filter(|s| !s.is_empty()),
workstations: user
.usri3_workstations
.to_string()
.map(|s| {
s.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
})
.ok()
.filter(|h: &HashSet<String>| !h.is_empty()),
last_logon: if user.usri3_last_logon != 0 {
DateTime::<Utc>::from_timestamp(user.usri3_last_logon.into(), 0)
} else {
None
},
last_logoff: if user.usri3_last_logoff != 0 {
DateTime::<Utc>::from_timestamp(user.usri3_last_logoff.into(), 0)
} else {
None
},
acct_expires: if user.usri3_acct_expires != u32::MAX {
DateTime::<Utc>::from_timestamp(user.usri3_acct_expires.into(), 0)
} else {
None
},
max_storage: user.usri3_max_storage.into(),
units_per_week: user.usri3_units_per_week.into(),
logon_hours: if user.usri3_logon_hours.is_null() {
None
} else {
Some(user.usri3_logon_hours.into())
},
bad_pw_count: user.usri3_bad_pw_count.into(),
num_logons: user.usri3_num_logons.into(),
logon_server: user
.usri3_logon_server
.to_string()
.ok()
.filter(|s| !s.is_empty()),
country_code: if user.usri3_country_code != 0 {
Some(user.usri3_country_code)
} else {
None
},
code_page: if user.usri3_code_page != 0 {
Some(user.usri3_code_page)
} else {
None
},
user_id: user.usri3_user_id.into(),
user_sid: None,
primary_group_id: user.usri3_primary_group_id.into(),
profile: user
.usri3_profile
.to_string()
.ok()
.filter(|s| !s.is_empty()),
home_dir_drive: user
.usri3_home_dir_drive
.to_string()
.ok()
.filter(|s| !s.is_empty()),
password_expired: (user.usri3_password_expired != 0).into(),
})
}
}
}
impl TryFrom<&USER_INFO_4> for User {
type Error = WindowsUsersError;
fn try_from(user: &USER_INFO_4) -> Result<Self, WindowsUsersError> {
unsafe {
Ok(Self {
name: user.usri4_name.to_string()?,
password: None,
password_age: if user.usri4_password_age != 0 {
Some(Duration::from_secs(user.usri4_password_age.into()))
} else {
None
},
priv_level: user.usri4_priv.try_into()?,
home_dir: user
.usri4_home_dir
.to_string()
.ok()
.filter(|s| !s.is_empty()),
comment: user
.usri4_comment
.to_string()
.ok()
.filter(|s| !s.is_empty()),
flags: user.usri4_flags.try_into()?,
script_path: user
.usri4_script_path
.to_string()
.ok()
.filter(|s| !s.is_empty()),
auth_flags: user
.usri4_auth_flags
.try_into()
.ok()
.filter(|f: &UserAuthFlags| !f.is_empty()),
full_name: user
.usri4_full_name
.to_string()
.ok()
.filter(|s| !s.is_empty()),
user_comment: user
.usri4_usr_comment
.to_string()
.ok()
.filter(|s| !s.is_empty()),
parms: user.usri4_parms.to_string().ok().filter(|s| !s.is_empty()),
workstations: user
.usri4_workstations
.to_string()
.map(|s| {
s.split(',')
.filter(|s| !s.is_empty())
.map(String::from)
.collect()
})
.ok()
.filter(|h: &HashSet<String>| !h.is_empty()),
last_logon: if user.usri4_last_logon != 0 {
DateTime::<Utc>::from_timestamp(user.usri4_last_logon.into(), 0)
} else {
None
},
last_logoff: if user.usri4_last_logoff != 0 {
DateTime::<Utc>::from_timestamp(user.usri4_last_logoff.into(), 0)
} else {
None
},
acct_expires: if user.usri4_acct_expires != u32::MAX {
DateTime::<Utc>::from_timestamp(user.usri4_acct_expires.into(), 0)
} else {
None
},
max_storage: user.usri4_max_storage.into(),
units_per_week: user.usri4_units_per_week.into(),
logon_hours: if user.usri4_logon_hours.is_null() {
None
} else {
Some(user.usri4_logon_hours.into())
},
bad_pw_count: user.usri4_bad_pw_count.into(),
num_logons: user.usri4_num_logons.into(),
logon_server: user
.usri4_logon_server
.to_string()
.ok()
.filter(|s| !s.is_empty()),
country_code: if user.usri4_country_code != 0 {
Some(user.usri4_country_code)
} else {
None
},
code_page: if user.usri4_code_page != 0 {
Some(user.usri4_code_page)
} else {
None
},
user_id: None,
user_sid: Some(psid_to_string(user.usri4_user_sid)?),
primary_group_id: user.usri4_primary_group_id.into(),
profile: user
.usri4_profile
.to_string()
.ok()
.filter(|s| !s.is_empty()),
home_dir_drive: user
.usri4_home_dir_drive
.to_string()
.ok()
.filter(|s| !s.is_empty()),
password_expired: (user.usri4_password_expired != 0).into(),
})
}
}
}
#[derive(Debug, Clone)]
pub struct UserInfo4Buffer {
pub user_info: USER_INFO_4,
_strings: Vec<Vec<u16>>,
_buffers: Vec<Vec<u8>>,
}
impl User {
pub fn to_user_info_4(&self) -> UserInfo4Buffer {
let mut string_storage: Vec<Vec<u16>> = Vec::new();
let mut buffer_storage: Vec<Vec<u8>> = Vec::new();
let push_string = |s: Option<Vec<u16>>, storage: &mut Vec<Vec<u16>>| -> PWSTR {
match s {
Some(mut vec) => {
let ptr = vec.as_mut_ptr();
storage.push(vec);
PWSTR(ptr)
}
None => PWSTR::null(),
}
};
let push_buffer = |b: Option<Vec<u8>>, storage: &mut Vec<Vec<u8>>| -> *mut u8 {
match b {
Some(mut vec) => {
let ptr = vec.as_mut_ptr();
storage.push(vec);
ptr
}
None => std::ptr::null_mut(),
}
};
let workstations_str = self
.workstations
.as_ref()
.map(|ws| ws.iter().cloned().collect::<Vec<_>>().join(","))
.unwrap_or_default();
let user_info = USER_INFO_4 {
usri4_name: push_string(Some(to_wide(&self.name)), &mut string_storage),
usri4_password: push_string(option_to_wide(&self.password), &mut string_storage),
usri4_password_age: self
.password_age
.map(|d| d.as_secs() as u32)
.unwrap_or_default(),
usri4_priv: self.priv_level.into(),
usri4_home_dir: push_string(option_to_wide(&self.home_dir), &mut string_storage),
usri4_comment: push_string(option_to_wide(&self.comment), &mut string_storage),
usri4_flags: self.flags.into(),
usri4_script_path: push_string(option_to_wide(&self.script_path), &mut string_storage),
usri4_auth_flags: self.auth_flags.map(AF_OP::from).unwrap_or_default(),
usri4_full_name: push_string(option_to_wide(&self.full_name), &mut string_storage),
usri4_usr_comment: push_string(option_to_wide(&self.user_comment), &mut string_storage),
usri4_parms: push_string(option_to_wide(&self.parms), &mut string_storage),
usri4_workstations: push_string(Some(to_wide(&workstations_str)), &mut string_storage),
usri4_last_logon: self
.last_logon
.map(|d| d.timestamp() as u32)
.unwrap_or_default(),
usri4_last_logoff: self
.last_logoff
.map(|d| d.timestamp() as u32)
.unwrap_or_default(),
usri4_acct_expires: self
.acct_expires
.map(|d| d.timestamp() as u32)
.unwrap_or(u32::MAX),
usri4_max_storage: self.max_storage.unwrap_or_default(),
usri4_units_per_week: self.units_per_week.unwrap_or(LogonHours::UNITS_PER_WEEK),
usri4_logon_hours: push_buffer(
self.logon_hours.clone().map(|lh| lh.into()),
&mut buffer_storage,
),
usri4_bad_pw_count: self.bad_pw_count.unwrap_or_default(),
usri4_num_logons: self.num_logons.unwrap_or_default(),
usri4_logon_server: push_string(
option_to_wide(&self.logon_server),
&mut string_storage,
),
usri4_country_code: self.country_code.unwrap_or_default(),
usri4_code_page: self.code_page.unwrap_or_default(),
usri4_user_sid: PSID(std::ptr::null_mut()), usri4_primary_group_id: self.primary_group_id.unwrap_or(DOMAIN_GROUP_RID_USERS),
usri4_profile: push_string(option_to_wide(&self.profile), &mut string_storage),
usri4_home_dir_drive: push_string(
option_to_wide(&self.home_dir_drive),
&mut string_storage,
),
usri4_password_expired: self.password_expired.unwrap_or(false) as u32,
};
UserInfo4Buffer {
user_info,
_strings: string_storage,
_buffers: buffer_storage,
}
}
}
#[derive(Debug, Clone, TypedBuilder)]
pub struct UserUpdate {
#[builder(default, setter(strip_option, into))]
pub(crate) name: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) password: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) home_dir: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) comment: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) flags: Option<UserAccountFlags>,
#[builder(default, setter(strip_option, into))]
pub(crate) script_path: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) full_name: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) user_comment: Option<String>,
#[builder(default, setter(transform = |items: impl IntoIterator<Item = impl Into<String>>| Some(into_hashset(items))))]
pub(crate) workstations: Option<HashSet<String>>,
#[builder(default, setter(strip_option, into))]
pub(crate) acct_expires: Option<DateTime<Utc>>,
#[builder(default, setter(strip_option, into))]
pub(crate) logon_hours: Option<LogonHours>,
#[builder(default, setter(strip_option, into))]
pub(crate) country_code: Option<u32>,
#[builder(default, setter(strip_option, into))]
pub(crate) primary_group_id: Option<u32>,
#[builder(default, setter(strip_option, into))]
pub(crate) profile: Option<String>,
#[builder(default, setter(strip_option, into))]
pub(crate) home_dir_drive: Option<String>,
}
impl From<User> for UserUpdate {
fn from(user: User) -> Self {
Self {
name: Some(user.name),
password: user.password,
home_dir: user.home_dir,
comment: user.comment,
flags: Some(user.flags),
script_path: user.script_path,
full_name: user.full_name,
user_comment: user.user_comment,
workstations: user.workstations,
acct_expires: user.acct_expires,
logon_hours: user.logon_hours,
country_code: user.country_code,
primary_group_id: user.primary_group_id,
profile: user.profile,
home_dir_drive: user.home_dir_drive,
}
}
}