1use serde::{Deserialize, Serialize};
2use serde_json::Value;
3use std::convert::TryFrom;
4
5use sysknife_proto::sysknife::v1 as proto;
6
7#[derive(Clone, Debug, PartialEq, Eq)]
26pub struct DistroHint {
27 pub family: &'static str,
32 pub version: Option<String>,
35}
36
37pub const DISTRO_FAMILY_FEDORA: &str = "fedora";
39
40pub const DISTRO_FAMILY_DEBIAN: &str = "debian";
42
43pub const DISTRO_FAMILY_OTHER: &str = "other";
45
46pub const KNOWN_ACTION_NAMES: &[&str] = &[
71 "GetSystemState",
72 "CollectDiagnostics",
73 "GetDeploymentHistory",
74 "ListDeployments",
75 "UpdateSystem",
76 "CleanupDeployments",
77 "RebootSystem",
78 "RollbackDeployment",
79 "GetKernelArguments",
80 "PinDeployment",
81 "UnpinDeployment",
82 "RebaseSystem",
83 "SetKernelArguments",
84 "InstallFlatpak",
85 "RemoveFlatpak",
86 "UpdateFlatpak",
87 "SearchFlatpakApps",
88 "ListFlatpakRemotes",
89 "ListInstalledFlatpaks",
90 "AddFlatpakRemote",
91 "RemoveFlatpakRemote",
92 "GetFlatpakAppInfo",
93 "ListToolboxes",
94 "CreateToolbox",
95 "RemoveToolbox",
96 "GetLayeredPackages",
97 "ResetLayeredPackageOverride",
98 "GetPendingUpdates",
99 "InstallPackages",
100 "RemovePackages",
101 "AddLayeredPackage",
102 "RemoveLayeredPackage",
103 "ReplaceLayeredPackage",
104 "RemoveBasePackage",
105 "ListServices",
106 "ListTimers",
107 "ReloadDaemon",
108 "CreateScheduledJob",
109 "StartService",
110 "StopService",
111 "RestartService",
112 "ReloadService",
113 "SetServiceEnabled",
114 "MaskService",
115 "UnmaskService",
116 "GetServiceLogs",
117 "GetServiceStatus",
118 "GetServiceResourceLimits",
119 "SetServiceResourceLimits",
120 "GetFirewallState",
121 "GetNetworkStatus",
122 "GetListeningPorts",
123 "ConfigureWifi",
124 "SetDnsServers",
125 "ConfigureFirewall",
126 "GetDiskUsage",
127 "ListProcesses",
128 "SignalProcess",
129 "GetMemoryInfo",
130 "GetHostState",
131 "GetJournalLog",
133 "VacuumJournal",
134 "GetLvmReport",
136 "ExtendLogicalVolume",
137 "CreateLogicalVolume",
138 "CreateLvSnapshot",
139 "GetSysctl",
141 "SetSysctl",
142 "GetMounts",
144 "AddMount",
145 "RemoveMount",
146 "AddSwap",
147 "RemoveSwap",
148 "GetSudoGrants",
150 "GrantSudoAccess",
151 "RevokeSudoAccess",
152 "GetLogrotateStatus",
154 "ConfigureLogRotation",
155 "RemoveLogRotation",
156 "ConfigureRemoteSyslog",
157 "RemoveRemoteSyslog",
158 "GetPasswordAging",
160 "SetPasswordAging",
161 "SetPasswordPolicy",
162 "SetAccountLockout",
163 "GetAuditRules",
165 "AddAuditRule",
166 "RemoveAuditRule",
167 "GetCertificates",
169 "ObtainCertificate",
170 "RenewCertificates",
171 "GetDateTime",
172 "SetHostname",
173 "SetTimezone",
174 "SetLocale",
175 "SetNtp",
176 "ListPackageRepositories",
177 "AddPackageRepository",
178 "RemovePackageRepository",
179 "EnablePackageRepository",
180 "DisablePackageRepository",
181 "ListContainers",
182 "CreateContainer",
183 "StartContainer",
184 "StopContainer",
185 "RemoveContainer",
186 "GetContainerInfo",
187 "ListUsers",
188 "ListGroups",
189 "CreateUser",
190 "DeleteUser",
191 "AddUserToGroup",
192 "RemoveUserFromGroup",
193 "CreateGroup",
194 "DeleteGroup",
195 "LockUserAccount",
196 "UnlockUserAccount",
197 "GetAuthorizedKeys",
198 "AddAuthorizedKey",
199 "RemoveAuthorizedKey",
200 "SetSshdOption",
201 "ListJobHistory",
202 "AptUpdate",
204 "AptUpgrade",
205 "AptInstall",
206 "AptRemove",
207 "AptPurge",
208 "AptAutoremove",
209 "AptHold",
210 "AptUnhold",
211 "AptSearch",
212 "AptListInstalled",
213 "AptShow",
214 "AptListUpgradable",
215 "AptHistoryList",
216 "ConfigureUnattendedUpgrades",
217 "GetAptPins",
218 "SetAptPin",
219 "RemoveAptPin",
220 "AddPpa",
222 "RemovePpa",
223 "SnapInstall",
225 "SnapRemove",
226 "SnapRefresh",
227 "SnapHold",
228 "SnapUnhold",
229 "SnapList",
230 "SnapInfo",
231 "SnapRevert",
232 "SnapClassicInstall",
233 "UfwEnable",
235 "UfwDisable",
236 "UfwAllow",
237 "UfwDeny",
238 "UfwReset",
239 "UfwStatus",
240 "DistroboxList",
242 "DistroboxCreate",
243 "DistroboxRemove",
244 "NetplanGetConfig",
246 "NetplanApply",
247 "NetplanSet",
248 "NetplanGenerate",
249 "GrubGetKargs",
251 "GrubSetKargs",
252 "CheckPendingReboot",
254 "ResolvectlStatus",
256 "ResolvectlSetDns",
257 "AppArmorStatus",
259 "AppArmorEnforce",
260 "AppArmorComplain",
261 "CloudInitStatus",
263 "UbuntuInstallFlatpak",
265 "UbuntuRemoveFlatpak",
266 "UbuntuUpdateFlatpak",
267 "UbuntuListFlatpaks",
268 "Fail2banStatus",
270 "Fail2banBanIp",
271 "Fail2banUnbanIp",
272 "ConfigureFail2banJail",
273 "UbuntuReleaseUpgrade",
275 "ProStatus",
277 "ProAttach",
278 "ProDetach",
279 "EnableProService",
280 "DisableProService",
281 "LivepatchStatus",
283 "MultipassList",
285 "UfwDeleteRule",
287 "UfwLimit",
288];
289
290#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
297#[serde(transparent)]
298pub struct ActionName(String);
299
300impl ActionName {
301 pub fn parse(name: impl Into<String>) -> Result<Self, UnknownActionName> {
303 let name = name.into();
304 if KNOWN_ACTION_NAMES.contains(&name.as_str()) {
305 Ok(Self(name))
306 } else {
307 Err(UnknownActionName(name))
308 }
309 }
310
311 pub fn as_str(&self) -> &str {
313 &self.0
314 }
315}
316
317impl std::fmt::Display for ActionName {
318 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
319 f.write_str(&self.0)
320 }
321}
322
323impl AsRef<str> for ActionName {
324 fn as_ref(&self) -> &str {
325 &self.0
326 }
327}
328
329#[derive(Clone, Debug, PartialEq, Eq)]
331pub struct UnknownActionName(pub String);
332
333impl std::fmt::Display for UnknownActionName {
334 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
335 write!(f, "unknown action name: '{}'", self.0)
336 }
337}
338
339impl std::error::Error for UnknownActionName {}
340
341#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
350#[serde(transparent)]
351pub struct RequestHash(String);
352
353impl RequestHash {
354 pub fn new(s: impl Into<String>) -> Self {
355 Self(s.into())
356 }
357 pub fn as_str(&self) -> &str {
358 &self.0
359 }
360 pub fn into_inner(self) -> String {
361 self.0
362 }
363}
364
365impl std::fmt::Display for RequestHash {
366 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
367 f.write_str(&self.0)
368 }
369}
370
371impl AsRef<str> for RequestHash {
372 fn as_ref(&self) -> &str {
373 &self.0
374 }
375}
376
377impl From<String> for RequestHash {
378 fn from(s: String) -> Self {
379 Self(s)
380 }
381}
382
383#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
396#[serde(transparent)]
397pub struct TransactionId(String);
398
399#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
406#[serde(transparent)]
407pub struct ApprovalReceipt(String);
408
409impl std::fmt::Debug for ApprovalReceipt {
410 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
411 f.write_str("ApprovalReceipt(<redacted>)")
412 }
413}
414
415macro_rules! opaque_string_newtype {
416 ($name:ident) => {
417 impl $name {
418 pub fn new(s: impl Into<String>) -> Self {
419 Self(s.into())
420 }
421 pub fn as_str(&self) -> &str {
422 &self.0
423 }
424 pub fn into_inner(self) -> String {
425 self.0
426 }
427 }
428
429 impl AsRef<str> for $name {
430 fn as_ref(&self) -> &str {
431 &self.0
432 }
433 }
434
435 impl From<String> for $name {
436 fn from(s: String) -> Self {
437 Self(s)
438 }
439 }
440 };
441}
442
443opaque_string_newtype!(TransactionId);
444opaque_string_newtype!(ApprovalReceipt);
445
446impl std::fmt::Display for TransactionId {
451 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
452 f.write_str(&self.0)
453 }
454}
455
456#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
467#[serde(rename_all = "snake_case")]
468pub enum CallerRole {
469 Observer,
470 Dev,
471 Admin,
472 Boot,
473}
474
475impl CallerRole {
476 pub fn as_str(&self) -> &'static str {
484 match self {
485 Self::Observer => "observer",
486 Self::Dev => "dev",
487 Self::Admin => "admin",
488 Self::Boot => "boot",
489 }
490 }
491}
492
493#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
494#[serde(rename_all = "snake_case")]
495pub enum RiskLevel {
496 Low,
497 Medium,
498 High,
499}
500
501#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
502#[serde(rename_all = "snake_case")]
503pub enum JobState {
504 Queued,
505 Running,
506 Succeeded,
507 Failed,
508 Canceled,
509 RolledBack,
510 NeedsReboot,
511}
512
513#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
520#[serde(rename_all = "snake_case")]
521pub enum FailureCategory {
522 ValidationFailure,
523 AuthorizationFailure,
524 PolicyDenied,
525 StaleApproval,
526 ExecutionFailure,
527 TransientInfrastructureFailure,
528 Cancellation,
529 StuckExecution,
530 RebootRequired,
531 RollbackFailure,
532}
533
534#[derive(Debug, thiserror::Error)]
535pub enum BridgeError {
536 #[error("invalid json payload for {0}")]
537 InvalidJson(&'static str, #[source] serde_json::Error),
538
539 #[error("invalid enum value {value} for {field}")]
540 InvalidEnum { field: &'static str, value: i32 },
541}
542
543#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
544pub struct RequestEnvelope {
545 pub action_name: String,
546 pub request_id: String,
547 pub params: Value,
548 pub caller_role: CallerRole,
549 pub request_hash: RequestHash,
550}
551
552#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
553pub struct PreviewEnvelope {
554 pub summary: String,
555 pub risk_level: RiskLevel,
556 pub current_state: Value,
557 pub proposed_change: Value,
558 pub expected_side_effects: Vec<String>,
559 pub reboot_required: bool,
560 pub rollback_available: bool,
561 pub warnings: Vec<String>,
562 pub request_hash: RequestHash,
563}
564
565#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
566pub struct ResultEnvelope {
567 pub status: JobState,
568 pub summary: String,
569 pub warnings: Vec<String>,
570 pub job_id: Option<String>,
571 pub needs_reboot: bool,
572 pub rollback_ref: Option<String>,
573 pub transaction_id: String,
574}
575
576#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
577pub struct TransactionRecord {
578 pub transaction_id: String,
579 pub request_id: String,
580 pub request_hash: String,
581 pub action_name: String,
582 pub risk_level: RiskLevel,
583 pub status: JobState,
584 pub approval_id: Option<String>,
585 pub summary: String,
586 pub warnings: Vec<String>,
587}
588
589impl From<CallerRole> for proto::CallerRole {
590 fn from(value: CallerRole) -> Self {
595 match value {
596 CallerRole::Observer => proto::CallerRole::Observer,
597 CallerRole::Dev => proto::CallerRole::Dev,
598 CallerRole::Admin => proto::CallerRole::Admin,
599 CallerRole::Boot => proto::CallerRole::Boot,
600 }
601 }
602}
603
604impl TryFrom<proto::CallerRole> for CallerRole {
605 type Error = BridgeError;
606
607 fn try_from(value: proto::CallerRole) -> Result<Self, Self::Error> {
608 match i32::from(value) {
609 1 => Ok(CallerRole::Observer),
610 2 => Ok(CallerRole::Dev),
611 3 => Ok(CallerRole::Admin),
612 4 => Ok(CallerRole::Boot),
613 code => Err(BridgeError::InvalidEnum {
614 field: "caller_role",
615 value: code,
616 }),
617 }
618 }
619}
620
621impl From<RiskLevel> for proto::RiskLevel {
622 fn from(value: RiskLevel) -> Self {
623 match value {
624 RiskLevel::Low => proto::RiskLevel::Low,
625 RiskLevel::Medium => proto::RiskLevel::Medium,
626 RiskLevel::High => proto::RiskLevel::High,
627 }
628 }
629}
630
631impl TryFrom<proto::RiskLevel> for RiskLevel {
632 type Error = BridgeError;
633
634 fn try_from(value: proto::RiskLevel) -> Result<Self, Self::Error> {
635 match i32::from(value) {
636 1 => Ok(RiskLevel::Low),
637 2 => Ok(RiskLevel::Medium),
638 3 => Ok(RiskLevel::High),
639 code => Err(BridgeError::InvalidEnum {
640 field: "risk_level",
641 value: code,
642 }),
643 }
644 }
645}
646
647impl From<JobState> for proto::JobState {
648 fn from(value: JobState) -> Self {
649 match value {
650 JobState::Queued => proto::JobState::Queued,
651 JobState::Running => proto::JobState::Running,
652 JobState::Succeeded => proto::JobState::Succeeded,
653 JobState::Failed => proto::JobState::Failed,
654 JobState::Canceled => proto::JobState::Canceled,
655 JobState::RolledBack => proto::JobState::RolledBack,
656 JobState::NeedsReboot => proto::JobState::NeedsReboot,
657 }
658 }
659}
660
661impl TryFrom<proto::JobState> for JobState {
662 type Error = BridgeError;
663
664 fn try_from(value: proto::JobState) -> Result<Self, Self::Error> {
665 match i32::from(value) {
666 1 => Ok(JobState::Queued),
667 2 => Ok(JobState::Running),
668 3 => Ok(JobState::Succeeded),
669 4 => Ok(JobState::Failed),
670 5 => Ok(JobState::Canceled),
671 6 => Ok(JobState::RolledBack),
672 7 => Ok(JobState::NeedsReboot),
673 code => Err(BridgeError::InvalidEnum {
674 field: "job_state",
675 value: code,
676 }),
677 }
678 }
679}
680
681impl From<FailureCategory> for proto::FailureCategory {
682 fn from(value: FailureCategory) -> Self {
683 match value {
684 FailureCategory::ValidationFailure => proto::FailureCategory::ValidationFailure,
685 FailureCategory::AuthorizationFailure => proto::FailureCategory::AuthorizationFailure,
686 FailureCategory::PolicyDenied => proto::FailureCategory::PolicyDenied,
687 FailureCategory::StaleApproval => proto::FailureCategory::StaleApproval,
688 FailureCategory::ExecutionFailure => proto::FailureCategory::ExecutionFailure,
689 FailureCategory::TransientInfrastructureFailure => {
690 proto::FailureCategory::TransientInfrastructureFailure
691 }
692 FailureCategory::Cancellation => proto::FailureCategory::Cancellation,
693 FailureCategory::StuckExecution => proto::FailureCategory::StuckExecution,
694 FailureCategory::RebootRequired => proto::FailureCategory::RebootRequired,
695 FailureCategory::RollbackFailure => proto::FailureCategory::RollbackFailure,
696 }
697 }
698}
699
700impl TryFrom<proto::FailureCategory> for FailureCategory {
701 type Error = BridgeError;
702
703 fn try_from(value: proto::FailureCategory) -> Result<Self, Self::Error> {
704 match i32::from(value) {
705 1 => Ok(FailureCategory::ValidationFailure),
706 2 => Ok(FailureCategory::AuthorizationFailure),
707 3 => Ok(FailureCategory::PolicyDenied),
708 4 => Ok(FailureCategory::StaleApproval),
709 5 => Ok(FailureCategory::ExecutionFailure),
710 6 => Ok(FailureCategory::TransientInfrastructureFailure),
711 7 => Ok(FailureCategory::Cancellation),
712 8 => Ok(FailureCategory::StuckExecution),
713 9 => Ok(FailureCategory::RebootRequired),
714 10 => Ok(FailureCategory::RollbackFailure),
715 code => Err(BridgeError::InvalidEnum {
716 field: "failure_category",
717 value: code,
718 }),
719 }
720 }
721}
722
723impl From<RequestEnvelope> for proto::RequestEnvelope {
724 fn from(value: RequestEnvelope) -> Self {
725 Self {
726 action_name: value.action_name,
727 request_id: value.request_id,
728 params_json: serde_json::to_string(&value.params).expect("json serialization"),
729 caller_role: i32::from(proto::CallerRole::from(value.caller_role)),
730 request_hash: value.request_hash.into_inner(),
731 }
732 }
733}
734
735impl TryFrom<proto::RequestEnvelope> for RequestEnvelope {
736 type Error = BridgeError;
737
738 fn try_from(value: proto::RequestEnvelope) -> Result<Self, Self::Error> {
739 let params = serde_json::from_str(&value.params_json)
740 .map_err(|error| BridgeError::InvalidJson("params_json", error))?;
741
742 Ok(Self {
743 action_name: value.action_name,
744 request_id: value.request_id,
745 params,
746 caller_role: CallerRole::try_from(
747 proto::CallerRole::try_from(value.caller_role).map_err(|_| {
748 BridgeError::InvalidEnum {
749 field: "caller_role",
750 value: value.caller_role,
751 }
752 })?,
753 )?,
754 request_hash: RequestHash::from(value.request_hash),
755 })
756 }
757}
758
759impl From<PreviewEnvelope> for proto::PreviewEnvelope {
760 fn from(value: PreviewEnvelope) -> Self {
761 Self {
762 summary: value.summary,
763 risk_level: i32::from(proto::RiskLevel::from(value.risk_level)),
764 current_state_json: serde_json::to_string(&value.current_state)
765 .expect("json serialization"),
766 proposed_change_json: serde_json::to_string(&value.proposed_change)
767 .expect("json serialization"),
768 expected_side_effects: value.expected_side_effects,
769 reboot_required: value.reboot_required,
770 rollback_available: value.rollback_available,
771 warnings: value.warnings,
772 request_hash: value.request_hash.into_inner(),
773 }
774 }
775}
776
777impl TryFrom<proto::PreviewEnvelope> for PreviewEnvelope {
778 type Error = BridgeError;
779
780 fn try_from(value: proto::PreviewEnvelope) -> Result<Self, Self::Error> {
781 let current_state = serde_json::from_str(&value.current_state_json)
782 .map_err(|error| BridgeError::InvalidJson("current_state_json", error))?;
783 let proposed_change = serde_json::from_str(&value.proposed_change_json)
784 .map_err(|error| BridgeError::InvalidJson("proposed_change_json", error))?;
785
786 Ok(Self {
787 summary: value.summary,
788 risk_level: RiskLevel::try_from(
789 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
790 BridgeError::InvalidEnum {
791 field: "risk_level",
792 value: value.risk_level,
793 }
794 })?,
795 )?,
796 current_state,
797 proposed_change,
798 expected_side_effects: value.expected_side_effects,
799 reboot_required: value.reboot_required,
800 rollback_available: value.rollback_available,
801 warnings: value.warnings,
802 request_hash: RequestHash::from(value.request_hash),
803 })
804 }
805}
806
807impl From<ResultEnvelope> for proto::ResultEnvelope {
808 fn from(value: ResultEnvelope) -> Self {
809 Self {
810 status: i32::from(proto::JobState::from(value.status)),
811 summary: value.summary,
812 warnings: value.warnings,
813 job_id: value.job_id.unwrap_or_default(),
814 needs_reboot: value.needs_reboot,
815 rollback_ref: value.rollback_ref.unwrap_or_default(),
816 transaction_id: value.transaction_id,
817 }
818 }
819}
820
821impl TryFrom<proto::ResultEnvelope> for ResultEnvelope {
822 type Error = BridgeError;
823
824 fn try_from(value: proto::ResultEnvelope) -> Result<Self, Self::Error> {
825 Ok(Self {
826 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
827 BridgeError::InvalidEnum {
828 field: "job_state",
829 value: value.status,
830 }
831 })?)?,
832 summary: value.summary,
833 warnings: value.warnings,
834 job_id: if value.job_id.is_empty() {
835 None
836 } else {
837 Some(value.job_id)
838 },
839 needs_reboot: value.needs_reboot,
840 rollback_ref: if value.rollback_ref.is_empty() {
841 None
842 } else {
843 Some(value.rollback_ref)
844 },
845 transaction_id: value.transaction_id,
846 })
847 }
848}
849
850impl From<TransactionRecord> for proto::TransactionRecord {
851 fn from(value: TransactionRecord) -> Self {
852 Self {
853 transaction_id: value.transaction_id,
854 request_id: value.request_id,
855 request_hash: value.request_hash,
856 action_name: value.action_name,
857 risk_level: i32::from(proto::RiskLevel::from(value.risk_level)),
858 status: i32::from(proto::JobState::from(value.status)),
859 approval_id: value.approval_id.unwrap_or_default(),
860 summary: value.summary,
861 warnings: value.warnings,
862 }
863 }
864}
865
866impl TryFrom<proto::TransactionRecord> for TransactionRecord {
867 type Error = BridgeError;
868
869 fn try_from(value: proto::TransactionRecord) -> Result<Self, Self::Error> {
870 Ok(Self {
871 transaction_id: value.transaction_id,
872 request_id: value.request_id,
873 request_hash: value.request_hash,
874 action_name: value.action_name,
875 risk_level: RiskLevel::try_from(
876 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
877 BridgeError::InvalidEnum {
878 field: "risk_level",
879 value: value.risk_level,
880 }
881 })?,
882 )?,
883 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
884 BridgeError::InvalidEnum {
885 field: "job_state",
886 value: value.status,
887 }
888 })?)?,
889 approval_id: if value.approval_id.is_empty() {
890 None
891 } else {
892 Some(value.approval_id)
893 },
894 summary: value.summary,
895 warnings: value.warnings,
896 })
897 }
898}
899
900#[cfg(test)]
901mod request_hash_tests {
902 use super::*;
903
904 #[test]
905 fn serde_round_trips_via_transparent_string() {
906 let r = RequestHash::new("abc123".to_string());
909 let json = serde_json::to_string(&r).unwrap();
910 assert_eq!(json, "\"abc123\"");
911 let back: RequestHash = serde_json::from_str("\"abc123\"").unwrap();
912 assert_eq!(back, r);
913 }
914}
915
916#[cfg(test)]
917mod caller_role_ordering_tests {
918 use super::CallerRole;
919
920 #[test]
921 fn caller_role_orders_least_to_most_privileged() {
922 assert!(CallerRole::Observer < CallerRole::Dev);
925 assert!(CallerRole::Dev < CallerRole::Admin);
926 assert!(CallerRole::Admin < CallerRole::Boot);
927
928 assert!(CallerRole::Admin >= CallerRole::Dev);
930 assert!(!(CallerRole::Observer >= CallerRole::Dev));
931 }
932
933 #[test]
934 fn max_selects_the_higher_privilege() {
935 assert_eq!(
938 CallerRole::Observer.max(CallerRole::Admin),
939 CallerRole::Admin
940 );
941 assert_eq!(CallerRole::Boot.max(CallerRole::Dev), CallerRole::Boot);
942 }
943}