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