Skip to main content

windows_erg/security/
editor.rs

1//! Permission edit planning and in-memory application.
2
3use super::{AccessMask, Ace, AceType, Dacl, PermissionTarget, SecurityDescriptor, Sid};
4use crate::Result;
5use crate::error::{Error, PermissionEditError, SecurityError, SecurityUnsupportedError};
6
7/// How edits should be applied.
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum ApplyMode {
10    /// Validate only, do not change the DACL.
11    ValidateOnly,
12    /// Apply edits and return the updated DACL.
13    Apply,
14    /// Compute and return a diff with no mutation.
15    DryRunDiff,
16}
17
18/// Safety and behavior policy for permission edits.
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub struct PermissionEditPolicy {
21    /// Keep inherited ACEs unchanged when revoking or replacing trustees.
22    pub preserve_inherited: bool,
23    /// Fail when both grant and deny are requested for same trustee/mask.
24    pub reject_conflicting_changes: bool,
25}
26
27impl Default for PermissionEditPolicy {
28    fn default() -> Self {
29        Self {
30            preserve_inherited: true,
31            reject_conflicting_changes: true,
32        }
33    }
34}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
37enum PermissionOperation {
38    Grant {
39        trustee: Sid,
40        access: AccessMask,
41    },
42    Deny {
43        trustee: Sid,
44        access: AccessMask,
45    },
46    Revoke {
47        trustee: Sid,
48        access: Option<AccessMask>,
49    },
50    ReplaceTrustee {
51        old_trustee: Sid,
52        new_trustee: Sid,
53    },
54}
55
56/// A planned permission edit operation list.
57#[derive(Debug, Clone)]
58pub struct PermissionEditor {
59    policy: PermissionEditPolicy,
60    operations: Vec<PermissionOperation>,
61}
62
63impl PermissionEditor {
64    /// Create a new permission editor with default safety policy.
65    pub fn new() -> Self {
66        Self {
67            policy: PermissionEditPolicy::default(),
68            operations: Vec::new(),
69        }
70    }
71
72    /// Set a custom edit policy.
73    pub fn policy(mut self, policy: PermissionEditPolicy) -> Self {
74        self.policy = policy;
75        self
76    }
77
78    /// Add an allow ACE for trustee.
79    pub fn grant(mut self, trustee: Sid, access: AccessMask) -> Self {
80        self.operations
81            .push(PermissionOperation::Grant { trustee, access });
82        self
83    }
84
85    /// Add a deny ACE for trustee.
86    pub fn deny(mut self, trustee: Sid, access: AccessMask) -> Self {
87        self.operations
88            .push(PermissionOperation::Deny { trustee, access });
89        self
90    }
91
92    /// Revoke access for trustee.
93    ///
94    /// If `access` is `None`, all explicit ACEs for trustee are removed.
95    pub fn revoke(mut self, trustee: Sid, access: Option<AccessMask>) -> Self {
96        self.operations
97            .push(PermissionOperation::Revoke { trustee, access });
98        self
99    }
100
101    /// Replace all matching trustee SIDs.
102    pub fn replace_trustee(mut self, old_trustee: Sid, new_trustee: Sid) -> Self {
103        self.operations.push(PermissionOperation::ReplaceTrustee {
104            old_trustee,
105            new_trustee,
106        });
107        self
108    }
109
110    /// Validate operations and return a plan.
111    pub fn build(self) -> Result<PermissionEditPlan> {
112        if self.policy.reject_conflicting_changes {
113            for op in &self.operations {
114                if let PermissionOperation::Grant { trustee, access } = op {
115                    let conflict = self.operations.iter().any(|other| {
116                        matches!(other, PermissionOperation::Deny { trustee: t, access: a } if t == trustee && a == access)
117                    });
118
119                    if conflict {
120                        return Err(Error::Security(SecurityError::PermissionEdit(
121                            PermissionEditError::new(
122                                "grant/deny",
123                                "conflicting allow and deny operation for same trustee and mask",
124                            ),
125                        )));
126                    }
127                }
128            }
129        }
130
131        Ok(PermissionEditPlan {
132            policy: self.policy,
133            operations: self.operations,
134        })
135    }
136}
137
138impl Default for PermissionEditor {
139    fn default() -> Self {
140        Self::new()
141    }
142}
143
144/// Built permission edit plan.
145#[derive(Debug, Clone)]
146pub struct PermissionEditPlan {
147    policy: PermissionEditPolicy,
148    operations: Vec<PermissionOperation>,
149}
150
151impl PermissionEditPlan {
152    /// Apply plan according to mode.
153    pub fn execute(&self, current: &Dacl, mode: ApplyMode) -> PermissionEditResult {
154        let mut next = current.clone();
155
156        if matches!(mode, ApplyMode::ValidateOnly) {
157            return PermissionEditResult {
158                mode,
159                updated_dacl: None,
160                diff: PermissionDiff::between(current, current),
161            };
162        }
163
164        for op in &self.operations {
165            match op {
166                PermissionOperation::Grant { trustee, access } => {
167                    next.entries_mut()
168                        .push(Ace::new(trustee.clone(), AceType::Allow, *access));
169                }
170                PermissionOperation::Deny { trustee, access } => {
171                    next.entries_mut()
172                        .push(Ace::new(trustee.clone(), AceType::Deny, *access));
173                }
174                PermissionOperation::Revoke { trustee, access } => {
175                    next.entries_mut().retain(|ace| {
176                        if self.policy.preserve_inherited && ace.inherited {
177                            return true;
178                        }
179
180                        if &ace.trustee != trustee {
181                            return true;
182                        }
183
184                        match access {
185                            Some(mask) => ace.access_mask != *mask,
186                            None => false,
187                        }
188                    });
189                }
190                PermissionOperation::ReplaceTrustee {
191                    old_trustee,
192                    new_trustee,
193                } => {
194                    for ace in next.entries_mut().iter_mut() {
195                        if self.policy.preserve_inherited && ace.inherited {
196                            continue;
197                        }
198                        if ace.trustee == *old_trustee {
199                            ace.trustee = new_trustee.clone();
200                        }
201                    }
202                }
203            }
204        }
205
206        next.canonicalize();
207
208        PermissionEditResult {
209            mode,
210            updated_dacl: if matches!(mode, ApplyMode::Apply) {
211                Some(next.clone())
212            } else {
213                None
214            },
215            diff: PermissionDiff::between(current, &next),
216        }
217    }
218
219    /// Apply plan against a full security descriptor.
220    pub fn execute_on_descriptor(
221        &self,
222        current: &SecurityDescriptor,
223        mode: ApplyMode,
224    ) -> DescriptorEditResult {
225        let dacl_result = self.execute(current.dacl(), mode);
226        let mut updated = None;
227
228        if let Some(next_dacl) = dacl_result.updated_dacl.clone() {
229            let mut descriptor = current.clone();
230            *descriptor.dacl_mut() = next_dacl;
231            updated = Some(descriptor);
232        }
233
234        DescriptorEditResult {
235            mode: dacl_result.mode,
236            updated_descriptor: updated,
237            diff: dacl_result.diff,
238        }
239    }
240
241    /// Execute and optionally persist against a target backend.
242    ///
243    /// `Apply` mode requires backend write support.
244    pub fn execute_for_target(
245        &self,
246        target: &PermissionTarget,
247        current: &SecurityDescriptor,
248        mode: ApplyMode,
249    ) -> Result<DescriptorEditResult> {
250        let result = self.execute_on_descriptor(current, mode);
251
252        if matches!(mode, ApplyMode::Apply) {
253            if let Some(updated) = &result.updated_descriptor {
254                target.write_descriptor(updated)?;
255            } else {
256                return Err(Error::Security(SecurityError::Unsupported(
257                    SecurityUnsupportedError::new(
258                        "permission_target",
259                        "apply_without_updated_descriptor",
260                    ),
261                )));
262            }
263        }
264
265        Ok(result)
266    }
267
268    /// Read descriptor from target, execute plan, and optionally persist.
269    pub fn execute_against_target(
270        &self,
271        target: &PermissionTarget,
272        mode: ApplyMode,
273    ) -> Result<DescriptorEditResult> {
274        let current = target.read_descriptor()?;
275        self.execute_for_target(target, &current, mode)
276    }
277}
278
279/// Result of plan execution.
280#[derive(Debug, Clone)]
281pub struct PermissionEditResult {
282    /// Execution mode.
283    pub mode: ApplyMode,
284    /// Updated DACL (only for Apply mode).
285    pub updated_dacl: Option<Dacl>,
286    /// Structured diff against original DACL.
287    pub diff: PermissionDiff,
288}
289
290/// Result of descriptor-level plan execution.
291#[derive(Debug, Clone)]
292pub struct DescriptorEditResult {
293    /// Execution mode.
294    pub mode: ApplyMode,
295    /// Updated descriptor (only for Apply mode).
296    pub updated_descriptor: Option<SecurityDescriptor>,
297    /// Structured diff against original DACL.
298    pub diff: PermissionDiff,
299}
300
301/// A basic diff of ACL changes.
302#[derive(Debug, Clone, Default)]
303pub struct PermissionDiff {
304    /// ACEs newly added.
305    pub added: Vec<Ace>,
306    /// ACEs removed.
307    pub removed: Vec<Ace>,
308}
309
310impl PermissionDiff {
311    fn between(before: &Dacl, after: &Dacl) -> Self {
312        let mut added = Vec::new();
313        let mut removed = Vec::new();
314
315        for ace in after.entries() {
316            if !before.entries().contains(ace) {
317                added.push(ace.clone());
318            }
319        }
320
321        for ace in before.entries() {
322            if !after.entries().contains(ace) {
323                removed.push(ace.clone());
324            }
325        }
326
327        Self { added, removed }
328    }
329}
330
331#[cfg(test)]
332mod tests {
333    use super::{AccessMask, ApplyMode, PermissionEditor};
334    use crate::security::{Ace, AceType, Dacl, SecurityDescriptor, Sid};
335
336    #[test]
337    fn build_rejects_conflicting_grant_and_deny() {
338        let sid = Sid::parse("S-1-5-32-545").expect("valid sid");
339        let err = PermissionEditor::new()
340            .grant(sid.clone(), AccessMask::from_bits(0x1))
341            .deny(sid, AccessMask::from_bits(0x1))
342            .build()
343            .expect_err("expected conflict");
344
345        assert!(err.to_string().contains("conflicting allow and deny"));
346    }
347
348    #[test]
349    fn dry_run_diff_reports_added_ace() {
350        let sid = Sid::parse("S-1-5-32-545").expect("valid sid");
351        let dacl = Dacl::new();
352
353        let plan = PermissionEditor::new()
354            .grant(sid.clone(), AccessMask::from_bits(0x2))
355            .build()
356            .expect("build plan");
357
358        let result = plan.execute(&dacl, ApplyMode::DryRunDiff);
359        assert_eq!(result.diff.added.len(), 1);
360        assert!(result.updated_dacl.is_none());
361        assert_eq!(
362            result.diff.added[0],
363            Ace::new(sid, AceType::Allow, AccessMask::from_bits(0x2))
364        );
365    }
366
367    #[test]
368    fn execute_on_descriptor_updates_descriptor_dacl() {
369        let sid = Sid::parse("S-1-5-32-545").expect("valid sid");
370        let descriptor = SecurityDescriptor::new().with_dacl(Dacl::new());
371
372        let plan = PermissionEditor::new()
373            .grant(sid, AccessMask::from_bits(0x4))
374            .build()
375            .expect("build plan");
376
377        let result = plan.execute_on_descriptor(&descriptor, ApplyMode::Apply);
378        assert!(result.updated_descriptor.is_some());
379        assert_eq!(result.diff.added.len(), 1);
380    }
381}