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
589fn caller_role_code(value: CallerRole) -> i32 {
590 match value {
591 CallerRole::Observer => 1,
592 CallerRole::Dev => 2,
593 CallerRole::Admin => 3,
594 CallerRole::Boot => 4,
595 }
596}
597
598fn risk_level_code(value: RiskLevel) -> i32 {
599 match value {
600 RiskLevel::Low => 1,
601 RiskLevel::Medium => 2,
602 RiskLevel::High => 3,
603 }
604}
605
606fn job_state_code(value: JobState) -> i32 {
607 match value {
608 JobState::Queued => 1,
609 JobState::Running => 2,
610 JobState::Succeeded => 3,
611 JobState::Failed => 4,
612 JobState::Canceled => 5,
613 JobState::RolledBack => 6,
614 JobState::NeedsReboot => 7,
615 }
616}
617
618impl From<CallerRole> for proto::CallerRole {
619 fn from(value: CallerRole) -> Self {
624 match value {
625 CallerRole::Observer => proto::CallerRole::Observer,
626 CallerRole::Dev => proto::CallerRole::Dev,
627 CallerRole::Admin => proto::CallerRole::Admin,
628 CallerRole::Boot => proto::CallerRole::Boot,
629 }
630 }
631}
632
633impl TryFrom<proto::CallerRole> for CallerRole {
634 type Error = BridgeError;
635
636 fn try_from(value: proto::CallerRole) -> Result<Self, Self::Error> {
637 match i32::from(value) {
638 1 => Ok(CallerRole::Observer),
639 2 => Ok(CallerRole::Dev),
640 3 => Ok(CallerRole::Admin),
641 4 => Ok(CallerRole::Boot),
642 code => Err(BridgeError::InvalidEnum {
643 field: "caller_role",
644 value: code,
645 }),
646 }
647 }
648}
649
650impl From<RiskLevel> for proto::RiskLevel {
651 fn from(value: RiskLevel) -> Self {
652 match value {
653 RiskLevel::Low => proto::RiskLevel::Low,
654 RiskLevel::Medium => proto::RiskLevel::Medium,
655 RiskLevel::High => proto::RiskLevel::High,
656 }
657 }
658}
659
660impl TryFrom<proto::RiskLevel> for RiskLevel {
661 type Error = BridgeError;
662
663 fn try_from(value: proto::RiskLevel) -> Result<Self, Self::Error> {
664 match i32::from(value) {
665 1 => Ok(RiskLevel::Low),
666 2 => Ok(RiskLevel::Medium),
667 3 => Ok(RiskLevel::High),
668 code => Err(BridgeError::InvalidEnum {
669 field: "risk_level",
670 value: code,
671 }),
672 }
673 }
674}
675
676impl From<JobState> for proto::JobState {
677 fn from(value: JobState) -> Self {
678 match value {
679 JobState::Queued => proto::JobState::Queued,
680 JobState::Running => proto::JobState::Running,
681 JobState::Succeeded => proto::JobState::Succeeded,
682 JobState::Failed => proto::JobState::Failed,
683 JobState::Canceled => proto::JobState::Canceled,
684 JobState::RolledBack => proto::JobState::RolledBack,
685 JobState::NeedsReboot => proto::JobState::NeedsReboot,
686 }
687 }
688}
689
690impl TryFrom<proto::JobState> for JobState {
691 type Error = BridgeError;
692
693 fn try_from(value: proto::JobState) -> Result<Self, Self::Error> {
694 match i32::from(value) {
695 1 => Ok(JobState::Queued),
696 2 => Ok(JobState::Running),
697 3 => Ok(JobState::Succeeded),
698 4 => Ok(JobState::Failed),
699 5 => Ok(JobState::Canceled),
700 6 => Ok(JobState::RolledBack),
701 7 => Ok(JobState::NeedsReboot),
702 code => Err(BridgeError::InvalidEnum {
703 field: "job_state",
704 value: code,
705 }),
706 }
707 }
708}
709
710impl From<FailureCategory> for proto::FailureCategory {
711 fn from(value: FailureCategory) -> Self {
712 match value {
713 FailureCategory::ValidationFailure => proto::FailureCategory::ValidationFailure,
714 FailureCategory::AuthorizationFailure => proto::FailureCategory::AuthorizationFailure,
715 FailureCategory::PolicyDenied => proto::FailureCategory::PolicyDenied,
716 FailureCategory::StaleApproval => proto::FailureCategory::StaleApproval,
717 FailureCategory::ExecutionFailure => proto::FailureCategory::ExecutionFailure,
718 FailureCategory::TransientInfrastructureFailure => {
719 proto::FailureCategory::TransientInfrastructureFailure
720 }
721 FailureCategory::Cancellation => proto::FailureCategory::Cancellation,
722 FailureCategory::StuckExecution => proto::FailureCategory::StuckExecution,
723 FailureCategory::RebootRequired => proto::FailureCategory::RebootRequired,
724 FailureCategory::RollbackFailure => proto::FailureCategory::RollbackFailure,
725 }
726 }
727}
728
729impl TryFrom<proto::FailureCategory> for FailureCategory {
730 type Error = BridgeError;
731
732 fn try_from(value: proto::FailureCategory) -> Result<Self, Self::Error> {
733 match i32::from(value) {
734 1 => Ok(FailureCategory::ValidationFailure),
735 2 => Ok(FailureCategory::AuthorizationFailure),
736 3 => Ok(FailureCategory::PolicyDenied),
737 4 => Ok(FailureCategory::StaleApproval),
738 5 => Ok(FailureCategory::ExecutionFailure),
739 6 => Ok(FailureCategory::TransientInfrastructureFailure),
740 7 => Ok(FailureCategory::Cancellation),
741 8 => Ok(FailureCategory::StuckExecution),
742 9 => Ok(FailureCategory::RebootRequired),
743 10 => Ok(FailureCategory::RollbackFailure),
744 code => Err(BridgeError::InvalidEnum {
745 field: "failure_category",
746 value: code,
747 }),
748 }
749 }
750}
751
752impl From<RequestEnvelope> for proto::RequestEnvelope {
753 fn from(value: RequestEnvelope) -> Self {
754 Self {
755 action_name: value.action_name,
756 request_id: value.request_id,
757 params_json: serde_json::to_string(&value.params).expect("json serialization"),
758 caller_role: caller_role_code(value.caller_role),
759 request_hash: value.request_hash.into_inner(),
760 }
761 }
762}
763
764impl TryFrom<proto::RequestEnvelope> for RequestEnvelope {
765 type Error = BridgeError;
766
767 fn try_from(value: proto::RequestEnvelope) -> Result<Self, Self::Error> {
768 let params = serde_json::from_str(&value.params_json)
769 .map_err(|error| BridgeError::InvalidJson("params_json", error))?;
770
771 Ok(Self {
772 action_name: value.action_name,
773 request_id: value.request_id,
774 params,
775 caller_role: CallerRole::try_from(
776 proto::CallerRole::try_from(value.caller_role).map_err(|_| {
777 BridgeError::InvalidEnum {
778 field: "caller_role",
779 value: value.caller_role,
780 }
781 })?,
782 )?,
783 request_hash: RequestHash::from(value.request_hash),
784 })
785 }
786}
787
788impl From<PreviewEnvelope> for proto::PreviewEnvelope {
789 fn from(value: PreviewEnvelope) -> Self {
790 Self {
791 summary: value.summary,
792 risk_level: risk_level_code(value.risk_level),
793 current_state_json: serde_json::to_string(&value.current_state)
794 .expect("json serialization"),
795 proposed_change_json: serde_json::to_string(&value.proposed_change)
796 .expect("json serialization"),
797 expected_side_effects: value.expected_side_effects,
798 reboot_required: value.reboot_required,
799 rollback_available: value.rollback_available,
800 warnings: value.warnings,
801 request_hash: value.request_hash.into_inner(),
802 }
803 }
804}
805
806impl TryFrom<proto::PreviewEnvelope> for PreviewEnvelope {
807 type Error = BridgeError;
808
809 fn try_from(value: proto::PreviewEnvelope) -> Result<Self, Self::Error> {
810 let current_state = serde_json::from_str(&value.current_state_json)
811 .map_err(|error| BridgeError::InvalidJson("current_state_json", error))?;
812 let proposed_change = serde_json::from_str(&value.proposed_change_json)
813 .map_err(|error| BridgeError::InvalidJson("proposed_change_json", error))?;
814
815 Ok(Self {
816 summary: value.summary,
817 risk_level: RiskLevel::try_from(
818 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
819 BridgeError::InvalidEnum {
820 field: "risk_level",
821 value: value.risk_level,
822 }
823 })?,
824 )?,
825 current_state,
826 proposed_change,
827 expected_side_effects: value.expected_side_effects,
828 reboot_required: value.reboot_required,
829 rollback_available: value.rollback_available,
830 warnings: value.warnings,
831 request_hash: RequestHash::from(value.request_hash),
832 })
833 }
834}
835
836impl From<ResultEnvelope> for proto::ResultEnvelope {
837 fn from(value: ResultEnvelope) -> Self {
838 Self {
839 status: job_state_code(value.status),
840 summary: value.summary,
841 warnings: value.warnings,
842 job_id: value.job_id.unwrap_or_default(),
843 needs_reboot: value.needs_reboot,
844 rollback_ref: value.rollback_ref.unwrap_or_default(),
845 transaction_id: value.transaction_id,
846 }
847 }
848}
849
850impl TryFrom<proto::ResultEnvelope> for ResultEnvelope {
851 type Error = BridgeError;
852
853 fn try_from(value: proto::ResultEnvelope) -> Result<Self, Self::Error> {
854 Ok(Self {
855 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
856 BridgeError::InvalidEnum {
857 field: "job_state",
858 value: value.status,
859 }
860 })?)?,
861 summary: value.summary,
862 warnings: value.warnings,
863 job_id: if value.job_id.is_empty() {
864 None
865 } else {
866 Some(value.job_id)
867 },
868 needs_reboot: value.needs_reboot,
869 rollback_ref: if value.rollback_ref.is_empty() {
870 None
871 } else {
872 Some(value.rollback_ref)
873 },
874 transaction_id: value.transaction_id,
875 })
876 }
877}
878
879impl From<TransactionRecord> for proto::TransactionRecord {
880 fn from(value: TransactionRecord) -> Self {
881 Self {
882 transaction_id: value.transaction_id,
883 request_id: value.request_id,
884 request_hash: value.request_hash,
885 action_name: value.action_name,
886 risk_level: risk_level_code(value.risk_level),
887 status: job_state_code(value.status),
888 approval_id: value.approval_id.unwrap_or_default(),
889 summary: value.summary,
890 warnings: value.warnings,
891 }
892 }
893}
894
895impl TryFrom<proto::TransactionRecord> for TransactionRecord {
896 type Error = BridgeError;
897
898 fn try_from(value: proto::TransactionRecord) -> Result<Self, Self::Error> {
899 Ok(Self {
900 transaction_id: value.transaction_id,
901 request_id: value.request_id,
902 request_hash: value.request_hash,
903 action_name: value.action_name,
904 risk_level: RiskLevel::try_from(
905 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
906 BridgeError::InvalidEnum {
907 field: "risk_level",
908 value: value.risk_level,
909 }
910 })?,
911 )?,
912 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
913 BridgeError::InvalidEnum {
914 field: "job_state",
915 value: value.status,
916 }
917 })?)?,
918 approval_id: if value.approval_id.is_empty() {
919 None
920 } else {
921 Some(value.approval_id)
922 },
923 summary: value.summary,
924 warnings: value.warnings,
925 })
926 }
927}
928
929#[cfg(test)]
930mod request_hash_tests {
931 use super::*;
932
933 #[test]
934 fn serde_round_trips_via_transparent_string() {
935 let r = RequestHash::new("abc123".to_string());
938 let json = serde_json::to_string(&r).unwrap();
939 assert_eq!(json, "\"abc123\"");
940 let back: RequestHash = serde_json::from_str("\"abc123\"").unwrap();
941 assert_eq!(back, r);
942 }
943}
944
945#[cfg(test)]
946mod caller_role_ordering_tests {
947 use super::CallerRole;
948
949 #[test]
950 fn caller_role_orders_least_to_most_privileged() {
951 assert!(CallerRole::Observer < CallerRole::Dev);
954 assert!(CallerRole::Dev < CallerRole::Admin);
955 assert!(CallerRole::Admin < CallerRole::Boot);
956
957 assert!(CallerRole::Admin >= CallerRole::Dev);
959 assert!(!(CallerRole::Observer >= CallerRole::Dev));
960 }
961
962 #[test]
963 fn max_selects_the_higher_privilege() {
964 assert_eq!(
967 CallerRole::Observer.max(CallerRole::Admin),
968 CallerRole::Admin
969 );
970 assert_eq!(CallerRole::Boot.max(CallerRole::Dev), CallerRole::Boot);
971 }
972}