1use crate::error::RegentError;
54use crate::hosts::managed_host::InternalApiCallOutcome;
55use crate::hosts::managed_host::{AssessCompliance, ReachCompliance, Timeout};
56use crate::hosts::properties::{HostProperties, InitSystem, LinuxFlavor, LinuxSpecifics, OsKind};
57use crate::secrets::SecretProvidersPool;
58use crate::state::Check;
59use crate::state::attribute::HostHandler;
60use crate::state::attribute::Privilege;
61use crate::state::attribute::Remediation;
62use crate::state::attribute::RemediationsList;
63use crate::state::compliance::AttributeComplianceAssessment;
64use serde::{Deserialize, Serialize};
65use std::time::Duration;
66
67#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
68#[serde(rename_all = "PascalCase")]
69pub enum UserExpectedStatus {
70 Absent {
71 remove_home: Option<bool>,
73 },
74 #[serde(rename_all = "PascalCase")]
75 Present { details: UserDetails },
76}
77
78#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
79#[serde(deny_unknown_fields)]
80#[serde(rename_all = "PascalCase")]
81pub struct UserDetails {
82 uid: Option<u32>,
83 group: Option<String>,
85 groups: Option<Vec<String>>,
87 append: Option<bool>,
89 shell: Option<String>,
90 home: Option<String>,
91 comment: Option<String>,
93 password: Option<String>,
95 system: Option<bool>,
97 create_home: Option<bool>,
99}
100
101impl UserDetails {
102 pub fn default() -> Self {
103 Self {
104 uid: None,
105 group: None,
106 groups: None,
107 append: None,
108 shell: None,
109 home: None,
110 comment: None,
111 password: None,
112 system: None,
113 create_home: None,
114 }
115 }
116
117 pub fn with_uid(&mut self, uid: u32) -> &mut Self {
118 self.uid = Some(uid);
119 self
120 }
121
122 pub fn with_group(&mut self, group: &str) -> &mut Self {
123 self.group = Some(group.to_string());
124 self
125 }
126
127 pub fn with_groups(&mut self, groups: Vec<String>) -> &mut Self {
128 self.groups = Some(groups);
129 self
130 }
131
132 pub fn with_append(&mut self, append: bool) -> &mut Self {
133 self.append = Some(append);
134 self
135 }
136
137 pub fn with_shell(&mut self, shell: &str) -> &mut Self {
138 self.shell = Some(shell.to_string());
139 self
140 }
141
142 pub fn with_home(&mut self, home: &str) -> &mut Self {
143 self.home = Some(home.to_string());
144 self
145 }
146
147 pub fn with_comment(&mut self, comment: &str) -> &mut Self {
148 self.comment = Some(comment.to_string());
149 self
150 }
151
152 pub fn with_password(&mut self, password: &str) -> &mut Self {
153 self.password = Some(password.to_string());
154 self
155 }
156
157 pub fn with_system(&mut self, system: bool) -> &mut Self {
158 self.system = Some(system);
159 self
160 }
161
162 pub fn with_create_home(&mut self, create_home: bool) -> &mut Self {
163 self.create_home = Some(create_home);
164 self
165 }
166
167 pub fn finish(&mut self) -> Self {
168 self.clone()
169 }
170}
171
172#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(deny_unknown_fields)]
178#[serde(rename_all = "PascalCase")]
179pub struct UserExpectedState {
180 name: String,
181 state: UserExpectedStatus,
183}
184
185impl Timeout for UserExpectedState {
186 fn default_timeout(&self) -> Duration {
187 Duration::from_secs(5)
188 }
189}
190
191impl UserExpectedState {
192 pub fn absent(&mut self, username: &str, remove_home: Option<bool>) -> UserExpectedState {
193 UserExpectedState {
194 name: username.to_string(),
195 state: UserExpectedStatus::Absent { remove_home },
196 }
197 }
198
199 pub fn absent_keep_home(username: &str) -> UserExpectedState {
201 UserExpectedState {
202 name: username.to_string(),
203 state: UserExpectedStatus::Absent {
204 remove_home: Some(false),
205 },
206 }
207 }
208
209 pub fn absent_remove_home(username: &str) -> UserExpectedState {
211 UserExpectedState {
212 name: username.to_string(),
213 state: UserExpectedStatus::Absent {
214 remove_home: Some(true),
215 },
216 }
217 }
218
219 pub fn present_with_defaults(username: &str) -> UserExpectedState {
220 UserExpectedState {
221 name: username.to_string(),
222 state: UserExpectedStatus::Present {
223 details: UserDetails::default(),
224 },
225 }
226 }
227
228 pub fn present_with_details(username: &str, details: UserDetails) -> UserExpectedState {
229 UserExpectedState {
230 name: username.to_string(),
231 state: UserExpectedStatus::Present { details },
232 }
233 }
234}
235
236impl Check for UserExpectedState {
237 fn check(&self) -> Result<(), RegentError> {
238 Ok(())
239 }
240
241 fn check_host_compatibility(
242 &self,
243 host_properties: &HostProperties,
244 ) -> Result<(), RegentError> {
245 match host_properties.os_kind() {
246 OsKind::Linux(_) => Ok(()),
247 #[cfg(feature = "windows")]
248 OsKind::Windows(_) => Ok(()),
249 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
250 "Host is {:?} but user management is only supported on Linux or Windows (with windows feature)",
251 incompatible_os_kind
252 ))),
253 }
254 }
255}
256
257impl<Handler: HostHandler> AssessCompliance<Handler> for UserExpectedState {
258 async fn assess_compliance(
259 &self,
260 host_handler: &mut Handler,
261 host_properties: &Option<HostProperties>,
262 privilege: &Privilege,
263 _optional_secret_provider: &Option<SecretProvidersPool>,
264 ) -> Result<AttributeComplianceAssessment, RegentError> {
265 if let Some(props) = host_properties {
266 self.check_host_compatibility(props)?;
267 }
268
269 let os_kind = host_properties
271 .as_ref()
272 .map(|props| props.os_kind())
273 .unwrap_or(&OsKind::Linux(LinuxSpecifics {
274 linux_flavor: LinuxFlavor::Debian,
275 init_system: InitSystem::Systemd,
276 }));
277
278 let user_exists = match os_kind {
279 #[cfg(feature = "windows")]
280 OsKind::Windows(_) => match windows_user_exists(host_handler, &self.name).await {
281 Ok(exists) => exists,
282 Err(e) => return Err(RegentError::FailedDryRunEvaluation(e)),
283 },
284 OsKind::Linux(_) => match user_exists(host_handler, &self.name).await {
285 Ok(exists) => exists,
286 Err(e) => return Err(RegentError::FailedDryRunEvaluation(e)),
287 },
288 OsKind::MacOs(_) | OsKind::FreeBsd(_) | OsKind::Unknown => {
289 return Err(RegentError::FailedDryRunEvaluation(format!(
290 "User management is not supported on {:?}",
291 os_kind
292 )));
293 }
294 };
295
296 match &self.state {
297 UserExpectedStatus::Absent { remove_home } => {
298 if !user_exists {
299 return Ok(AttributeComplianceAssessment::Compliant);
300 }
301 Ok(AttributeComplianceAssessment::NonCompliant(
302 RemediationsList::from(vec![Remediation::User(UserApiCall::from(
303 UserModuleInternalApiCall::Delete {
304 username: self.name.clone(),
305 remove_home: remove_home.unwrap_or(false),
306 },
307 privilege.clone(),
308 ))])
309 .unwrap(),
310 ))
311 }
312 UserExpectedStatus::Present { details } => {
313 if !user_exists {
314 return Ok(AttributeComplianceAssessment::NonCompliant(
315 RemediationsList::from(vec![Remediation::User(UserApiCall::from(
316 UserModuleInternalApiCall::Add {
317 username: self.name.clone(),
318 uid: details.uid,
319 group: details.group.clone(),
320 groups: details.groups.clone(),
321 shell: details.shell.clone(),
322 home: details.home.clone(),
323 comment: details.comment.clone(),
324 password: details.password.clone(),
325 system: details.system.unwrap_or(false),
326 create_home: details.create_home.unwrap_or(true),
327 },
328 privilege.clone(),
329 ))])
330 .unwrap(),
331 ));
332 }
333
334 match os_kind {
337 #[cfg(feature = "windows")]
338 OsKind::Windows(_) => Ok(AttributeComplianceAssessment::Compliant),
339 OsKind::Linux(_) => {
340 let mut mod_uid: Option<u32> = None;
341 let mut mod_group: Option<String> = None;
342 let mut mod_groups: Option<Vec<String>> = None;
343 let mut mod_shell: Option<String> = None;
344 let mut mod_home: Option<String> = None;
345 let mut mod_comment: Option<String> = None;
346 let append = details.append.unwrap_or(true);
347
348 let passwd_entry = match get_passwd_entry(host_handler, &self.name).await {
349 Ok(entry) => entry,
350 Err(e) => return Err(RegentError::FailedDryRunEvaluation(e)),
351 };
352
353 if let Some(expected_uid) = details.uid
354 && passwd_entry.uid != expected_uid
355 {
356 mod_uid = Some(expected_uid);
357 }
358
359 if let Some(expected_shell) = &details.shell
360 && passwd_entry.shell != *expected_shell
361 {
362 mod_shell = Some(expected_shell.clone());
363 }
364
365 if let Some(expected_home) = &details.home
366 && passwd_entry.home != *expected_home
367 {
368 mod_home = Some(expected_home.clone());
369 }
370
371 if let Some(expected_comment) = &details.comment
372 && passwd_entry.comment != *expected_comment
373 {
374 mod_comment = Some(expected_comment.clone());
375 }
376
377 if let Some(expected_group) = &details.group {
378 let current_group =
379 match get_primary_group(host_handler, &self.name).await {
380 Ok(g) => g,
381 Err(e) => return Err(RegentError::FailedDryRunEvaluation(e)),
382 };
383 if ¤t_group != expected_group {
384 mod_group = Some(expected_group.clone());
385 }
386 }
387
388 if let Some(expected_groups) = &details.groups {
389 let current_supp_groups =
390 match get_supplementary_groups(host_handler, &self.name).await {
391 Ok(g) => g,
392 Err(e) => return Err(RegentError::FailedDryRunEvaluation(e)),
393 };
394
395 let groups_compliant = if append {
396 expected_groups
397 .iter()
398 .all(|g| current_supp_groups.contains(g))
399 } else {
400 let mut expected_sorted = expected_groups.clone();
401 let mut current_sorted = current_supp_groups.clone();
402 expected_sorted.sort();
403 current_sorted.sort();
404 expected_sorted == current_sorted
405 };
406
407 if !groups_compliant {
408 if append {
409 let missing_groups: Vec<String> = expected_groups
410 .iter()
411 .filter(|g| !current_supp_groups.contains(g))
412 .cloned()
413 .collect();
414 if !missing_groups.is_empty() {
415 mod_groups = Some(missing_groups);
416 }
417 } else {
418 mod_groups = Some(expected_groups.clone());
419 }
420 }
421 }
422
423 let needs_non_password_modification = mod_uid.is_some()
424 || mod_group.is_some()
425 || mod_groups.is_some()
426 || mod_shell.is_some()
427 || mod_home.is_some()
428 || mod_comment.is_some();
429
430 let password_needs_modification = details.password.is_some();
432
433 if needs_non_password_modification || password_needs_modification {
434 let mod_password = if password_needs_modification {
438 details.password.clone()
439 } else {
440 None
441 };
442
443 return Ok(AttributeComplianceAssessment::NonCompliant(
444 RemediationsList::from(vec![Remediation::User(UserApiCall::from(
445 UserModuleInternalApiCall::Modify {
446 username: self.name.clone(),
447 uid: mod_uid,
448 group: mod_group,
449 groups: mod_groups,
450 append,
451 shell: mod_shell,
452 home: mod_home,
453 comment: mod_comment,
454 password: mod_password,
455 },
456 privilege.clone(),
457 ))])
458 .unwrap(),
459 ));
460 }
461
462 Ok(AttributeComplianceAssessment::Compliant)
463 }
464 OsKind::MacOs(_) | OsKind::FreeBsd(_) | OsKind::Unknown => {
465 Err(RegentError::FailedDryRunEvaluation(format!(
466 "Detailed user management is not supported on {:?}",
467 os_kind
468 )))
469 }
470 }
471 }
472 }
473 }
474}
475
476#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
477#[serde(rename_all = "PascalCase")]
478pub enum UserModuleInternalApiCall {
479 Add {
480 username: String,
481 uid: Option<u32>,
482 group: Option<String>,
483 groups: Option<Vec<String>>,
484 shell: Option<String>,
485 home: Option<String>,
486 comment: Option<String>,
487 password: Option<String>,
488 system: bool,
489 create_home: bool,
490 },
491 Modify {
492 username: String,
493 uid: Option<u32>,
494 group: Option<String>,
495 groups: Option<Vec<String>>,
496 append: bool,
497 shell: Option<String>,
498 home: Option<String>,
499 comment: Option<String>,
500 password: Option<String>,
501 },
502 Delete {
503 username: String,
504 remove_home: bool,
505 },
506}
507
508impl std::fmt::Display for UserModuleInternalApiCall {
509 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
510 match self {
511 UserModuleInternalApiCall::Add { username, .. } => {
512 write!(f, "add user {}", username)
513 }
514 UserModuleInternalApiCall::Modify { username, .. } => {
515 write!(f, "modify user {}", username)
516 }
517 UserModuleInternalApiCall::Delete { username, .. } => {
518 write!(f, "delete user {}", username)
519 }
520 }
521 }
522}
523
524#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
525pub struct UserApiCall {
526 pub api_call: UserModuleInternalApiCall,
527 privilege: Privilege,
528}
529
530impl UserApiCall {
531 pub fn display(&self) -> String {
532 match &self.api_call {
533 UserModuleInternalApiCall::Add { username, .. } => format!("Add user {}", username),
534 UserModuleInternalApiCall::Modify { username, .. } => {
535 format!("Modify user {}", username)
536 }
537 UserModuleInternalApiCall::Delete { username, .. } => {
538 format!("Delete user {}", username)
539 }
540 }
541 }
542
543 fn from(api_call: UserModuleInternalApiCall, privilege: Privilege) -> UserApiCall {
544 UserApiCall {
545 api_call,
546 privilege,
547 }
548 }
549}
550
551impl Check for UserApiCall {
552 fn check(&self) -> Result<(), RegentError> {
553 Ok(())
554 }
555
556 fn check_host_compatibility(
557 &self,
558 host_properties: &HostProperties,
559 ) -> Result<(), RegentError> {
560 match host_properties.os_kind() {
561 OsKind::Linux(_) => Ok(()),
562 #[cfg(feature = "windows")]
563 OsKind::Windows(_) => Ok(()),
564 incompatible_os_kind => Err(RegentError::IncompatibleHost(format!(
565 "Host is {:?} but user management is only supported on Linux or Windows (with windows feature)",
566 incompatible_os_kind
567 ))),
568 }
569 }
570}
571
572impl<Handler: HostHandler> ReachCompliance<Handler> for UserApiCall {
573 async fn call(
574 &self,
575 host_handler: &mut Handler,
576 host_properties: &Option<HostProperties>,
577 _optional_secret_provider: &Option<SecretProvidersPool>,
578 ) -> Result<InternalApiCallOutcome, RegentError> {
579 if let Some(props) = host_properties {
580 self.check_host_compatibility(props)?;
581 }
582
583 let os_kind = host_properties
585 .as_ref()
586 .map(|props| props.os_kind())
587 .unwrap_or(&OsKind::Linux(LinuxSpecifics {
588 linux_flavor: LinuxFlavor::Debian,
589 init_system: InitSystem::Systemd,
590 }));
591
592 match os_kind {
593 #[cfg(feature = "windows")]
594 OsKind::Windows(_) => {
595 let cmd_result = match &self.api_call {
596 UserModuleInternalApiCall::Add {
597 username,
598 password,
599 comment,
600 ..
601 } => {
602 let mut cmd = format!("net user {} /add", username);
604
605 if let Some(pass) = password {
606 cmd.push_str(&format!(" {}", pass));
608 }
609
610 if let Some(comm) = comment {
611 cmd.push_str(&format!(" /comment:\"{}\"", comm));
612 }
613
614 host_handler.run_windows_command(&cmd).await
615 }
616 UserModuleInternalApiCall::Modify {
617 username,
618 comment,
619 password,
620 ..
621 } => {
622 let mut cmd = format!("net user {}", username);
623
624 if let Some(_pass) = password {
625 cmd.push_str(" * /password:req"); }
627
628 if let Some(comm) = comment {
629 cmd.push_str(&format!(" /comment:\"{}\"", comm));
630 }
631
632 host_handler.run_windows_command(&cmd).await
633 }
634 UserModuleInternalApiCall::Delete { username, .. } => {
635 let cmd = format!("net user {} /delete", username);
636 host_handler.run_windows_command(&cmd).await
637 }
638 };
639
640 match cmd_result {
641 Ok(result) => {
642 if result.return_code == 0 {
643 Ok(InternalApiCallOutcome::Success(None))
644 } else {
645 Ok(InternalApiCallOutcome::Failure(format!(
646 "RC: {}, STDOUT: {}, STDERR: {}",
647 result.return_code, result.stdout, result.stderr
648 )))
649 }
650 }
651 Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
652 "Command execution failed: {:?}",
653 e
654 ))),
655 }
656 }
657 OsKind::Linux(_) => {
658 let (cmd, privilege) = match &self.api_call {
659 UserModuleInternalApiCall::Add {
660 username,
661 uid,
662 group,
663 groups,
664 shell,
665 home,
666 comment,
667 password,
668 system,
669 create_home,
670 } => {
671 let mut args: Vec<String> = Vec::new();
672 if let Some(u) = uid {
673 args.push(format!("-u {}", u));
674 }
675 if let Some(g) = group {
676 args.push(format!("-g {}", g));
677 }
678 if let Some(gs) = groups
679 && !gs.is_empty()
680 {
681 args.push(format!("-G {}", gs.join(",")));
682 }
683 if let Some(s) = shell {
684 args.push(format!("-s {}", s));
685 }
686 if let Some(h) = home {
687 args.push(format!("-d {}", h));
688 }
689 if let Some(c) = comment {
690 args.push(format!("-c '{}'", c));
691 }
692 if let Some(p) = password {
693 args.push(format!("-p '{}'", p));
694 }
695 if *system {
696 args.push("-r".to_string());
697 }
698 if *create_home {
699 args.push("-m".to_string());
700 } else {
701 args.push("-M".to_string());
702 }
703 (
704 format!("useradd {} {}", args.join(" "), username),
705 &self.privilege,
706 )
707 }
708 UserModuleInternalApiCall::Modify {
709 username,
710 uid,
711 group,
712 groups,
713 append,
714 shell,
715 home,
716 comment,
717 password,
718 } => {
719 let mut args: Vec<String> = Vec::new();
720 if let Some(u) = uid {
721 args.push(format!("-u {}", u));
722 }
723 if let Some(g) = group {
724 args.push(format!("-g {}", g));
725 }
726 if let Some(gs) = groups {
727 args.push(format!("-G \"{}\"", gs.join(",")));
729 if *append && !gs.is_empty() {
730 args.push("-a".to_string());
731 }
732 }
733 if let Some(s) = shell {
734 args.push(format!("-s {}", s));
735 }
736 if let Some(h) = home {
737 args.push(format!("-d {}", h));
738 }
739 if let Some(c) = comment {
740 args.push(format!("-c '{}'", c));
741 }
742 if let Some(p) = password {
743 args.push(format!("-p '{}'", p));
744 }
745 (
746 format!("usermod {} {}", args.join(" "), username),
747 &self.privilege,
748 )
749 }
750 UserModuleInternalApiCall::Delete {
751 username,
752 remove_home,
753 } => {
754 let flags = if *remove_home { "-r " } else { "" };
755 (format!("userdel {}{}", flags, username), &self.privilege)
756 }
757 };
758
759 let cmd_result = host_handler.run_command(cmd.as_str(), privilege).await;
760
761 match cmd_result {
762 Ok(result) => {
763 if result.return_code == 0 {
764 Ok(InternalApiCallOutcome::Success(None))
765 } else {
766 Ok(InternalApiCallOutcome::Failure(format!(
767 "RC: {}, STDOUT: {}, STDERR: {}",
768 result.return_code, result.stdout, result.stderr
769 )))
770 }
771 }
772 Err(e) => Ok(InternalApiCallOutcome::Failure(format!(
773 "Command execution failed: {:?}",
774 e
775 ))),
776 }
777 }
778 OsKind::MacOs(_) | OsKind::FreeBsd(_) | OsKind::Unknown => {
779 Err(RegentError::FailedDryRunEvaluation(format!(
780 "User management is not supported on {:?}",
781 os_kind
782 )))
783 }
784 }
785 }
786}
787
788struct PasswdEntry {
789 uid: u32,
790 comment: String,
791 home: String,
792 shell: String,
793}
794
795async fn user_exists<Handler: HostHandler>(
796 host_handler: &mut Handler,
797 username: &str,
798) -> Result<bool, String> {
799 match host_handler
800 .run_command(&format!("id {}", username), &Privilege::None)
801 .await
802 {
803 Ok(result) => Ok(result.return_code == 0),
804 Err(e) => Err(format!("Unable to check if user exists: {:?}", e)),
805 }
806}
807
808#[cfg(feature = "windows")]
809async fn windows_user_exists<Handler: HostHandler>(
810 host_handler: &mut Handler,
811 username: &str,
812) -> Result<bool, String> {
813 match host_handler
814 .run_windows_command(&format!("net user {}", username))
815 .await
816 {
817 Ok(result) => {
818 if result.return_code == 0 {
821 Ok(true)
822 } else if result.stderr.contains("not found") || result.stdout.contains("not found") {
823 Ok(false)
824 } else {
825 Ok(false)
827 }
828 }
829 Err(e) => Err(format!(
830 "Unable to check if user exists on Windows: {:?}",
831 e
832 )),
833 }
834}
835
836async fn get_passwd_entry<Handler: HostHandler>(
837 host_handler: &mut Handler,
838 username: &str,
839) -> Result<PasswdEntry, String> {
840 match host_handler
841 .run_command(&format!("getent passwd {}", username), &Privilege::None)
842 .await
843 {
844 Ok(result) => {
845 if result.return_code != 0 {
846 return Err(format!("getent passwd failed for user {}", username));
847 }
848 let fields: Vec<&str> = result.stdout.trim().splitn(7, ':').collect();
850 if fields.len() < 7 {
851 return Err(format!(
852 "Unexpected getent passwd output for {}: {}",
853 username, result.stdout
854 ));
855 }
856 let uid = fields[2]
857 .parse::<u32>()
858 .map_err(|e| format!("Invalid UID '{}': {}", fields[2], e))?;
859 Ok(PasswdEntry {
860 uid,
861 comment: fields[4].to_string(),
862 home: fields[5].to_string(),
863 shell: fields[6].to_string(),
864 })
865 }
866 Err(e) => Err(format!(
867 "Unable to get passwd entry for {}: {:?}",
868 username, e
869 )),
870 }
871}
872
873async fn get_primary_group<Handler: HostHandler>(
874 host_handler: &mut Handler,
875 username: &str,
876) -> Result<String, String> {
877 match host_handler
878 .run_command(&format!("id -gn {}", username), &Privilege::None)
879 .await
880 {
881 Ok(result) => {
882 if result.return_code != 0 {
883 return Err(format!("id -gn failed for user {}", username));
884 }
885 Ok(result.stdout.trim().to_string())
886 }
887 Err(e) => Err(format!(
888 "Unable to get primary group for {}: {:?}",
889 username, e
890 )),
891 }
892}
893
894async fn get_supplementary_groups<Handler: HostHandler>(
895 host_handler: &mut Handler,
896 username: &str,
897) -> Result<Vec<String>, String> {
898 let primary_group = get_primary_group(host_handler, username).await?;
899
900 match host_handler
901 .run_command(&format!("id -Gn {}", username), &Privilege::None)
902 .await
903 {
904 Ok(result) => {
905 if result.return_code != 0 {
906 return Err(format!("id -Gn failed for user {}", username));
907 }
908 Ok(result
910 .stdout
911 .split_whitespace()
912 .map(|s| s.to_string())
913 .filter(|g| g != &primary_group)
914 .collect())
915 }
916 Err(e) => Err(format!(
917 "Unable to get supplementary groups for {}: {:?}",
918 username, e
919 )),
920 }
921}
922
923#[cfg(test)]
924mod tests {
925 use super::*;
926
927 #[test]
928 fn parsing_user_module_block_from_yaml_str() {
929 let raw_attributes = "---
930- Name: alice
931 State: !Present
932 Details:
933 Shell: /bin/bash
934 Comment: Alice Smith
935 Groups:
936 - sudo
937 - docker
938
939- Name: bob
940 State: !Absent
941 ";
942
943 let _attributes: Vec<UserExpectedState> = yaml_serde::from_str(raw_attributes).unwrap();
944 }
945
946 #[test]
947 fn yaml_and_rusty_api_matching() {
948 let raw_yaml_attribute = "---
949Name: alice
950State: !Present
951 Details:
952 Shell: /bin/bash
953 Comment: Alice Smith
954 Groups:
955 - sudo
956 - docker
957";
958
959 let yaml_defined: UserExpectedState = yaml_serde::from_str(raw_yaml_attribute).unwrap();
960
961 let rusty_defined = UserExpectedState::present_with_details(
962 "alice",
963 UserDetails::default()
964 .with_shell("/bin/bash")
965 .with_comment("Alice Smith")
966 .with_groups(vec!["sudo".to_string(), "docker".to_string()])
967 .finish(),
968 );
969
970 assert_eq!(yaml_defined, rusty_defined);
971 }
972
973 #[test]
974 fn absent_constructors_match_explicit_form() {
975 let mut any = UserExpectedState::present_with_defaults("placeholder");
976 assert_eq!(
977 UserExpectedState::absent_keep_home("bob"),
978 any.absent("bob", Some(false))
979 );
980 assert_eq!(
981 UserExpectedState::absent_remove_home("bob"),
982 any.absent("bob", Some(true))
983 );
984 }
985}