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 let mut priv_acc: HashMap<String, HashMap<String, Vec<TypedPrincipal>>> = HashMap::new();
426
427 for ou in ous.iter_mut() {
428 let label = ou.properties().distinguishedname().clone();
429 let links: Vec<Link> = ou.get_links().to_vec();
430 fill_container("OU", &label, &links, ou.gpo_changes_mut(), &by_sid, &resolver, &mut priv_acc);
431 }
432 for dom in domains.iter_mut() {
433 let label = dom.properties().distinguishedname().clone();
434 let links: Vec<Link> = dom.get_links().to_vec();
435 fill_container("Domain", &label, &links, dom.gpo_changes_mut(), &by_sid, &resolver, &mut priv_acc);
436 }
437
438 for c in computers.iter_mut() {
439 let Some(per) = priv_acc.get(c.object_identifier()) else { continue };
440 let ur = c.users_rights_mut();
441 for (privilege, members) in per {
442 let mut right = UserRight::new();
443 *right.privilege_mut() = privilege.clone();
444 *right.results_mut() = to_members(members);
445 *right.collected_mut() = true;
446 ur.push(right);
447 }
448 log::trace!("[gpo] Computer {}: {} UserRight(s) set", c.object_identifier(), per.len());
449 }
450}
451
452fn fill_container(
453 kind: &str,
454 label: &str,
455 links_src: &[Link],
456 changes: &mut GPOChange,
457 by_sid: &HashMap<String, &SysvolGpo>,
458 resolver: &impl Resolver,
459 priv_acc: &mut HashMap<String, HashMap<String, Vec<TypedPrincipal>>>,
460) {
461 let mut links: Vec<(bool, String)> = links_src
463 .iter()
464 .map(|l| (*l.is_enforced(), l.guid().clone()))
465 .collect();
466 if links.is_empty() {
467 return;
468 }
469 links.sort_by_key(|(enforced, _)| *enforced);
470
471 let ordered: Vec<&SysvolGpo> = links
472 .iter()
473 .filter_map(|(_, sid)| by_sid.get(&sid.to_uppercase()).copied())
474 .collect();
475 if ordered.is_empty() {
476 return;
477 }
478
479 let merged = compute_merged(&ordered, resolver);
481 *changes.local_admins_mut() = to_members(&merged.local_admins);
482 *changes.remote_desktop_users_mut() = to_members(&merged.remote_desktop_users);
483 *changes.dcom_users_mut() = to_members(&merged.dcom_users);
484 *changes.psremote_users_mut() = to_members(&merged.psremote_users);
485
486 log::trace!(
487 "[gpo] {kind} {label}: GPOChanges set from {} linked GPO(s) -> {} local admin(s), {} RDP, {} DCOM, {} PSRemote",
488 ordered.len(),
489 merged.local_admins.len(),
490 merged.remote_desktop_users.len(),
491 merged.dcom_users.len(),
492 merged.psremote_users.len(),
493 );
494
495 let privs = resolve_privileges(&ordered, resolver);
497 if privs.is_empty() {
498 return;
499 }
500 for m in changes.affected_computers() {
501 let per = priv_acc.entry(m.object_identifier().clone()).or_default();
502 for (privilege, members) in &privs {
503 let e = per.entry(privilege.clone()).or_default();
504 for p in members {
505 if !e.iter().any(|x| x.sid == p.sid) {
506 e.push(p.clone());
507 }
508 }
509 }
510 }
511}
512
513#[cfg(test)]
516mod tests {
517 use super::*;
518
519 struct FakeResolver;
520 impl Resolver for FakeResolver {
521 fn resolve_name(&self, name: &str) -> Option<(String, String)> {
522 match name.to_uppercase().as_str() {
523 "ALICE" => Some(("S-1-5-21-1-1-1105".into(), "User".into())),
524 "HELPDESK" => Some(("S-1-5-21-1-1-1200".into(), "Group".into())),
525 _ => None,
526 }
527 }
528 fn type_of_sid(&self, sid: &str) -> Option<String> {
529 match sid {
530 "S-1-5-21-1-1-512" => Some("Group".into()),
531 _ => None,
532 }
533 }
534 }
535
536 fn gpo_with_restricted(target: &str, op: RestrictedGroupOperation, principals: &[&str]) -> SysvolGpo {
537 let mut g = SysvolGpo::default();
538 g.guid = "{G}".into();
539 g.restricted_groups = vec![RestrictedGroupDirective::new(
540 target,
541 op,
542 principals.iter().map(|s| s.to_string()).collect(),
543 )];
544 g
545 }
546
547 #[test]
548 fn replace_members_fills_local_admins() {
549 let g = gpo_with_restricted(
550 "S-1-5-32-544",
551 RestrictedGroupOperation::ReplaceMembers,
552 &["*S-1-5-21-1-1-512", "*ALICE"],
553 );
554 let merged = compute_merged(&[&g], &FakeResolver);
555 let sids: Vec<&str> = merged.local_admins.iter().map(|p| p.sid.as_str()).collect();
556 assert_eq!(sids, vec!["S-1-5-21-1-1-512", "S-1-5-21-1-1-1105"]);
557 assert_eq!(merged.local_admins[0].object_type, "Group");
558 assert_eq!(merged.local_admins[1].object_type, "User");
559 assert!(merged.remote_desktop_users.is_empty());
560 }
561
562 #[test]
563 fn memberof_targets_the_parent_group_rid() {
564 let g = gpo_with_restricted(
565 "HELPDESK",
566 RestrictedGroupOperation::AddToParentGroups,
567 &["S-1-5-32-555"],
568 );
569 let merged = compute_merged(&[&g], &FakeResolver);
570 assert_eq!(merged.remote_desktop_users.len(), 1);
571 assert_eq!(merged.remote_desktop_users[0].sid, "S-1-5-21-1-1-1200");
572 assert!(merged.local_admins.is_empty());
573 }
574
575 #[test]
576 fn restricted_member_overrides_local_groups() {
577 let g = gpo_with_restricted(
578 "S-1-5-32-544",
579 RestrictedGroupOperation::ReplaceMembers,
580 &["*ALICE"],
581 );
582 let merged = compute_merged(&[&g], &FakeResolver);
583 assert_eq!(merged.local_admins.len(), 1);
584 assert_eq!(merged.local_admins[0].sid, "S-1-5-21-1-1-1105");
585 }
586
587 }