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 "GetJournalLog",
132 "VacuumJournal",
133 "GetLvmReport",
135 "ExtendLogicalVolume",
136 "CreateLogicalVolume",
137 "CreateLvSnapshot",
138 "GetSysctl",
140 "SetSysctl",
141 "GetMounts",
143 "AddMount",
144 "RemoveMount",
145 "AddSwap",
146 "RemoveSwap",
147 "GetSudoGrants",
149 "GrantSudoAccess",
150 "RevokeSudoAccess",
151 "GetLogrotateStatus",
153 "ConfigureLogRotation",
154 "RemoveLogRotation",
155 "ConfigureRemoteSyslog",
156 "RemoveRemoteSyslog",
157 "GetPasswordAging",
159 "SetPasswordAging",
160 "SetPasswordPolicy",
161 "SetAccountLockout",
162 "GetAuditRules",
164 "AddAuditRule",
165 "RemoveAuditRule",
166 "GetCertificates",
168 "ObtainCertificate",
169 "RenewCertificates",
170 "GetDateTime",
171 "SetHostname",
172 "SetTimezone",
173 "SetLocale",
174 "SetNtp",
175 "ListPackageRepositories",
176 "AddPackageRepository",
177 "RemovePackageRepository",
178 "EnablePackageRepository",
179 "DisablePackageRepository",
180 "ListContainers",
181 "CreateContainer",
182 "StartContainer",
183 "StopContainer",
184 "RemoveContainer",
185 "GetContainerInfo",
186 "ListUsers",
187 "ListGroups",
188 "CreateUser",
189 "DeleteUser",
190 "AddUserToGroup",
191 "RemoveUserFromGroup",
192 "CreateGroup",
193 "DeleteGroup",
194 "LockUserAccount",
195 "UnlockUserAccount",
196 "GetAuthorizedKeys",
197 "AddAuthorizedKey",
198 "RemoveAuthorizedKey",
199 "SetSshdOption",
200 "ListJobHistory",
201 "AptUpdate",
203 "AptUpgrade",
204 "AptInstall",
205 "AptRemove",
206 "AptPurge",
207 "AptAutoremove",
208 "AptHold",
209 "AptUnhold",
210 "AptSearch",
211 "AptListInstalled",
212 "AptShow",
213 "AptListUpgradable",
214 "AptHistoryList",
215 "ConfigureUnattendedUpgrades",
216 "GetAptPins",
217 "SetAptPin",
218 "RemoveAptPin",
219 "AddPpa",
221 "RemovePpa",
222 "SnapInstall",
224 "SnapRemove",
225 "SnapRefresh",
226 "SnapHold",
227 "SnapUnhold",
228 "SnapList",
229 "SnapInfo",
230 "SnapRevert",
231 "SnapClassicInstall",
232 "UfwEnable",
234 "UfwDisable",
235 "UfwAllow",
236 "UfwDeny",
237 "UfwReset",
238 "UfwStatus",
239 "DistroboxList",
241 "DistroboxCreate",
242 "DistroboxRemove",
243 "NetplanGetConfig",
245 "NetplanApply",
246 "NetplanSet",
247 "NetplanGenerate",
248 "GrubGetKargs",
250 "GrubSetKargs",
251 "CheckPendingReboot",
253 "ResolvectlStatus",
255 "ResolvectlSetDns",
256 "AppArmorStatus",
258 "AppArmorEnforce",
259 "AppArmorComplain",
260 "CloudInitStatus",
262 "UbuntuInstallFlatpak",
264 "UbuntuRemoveFlatpak",
265 "UbuntuUpdateFlatpak",
266 "UbuntuListFlatpaks",
267 "Fail2banStatus",
269 "Fail2banBanIp",
270 "Fail2banUnbanIp",
271 "ConfigureFail2banJail",
272 "UbuntuReleaseUpgrade",
274 "ProStatus",
276 "ProAttach",
277 "ProDetach",
278 "EnableProService",
279 "DisableProService",
280 "LivepatchStatus",
282 "MultipassList",
284 "UfwDeleteRule",
286 "UfwLimit",
287];
288
289#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
296#[serde(transparent)]
297pub struct ActionName(String);
298
299impl ActionName {
300 pub fn parse(name: impl Into<String>) -> Result<Self, UnknownActionName> {
302 let name = name.into();
303 if KNOWN_ACTION_NAMES.contains(&name.as_str()) {
304 Ok(Self(name))
305 } else {
306 Err(UnknownActionName(name))
307 }
308 }
309
310 pub fn as_str(&self) -> &str {
312 &self.0
313 }
314}
315
316impl std::fmt::Display for ActionName {
317 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
318 f.write_str(&self.0)
319 }
320}
321
322impl AsRef<str> for ActionName {
323 fn as_ref(&self) -> &str {
324 &self.0
325 }
326}
327
328#[derive(Clone, Debug, PartialEq, Eq)]
330pub struct UnknownActionName(pub String);
331
332impl std::fmt::Display for UnknownActionName {
333 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
334 write!(f, "unknown action name: '{}'", self.0)
335 }
336}
337
338impl std::error::Error for UnknownActionName {}
339
340#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
349#[serde(transparent)]
350pub struct RequestHash(String);
351
352impl RequestHash {
353 pub fn new(s: impl Into<String>) -> Self {
354 Self(s.into())
355 }
356 pub fn as_str(&self) -> &str {
357 &self.0
358 }
359 pub fn into_inner(self) -> String {
360 self.0
361 }
362}
363
364impl std::fmt::Display for RequestHash {
365 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
366 f.write_str(&self.0)
367 }
368}
369
370impl AsRef<str> for RequestHash {
371 fn as_ref(&self) -> &str {
372 &self.0
373 }
374}
375
376impl From<String> for RequestHash {
377 fn from(s: String) -> Self {
378 Self(s)
379 }
380}
381
382#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
393#[serde(rename_all = "snake_case")]
394pub enum CallerRole {
395 Observer,
396 Dev,
397 Admin,
398 Boot,
399}
400
401#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
402#[serde(rename_all = "snake_case")]
403pub enum RiskLevel {
404 Low,
405 Medium,
406 High,
407}
408
409#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
410#[serde(rename_all = "snake_case")]
411pub enum JobState {
412 Queued,
413 Running,
414 Succeeded,
415 Failed,
416 Canceled,
417 RolledBack,
418 NeedsReboot,
419}
420
421#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
428#[serde(rename_all = "snake_case")]
429pub enum FailureCategory {
430 ValidationFailure,
431 AuthorizationFailure,
432 PolicyDenied,
433 StaleApproval,
434 ExecutionFailure,
435 TransientInfrastructureFailure,
436 Cancellation,
437 StuckExecution,
438 RebootRequired,
439 RollbackFailure,
440}
441
442#[derive(Debug, thiserror::Error)]
443pub enum BridgeError {
444 #[error("invalid json payload for {0}")]
445 InvalidJson(&'static str, #[source] serde_json::Error),
446
447 #[error("invalid enum value {value} for {field}")]
448 InvalidEnum { field: &'static str, value: i32 },
449}
450
451#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
452pub struct RequestEnvelope {
453 pub action_name: String,
454 pub request_id: String,
455 pub params: Value,
456 pub caller_role: CallerRole,
457 pub request_hash: RequestHash,
458}
459
460#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
461pub struct PreviewEnvelope {
462 pub summary: String,
463 pub risk_level: RiskLevel,
464 pub current_state: Value,
465 pub proposed_change: Value,
466 pub expected_side_effects: Vec<String>,
467 pub reboot_required: bool,
468 pub rollback_available: bool,
469 pub warnings: Vec<String>,
470 pub request_hash: RequestHash,
471}
472
473#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
474pub struct ResultEnvelope {
475 pub status: JobState,
476 pub summary: String,
477 pub warnings: Vec<String>,
478 pub job_id: Option<String>,
479 pub needs_reboot: bool,
480 pub rollback_ref: Option<String>,
481 pub transaction_id: String,
482}
483
484#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
485pub struct TransactionRecord {
486 pub transaction_id: String,
487 pub request_id: String,
488 pub request_hash: String,
489 pub action_name: String,
490 pub risk_level: RiskLevel,
491 pub status: JobState,
492 pub approval_id: Option<String>,
493 pub summary: String,
494 pub warnings: Vec<String>,
495}
496
497fn caller_role_code(value: CallerRole) -> i32 {
498 match value {
499 CallerRole::Observer => 1,
500 CallerRole::Dev => 2,
501 CallerRole::Admin => 3,
502 CallerRole::Boot => 4,
503 }
504}
505
506fn risk_level_code(value: RiskLevel) -> i32 {
507 match value {
508 RiskLevel::Low => 1,
509 RiskLevel::Medium => 2,
510 RiskLevel::High => 3,
511 }
512}
513
514fn job_state_code(value: JobState) -> i32 {
515 match value {
516 JobState::Queued => 1,
517 JobState::Running => 2,
518 JobState::Succeeded => 3,
519 JobState::Failed => 4,
520 JobState::Canceled => 5,
521 JobState::RolledBack => 6,
522 JobState::NeedsReboot => 7,
523 }
524}
525
526impl From<CallerRole> for proto::CallerRole {
527 fn from(value: CallerRole) -> Self {
532 match value {
533 CallerRole::Observer => proto::CallerRole::Observer,
534 CallerRole::Dev => proto::CallerRole::Dev,
535 CallerRole::Admin => proto::CallerRole::Admin,
536 CallerRole::Boot => proto::CallerRole::Boot,
537 }
538 }
539}
540
541impl TryFrom<proto::CallerRole> for CallerRole {
542 type Error = BridgeError;
543
544 fn try_from(value: proto::CallerRole) -> Result<Self, Self::Error> {
545 match i32::from(value) {
546 1 => Ok(CallerRole::Observer),
547 2 => Ok(CallerRole::Dev),
548 3 => Ok(CallerRole::Admin),
549 4 => Ok(CallerRole::Boot),
550 code => Err(BridgeError::InvalidEnum {
551 field: "caller_role",
552 value: code,
553 }),
554 }
555 }
556}
557
558impl From<RiskLevel> for proto::RiskLevel {
559 fn from(value: RiskLevel) -> Self {
560 match value {
561 RiskLevel::Low => proto::RiskLevel::Low,
562 RiskLevel::Medium => proto::RiskLevel::Medium,
563 RiskLevel::High => proto::RiskLevel::High,
564 }
565 }
566}
567
568impl TryFrom<proto::RiskLevel> for RiskLevel {
569 type Error = BridgeError;
570
571 fn try_from(value: proto::RiskLevel) -> Result<Self, Self::Error> {
572 match i32::from(value) {
573 1 => Ok(RiskLevel::Low),
574 2 => Ok(RiskLevel::Medium),
575 3 => Ok(RiskLevel::High),
576 code => Err(BridgeError::InvalidEnum {
577 field: "risk_level",
578 value: code,
579 }),
580 }
581 }
582}
583
584impl From<JobState> for proto::JobState {
585 fn from(value: JobState) -> Self {
586 match value {
587 JobState::Queued => proto::JobState::Queued,
588 JobState::Running => proto::JobState::Running,
589 JobState::Succeeded => proto::JobState::Succeeded,
590 JobState::Failed => proto::JobState::Failed,
591 JobState::Canceled => proto::JobState::Canceled,
592 JobState::RolledBack => proto::JobState::RolledBack,
593 JobState::NeedsReboot => proto::JobState::NeedsReboot,
594 }
595 }
596}
597
598impl TryFrom<proto::JobState> for JobState {
599 type Error = BridgeError;
600
601 fn try_from(value: proto::JobState) -> Result<Self, Self::Error> {
602 match i32::from(value) {
603 1 => Ok(JobState::Queued),
604 2 => Ok(JobState::Running),
605 3 => Ok(JobState::Succeeded),
606 4 => Ok(JobState::Failed),
607 5 => Ok(JobState::Canceled),
608 6 => Ok(JobState::RolledBack),
609 7 => Ok(JobState::NeedsReboot),
610 code => Err(BridgeError::InvalidEnum {
611 field: "job_state",
612 value: code,
613 }),
614 }
615 }
616}
617
618impl From<FailureCategory> for proto::FailureCategory {
619 fn from(value: FailureCategory) -> Self {
620 match value {
621 FailureCategory::ValidationFailure => proto::FailureCategory::ValidationFailure,
622 FailureCategory::AuthorizationFailure => proto::FailureCategory::AuthorizationFailure,
623 FailureCategory::PolicyDenied => proto::FailureCategory::PolicyDenied,
624 FailureCategory::StaleApproval => proto::FailureCategory::StaleApproval,
625 FailureCategory::ExecutionFailure => proto::FailureCategory::ExecutionFailure,
626 FailureCategory::TransientInfrastructureFailure => {
627 proto::FailureCategory::TransientInfrastructureFailure
628 }
629 FailureCategory::Cancellation => proto::FailureCategory::Cancellation,
630 FailureCategory::StuckExecution => proto::FailureCategory::StuckExecution,
631 FailureCategory::RebootRequired => proto::FailureCategory::RebootRequired,
632 FailureCategory::RollbackFailure => proto::FailureCategory::RollbackFailure,
633 }
634 }
635}
636
637impl TryFrom<proto::FailureCategory> for FailureCategory {
638 type Error = BridgeError;
639
640 fn try_from(value: proto::FailureCategory) -> Result<Self, Self::Error> {
641 match i32::from(value) {
642 1 => Ok(FailureCategory::ValidationFailure),
643 2 => Ok(FailureCategory::AuthorizationFailure),
644 3 => Ok(FailureCategory::PolicyDenied),
645 4 => Ok(FailureCategory::StaleApproval),
646 5 => Ok(FailureCategory::ExecutionFailure),
647 6 => Ok(FailureCategory::TransientInfrastructureFailure),
648 7 => Ok(FailureCategory::Cancellation),
649 8 => Ok(FailureCategory::StuckExecution),
650 9 => Ok(FailureCategory::RebootRequired),
651 10 => Ok(FailureCategory::RollbackFailure),
652 code => Err(BridgeError::InvalidEnum {
653 field: "failure_category",
654 value: code,
655 }),
656 }
657 }
658}
659
660impl From<RequestEnvelope> for proto::RequestEnvelope {
661 fn from(value: RequestEnvelope) -> Self {
662 Self {
663 action_name: value.action_name,
664 request_id: value.request_id,
665 params_json: serde_json::to_string(&value.params).expect("json serialization"),
666 caller_role: caller_role_code(value.caller_role),
667 request_hash: value.request_hash.into_inner(),
668 }
669 }
670}
671
672impl TryFrom<proto::RequestEnvelope> for RequestEnvelope {
673 type Error = BridgeError;
674
675 fn try_from(value: proto::RequestEnvelope) -> Result<Self, Self::Error> {
676 let params = serde_json::from_str(&value.params_json)
677 .map_err(|error| BridgeError::InvalidJson("params_json", error))?;
678
679 Ok(Self {
680 action_name: value.action_name,
681 request_id: value.request_id,
682 params,
683 caller_role: CallerRole::try_from(
684 proto::CallerRole::try_from(value.caller_role).map_err(|_| {
685 BridgeError::InvalidEnum {
686 field: "caller_role",
687 value: value.caller_role,
688 }
689 })?,
690 )?,
691 request_hash: RequestHash::from(value.request_hash),
692 })
693 }
694}
695
696impl From<PreviewEnvelope> for proto::PreviewEnvelope {
697 fn from(value: PreviewEnvelope) -> Self {
698 Self {
699 summary: value.summary,
700 risk_level: risk_level_code(value.risk_level),
701 current_state_json: serde_json::to_string(&value.current_state)
702 .expect("json serialization"),
703 proposed_change_json: serde_json::to_string(&value.proposed_change)
704 .expect("json serialization"),
705 expected_side_effects: value.expected_side_effects,
706 reboot_required: value.reboot_required,
707 rollback_available: value.rollback_available,
708 warnings: value.warnings,
709 request_hash: value.request_hash.into_inner(),
710 }
711 }
712}
713
714impl TryFrom<proto::PreviewEnvelope> for PreviewEnvelope {
715 type Error = BridgeError;
716
717 fn try_from(value: proto::PreviewEnvelope) -> Result<Self, Self::Error> {
718 let current_state = serde_json::from_str(&value.current_state_json)
719 .map_err(|error| BridgeError::InvalidJson("current_state_json", error))?;
720 let proposed_change = serde_json::from_str(&value.proposed_change_json)
721 .map_err(|error| BridgeError::InvalidJson("proposed_change_json", error))?;
722
723 Ok(Self {
724 summary: value.summary,
725 risk_level: RiskLevel::try_from(
726 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
727 BridgeError::InvalidEnum {
728 field: "risk_level",
729 value: value.risk_level,
730 }
731 })?,
732 )?,
733 current_state,
734 proposed_change,
735 expected_side_effects: value.expected_side_effects,
736 reboot_required: value.reboot_required,
737 rollback_available: value.rollback_available,
738 warnings: value.warnings,
739 request_hash: RequestHash::from(value.request_hash),
740 })
741 }
742}
743
744impl From<ResultEnvelope> for proto::ResultEnvelope {
745 fn from(value: ResultEnvelope) -> Self {
746 Self {
747 status: job_state_code(value.status),
748 summary: value.summary,
749 warnings: value.warnings,
750 job_id: value.job_id.unwrap_or_default(),
751 needs_reboot: value.needs_reboot,
752 rollback_ref: value.rollback_ref.unwrap_or_default(),
753 transaction_id: value.transaction_id,
754 }
755 }
756}
757
758impl TryFrom<proto::ResultEnvelope> for ResultEnvelope {
759 type Error = BridgeError;
760
761 fn try_from(value: proto::ResultEnvelope) -> Result<Self, Self::Error> {
762 Ok(Self {
763 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
764 BridgeError::InvalidEnum {
765 field: "job_state",
766 value: value.status,
767 }
768 })?)?,
769 summary: value.summary,
770 warnings: value.warnings,
771 job_id: if value.job_id.is_empty() {
772 None
773 } else {
774 Some(value.job_id)
775 },
776 needs_reboot: value.needs_reboot,
777 rollback_ref: if value.rollback_ref.is_empty() {
778 None
779 } else {
780 Some(value.rollback_ref)
781 },
782 transaction_id: value.transaction_id,
783 })
784 }
785}
786
787impl From<TransactionRecord> for proto::TransactionRecord {
788 fn from(value: TransactionRecord) -> Self {
789 Self {
790 transaction_id: value.transaction_id,
791 request_id: value.request_id,
792 request_hash: value.request_hash,
793 action_name: value.action_name,
794 risk_level: risk_level_code(value.risk_level),
795 status: job_state_code(value.status),
796 approval_id: value.approval_id.unwrap_or_default(),
797 summary: value.summary,
798 warnings: value.warnings,
799 }
800 }
801}
802
803impl TryFrom<proto::TransactionRecord> for TransactionRecord {
804 type Error = BridgeError;
805
806 fn try_from(value: proto::TransactionRecord) -> Result<Self, Self::Error> {
807 Ok(Self {
808 transaction_id: value.transaction_id,
809 request_id: value.request_id,
810 request_hash: value.request_hash,
811 action_name: value.action_name,
812 risk_level: RiskLevel::try_from(
813 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
814 BridgeError::InvalidEnum {
815 field: "risk_level",
816 value: value.risk_level,
817 }
818 })?,
819 )?,
820 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
821 BridgeError::InvalidEnum {
822 field: "job_state",
823 value: value.status,
824 }
825 })?)?,
826 approval_id: if value.approval_id.is_empty() {
827 None
828 } else {
829 Some(value.approval_id)
830 },
831 summary: value.summary,
832 warnings: value.warnings,
833 })
834 }
835}
836
837#[cfg(test)]
838mod request_hash_tests {
839 use super::*;
840
841 #[test]
842 fn serde_round_trips_via_transparent_string() {
843 let r = RequestHash::new("abc123".to_string());
846 let json = serde_json::to_string(&r).unwrap();
847 assert_eq!(json, "\"abc123\"");
848 let back: RequestHash = serde_json::from_str("\"abc123\"").unwrap();
849 assert_eq!(back, r);
850 }
851}
852
853#[cfg(test)]
854mod caller_role_ordering_tests {
855 use super::CallerRole;
856
857 #[test]
858 fn caller_role_orders_least_to_most_privileged() {
859 assert!(CallerRole::Observer < CallerRole::Dev);
862 assert!(CallerRole::Dev < CallerRole::Admin);
863 assert!(CallerRole::Admin < CallerRole::Boot);
864
865 assert!(CallerRole::Admin >= CallerRole::Dev);
867 assert!(!(CallerRole::Observer >= CallerRole::Dev));
868 }
869
870 #[test]
871 fn max_selects_the_higher_privilege() {
872 assert_eq!(
875 CallerRole::Observer.max(CallerRole::Admin),
876 CallerRole::Admin
877 );
878 assert_eq!(CallerRole::Boot.max(CallerRole::Dev), CallerRole::Boot);
879 }
880}