use std::fmt;
#[derive(Debug)]
pub enum GpoError {
InvalidEncoding(String),
MalformedContent(String),
Io(std::io::Error),
}
impl fmt::Display for GpoError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidEncoding(msg) => write!(f, "Invalid policy encoding: {msg}"),
Self::MalformedContent(msg) => write!(f, "Malformed policy content: {msg}"),
Self::Io(err) => write!(f, "Policy I/O error: {err}"),
}
}
}
impl std::error::Error for GpoError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Self::Io(err) => Some(err),
_ => None,
}
}
}
impl From<std::io::Error> for GpoError {
fn from(err: std::io::Error) -> Self {
Self::Io(err)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PrivilegeAssignment {
privilege: String,
principals: Vec<String>,
}
impl PrivilegeAssignment {
pub fn new(privilege: impl Into<String>, principals: Vec<String>) -> Self {
Self {
privilege: privilege.into(),
principals,
}
}
pub fn privilege(&self) -> &str {
&self.privilege
}
pub fn principals(&self) -> &[String] {
&self.principals
}
pub fn normalized_principals(&self) -> impl Iterator<Item = &str> {
self.principals
.iter()
.map(|p| p.strip_prefix('*').unwrap_or(p))
}
pub fn sid_candidates(&self) -> impl Iterator<Item = &str> {
self.normalized_principals()
.filter(|p| p.starts_with("S-1-") || p.starts_with("s-1-"))
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct GptTmplPolicy {
privilege_rights: Vec<PrivilegeAssignment>,
}
impl GptTmplPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn with_privilege_rights(privilege_rights: Vec<PrivilegeAssignment>) -> Self {
Self { privilege_rights }
}
pub fn privilege_rights(&self) -> &[PrivilegeAssignment] {
&self.privilege_rights
}
pub fn get_privilege(&self, privilege_name: &str) -> Option<&PrivilegeAssignment> {
self.privilege_rights
.iter()
.find(|p| p.privilege.eq_ignore_ascii_case(privilege_name))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn privilege_assignment_normalized_principals_and_sid_candidates() {
let assignment = PrivilegeAssignment::new(
"SeRemoteInteractiveLogonRight",
vec![
"*S-1-5-32-544".to_string(),
"S-1-5-32-545".to_string(),
"*DOMAIN\\Administrators".to_string(),
"LocalUser".to_string(),
],
);
assert_eq!(assignment.privilege(), "SeRemoteInteractiveLogonRight");
assert_eq!(assignment.principals().len(), 4);
let normalized: Vec<&str> = assignment.normalized_principals().collect();
assert_eq!(
normalized,
vec![
"S-1-5-32-544",
"S-1-5-32-545",
"DOMAIN\\Administrators",
"LocalUser"
]
);
let sids: Vec<&str> = assignment.sid_candidates().collect();
assert_eq!(sids, vec!["S-1-5-32-544", "S-1-5-32-545"]);
}
#[test]
fn gpttmpl_policy_lookup_is_case_insensitive() {
let policy = GptTmplPolicy::with_privilege_rights(vec![
PrivilegeAssignment::new("SeDebugPrivilege", vec!["*S-1-5-32-544".to_string()]),
PrivilegeAssignment::new(
"SeRemoteInteractiveLogonRight",
vec!["*S-1-5-32-555".to_string()],
),
]);
assert!(policy.get_privilege("sedebugprivilege").is_some());
assert!(policy.get_privilege("SEDEBUGPRIVILEGE").is_some());
assert!(policy.get_privilege("SeDebugPrivilege").is_some());
assert!(policy.get_privilege("SeNonExistentPrivilege").is_none());
}
#[test]
fn gpo_error_display_formatting() {
let err = GpoError::MalformedContent("invalid syntax at line 5".to_string());
assert_eq!(
err.to_string(),
"Malformed policy content: invalid syntax at line 5"
);
let err2 = GpoError::InvalidEncoding("unsupported encoding".to_string());
assert_eq!(
err2.to_string(),
"Invalid policy encoding: unsupported encoding"
);
}
}