1use std::collections::HashMap;
20
21use crate::objects::common::{GPOChange, LdapObject, Link, Member, UserRight};
22use crate::objects::computer::Computer;
23use crate::objects::domain::Domain;
24use crate::objects::group::Group;
25use crate::objects::ou::Ou;
26use crate::objects::user::User;
27
28use super::sysvol::SysvolGpo;
29use super::types::{
30 GppGroupAction, GppMemberAction, RestrictedGroupDirective,
31 RestrictedGroupOperation,
32};
33
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
38pub enum TargetGroup {
39 LocalAdmins,
40 RemoteDesktopUsers,
41 DcomUsers,
42 PsRemote,
43}
44
45impl TargetGroup {
46 fn from_rid(rid: u32) -> Option<Self> {
47 match rid {
48 544 => Some(Self::LocalAdmins),
49 555 => Some(Self::RemoteDesktopUsers),
50 562 => Some(Self::DcomUsers),
51 580 => Some(Self::PsRemote),
52 _ => None,
53 }
54 }
55
56 fn from_group_name(name: &str) -> Option<Self> {
57 match name.trim().to_ascii_lowercase().as_str() {
58 "administrators" => Some(Self::LocalAdmins),
59 "remote desktop users" => Some(Self::RemoteDesktopUsers),
60 "distributed com users" => Some(Self::DcomUsers),
61 "remote management users" => Some(Self::PsRemote),
62 _ => None,
63 }
64 }
65}
66
67#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct TypedPrincipal {
70 pub sid: String,
71 pub object_type: String,
72}
73
74pub trait Resolver {
78 fn resolve_name(&self, name: &str) -> Option<(String, String)>;
79 fn type_of_sid(&self, sid: &str) -> Option<String>;
80}
81
82pub struct ObjectResolver {
84 by_name: HashMap<String, (String, String)>, by_sid: HashMap<String, String>, }
87
88fn index_object(
89 sid: &str,
90 display: &str,
91 ty: &str,
92 by_name: &mut HashMap<String, (String, String)>,
93 by_sid: &mut HashMap<String, String>,
94) {
95 if sid.is_empty() {
96 return;
97 }
98 by_sid.insert(sid.to_uppercase(), ty.to_string());
99 let bare = display
100 .rsplit(['\\', '/'])
101 .next()
102 .unwrap_or(display)
103 .split('@')
104 .next()
105 .unwrap_or(display)
106 .trim()
107 .to_uppercase();
108 if !bare.is_empty() {
109 by_name
110 .entry(bare)
111 .or_insert_with(|| (sid.to_string(), ty.to_string()));
112 }
113}
114
115impl ObjectResolver {
116 pub fn build(users: &[User], groups: &[Group], computers: &[Computer]) -> Self {
117 let mut by_name = HashMap::new();
118 let mut by_sid = HashMap::new();
119
120 for u in users {
121 index_object(u.object_identifier(), u.properties().name(), "User", &mut by_name, &mut by_sid);
122 }
123 for g in groups {
124 index_object(g.object_identifier(), g.properties().name(), "Group", &mut by_name, &mut by_sid);
125 }
126 for c in computers {
127 let sid = c.object_identifier();
128 let name = c.properties().name();
129 index_object(sid, name, "Computer", &mut by_name, &mut by_sid);
130 let short = name.split('.').next().unwrap_or(name).to_uppercase();
131 if !short.is_empty() {
132 by_name
133 .entry(format!("{short}$"))
134 .or_insert_with(|| (sid.to_string(), "Computer".to_string()));
135 }
136 }
137
138 ObjectResolver { by_name, by_sid }
139 }
140}
141
142impl Resolver for ObjectResolver {
143 fn resolve_name(&self, name: &str) -> Option<(String, String)> {
144 self.by_name.get(&name.to_uppercase()).cloned()
145 }
146 fn type_of_sid(&self, sid: &str) -> Option<String> {
147 self.by_sid.get(&sid.to_uppercase()).cloned()
148 }
149}
150
151fn builtin_rid(s: &str) -> Option<u32> {
153 let up = s.trim().trim_start_matches('*').to_uppercase();
154 let rest = up.strip_prefix("S-1-5-32-")?;
155 let digits: String = rest.chars().take_while(|c| c.is_ascii_digit()).collect();
156 digits.parse().ok()
157}
158
159fn resolve_principal(raw: &str, resolver: &impl Resolver) -> Option<TypedPrincipal> {
161 let p = raw.trim().trim_start_matches('*').trim();
162 if p.is_empty() {
163 return None;
164 }
165 if p.len() >= 4 && p[..4].eq_ignore_ascii_case("S-1-") {
166 let sid = p.to_uppercase();
167 let object_type = resolver.type_of_sid(&sid).unwrap_or_else(|| "Base".to_string());
168 return Some(TypedPrincipal { sid, object_type });
169 }
170 let bare = p.rsplit(['\\', '/']).next().unwrap_or(p);
171 resolver
172 .resolve_name(bare)
173 .map(|(sid, object_type)| TypedPrincipal { sid, object_type })
174}
175
176#[derive(Debug, Clone, Copy, PartialEq, Eq)]
179enum Op {
180 Add,
181 Delete,
182 DeleteUsers,
183 DeleteGroups,
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq)]
187enum Kind {
188 RestrictedMember,
189 RestrictedMemberOf,
190 LocalGroup,
191}
192
193#[derive(Debug, Clone)]
194struct Action {
195 group: TargetGroup,
196 kind: Kind,
197 op: Op,
198 principal: Option<TypedPrincipal>,
199}
200
201fn actions_for_gpo(gpo: &SysvolGpo, resolver: &impl Resolver) -> Vec<Action> {
203 let mut out = Vec::new();
204
205 for grp in &gpo.gpp_local_groups {
206 if grp.action() != GppGroupAction::Update {
207 continue;
208 }
209 let target = grp
210 .sid()
211 .and_then(builtin_rid)
212 .and_then(TargetGroup::from_rid)
213 .or_else(|| grp.name().and_then(TargetGroup::from_group_name));
214 let Some(group) = target else { continue };
215
216 if grp.delete_all_users() {
217 out.push(Action { group, kind: Kind::LocalGroup, op: Op::DeleteUsers, principal: None });
218 }
219 if grp.delete_all_groups() {
220 out.push(Action { group, kind: Kind::LocalGroup, op: Op::DeleteGroups, principal: None });
221 }
222 for m in grp.members() {
223 let op = match m.action() {
224 GppMemberAction::Add => Op::Add,
225 GppMemberAction::Remove => Op::Delete,
226 };
227 let principal = m
228 .sid()
229 .and_then(|s| resolve_principal(s, resolver))
230 .or_else(|| m.name().and_then(|n| resolve_principal(n, resolver)));
231 if let Some(p) = principal {
232 out.push(Action { group, kind: Kind::LocalGroup, op, principal: Some(p) });
233 }
234 }
235 }
236
237 for dir in &gpo.restricted_groups {
238 push_restricted(dir, resolver, &mut out);
239 }
240
241 out
242}
243
244fn push_restricted(dir: &RestrictedGroupDirective, resolver: &impl Resolver, out: &mut Vec<Action>) {
245 match dir.operation() {
246 RestrictedGroupOperation::ReplaceMembers => {
247 let Some(group) = builtin_rid(dir.target()).and_then(TargetGroup::from_rid) else {
248 return;
249 };
250 for raw in dir.principals() {
251 if let Some(p) = resolve_principal(raw, resolver) {
252 out.push(Action { group, kind: Kind::RestrictedMember, op: Op::Add, principal: Some(p) });
253 }
254 }
255 }
256 RestrictedGroupOperation::AddToParentGroups => {
257 let Some(member) = resolve_principal(dir.target(), resolver) else {
258 return;
259 };
260 for raw in dir.principals() {
261 if let Some(group) = builtin_rid(raw).and_then(TargetGroup::from_rid) {
262 out.push(Action {
263 group,
264 kind: Kind::RestrictedMemberOf,
265 op: Op::Add,
266 principal: Some(member.clone()),
267 });
268 }
269 }
270 }
271 }
272}
273
274#[derive(Debug, Default)]
278pub struct MergedGroups {
279 pub local_admins: Vec<TypedPrincipal>,
280 pub remote_desktop_users: Vec<TypedPrincipal>,
281 pub dcom_users: Vec<TypedPrincipal>,
282 pub psremote_users: Vec<TypedPrincipal>,
283}
284
285fn dedup_by_sid(v: &mut Vec<TypedPrincipal>) {
286 let mut seen = std::collections::HashSet::new();
287 v.retain(|p| seen.insert(p.sid.clone()));
288}
289
290fn merge(actions: &[Action]) -> MergedGroups {
292 let mut merged = MergedGroups::default();
293 for group in [
294 TargetGroup::LocalAdmins,
295 TargetGroup::RemoteDesktopUsers,
296 TargetGroup::DcomUsers,
297 TargetGroup::PsRemote,
298 ] {
299 let mut restricted_member: Vec<TypedPrincipal> = Vec::new();
300 let mut restricted_memberof: Vec<TypedPrincipal> = Vec::new();
301 let mut local_groups: Vec<TypedPrincipal> = Vec::new();
302
303 for a in actions.iter().filter(|a| a.group == group) {
304 match (a.kind, a.op) {
305 (Kind::RestrictedMember, _) => {
306 if let Some(p) = &a.principal { restricted_member.push(p.clone()); }
307 }
308 (Kind::RestrictedMemberOf, _) => {
309 if let Some(p) = &a.principal { restricted_memberof.push(p.clone()); }
310 }
311 (Kind::LocalGroup, Op::Add) => {
312 if let Some(p) = &a.principal { local_groups.push(p.clone()); }
313 }
314 (Kind::LocalGroup, Op::Delete) => {
315 if let Some(p) = &a.principal { local_groups.retain(|x| x.sid != p.sid); }
316 }
317 (Kind::LocalGroup, Op::DeleteUsers) => {
318 local_groups.retain(|x| x.object_type != "User");
319 }
320 (Kind::LocalGroup, Op::DeleteGroups) => {
321 local_groups.retain(|x| x.object_type != "Group");
322 }
323 }
324 }
325
326 let mut final_set = restricted_memberof;
327 if restricted_member.is_empty() {
328 final_set.extend(local_groups);
329 } else {
330 final_set.extend(restricted_member);
331 }
332 dedup_by_sid(&mut final_set);
333
334 match group {
335 TargetGroup::LocalAdmins => merged.local_admins = final_set,
336 TargetGroup::RemoteDesktopUsers => merged.remote_desktop_users = final_set,
337 TargetGroup::DcomUsers => merged.dcom_users = final_set,
338 TargetGroup::PsRemote => merged.psremote_users = final_set,
339 }
340 }
341 merged
342}
343
344pub fn compute_merged(ordered_gpos: &[&SysvolGpo], resolver: &impl Resolver) -> MergedGroups {
347 let mut actions = Vec::new();
348 for gpo in ordered_gpos {
349 actions.extend(actions_for_gpo(gpo, resolver));
350 }
351 merge(&actions)
352}
353
354pub fn resolve_privileges(
356 ordered_gpos: &[&SysvolGpo],
357 resolver: &impl Resolver,
358) -> Vec<(String, Vec<TypedPrincipal>)> {
359 let mut order: Vec<String> = Vec::new();
360 let mut map: HashMap<String, Vec<TypedPrincipal>> = HashMap::new();
361
362 for gpo in ordered_gpos {
363 for pa in &gpo.privileges {
364 let key = pa.privilege().to_string();
365 if !map.contains_key(&key) {
366 order.push(key.clone());
367 map.insert(key.clone(), Vec::new());
368 }
369 let entry = map.get_mut(&key).unwrap();
370 for raw in pa.principals() {
371 if let Some(p) = resolve_principal(raw, resolver) {
372 if !entry.iter().any(|x| x.sid == p.sid) {
373 entry.push(p);
374 }
375 }
376 }
377 }
378 }
379 order.into_iter().map(|k| { let v = map.remove(&k).unwrap(); (k, v) }).collect()
380}
381
382fn to_member(p: &TypedPrincipal) -> Member {
385 let mut m = Member::new();
386 *m.object_identifier_mut() = p.sid.clone();
387 *m.object_type_mut() = p.object_type.clone();
388 m
389}
390
391fn to_members(list: &[TypedPrincipal]) -> Vec<Member> {
392 list.iter().map(to_member).collect()
393}
394
395pub fn apply_gpo(
405 ous: &mut [Ou],
406 domains: &mut [Domain],
407 users: &[User],
408 groups: &[Group],
409 computers: &mut Vec<Computer>,
410 sysvol: &[SysvolGpo],
411 dn_sid: &HashMap<String, String>,
412) {
413 let resolver = ObjectResolver::build(users, groups, computers);
414
415 let mut by_sid: HashMap<String, &SysvolGpo> = HashMap::new();
417 for g in sysvol {
418 let needle = g.guid.to_uppercase();
419 if let Some((_, sid)) = dn_sid.iter().find(|(dn, _)| dn.to_uppercase().contains(&needle)) {
420 by_sid.insert(sid.to_uppercase(), g);
421 }
422 }
423
424 log::debug!("[gpo] {} SYSVOL GPO(s) bridged to a SID via dn_sid", by_sid.len());
425 for (sid, g) in &by_sid {
426 log::debug!("[gpo] {} -> {}", g.guid, sid);
427 }
428
429 let mut priv_acc: HashMap<String, HashMap<String, Vec<TypedPrincipal>>> = HashMap::new();
431
432 for ou in ous.iter_mut() {
433 let label = ou.properties().distinguishedname().clone();
434 let links: Vec<Link> = ou.get_links().to_vec();
435 fill_container("OU", &label, &links, ou.gpo_changes_mut(), &by_sid, &resolver, &mut priv_acc);
436 }
437 for dom in domains.iter_mut() {
438 let label = dom.properties().distinguishedname().clone();
439 let links: Vec<Link> = dom.get_links().to_vec();
440 fill_container("Domain", &label, &links, dom.gpo_changes_mut(), &by_sid, &resolver, &mut priv_acc);
441 }
442
443 for c in computers.iter_mut() {
444 let Some(per) = priv_acc.get(c.object_identifier()) else { continue };
445 let ur = c.users_rights_mut();
446 for (privilege, members) in per {
447 let mut right = UserRight::new();
448 *right.privilege_mut() = privilege.clone();
449 *right.results_mut() = to_members(members);
450 *right.collected_mut() = true;
451 ur.push(right);
452 }
453 log::trace!("[gpo] Computer {}: {} UserRight(s) set", c.object_identifier(), per.len());
454 }
455
456}
457
458fn fill_container(
459 kind: &str,
460 label: &str,
461 links_src: &[Link],
462 changes: &mut GPOChange,
463 by_sid: &HashMap<String, &SysvolGpo>,
464 resolver: &impl Resolver,
465 priv_acc: &mut HashMap<String, HashMap<String, Vec<TypedPrincipal>>>,
466) {
467 let mut links: Vec<(bool, String)> = links_src
469 .iter()
470 .map(|l| (*l.is_enforced(), l.guid().clone()))
471 .collect();
472 if links.is_empty() {
473 return;
474 }
475 links.sort_by_key(|(enforced, _)| *enforced);
476
477 let ordered: Vec<&SysvolGpo> = links
478 .iter()
479 .filter_map(|(_, sid)| by_sid.get(&sid.to_uppercase()).copied())
480 .collect();
481 if ordered.is_empty() {
482 return;
483 }
484
485 let merged = compute_merged(&ordered, resolver);
487 *changes.local_admins_mut() = to_members(&merged.local_admins);
488 *changes.remote_desktop_users_mut() = to_members(&merged.remote_desktop_users);
489 *changes.dcom_users_mut() = to_members(&merged.dcom_users);
490 *changes.psremote_users_mut() = to_members(&merged.psremote_users);
491
492 log::trace!(
493 "[gpo] {kind} {label}: GPOChanges set from {} linked GPO(s) -> {} local admin(s), {} RDP, {} DCOM, {} PSRemote",
494 ordered.len(),
495 merged.local_admins.len(),
496 merged.remote_desktop_users.len(),
497 merged.dcom_users.len(),
498 merged.psremote_users.len(),
499 );
500
501
502 let privs = resolve_privileges(&ordered, resolver);
504 if privs.is_empty() {
505 return;
506 }
507 log::debug!(
508 "[gpo] {kind} {label}: {} privilege(s) resolved, {} affected computer(s)",
509 privs.len(), changes.affected_computers().len()
510 );
511 for m in changes.affected_computers() {
512 let per = priv_acc.entry(m.object_identifier().clone()).or_default();
513 for (privilege, members) in &privs {
514 let e = per.entry(privilege.clone()).or_default();
515 for p in members {
516 if !e.iter().any(|x| x.sid == p.sid) {
517 e.push(p.clone());
518 }
519 }
520 }
521 }
522}
523
524#[cfg(test)]
527mod tests {
528 use super::*;
529
530 struct FakeResolver;
531 impl Resolver for FakeResolver {
532 fn resolve_name(&self, name: &str) -> Option<(String, String)> {
533 match name.to_uppercase().as_str() {
534 "ALICE" => Some(("S-1-5-21-1-1-1105".into(), "User".into())),
535 "HELPDESK" => Some(("S-1-5-21-1-1-1200".into(), "Group".into())),
536 _ => None,
537 }
538 }
539 fn type_of_sid(&self, sid: &str) -> Option<String> {
540 match sid {
541 "S-1-5-21-1-1-512" => Some("Group".into()),
542 _ => None,
543 }
544 }
545 }
546
547 fn gpo_with_restricted(target: &str, op: RestrictedGroupOperation, principals: &[&str]) -> SysvolGpo {
548 let mut g = SysvolGpo::default();
549 g.guid = "{G}".into();
550 g.restricted_groups = vec![RestrictedGroupDirective::new(
551 target,
552 op,
553 principals.iter().map(|s| s.to_string()).collect(),
554 )];
555 g
556 }
557
558 #[test]
559 fn replace_members_fills_local_admins() {
560 let g = gpo_with_restricted(
561 "S-1-5-32-544",
562 RestrictedGroupOperation::ReplaceMembers,
563 &["*S-1-5-21-1-1-512", "*ALICE"],
564 );
565 let merged = compute_merged(&[&g], &FakeResolver);
566 let sids: Vec<&str> = merged.local_admins.iter().map(|p| p.sid.as_str()).collect();
567 assert_eq!(sids, vec!["S-1-5-21-1-1-512", "S-1-5-21-1-1-1105"]);
568 assert_eq!(merged.local_admins[0].object_type, "Group");
569 assert_eq!(merged.local_admins[1].object_type, "User");
570 assert!(merged.remote_desktop_users.is_empty());
571 }
572
573 #[test]
574 fn memberof_targets_the_parent_group_rid() {
575 let g = gpo_with_restricted(
576 "HELPDESK",
577 RestrictedGroupOperation::AddToParentGroups,
578 &["S-1-5-32-555"],
579 );
580 let merged = compute_merged(&[&g], &FakeResolver);
581 assert_eq!(merged.remote_desktop_users.len(), 1);
582 assert_eq!(merged.remote_desktop_users[0].sid, "S-1-5-21-1-1-1200");
583 assert!(merged.local_admins.is_empty());
584 }
585
586 #[test]
587 fn restricted_member_overrides_local_groups() {
588 let g = gpo_with_restricted(
589 "S-1-5-32-544",
590 RestrictedGroupOperation::ReplaceMembers,
591 &["*ALICE"],
592 );
593 let merged = compute_merged(&[&g], &FakeResolver);
594 assert_eq!(merged.local_admins.len(), 1);
595 assert_eq!(merged.local_admins[0].sid, "S-1-5-21-1-1-1105");
596 }
597
598 }