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] = &[
65 "GetSystemState",
66 "CollectDiagnostics",
67 "GetDeploymentHistory",
68 "ListDeployments",
69 "UpdateSystem",
70 "CleanupDeployments",
71 "RebootSystem",
72 "RollbackDeployment",
73 "GetKernelArguments",
74 "PinDeployment",
75 "UnpinDeployment",
76 "RebaseSystem",
77 "SetKernelArguments",
78 "InstallFlatpak",
79 "RemoveFlatpak",
80 "UpdateFlatpak",
81 "SearchFlatpakApps",
82 "ListFlatpakRemotes",
83 "ListInstalledFlatpaks",
84 "AddFlatpakRemote",
85 "RemoveFlatpakRemote",
86 "GetFlatpakAppInfo",
87 "ListToolboxes",
88 "CreateToolbox",
89 "RemoveToolbox",
90 "GetLayeredPackages",
91 "ResetLayeredPackageOverride",
92 "GetPendingUpdates",
93 "InstallPackages",
94 "RemovePackages",
95 "AddLayeredPackage",
96 "RemoveLayeredPackage",
97 "ReplaceLayeredPackage",
98 "RemoveBasePackage",
99 "ListServices",
100 "ListTimers",
101 "ReloadDaemon",
102 "CreateScheduledJob",
103 "StartService",
104 "StopService",
105 "RestartService",
106 "ReloadService",
107 "SetServiceEnabled",
108 "MaskService",
109 "UnmaskService",
110 "GetServiceLogs",
111 "GetServiceStatus",
112 "GetServiceResourceLimits",
113 "SetServiceResourceLimits",
114 "GetFirewallState",
115 "GetNetworkStatus",
116 "GetListeningPorts",
117 "ConfigureWifi",
118 "SetDnsServers",
119 "ConfigureFirewall",
120 "GetDiskUsage",
121 "ListProcesses",
122 "SignalProcess",
123 "GetMemoryInfo",
124 "GetJournalLog",
126 "VacuumJournal",
127 "GetLvmReport",
129 "ExtendLogicalVolume",
130 "CreateLogicalVolume",
131 "CreateLvSnapshot",
132 "GetSysctl",
134 "SetSysctl",
135 "GetMounts",
137 "AddMount",
138 "RemoveMount",
139 "AddSwap",
140 "RemoveSwap",
141 "GetSudoGrants",
143 "GrantSudoAccess",
144 "RevokeSudoAccess",
145 "GetLogrotateStatus",
147 "ConfigureLogRotation",
148 "RemoveLogRotation",
149 "ConfigureRemoteSyslog",
150 "RemoveRemoteSyslog",
151 "GetPasswordAging",
153 "SetPasswordAging",
154 "SetPasswordPolicy",
155 "SetAccountLockout",
156 "GetAuditRules",
158 "AddAuditRule",
159 "RemoveAuditRule",
160 "GetCertificates",
162 "ObtainCertificate",
163 "RenewCertificates",
164 "GetDateTime",
165 "SetHostname",
166 "SetTimezone",
167 "SetLocale",
168 "SetNtp",
169 "ListPackageRepositories",
170 "AddPackageRepository",
171 "RemovePackageRepository",
172 "EnablePackageRepository",
173 "DisablePackageRepository",
174 "ListContainers",
175 "CreateContainer",
176 "StartContainer",
177 "StopContainer",
178 "RemoveContainer",
179 "GetContainerInfo",
180 "ListUsers",
181 "ListGroups",
182 "CreateUser",
183 "DeleteUser",
184 "AddUserToGroup",
185 "RemoveUserFromGroup",
186 "CreateGroup",
187 "DeleteGroup",
188 "LockUserAccount",
189 "UnlockUserAccount",
190 "GetAuthorizedKeys",
191 "AddAuthorizedKey",
192 "RemoveAuthorizedKey",
193 "SetSshdOption",
194 "ListJobHistory",
195 "AptUpdate",
197 "AptUpgrade",
198 "AptInstall",
199 "AptRemove",
200 "AptPurge",
201 "AptAutoremove",
202 "AptHold",
203 "AptUnhold",
204 "AptSearch",
205 "AptListInstalled",
206 "AptShow",
207 "AptListUpgradable",
208 "AptHistoryList",
209 "ConfigureUnattendedUpgrades",
210 "GetAptPins",
211 "SetAptPin",
212 "RemoveAptPin",
213 "AddPpa",
215 "RemovePpa",
216 "SnapInstall",
218 "SnapRemove",
219 "SnapRefresh",
220 "SnapHold",
221 "SnapUnhold",
222 "SnapList",
223 "SnapInfo",
224 "SnapRevert",
225 "SnapClassicInstall",
226 "UfwEnable",
228 "UfwDisable",
229 "UfwAllow",
230 "UfwDeny",
231 "UfwReset",
232 "UfwStatus",
233 "DistroboxList",
235 "DistroboxCreate",
236 "DistroboxRemove",
237 "NetplanGetConfig",
239 "NetplanApply",
240 "NetplanSet",
241 "NetplanGenerate",
242 "GrubGetKargs",
244 "GrubSetKargs",
245 "CheckPendingReboot",
247 "ResolvectlStatus",
249 "ResolvectlSetDns",
250 "AppArmorStatus",
252 "AppArmorEnforce",
253 "AppArmorComplain",
254 "CloudInitStatus",
256 "UbuntuInstallFlatpak",
258 "UbuntuRemoveFlatpak",
259 "UbuntuUpdateFlatpak",
260 "UbuntuListFlatpaks",
261 "Fail2banStatus",
263 "Fail2banBanIp",
264 "Fail2banUnbanIp",
265 "ConfigureFail2banJail",
266 "UbuntuReleaseUpgrade",
268 "ProStatus",
270 "ProAttach",
271 "ProDetach",
272 "EnableProService",
273 "DisableProService",
274 "LivepatchStatus",
276 "MultipassList",
278 "UfwDeleteRule",
280 "UfwLimit",
281];
282
283#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize)]
290#[serde(transparent)]
291pub struct ActionName(String);
292
293impl ActionName {
294 pub fn parse(name: impl Into<String>) -> Result<Self, UnknownActionName> {
296 let name = name.into();
297 if KNOWN_ACTION_NAMES.contains(&name.as_str()) {
298 Ok(Self(name))
299 } else {
300 Err(UnknownActionName(name))
301 }
302 }
303
304 pub fn as_str(&self) -> &str {
306 &self.0
307 }
308}
309
310impl std::fmt::Display for ActionName {
311 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
312 f.write_str(&self.0)
313 }
314}
315
316impl AsRef<str> for ActionName {
317 fn as_ref(&self) -> &str {
318 &self.0
319 }
320}
321
322#[derive(Clone, Debug, PartialEq, Eq)]
324pub struct UnknownActionName(pub String);
325
326impl std::fmt::Display for UnknownActionName {
327 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
328 write!(f, "unknown action name: '{}'", self.0)
329 }
330}
331
332impl std::error::Error for UnknownActionName {}
333
334#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
343#[serde(transparent)]
344pub struct RequestHash(String);
345
346impl RequestHash {
347 pub fn new(s: impl Into<String>) -> Self {
348 Self(s.into())
349 }
350 pub fn as_str(&self) -> &str {
351 &self.0
352 }
353 pub fn into_inner(self) -> String {
354 self.0
355 }
356}
357
358impl std::fmt::Display for RequestHash {
359 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
360 f.write_str(&self.0)
361 }
362}
363
364impl AsRef<str> for RequestHash {
365 fn as_ref(&self) -> &str {
366 &self.0
367 }
368}
369
370impl From<String> for RequestHash {
371 fn from(s: String) -> Self {
372 Self(s)
373 }
374}
375
376#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
377#[serde(rename_all = "snake_case")]
378pub enum CallerRole {
379 Observer,
380 Dev,
381 Admin,
382 Boot,
383}
384
385#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
386#[serde(rename_all = "snake_case")]
387pub enum RiskLevel {
388 Low,
389 Medium,
390 High,
391}
392
393#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
394#[serde(rename_all = "snake_case")]
395pub enum JobState {
396 Queued,
397 Running,
398 Succeeded,
399 Failed,
400 Canceled,
401 RolledBack,
402 NeedsReboot,
403}
404
405#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
412#[serde(rename_all = "snake_case")]
413pub enum FailureCategory {
414 ValidationFailure,
415 AuthorizationFailure,
416 PolicyDenied,
417 StaleApproval,
418 ExecutionFailure,
419 TransientInfrastructureFailure,
420 Cancellation,
421 StuckExecution,
422 RebootRequired,
423 RollbackFailure,
424}
425
426#[derive(Debug, thiserror::Error)]
427pub enum BridgeError {
428 #[error("invalid json payload for {0}")]
429 InvalidJson(&'static str, #[source] serde_json::Error),
430
431 #[error("invalid enum value {value} for {field}")]
432 InvalidEnum { field: &'static str, value: i32 },
433}
434
435#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
436pub struct RequestEnvelope {
437 pub action_name: String,
438 pub request_id: String,
439 pub params: Value,
440 pub caller_role: CallerRole,
441 pub request_hash: RequestHash,
442}
443
444#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
445pub struct PreviewEnvelope {
446 pub summary: String,
447 pub risk_level: RiskLevel,
448 pub current_state: Value,
449 pub proposed_change: Value,
450 pub expected_side_effects: Vec<String>,
451 pub reboot_required: bool,
452 pub rollback_available: bool,
453 pub warnings: Vec<String>,
454 pub request_hash: RequestHash,
455}
456
457#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
458pub struct ResultEnvelope {
459 pub status: JobState,
460 pub summary: String,
461 pub warnings: Vec<String>,
462 pub job_id: Option<String>,
463 pub needs_reboot: bool,
464 pub rollback_ref: Option<String>,
465 pub transaction_id: String,
466}
467
468#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
469pub struct TransactionRecord {
470 pub transaction_id: String,
471 pub request_id: String,
472 pub request_hash: String,
473 pub action_name: String,
474 pub risk_level: RiskLevel,
475 pub status: JobState,
476 pub approval_id: Option<String>,
477 pub summary: String,
478 pub warnings: Vec<String>,
479}
480
481fn caller_role_code(value: CallerRole) -> i32 {
482 match value {
483 CallerRole::Observer => 1,
484 CallerRole::Dev => 2,
485 CallerRole::Admin => 3,
486 CallerRole::Boot => 4,
487 }
488}
489
490fn risk_level_code(value: RiskLevel) -> i32 {
491 match value {
492 RiskLevel::Low => 1,
493 RiskLevel::Medium => 2,
494 RiskLevel::High => 3,
495 }
496}
497
498fn job_state_code(value: JobState) -> i32 {
499 match value {
500 JobState::Queued => 1,
501 JobState::Running => 2,
502 JobState::Succeeded => 3,
503 JobState::Failed => 4,
504 JobState::Canceled => 5,
505 JobState::RolledBack => 6,
506 JobState::NeedsReboot => 7,
507 }
508}
509
510fn failure_category_code(value: FailureCategory) -> i32 {
511 match value {
512 FailureCategory::ValidationFailure => 1,
513 FailureCategory::AuthorizationFailure => 2,
514 FailureCategory::PolicyDenied => 3,
515 FailureCategory::StaleApproval => 4,
516 FailureCategory::ExecutionFailure => 5,
517 FailureCategory::TransientInfrastructureFailure => 6,
518 FailureCategory::Cancellation => 7,
519 FailureCategory::StuckExecution => 8,
520 FailureCategory::RebootRequired => 9,
521 FailureCategory::RollbackFailure => 10,
522 }
523}
524
525impl From<CallerRole> for proto::CallerRole {
526 fn from(value: CallerRole) -> Self {
527 proto::CallerRole::try_from(caller_role_code(value)).expect("valid caller role")
528 }
529}
530
531impl TryFrom<proto::CallerRole> for CallerRole {
532 type Error = BridgeError;
533
534 fn try_from(value: proto::CallerRole) -> Result<Self, Self::Error> {
535 match i32::from(value) {
536 1 => Ok(CallerRole::Observer),
537 2 => Ok(CallerRole::Dev),
538 3 => Ok(CallerRole::Admin),
539 4 => Ok(CallerRole::Boot),
540 code => Err(BridgeError::InvalidEnum {
541 field: "caller_role",
542 value: code,
543 }),
544 }
545 }
546}
547
548impl From<RiskLevel> for proto::RiskLevel {
549 fn from(value: RiskLevel) -> Self {
550 proto::RiskLevel::try_from(risk_level_code(value)).expect("valid risk level")
551 }
552}
553
554impl TryFrom<proto::RiskLevel> for RiskLevel {
555 type Error = BridgeError;
556
557 fn try_from(value: proto::RiskLevel) -> Result<Self, Self::Error> {
558 match i32::from(value) {
559 1 => Ok(RiskLevel::Low),
560 2 => Ok(RiskLevel::Medium),
561 3 => Ok(RiskLevel::High),
562 code => Err(BridgeError::InvalidEnum {
563 field: "risk_level",
564 value: code,
565 }),
566 }
567 }
568}
569
570impl From<JobState> for proto::JobState {
571 fn from(value: JobState) -> Self {
572 proto::JobState::try_from(job_state_code(value)).expect("valid job state")
573 }
574}
575
576impl TryFrom<proto::JobState> for JobState {
577 type Error = BridgeError;
578
579 fn try_from(value: proto::JobState) -> Result<Self, Self::Error> {
580 match i32::from(value) {
581 1 => Ok(JobState::Queued),
582 2 => Ok(JobState::Running),
583 3 => Ok(JobState::Succeeded),
584 4 => Ok(JobState::Failed),
585 5 => Ok(JobState::Canceled),
586 6 => Ok(JobState::RolledBack),
587 7 => Ok(JobState::NeedsReboot),
588 code => Err(BridgeError::InvalidEnum {
589 field: "job_state",
590 value: code,
591 }),
592 }
593 }
594}
595
596impl From<FailureCategory> for proto::FailureCategory {
597 fn from(value: FailureCategory) -> Self {
598 proto::FailureCategory::try_from(failure_category_code(value))
599 .expect("valid failure category")
600 }
601}
602
603impl TryFrom<proto::FailureCategory> for FailureCategory {
604 type Error = BridgeError;
605
606 fn try_from(value: proto::FailureCategory) -> Result<Self, Self::Error> {
607 match i32::from(value) {
608 1 => Ok(FailureCategory::ValidationFailure),
609 2 => Ok(FailureCategory::AuthorizationFailure),
610 3 => Ok(FailureCategory::PolicyDenied),
611 4 => Ok(FailureCategory::StaleApproval),
612 5 => Ok(FailureCategory::ExecutionFailure),
613 6 => Ok(FailureCategory::TransientInfrastructureFailure),
614 7 => Ok(FailureCategory::Cancellation),
615 8 => Ok(FailureCategory::StuckExecution),
616 9 => Ok(FailureCategory::RebootRequired),
617 10 => Ok(FailureCategory::RollbackFailure),
618 code => Err(BridgeError::InvalidEnum {
619 field: "failure_category",
620 value: code,
621 }),
622 }
623 }
624}
625
626impl From<RequestEnvelope> for proto::RequestEnvelope {
627 fn from(value: RequestEnvelope) -> Self {
628 Self {
629 action_name: value.action_name,
630 request_id: value.request_id,
631 params_json: serde_json::to_string(&value.params).expect("json serialization"),
632 caller_role: caller_role_code(value.caller_role),
633 request_hash: value.request_hash.into_inner(),
634 }
635 }
636}
637
638impl TryFrom<proto::RequestEnvelope> for RequestEnvelope {
639 type Error = BridgeError;
640
641 fn try_from(value: proto::RequestEnvelope) -> Result<Self, Self::Error> {
642 let params = serde_json::from_str(&value.params_json)
643 .map_err(|error| BridgeError::InvalidJson("params_json", error))?;
644
645 Ok(Self {
646 action_name: value.action_name,
647 request_id: value.request_id,
648 params,
649 caller_role: CallerRole::try_from(
650 proto::CallerRole::try_from(value.caller_role).map_err(|_| {
651 BridgeError::InvalidEnum {
652 field: "caller_role",
653 value: value.caller_role,
654 }
655 })?,
656 )?,
657 request_hash: RequestHash::from(value.request_hash),
658 })
659 }
660}
661
662impl From<PreviewEnvelope> for proto::PreviewEnvelope {
663 fn from(value: PreviewEnvelope) -> Self {
664 Self {
665 summary: value.summary,
666 risk_level: risk_level_code(value.risk_level),
667 current_state_json: serde_json::to_string(&value.current_state)
668 .expect("json serialization"),
669 proposed_change_json: serde_json::to_string(&value.proposed_change)
670 .expect("json serialization"),
671 expected_side_effects: value.expected_side_effects,
672 reboot_required: value.reboot_required,
673 rollback_available: value.rollback_available,
674 warnings: value.warnings,
675 request_hash: value.request_hash.into_inner(),
676 }
677 }
678}
679
680impl TryFrom<proto::PreviewEnvelope> for PreviewEnvelope {
681 type Error = BridgeError;
682
683 fn try_from(value: proto::PreviewEnvelope) -> Result<Self, Self::Error> {
684 let current_state = serde_json::from_str(&value.current_state_json)
685 .map_err(|error| BridgeError::InvalidJson("current_state_json", error))?;
686 let proposed_change = serde_json::from_str(&value.proposed_change_json)
687 .map_err(|error| BridgeError::InvalidJson("proposed_change_json", error))?;
688
689 Ok(Self {
690 summary: value.summary,
691 risk_level: RiskLevel::try_from(
692 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
693 BridgeError::InvalidEnum {
694 field: "risk_level",
695 value: value.risk_level,
696 }
697 })?,
698 )?,
699 current_state,
700 proposed_change,
701 expected_side_effects: value.expected_side_effects,
702 reboot_required: value.reboot_required,
703 rollback_available: value.rollback_available,
704 warnings: value.warnings,
705 request_hash: RequestHash::from(value.request_hash),
706 })
707 }
708}
709
710impl From<ResultEnvelope> for proto::ResultEnvelope {
711 fn from(value: ResultEnvelope) -> Self {
712 Self {
713 status: job_state_code(value.status),
714 summary: value.summary,
715 warnings: value.warnings,
716 job_id: value.job_id.unwrap_or_default(),
717 needs_reboot: value.needs_reboot,
718 rollback_ref: value.rollback_ref.unwrap_or_default(),
719 transaction_id: value.transaction_id,
720 }
721 }
722}
723
724impl TryFrom<proto::ResultEnvelope> for ResultEnvelope {
725 type Error = BridgeError;
726
727 fn try_from(value: proto::ResultEnvelope) -> Result<Self, Self::Error> {
728 Ok(Self {
729 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
730 BridgeError::InvalidEnum {
731 field: "job_state",
732 value: value.status,
733 }
734 })?)?,
735 summary: value.summary,
736 warnings: value.warnings,
737 job_id: if value.job_id.is_empty() {
738 None
739 } else {
740 Some(value.job_id)
741 },
742 needs_reboot: value.needs_reboot,
743 rollback_ref: if value.rollback_ref.is_empty() {
744 None
745 } else {
746 Some(value.rollback_ref)
747 },
748 transaction_id: value.transaction_id,
749 })
750 }
751}
752
753impl From<TransactionRecord> for proto::TransactionRecord {
754 fn from(value: TransactionRecord) -> Self {
755 Self {
756 transaction_id: value.transaction_id,
757 request_id: value.request_id,
758 request_hash: value.request_hash,
759 action_name: value.action_name,
760 risk_level: risk_level_code(value.risk_level),
761 status: job_state_code(value.status),
762 approval_id: value.approval_id.unwrap_or_default(),
763 summary: value.summary,
764 warnings: value.warnings,
765 }
766 }
767}
768
769impl TryFrom<proto::TransactionRecord> for TransactionRecord {
770 type Error = BridgeError;
771
772 fn try_from(value: proto::TransactionRecord) -> Result<Self, Self::Error> {
773 Ok(Self {
774 transaction_id: value.transaction_id,
775 request_id: value.request_id,
776 request_hash: value.request_hash,
777 action_name: value.action_name,
778 risk_level: RiskLevel::try_from(
779 proto::RiskLevel::try_from(value.risk_level).map_err(|_| {
780 BridgeError::InvalidEnum {
781 field: "risk_level",
782 value: value.risk_level,
783 }
784 })?,
785 )?,
786 status: JobState::try_from(proto::JobState::try_from(value.status).map_err(|_| {
787 BridgeError::InvalidEnum {
788 field: "job_state",
789 value: value.status,
790 }
791 })?)?,
792 approval_id: if value.approval_id.is_empty() {
793 None
794 } else {
795 Some(value.approval_id)
796 },
797 summary: value.summary,
798 warnings: value.warnings,
799 })
800 }
801}
802
803#[cfg(test)]
804mod request_hash_tests {
805 use super::*;
806
807 #[test]
808 fn serde_round_trips_via_transparent_string() {
809 let r = RequestHash::new("abc123".to_string());
812 let json = serde_json::to_string(&r).unwrap();
813 assert_eq!(json, "\"abc123\"");
814 let back: RequestHash = serde_json::from_str("\"abc123\"").unwrap();
815 assert_eq!(back, r);
816 }
817}