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>,
restricted_groups: Vec<RestrictedGroupDirective>,
}
impl GptTmplPolicy {
pub fn new() -> Self {
Self::default()
}
pub fn with_privilege_rights(privilege_rights: Vec<PrivilegeAssignment>) -> Self {
Self {
privilege_rights,
restricted_groups: Vec::new(),
}
}
pub fn with_entries(
privilege_rights: Vec<PrivilegeAssignment>,
restricted_groups: Vec<RestrictedGroupDirective>,
) -> Self {
Self {
privilege_rights,
restricted_groups,
}
}
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))
}
pub fn restricted_groups(&self) -> &[RestrictedGroupDirective] {
&self.restricted_groups
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RestrictedGroupOperation {
ReplaceMembers,
AddToParentGroups,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RestrictedGroupDirective {
target: String,
operation: RestrictedGroupOperation,
principals: Vec<String>,
}
impl RestrictedGroupDirective {
pub fn new(
target: impl Into<String>,
operation: RestrictedGroupOperation,
principals: Vec<String>,
) -> Self {
Self {
target: target.into(),
operation,
principals,
}
}
pub fn target(&self) -> &str {
&self.target
}
pub fn operation(&self) -> RestrictedGroupOperation {
self.operation
}
pub fn principals(&self) -> &[String] {
&self.principals
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GppGroupAction {
Create,
Delete,
Replace,
Update,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GppMemberAction {
Add,
Remove,
}
fn nonempty_identity(value: Option<String>) -> Option<String> {
value.filter(|value| !value.trim().is_empty())
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GppGroupMember {
sid: Option<String>,
name: Option<String>,
action: GppMemberAction,
}
impl GppGroupMember {
pub fn new(sid: Option<String>, name: Option<String>, action: GppMemberAction) -> Self {
Self {
sid: nonempty_identity(sid),
name: nonempty_identity(name),
action,
}
}
pub fn principal(&self) -> Option<&str> {
self.sid.as_deref().or(self.name.as_deref())
}
pub fn sid(&self) -> Option<&str> {
self.sid.as_deref()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn action(&self) -> GppMemberAction {
self.action
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GppLocalGroup {
sid: Option<String>,
name: Option<String>,
action: GppGroupAction,
delete_all_users: bool,
delete_all_groups: bool,
has_item_level_targeting: bool,
members: Vec<GppGroupMember>,
}
impl GppLocalGroup {
#[allow(clippy::too_many_arguments)]
pub fn new(
sid: Option<String>,
name: Option<String>,
action: GppGroupAction,
delete_all_users: bool,
delete_all_groups: bool,
has_item_level_targeting: bool,
members: Vec<GppGroupMember>,
) -> Self {
Self {
sid: nonempty_identity(sid),
name: nonempty_identity(name),
action,
delete_all_users,
delete_all_groups,
has_item_level_targeting,
members,
}
}
pub fn target(&self) -> Option<&str> {
self.sid.as_deref().or(self.name.as_deref())
}
pub fn sid(&self) -> Option<&str> {
self.sid.as_deref()
}
pub fn name(&self) -> Option<&str> {
self.name.as_deref()
}
pub fn action(&self) -> GppGroupAction {
self.action
}
pub fn delete_all_users(&self) -> bool {
self.delete_all_users
}
pub fn delete_all_groups(&self) -> bool {
self.delete_all_groups
}
pub fn has_item_level_targeting(&self) -> bool {
self.has_item_level_targeting
}
pub fn members(&self) -> &[GppGroupMember] {
&self.members
}
}
#[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"
);
}
}