1use std::collections::HashMap;
15
16use sysknife_types::{CallerRole, RiskLevel};
17
18use crate::auth::role_rank;
19
20pub fn min_role_for_action(action_name: &str) -> Option<CallerRole> {
31 let role = match action_name {
32 "GetSystemState"
34 | "CollectDiagnostics"
35 | "GetDeploymentHistory"
36 | "ListDeployments"
37 | "GetKernelArguments"
38 | "GetPendingUpdates"
39 | "ListFlatpakRemotes"
40 | "SearchFlatpakApps"
41 | "GetFlatpakAppInfo"
42 | "ListInstalledFlatpaks"
43 | "ListContainers"
44 | "GetContainerInfo"
45 | "GetLayeredPackages"
46 | "ListPackageRepositories"
47 | "ListServices"
48 | "GetServiceLogs"
49 | "GetServiceStatus"
50 | "GetServiceResourceLimits"
51 | "ListTimers"
52 | "ListToolboxes"
53 | "GetFirewallState"
54 | "GetNetworkStatus"
55 | "GetListeningPorts"
56 | "GetDiskUsage"
57 | "GetDateTime"
58 | "ListProcesses"
59 | "GetMemoryInfo"
60 | "GetJournalLog"
62 | "GetLvmReport"
64 | "GetSysctl"
66 | "GetMounts"
68 | "GetSudoGrants"
70 | "GetLogrotateStatus"
72 | "GetPasswordAging"
74 | "GetAuditRules"
76 | "GetCertificates"
77 | "GetAuthorizedKeys"
78 | "ListUsers"
79 | "ListGroups"
80 | "ListJobHistory"
81 | "AptUpdate"
86 | "AptSearch"
87 | "AptListInstalled"
88 | "AptShow"
89 | "AptListUpgradable"
90 | "AptHistoryList"
91 | "GetAptPins"
93 | "SnapList"
95 | "SnapInfo"
96 | "UfwStatus"
98 | "DistroboxList"
100 | "NetplanGetConfig"
102 | "GrubGetKargs"
104 | "CheckPendingReboot"
108 | "ResolvectlStatus"
110 | "AppArmorStatus"
112 | "CloudInitStatus"
114 | "UbuntuListFlatpaks"
116 | "Fail2banStatus"
118 | "ProStatus"
123 | "LivepatchStatus"
124 | "MultipassList" => CallerRole::Observer,
125
126 "InstallFlatpak"
132 | "RemoveFlatpak"
133 | "UpdateFlatpak"
134 | "AddFlatpakRemote"
135 | "RemoveFlatpakRemote"
136 | "CreateContainer"
137 | "StartContainer"
138 | "StopContainer"
139 | "RemoveContainer"
140 | "StartService"
141 | "StopService"
142 | "RestartService"
143 | "ReloadService"
144 | "ReloadDaemon"
145 | "SetServiceEnabled"
146 | "UnmaskService"
147 | "CreateToolbox"
148 | "RemoveToolbox"
149 | "SetHostname"
150 | "SetTimezone"
151 | "SetLocale"
152 | "SetNtp"
153 | "ConfigureWifi"
154 | "RemovePackageRepository"
155 | "EnablePackageRepository"
156 | "DisablePackageRepository"
157 | "AptAutoremove"
162 | "AptInstall"
163 | "AptRemove"
164 | "AptPurge"
165 | "AptHold"
166 | "AptUnhold"
167 | "SnapInstall"
169 | "SnapRemove"
170 | "SnapRefresh"
171 | "SnapHold"
172 | "SnapUnhold"
173 | "SnapRevert"
174 | "SnapClassicInstall"
175 | "DistroboxCreate"
177 | "DistroboxRemove"
178 | "AddPpa"
183 | "RemovePpa"
184 | "SetAptPin"
186 | "RemoveAptPin"
187 | "ConfigureLogRotation"
189 | "RemoveLogRotation"
190 | "UbuntuInstallFlatpak"
192 | "UbuntuRemoveFlatpak"
193 | "UbuntuUpdateFlatpak"
194 | "Fail2banUnbanIp"
198 | "NetplanGenerate"
203 | "VacuumJournal" => CallerRole::Dev,
209
210 "UpdateSystem"
221 | "PinDeployment"
222 | "UnpinDeployment"
223 | "RebaseSystem"
224 | "CleanupDeployments"
225 | "RebootSystem"
226 | "RollbackDeployment"
227 | "SetKernelArguments"
228 | "InstallPackages"
229 | "RemovePackages"
230 | "AddLayeredPackage"
231 | "RemoveLayeredPackage"
232 | "ReplaceLayeredPackage"
233 | "ResetLayeredPackageOverride"
234 | "RemoveBasePackage"
235 | "AddUserToGroup"
236 | "RemoveUserFromGroup"
237 | "CreateGroup"
238 | "DeleteGroup"
239 | "LockUserAccount"
240 | "UnlockUserAccount"
241 | "SignalProcess"
242 | "SetSshdOption"
245 | "ConfigureUnattendedUpgrades"
246 | "CreateScheduledJob"
248 | "ExtendLogicalVolume"
254 | "CreateLogicalVolume"
255 | "CreateLvSnapshot"
256 | "SetSysctl"
262 | "SetServiceResourceLimits"
266 | "AddMount"
273 | "RemoveMount"
274 | "AddSwap"
275 | "RemoveSwap"
276 | "GrantSudoAccess"
283 | "RevokeSudoAccess"
284 | "ConfigureRemoteSyslog"
289 | "RemoveRemoteSyslog"
290 | "SetPasswordAging"
292 | "SetPasswordPolicy"
293 | "SetAccountLockout"
294 | "AddAuditRule"
296 | "RemoveAuditRule"
297 | "ObtainCertificate"
298 | "RenewCertificates"
299 | "ConfigureFail2banJail"
300 | "DeleteUser"
301 | "AddAuthorizedKey"
302 | "RemoveAuthorizedKey"
303 | "CreateUser"
304 | "ConfigureFirewall"
305 | "MaskService"
306 | "AddPackageRepository"
307 | "SetDnsServers"
308 | "ResolvectlSetDns"
312 | "AptUpgrade"
318 | "UfwEnable"
323 | "UfwDisable"
324 | "UfwAllow"
325 | "UfwDeny"
326 | "UfwReset"
327 | "NetplanApply"
332 | "GrubSetKargs"
338 | "AppArmorEnforce"
347 | "AppArmorComplain"
348 | "Fail2banBanIp"
354 | "UfwDeleteRule"
362 | "UfwLimit"
363 | "NetplanSet"
368 | "ProAttach"
373 | "ProDetach"
374 | "EnableProService"
375 | "DisableProService"
376 | "UbuntuReleaseUpgrade" => CallerRole::Admin,
382
383 _ => return None,
384 };
385 Some(role)
386}
387
388pub fn action_allowed(caller: &CallerRole, action_name: &str) -> bool {
401 match min_role_for_action(action_name) {
402 Some(required) => role_rank(caller) >= role_rank(&required),
403 None => false,
404 }
405}
406
407#[derive(Clone, Debug, Default)]
421pub struct PolicyTable {
422 overrides: HashMap<String, CallerRole>,
424}
425
426#[derive(Debug, thiserror::Error, PartialEq, Eq)]
428pub enum PolicyValidationError {
429 #[error("unknown action in [policy.risk_overrides]: {action}")]
430 UnknownAction { action: String },
431
432 #[error(
433 "invalid risk level {value:?} for action {action}: \
434 must be \"Low\", \"Medium\", or \"High\""
435 )]
436 InvalidRiskLevel { action: String, value: String },
437
438 #[error(
439 "policy override for {action} would lower the minimum role from \
440 {baseline:?} to {attempted:?}; overrides may only raise"
441 )]
442 CannotLower {
443 action: String,
444 baseline: CallerRole,
445 attempted: CallerRole,
446 },
447}
448
449impl PolicyTable {
450 pub fn empty() -> Self {
453 Self {
454 overrides: HashMap::new(),
455 }
456 }
457
458 pub fn from_overrides(raw: &HashMap<String, String>) -> Result<Self, PolicyValidationError> {
468 let mut overrides = HashMap::with_capacity(raw.len());
469 for (action, level_str) in raw {
470 let baseline = min_role_for_action(action).ok_or_else(|| {
471 PolicyValidationError::UnknownAction {
472 action: action.clone(),
473 }
474 })?;
475
476 let level = parse_risk_level(level_str).ok_or_else(|| {
477 PolicyValidationError::InvalidRiskLevel {
478 action: action.clone(),
479 value: level_str.clone(),
480 }
481 })?;
482
483 let attempted = role_for_risk_level(level);
484
485 if role_rank(&attempted) < role_rank(&baseline) {
486 return Err(PolicyValidationError::CannotLower {
487 action: action.clone(),
488 baseline,
489 attempted,
490 });
491 }
492
493 overrides.insert(action.clone(), attempted);
494 }
495 Ok(Self { overrides })
496 }
497
498 pub fn min_role_for_action(&self, action_name: &str) -> Option<CallerRole> {
501 let baseline = min_role_for_action(action_name)?;
502 Some(self.overrides.get(action_name).copied().unwrap_or(baseline))
504 }
505
506 pub fn action_allowed(&self, caller: &CallerRole, action_name: &str) -> bool {
509 match self.min_role_for_action(action_name) {
510 Some(required) => role_rank(caller) >= role_rank(&required),
511 None => false,
512 }
513 }
514
515 pub fn active_overrides(&self) -> Vec<(&str, CallerRole)> {
518 let mut entries: Vec<(&str, CallerRole)> = self
519 .overrides
520 .iter()
521 .map(|(k, v)| (k.as_str(), *v))
522 .collect();
523 entries.sort_by_key(|(k, _)| *k);
524 entries
525 }
526
527 pub fn override_count(&self) -> usize {
529 self.overrides.len()
530 }
531}
532
533fn role_for_risk_level(level: RiskLevel) -> CallerRole {
538 match level {
539 RiskLevel::Low => CallerRole::Observer,
540 RiskLevel::Medium => CallerRole::Dev,
541 RiskLevel::High => CallerRole::Admin,
542 }
543}
544
545fn parse_risk_level(s: &str) -> Option<RiskLevel> {
547 match s.trim().to_ascii_lowercase().as_str() {
548 "low" => Some(RiskLevel::Low),
549 "medium" => Some(RiskLevel::Medium),
550 "high" => Some(RiskLevel::High),
551 _ => None,
552 }
553}
554
555#[cfg(test)]
560mod tests {
561 use super::*;
562
563 #[test]
568 fn observer_can_call_read_only_actions() {
569 let role = CallerRole::Observer;
570 assert!(action_allowed(&role, "GetSystemState"));
571 assert!(action_allowed(&role, "CollectDiagnostics"));
572 assert!(action_allowed(&role, "GetDeploymentHistory"));
573 assert!(action_allowed(&role, "ListDeployments"));
574 assert!(action_allowed(&role, "GetKernelArguments"));
575 assert!(action_allowed(&role, "ListFlatpakRemotes"));
576 assert!(action_allowed(&role, "SearchFlatpakApps"));
577 assert!(action_allowed(&role, "GetFlatpakAppInfo"));
578 assert!(action_allowed(&role, "ListContainers"));
579 assert!(action_allowed(&role, "GetContainerInfo"));
580 assert!(action_allowed(&role, "GetLayeredPackages"));
581 assert!(action_allowed(&role, "ListPackageRepositories"));
582 assert!(action_allowed(&role, "ListServices"));
583 assert!(action_allowed(&role, "GetServiceLogs"));
584 assert!(action_allowed(&role, "ListToolboxes"));
585 assert!(action_allowed(&role, "GetFirewallState"));
586 assert!(action_allowed(&role, "ListUsers"));
587 assert!(action_allowed(&role, "ListGroups"));
588 assert!(action_allowed(&role, "GetDiskUsage"));
589 assert!(action_allowed(&role, "ListProcesses"));
590 assert!(action_allowed(&role, "GetMemoryInfo"));
591 assert!(action_allowed(&role, "GetNetworkStatus"));
592 assert!(action_allowed(&role, "GetAuthorizedKeys"));
593 assert!(action_allowed(&role, "ListJobHistory"));
594 }
595
596 #[test]
601 fn observer_cannot_call_medium_or_high_risk_actions() {
602 let role = CallerRole::Observer;
603 assert!(!action_allowed(&role, "InstallFlatpak"));
605 assert!(!action_allowed(&role, "RemoveFlatpak"));
606 assert!(!action_allowed(&role, "CreateContainer"));
607 assert!(!action_allowed(&role, "StartService"));
608 assert!(!action_allowed(&role, "CreateToolbox"));
609 assert!(!action_allowed(&role, "SetHostname"));
610 assert!(!action_allowed(&role, "ConfigureWifi"));
611 assert!(!action_allowed(&role, "AddPackageRepository"));
612 assert!(!action_allowed(&role, "UpdateSystem"));
614 assert!(!action_allowed(&role, "RebaseSystem"));
615 assert!(!action_allowed(&role, "InstallPackages"));
616 assert!(!action_allowed(&role, "AddUserToGroup"));
617 assert!(!action_allowed(&role, "RebootSystem"));
618 assert!(!action_allowed(&role, "SetKernelArguments"));
619 }
620
621 #[test]
626 fn dev_can_call_medium_risk_actions() {
627 let role = CallerRole::Dev;
628 assert!(action_allowed(&role, "InstallFlatpak"));
630 assert!(action_allowed(&role, "RemoveFlatpak"));
631 assert!(action_allowed(&role, "UpdateFlatpak"));
632 assert!(action_allowed(&role, "AddFlatpakRemote"));
633 assert!(action_allowed(&role, "RemoveFlatpakRemote"));
634 assert!(action_allowed(&role, "CreateContainer"));
635 assert!(action_allowed(&role, "StartContainer"));
636 assert!(action_allowed(&role, "StopContainer"));
637 assert!(action_allowed(&role, "RemoveContainer"));
638 assert!(action_allowed(&role, "StartService"));
639 assert!(action_allowed(&role, "StopService"));
640 assert!(action_allowed(&role, "RestartService"));
641 assert!(action_allowed(&role, "ReloadService"));
642 assert!(action_allowed(&role, "ReloadDaemon"));
643 assert!(action_allowed(&role, "SetServiceEnabled"));
644 assert!(action_allowed(&role, "UnmaskService"));
645 assert!(action_allowed(&role, "CreateToolbox"));
646 assert!(action_allowed(&role, "RemoveToolbox"));
647 assert!(action_allowed(&role, "SetHostname"));
648 assert!(action_allowed(&role, "SetTimezone"));
649 assert!(action_allowed(&role, "SetLocale"));
650 assert!(action_allowed(&role, "SetNtp"));
651 assert!(action_allowed(&role, "ConfigureWifi"));
652 assert!(action_allowed(&role, "RemovePackageRepository"));
653 assert!(action_allowed(&role, "EnablePackageRepository"));
654 assert!(action_allowed(&role, "DisablePackageRepository"));
655 assert!(action_allowed(&role, "GetSystemState"));
657 assert!(action_allowed(&role, "ListServices"));
658 assert!(action_allowed(&role, "ListContainers"));
659 assert!(action_allowed(&role, "ListInstalledFlatpaks"));
660 assert!(action_allowed(&role, "GetPendingUpdates"));
661 assert!(action_allowed(&role, "GetServiceStatus"));
662 assert!(action_allowed(&role, "ListTimers"));
663 }
664
665 #[test]
670 fn dev_cannot_call_high_risk_actions() {
671 let role = CallerRole::Dev;
672 assert!(!action_allowed(&role, "UpdateSystem"));
673 assert!(!action_allowed(&role, "PinDeployment"));
674 assert!(!action_allowed(&role, "UnpinDeployment"));
675 assert!(!action_allowed(&role, "RebaseSystem"));
676 assert!(!action_allowed(&role, "CleanupDeployments"));
677 assert!(!action_allowed(&role, "RebootSystem"));
678 assert!(!action_allowed(&role, "RollbackDeployment"));
679 assert!(!action_allowed(&role, "SetKernelArguments"));
680 assert!(!action_allowed(&role, "InstallPackages"));
681 assert!(!action_allowed(&role, "RemovePackages"));
682 assert!(!action_allowed(&role, "AddLayeredPackage"));
683 assert!(!action_allowed(&role, "RemoveLayeredPackage"));
684 assert!(!action_allowed(&role, "ReplaceLayeredPackage"));
685 assert!(!action_allowed(&role, "ResetLayeredPackageOverride"));
686 assert!(!action_allowed(&role, "RemoveBasePackage"));
687 assert!(!action_allowed(&role, "AddUserToGroup"));
688 assert!(!action_allowed(&role, "RemoveUserFromGroup"));
689 assert!(!action_allowed(&role, "DeleteUser"));
691 assert!(!action_allowed(&role, "AddAuthorizedKey"));
692 assert!(!action_allowed(&role, "RemoveAuthorizedKey"));
693 assert!(!action_allowed(&role, "CreateUser"));
695 assert!(!action_allowed(&role, "ConfigureFirewall"));
696 assert!(!action_allowed(&role, "MaskService"));
697 assert!(!action_allowed(&role, "AddPackageRepository"));
698 assert!(!action_allowed(&role, "SetDnsServers"));
699 }
700
701 #[test]
706 fn admin_can_call_high_risk_actions() {
707 let role = CallerRole::Admin;
708 assert!(action_allowed(&role, "UpdateSystem"));
710 assert!(action_allowed(&role, "PinDeployment"));
711 assert!(action_allowed(&role, "UnpinDeployment"));
712 assert!(action_allowed(&role, "RebaseSystem"));
713 assert!(action_allowed(&role, "CleanupDeployments"));
714 assert!(action_allowed(&role, "RebootSystem"));
715 assert!(action_allowed(&role, "RollbackDeployment"));
716 assert!(action_allowed(&role, "SetKernelArguments"));
717 assert!(action_allowed(&role, "InstallPackages"));
718 assert!(action_allowed(&role, "RemovePackages"));
719 assert!(action_allowed(&role, "AddLayeredPackage"));
720 assert!(action_allowed(&role, "RemoveLayeredPackage"));
721 assert!(action_allowed(&role, "ReplaceLayeredPackage"));
722 assert!(action_allowed(&role, "ResetLayeredPackageOverride"));
723 assert!(action_allowed(&role, "RemoveBasePackage"));
724 assert!(action_allowed(&role, "AddUserToGroup"));
725 assert!(action_allowed(&role, "RemoveUserFromGroup"));
726 assert!(action_allowed(&role, "DeleteUser"));
727 assert!(action_allowed(&role, "AddAuthorizedKey"));
728 assert!(action_allowed(&role, "RemoveAuthorizedKey"));
729 assert!(action_allowed(&role, "CreateUser"));
731 assert!(action_allowed(&role, "ConfigureFirewall"));
732 assert!(action_allowed(&role, "MaskService"));
733 assert!(action_allowed(&role, "AddPackageRepository"));
734 assert!(action_allowed(&role, "SetDnsServers"));
735 assert!(action_allowed(&role, "InstallFlatpak"));
737 assert!(action_allowed(&role, "UpdateFlatpak"));
738 assert!(action_allowed(&role, "CreateToolbox"));
739 assert!(action_allowed(&role, "StartService"));
740 assert!(action_allowed(&role, "ReloadService"));
741 assert!(action_allowed(&role, "ReloadDaemon"));
742 assert!(action_allowed(&role, "GetSystemState"));
744 assert!(action_allowed(&role, "ListUsers"));
745 assert!(action_allowed(&role, "ListInstalledFlatpaks"));
746 assert!(action_allowed(&role, "GetPendingUpdates"));
747 assert!(action_allowed(&role, "GetServiceStatus"));
748 assert!(action_allowed(&role, "ListTimers"));
749 }
750
751 #[test]
756 fn boot_can_call_everything() {
757 let role = CallerRole::Boot;
758 assert!(action_allowed(&role, "GetSystemState"));
760 assert!(action_allowed(&role, "ListDeployments"));
761 assert!(action_allowed(&role, "ListContainers"));
762 assert!(action_allowed(&role, "GetFirewallState"));
763 assert!(action_allowed(&role, "ListInstalledFlatpaks"));
764 assert!(action_allowed(&role, "GetPendingUpdates"));
765 assert!(action_allowed(&role, "GetServiceStatus"));
766 assert!(action_allowed(&role, "ListTimers"));
767 assert!(action_allowed(&role, "InstallFlatpak"));
768 assert!(action_allowed(&role, "UpdateFlatpak"));
769 assert!(action_allowed(&role, "CreateToolbox"));
770 assert!(action_allowed(&role, "StartService"));
771 assert!(action_allowed(&role, "ReloadService"));
772 assert!(action_allowed(&role, "ReloadDaemon"));
773 assert!(action_allowed(&role, "SetHostname"));
774 assert!(action_allowed(&role, "ConfigureWifi"));
775 assert!(action_allowed(&role, "CreateUser"));
776 assert!(action_allowed(&role, "UpdateSystem"));
777 assert!(action_allowed(&role, "RebaseSystem"));
778 assert!(action_allowed(&role, "RebootSystem"));
779 assert!(action_allowed(&role, "InstallPackages"));
780 assert!(action_allowed(&role, "RemoveBasePackage"));
781 assert!(action_allowed(&role, "AddUserToGroup"));
782 assert!(action_allowed(&role, "RemoveUserFromGroup"));
783 assert!(action_allowed(&role, "DeleteUser"));
784 assert!(action_allowed(&role, "AddAuthorizedKey"));
785 assert!(action_allowed(&role, "RemoveAuthorizedKey"));
786 }
787
788 #[test]
800 fn dev_cannot_create_user() {
801 assert!(!action_allowed(&CallerRole::Dev, "CreateUser"));
802 }
803
804 #[test]
805 fn dev_cannot_configure_firewall() {
806 assert!(!action_allowed(&CallerRole::Dev, "ConfigureFirewall"));
807 }
808
809 #[test]
810 fn dev_cannot_mask_service() {
811 assert!(!action_allowed(&CallerRole::Dev, "MaskService"));
812 }
813
814 #[test]
815 fn dev_cannot_add_package_repository() {
816 assert!(!action_allowed(&CallerRole::Dev, "AddPackageRepository"));
817 }
818
819 #[test]
820 fn dev_cannot_set_dns_servers() {
821 assert!(!action_allowed(&CallerRole::Dev, "SetDnsServers"));
822 }
823
824 #[test]
825 fn admin_can_create_user() {
826 assert!(action_allowed(&CallerRole::Admin, "CreateUser"));
827 }
828
829 #[test]
830 fn admin_can_configure_firewall() {
831 assert!(action_allowed(&CallerRole::Admin, "ConfigureFirewall"));
832 }
833
834 #[test]
835 fn admin_can_mask_service() {
836 assert!(action_allowed(&CallerRole::Admin, "MaskService"));
837 }
838
839 #[test]
840 fn admin_can_add_package_repository() {
841 assert!(action_allowed(&CallerRole::Admin, "AddPackageRepository"));
842 }
843
844 #[test]
845 fn admin_can_set_dns_servers() {
846 assert!(action_allowed(&CallerRole::Admin, "SetDnsServers"));
847 }
848
849 #[test]
854 fn unknown_action_denied_for_all_roles() {
855 assert!(!action_allowed(&CallerRole::Observer, "NonExistent"));
856 assert!(!action_allowed(&CallerRole::Dev, "NonExistent"));
857 assert!(!action_allowed(&CallerRole::Admin, "NonExistent"));
858 assert!(!action_allowed(&CallerRole::Boot, "NonExistent"));
859 }
860
861 #[test]
866 fn empty_policy_table_matches_baseline() {
867 let table = PolicyTable::empty();
868 assert_eq!(
869 table.min_role_for_action("InstallFlatpak"),
870 Some(CallerRole::Dev)
871 );
872 assert_eq!(
873 table.min_role_for_action("UpdateSystem"),
874 Some(CallerRole::Admin)
875 );
876 assert!(table.action_allowed(&CallerRole::Dev, "InstallFlatpak"));
877 assert!(!table.action_allowed(&CallerRole::Dev, "UpdateSystem"));
878 assert_eq!(table.override_count(), 0);
879 }
880
881 #[test]
882 fn override_raises_minimum_role() {
883 let mut raw = HashMap::new();
884 raw.insert("InstallFlatpak".to_string(), "High".to_string());
885 let table = PolicyTable::from_overrides(&raw).expect("valid raise");
886
887 assert_eq!(
888 table.min_role_for_action("InstallFlatpak"),
889 Some(CallerRole::Admin)
890 );
891 assert!(!table.action_allowed(&CallerRole::Dev, "InstallFlatpak"));
893 assert!(table.action_allowed(&CallerRole::Admin, "InstallFlatpak"));
894 assert_eq!(
896 table.min_role_for_action("GetSystemState"),
897 Some(CallerRole::Observer)
898 );
899 }
900
901 #[test]
902 fn override_at_baseline_is_noop() {
903 let mut raw = HashMap::new();
905 raw.insert("GetSystemState".to_string(), "Low".to_string());
906 let table = PolicyTable::from_overrides(&raw).expect("same-level override");
907 assert_eq!(
908 table.min_role_for_action("GetSystemState"),
909 Some(CallerRole::Observer)
910 );
911 }
912
913 #[test]
914 fn override_below_baseline_is_rejected() {
915 let mut raw = HashMap::new();
917 raw.insert("UpdateSystem".to_string(), "Medium".to_string());
918 let err = PolicyTable::from_overrides(&raw).expect_err("downgrade rejected");
919 match err {
920 PolicyValidationError::CannotLower {
921 action,
922 baseline,
923 attempted,
924 } => {
925 assert_eq!(action, "UpdateSystem");
926 assert_eq!(baseline, CallerRole::Admin);
927 assert_eq!(attempted, CallerRole::Dev);
928 }
929 other => panic!("wrong error variant: {other:?}"),
930 }
931 }
932
933 #[test]
934 fn override_unknown_action_is_rejected() {
935 let mut raw = HashMap::new();
936 raw.insert("DefinitelyNotAnAction".to_string(), "High".to_string());
937 let err = PolicyTable::from_overrides(&raw).expect_err("unknown action rejected");
938 match err {
939 PolicyValidationError::UnknownAction { action } => {
940 assert_eq!(action, "DefinitelyNotAnAction");
941 }
942 other => panic!("wrong error variant: {other:?}"),
943 }
944 }
945
946 #[test]
947 fn invalid_risk_level_is_rejected() {
948 let mut raw = HashMap::new();
949 raw.insert("InstallFlatpak".to_string(), "Critical".to_string());
950 let err = PolicyTable::from_overrides(&raw).expect_err("invalid level rejected");
951 match err {
952 PolicyValidationError::InvalidRiskLevel { action, value } => {
953 assert_eq!(action, "InstallFlatpak");
954 assert_eq!(value, "Critical");
955 }
956 other => panic!("wrong error variant: {other:?}"),
957 }
958 }
959
960 #[test]
961 fn risk_level_parsing_is_case_insensitive() {
962 let mut raw = HashMap::new();
963 raw.insert("InstallFlatpak".to_string(), "high".to_string());
964 let table = PolicyTable::from_overrides(&raw).expect("lowercase accepted");
965 assert_eq!(
966 table.min_role_for_action("InstallFlatpak"),
967 Some(CallerRole::Admin)
968 );
969
970 let mut raw2 = HashMap::new();
971 raw2.insert("InstallFlatpak".to_string(), " HIGH ".to_string());
972 let table2 = PolicyTable::from_overrides(&raw2).expect("trim + uppercase");
973 assert_eq!(
974 table2.min_role_for_action("InstallFlatpak"),
975 Some(CallerRole::Admin)
976 );
977 }
978
979 #[test]
980 fn active_overrides_returns_sorted_entries() {
981 let mut raw = HashMap::new();
982 raw.insert("InstallFlatpak".to_string(), "High".to_string());
983 raw.insert("CreateContainer".to_string(), "High".to_string());
984 let table = PolicyTable::from_overrides(&raw).unwrap();
985
986 let active = table.active_overrides();
987 assert_eq!(active.len(), 2);
988 assert_eq!(active[0].0, "CreateContainer");
989 assert_eq!(active[0].1, CallerRole::Admin);
990 assert_eq!(active[1].0, "InstallFlatpak");
991 assert_eq!(active[1].1, CallerRole::Admin);
992 }
993
994 #[test]
995 fn unknown_action_denied_under_override_table_too() {
996 let table = PolicyTable::empty();
997 assert!(!table.action_allowed(&CallerRole::Admin, "NonExistent"));
998 assert_eq!(table.min_role_for_action("NonExistent"), None);
999 }
1000
1001 #[test]
1002 fn role_for_risk_level_mapping() {
1003 assert_eq!(role_for_risk_level(RiskLevel::Low), CallerRole::Observer);
1004 assert_eq!(role_for_risk_level(RiskLevel::Medium), CallerRole::Dev);
1005 assert_eq!(role_for_risk_level(RiskLevel::High), CallerRole::Admin);
1006 }
1007
1008 #[test]
1009 fn dns_and_apparmor_mutations_require_admin() {
1010 assert_eq!(
1015 min_role_for_action("ResolvectlSetDns"),
1016 Some(CallerRole::Admin)
1017 );
1018 assert_eq!(
1019 min_role_for_action("SetDnsServers"),
1020 min_role_for_action("ResolvectlSetDns"),
1021 "ResolvectlSetDns must match SetDnsServers"
1022 );
1023 assert_eq!(
1024 min_role_for_action("AppArmorComplain"),
1025 Some(CallerRole::Admin)
1026 );
1027 assert_eq!(
1028 min_role_for_action("AppArmorEnforce"),
1029 min_role_for_action("AppArmorComplain"),
1030 "AppArmorComplain must match AppArmorEnforce"
1031 );
1032 }
1033
1034 #[test]
1035 fn group_lifecycle_is_admin_and_listening_ports_is_observer() {
1036 assert_eq!(min_role_for_action("CreateGroup"), Some(CallerRole::Admin));
1039 assert_eq!(min_role_for_action("DeleteGroup"), Some(CallerRole::Admin));
1040 assert!(!action_allowed(&CallerRole::Dev, "CreateGroup"));
1041 assert!(action_allowed(&CallerRole::Admin, "DeleteGroup"));
1042 assert_eq!(
1044 min_role_for_action("GetListeningPorts"),
1045 Some(CallerRole::Observer)
1046 );
1047 assert!(action_allowed(&CallerRole::Observer, "GetListeningPorts"));
1048 }
1049
1050 #[test]
1051 fn process_and_account_control_require_admin() {
1052 for action in [
1053 "SignalProcess",
1054 "LockUserAccount",
1055 "UnlockUserAccount",
1056 "SetSshdOption",
1057 "ConfigureUnattendedUpgrades",
1058 "CreateScheduledJob",
1059 ] {
1060 assert_eq!(
1061 min_role_for_action(action),
1062 Some(CallerRole::Admin),
1063 "{action} must require Admin"
1064 );
1065 assert!(
1066 !action_allowed(&CallerRole::Dev, action),
1067 "{action} must reject Dev"
1068 );
1069 }
1070 }
1071}