Skip to main content

ad_acl/
schema.rs

1//! Runtime `schemaIDGUID` lookup.
2//!
3//! Attributes and classes added by a schema extension get a GUID generated at install
4//! time, so they cannot be hard-coded: LAPS (`ms-Mcs-AdmPwd`, `msLAPS-Password`,
5//! `msLAPS-EncryptedPassword`), the gMSA blob (`msDS-ManagedPassword`) and the dMSA class
6//! (`msDS-DelegatedManagedServiceAccount`) all differ per forest. Fill this map from
7//! `CN=Schema,CN=Configuration,<root>` — `(lDAPDisplayName, schemaIDGUID)` — and pass it to
8//! [`crate::grants_with`] so those ACEs resolve into real primitives.
9
10use std::collections::HashMap;
11use windows_sddl::sid::Guid;
12
13/// Attribute / class names this crate reacts to when they are present in the map.
14pub mod names {
15    /// Legacy LAPS password attribute (readable = local admin on the machine).
16    pub const LAPS_LEGACY: &str = "ms-Mcs-AdmPwd";
17    /// Windows LAPS cleartext password attribute.
18    pub const LAPS_PASSWORD: &str = "msLAPS-Password";
19    /// Windows LAPS DPAPI-NG-encrypted password attribute.
20    pub const LAPS_ENCRYPTED: &str = "msLAPS-EncryptedPassword";
21    /// gMSA managed-password blob.
22    pub const MANAGED_PASSWORD: &str = "msDS-ManagedPassword";
23    /// Delegated MSA class — `CreateChild` for it on an OU is the BadSuccessor primitive.
24    pub const DMSA_CLASS: &str = "msDS-DelegatedManagedServiceAccount";
25}
26
27/// `lDAPDisplayName` ⇄ `schemaIDGUID`, case-insensitive by name.
28#[derive(Clone, Debug, Default)]
29pub struct SchemaMap {
30    by_guid: HashMap<Guid, String>,
31    by_name: HashMap<String, Guid>,
32}
33
34impl SchemaMap {
35    pub fn new() -> Self {
36        Self::default()
37    }
38
39    /// Build from `(lDAPDisplayName, schemaIDGUID)` pairs as read from the schema NC.
40    pub fn from_entries<I, S>(entries: I) -> Self
41    where
42        I: IntoIterator<Item = (S, Guid)>,
43        S: AsRef<str>,
44    {
45        let mut m = SchemaMap::new();
46        for (name, guid) in entries {
47            m.insert(name.as_ref(), guid);
48        }
49        m
50    }
51
52    pub fn insert(&mut self, name: &str, guid: Guid) {
53        self.by_guid.insert(guid, name.to_string());
54        self.by_name.insert(name.to_ascii_lowercase(), guid);
55    }
56
57    pub fn is_empty(&self) -> bool {
58        self.by_name.is_empty()
59    }
60
61    /// `lDAPDisplayName` for a GUID seen in an ACE.
62    pub fn name(&self, g: &Guid) -> Option<&str> {
63        self.by_guid.get(g).map(String::as_str)
64    }
65
66    /// `schemaIDGUID` for an attribute or class name.
67    pub fn guid(&self, name: &str) -> Option<Guid> {
68        self.by_name.get(&name.to_ascii_lowercase()).copied()
69    }
70
71    /// True if `g` is any of the LAPS password attributes present in this forest.
72    pub fn is_laps_attr(&self, g: &Guid) -> bool {
73        matches!(
74            self.name(g),
75            Some(n) if n.eq_ignore_ascii_case(names::LAPS_LEGACY)
76                || n.eq_ignore_ascii_case(names::LAPS_PASSWORD)
77                || n.eq_ignore_ascii_case(names::LAPS_ENCRYPTED)
78        )
79    }
80
81    /// True if `g` is the gMSA managed-password blob attribute.
82    pub fn is_managed_password_attr(&self, g: &Guid) -> bool {
83        matches!(self.name(g), Some(n) if n.eq_ignore_ascii_case(names::MANAGED_PASSWORD))
84    }
85
86    /// True if `g` is the delegated-MSA class (BadSuccessor).
87    pub fn is_dmsa_class(&self, g: &Guid) -> bool {
88        matches!(self.name(g), Some(n) if n.eq_ignore_ascii_case(names::DMSA_CLASS))
89    }
90}