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> {
37 if let Some(role) = role_exception(action_name) {
38 return Some(role);
39 }
40 crate::actions::spec_meta(action_name).map(|meta| role_for_risk_level(meta.risk_level))
41}
42
43fn role_exception(action_name: &str) -> Option<CallerRole> {
47 match action_name {
48 "ListJobHistory" => Some(CallerRole::Observer),
50 _ => None,
51 }
52}
53
54pub fn action_allowed(caller: &CallerRole, action_name: &str) -> bool {
67 match min_role_for_action(action_name) {
68 Some(required) => role_rank(caller) >= role_rank(&required),
69 None => false,
70 }
71}
72
73#[derive(Clone, Debug, Default)]
87pub struct PolicyTable {
88 overrides: HashMap<String, CallerRole>,
90}
91
92#[derive(Debug, thiserror::Error, PartialEq, Eq)]
94pub enum PolicyValidationError {
95 #[error("unknown action in [policy.risk_overrides]: {action}")]
96 UnknownAction { action: String },
97
98 #[error(
99 "invalid risk level {value:?} for action {action}: \
100 must be \"Low\", \"Medium\", or \"High\""
101 )]
102 InvalidRiskLevel { action: String, value: String },
103
104 #[error(
105 "policy override for {action} would lower the minimum role from \
106 {baseline:?} to {attempted:?}; overrides may only raise"
107 )]
108 CannotLower {
109 action: String,
110 baseline: CallerRole,
111 attempted: CallerRole,
112 },
113}
114
115impl PolicyTable {
116 pub fn empty() -> Self {
119 Self {
120 overrides: HashMap::new(),
121 }
122 }
123
124 pub fn from_overrides(raw: &HashMap<String, String>) -> Result<Self, PolicyValidationError> {
134 let mut overrides = HashMap::with_capacity(raw.len());
135 for (action, level_str) in raw {
136 let baseline = min_role_for_action(action).ok_or_else(|| {
137 PolicyValidationError::UnknownAction {
138 action: action.clone(),
139 }
140 })?;
141
142 let level = parse_risk_level(level_str).ok_or_else(|| {
143 PolicyValidationError::InvalidRiskLevel {
144 action: action.clone(),
145 value: level_str.clone(),
146 }
147 })?;
148
149 let attempted = role_for_risk_level(level);
150
151 if role_rank(&attempted) < role_rank(&baseline) {
152 return Err(PolicyValidationError::CannotLower {
153 action: action.clone(),
154 baseline,
155 attempted,
156 });
157 }
158
159 overrides.insert(action.clone(), attempted);
160 }
161 Ok(Self { overrides })
162 }
163
164 pub fn min_role_for_action(&self, action_name: &str) -> Option<CallerRole> {
167 let baseline = min_role_for_action(action_name)?;
168 Some(self.overrides.get(action_name).copied().unwrap_or(baseline))
170 }
171
172 pub fn action_allowed(&self, caller: &CallerRole, action_name: &str) -> bool {
175 match self.min_role_for_action(action_name) {
176 Some(required) => role_rank(caller) >= role_rank(&required),
177 None => false,
178 }
179 }
180
181 pub fn active_overrides(&self) -> Vec<(&str, CallerRole)> {
184 let mut entries: Vec<(&str, CallerRole)> = self
185 .overrides
186 .iter()
187 .map(|(k, v)| (k.as_str(), *v))
188 .collect();
189 entries.sort_by_key(|(k, _)| *k);
190 entries
191 }
192
193 pub fn override_count(&self) -> usize {
195 self.overrides.len()
196 }
197}
198
199pub fn role_for_risk_level(level: RiskLevel) -> CallerRole {
205 match level {
206 RiskLevel::Low => CallerRole::Observer,
207 RiskLevel::Medium => CallerRole::Dev,
208 RiskLevel::High => CallerRole::Admin,
209 }
210}
211
212fn parse_risk_level(s: &str) -> Option<RiskLevel> {
214 match s.trim().to_ascii_lowercase().as_str() {
215 "low" => Some(RiskLevel::Low),
216 "medium" => Some(RiskLevel::Medium),
217 "high" => Some(RiskLevel::High),
218 _ => None,
219 }
220}
221
222#[cfg(test)]
227mod tests {
228 use super::*;
229
230 #[test]
235 fn observer_can_call_read_only_actions() {
236 let role = CallerRole::Observer;
237 assert!(action_allowed(&role, "GetSystemState"));
238 assert!(action_allowed(&role, "CollectDiagnostics"));
239 assert!(action_allowed(&role, "GetDeploymentHistory"));
240 assert!(action_allowed(&role, "ListDeployments"));
241 assert!(action_allowed(&role, "GetKernelArguments"));
242 assert!(action_allowed(&role, "ListFlatpakRemotes"));
243 assert!(action_allowed(&role, "SearchFlatpakApps"));
244 assert!(action_allowed(&role, "GetFlatpakAppInfo"));
245 assert!(action_allowed(&role, "ListContainers"));
246 assert!(action_allowed(&role, "GetContainerInfo"));
247 assert!(action_allowed(&role, "GetLayeredPackages"));
248 assert!(action_allowed(&role, "ListPackageRepositories"));
249 assert!(action_allowed(&role, "ListServices"));
250 assert!(action_allowed(&role, "GetServiceLogs"));
251 assert!(action_allowed(&role, "ListToolboxes"));
252 assert!(action_allowed(&role, "GetFirewallState"));
253 assert!(action_allowed(&role, "ListUsers"));
254 assert!(action_allowed(&role, "ListGroups"));
255 assert!(action_allowed(&role, "GetDiskUsage"));
256 assert!(action_allowed(&role, "ListProcesses"));
257 assert!(action_allowed(&role, "GetMemoryInfo"));
258 assert!(action_allowed(&role, "GetNetworkStatus"));
259 assert!(action_allowed(&role, "GetAuthorizedKeys"));
260 assert!(action_allowed(&role, "ListJobHistory"));
261 }
262
263 #[test]
268 fn observer_cannot_call_medium_or_high_risk_actions() {
269 let role = CallerRole::Observer;
270 assert!(!action_allowed(&role, "InstallFlatpak"));
272 assert!(!action_allowed(&role, "RemoveFlatpak"));
273 assert!(!action_allowed(&role, "CreateContainer"));
274 assert!(!action_allowed(&role, "StartService"));
275 assert!(!action_allowed(&role, "CreateToolbox"));
276 assert!(!action_allowed(&role, "SetHostname"));
277 assert!(!action_allowed(&role, "ConfigureWifi"));
278 assert!(!action_allowed(&role, "AddPackageRepository"));
279 assert!(!action_allowed(&role, "UpdateSystem"));
281 assert!(!action_allowed(&role, "RebaseSystem"));
282 assert!(!action_allowed(&role, "InstallPackages"));
283 assert!(!action_allowed(&role, "AddUserToGroup"));
284 assert!(!action_allowed(&role, "RebootSystem"));
285 assert!(!action_allowed(&role, "SetKernelArguments"));
286 }
287
288 #[test]
293 fn dev_can_call_medium_risk_actions() {
294 let role = CallerRole::Dev;
295 assert!(action_allowed(&role, "InstallFlatpak"));
297 assert!(action_allowed(&role, "RemoveFlatpak"));
298 assert!(action_allowed(&role, "UpdateFlatpak"));
299 assert!(action_allowed(&role, "AddFlatpakRemote"));
300 assert!(action_allowed(&role, "RemoveFlatpakRemote"));
301 assert!(action_allowed(&role, "CreateContainer"));
302 assert!(action_allowed(&role, "StartContainer"));
303 assert!(action_allowed(&role, "StopContainer"));
304 assert!(action_allowed(&role, "RemoveContainer"));
305 assert!(action_allowed(&role, "StartService"));
306 assert!(action_allowed(&role, "StopService"));
307 assert!(action_allowed(&role, "RestartService"));
308 assert!(action_allowed(&role, "ReloadService"));
309 assert!(action_allowed(&role, "ReloadDaemon"));
310 assert!(action_allowed(&role, "SetServiceEnabled"));
311 assert!(action_allowed(&role, "UnmaskService"));
312 assert!(action_allowed(&role, "CreateToolbox"));
313 assert!(action_allowed(&role, "RemoveToolbox"));
314 assert!(action_allowed(&role, "SetHostname"));
315 assert!(action_allowed(&role, "SetTimezone"));
316 assert!(action_allowed(&role, "SetLocale"));
317 assert!(action_allowed(&role, "SetNtp"));
318 assert!(action_allowed(&role, "RemovePackageRepository"));
319 assert!(action_allowed(&role, "EnablePackageRepository"));
320 assert!(action_allowed(&role, "DisablePackageRepository"));
321 assert!(action_allowed(&role, "GetSystemState"));
323 assert!(action_allowed(&role, "ListServices"));
324 assert!(action_allowed(&role, "ListContainers"));
325 assert!(action_allowed(&role, "ListInstalledFlatpaks"));
326 assert!(action_allowed(&role, "GetPendingUpdates"));
327 assert!(action_allowed(&role, "GetServiceStatus"));
328 assert!(action_allowed(&role, "ListTimers"));
329 }
330
331 #[test]
336 fn dev_cannot_call_high_risk_actions() {
337 let role = CallerRole::Dev;
338 assert!(!action_allowed(&role, "UpdateSystem"));
339 assert!(!action_allowed(&role, "PinDeployment"));
340 assert!(!action_allowed(&role, "UnpinDeployment"));
341 assert!(!action_allowed(&role, "RebaseSystem"));
342 assert!(!action_allowed(&role, "CleanupDeployments"));
343 assert!(!action_allowed(&role, "RebootSystem"));
344 assert!(!action_allowed(&role, "RollbackDeployment"));
345 assert!(!action_allowed(&role, "SetKernelArguments"));
346 assert!(!action_allowed(&role, "InstallPackages"));
347 assert!(!action_allowed(&role, "RemovePackages"));
348 assert!(!action_allowed(&role, "AddLayeredPackage"));
349 assert!(!action_allowed(&role, "RemoveLayeredPackage"));
350 assert!(!action_allowed(&role, "ReplaceLayeredPackage"));
351 assert!(!action_allowed(&role, "ResetLayeredPackageOverride"));
352 assert!(!action_allowed(&role, "RemoveBasePackage"));
353 assert!(!action_allowed(&role, "AddUserToGroup"));
354 assert!(!action_allowed(&role, "RemoveUserFromGroup"));
355 assert!(!action_allowed(&role, "DeleteUser"));
357 assert!(!action_allowed(&role, "AddAuthorizedKey"));
358 assert!(!action_allowed(&role, "RemoveAuthorizedKey"));
359 assert!(!action_allowed(&role, "CreateUser"));
361 assert!(!action_allowed(&role, "ConfigureFirewall"));
362 assert!(!action_allowed(&role, "MaskService"));
363 assert!(!action_allowed(&role, "AddPackageRepository"));
364 assert!(!action_allowed(&role, "SetDnsServers"));
365 }
366
367 #[test]
372 fn admin_can_call_high_risk_actions() {
373 let role = CallerRole::Admin;
374 assert!(action_allowed(&role, "UpdateSystem"));
376 assert!(action_allowed(&role, "PinDeployment"));
377 assert!(action_allowed(&role, "UnpinDeployment"));
378 assert!(action_allowed(&role, "RebaseSystem"));
379 assert!(action_allowed(&role, "CleanupDeployments"));
380 assert!(action_allowed(&role, "RebootSystem"));
381 assert!(action_allowed(&role, "RollbackDeployment"));
382 assert!(action_allowed(&role, "SetKernelArguments"));
383 assert!(action_allowed(&role, "InstallPackages"));
384 assert!(action_allowed(&role, "RemovePackages"));
385 assert!(action_allowed(&role, "AddLayeredPackage"));
386 assert!(action_allowed(&role, "RemoveLayeredPackage"));
387 assert!(action_allowed(&role, "ReplaceLayeredPackage"));
388 assert!(action_allowed(&role, "ResetLayeredPackageOverride"));
389 assert!(action_allowed(&role, "RemoveBasePackage"));
390 assert!(action_allowed(&role, "AddUserToGroup"));
391 assert!(action_allowed(&role, "RemoveUserFromGroup"));
392 assert!(action_allowed(&role, "DeleteUser"));
393 assert!(action_allowed(&role, "AddAuthorizedKey"));
394 assert!(action_allowed(&role, "RemoveAuthorizedKey"));
395 assert!(action_allowed(&role, "CreateUser"));
397 assert!(action_allowed(&role, "ConfigureFirewall"));
398 assert!(action_allowed(&role, "MaskService"));
399 assert!(action_allowed(&role, "AddPackageRepository"));
400 assert!(action_allowed(&role, "SetDnsServers"));
401 assert!(action_allowed(&role, "InstallFlatpak"));
403 assert!(action_allowed(&role, "UpdateFlatpak"));
404 assert!(action_allowed(&role, "CreateToolbox"));
405 assert!(action_allowed(&role, "StartService"));
406 assert!(action_allowed(&role, "ReloadService"));
407 assert!(action_allowed(&role, "ReloadDaemon"));
408 assert!(action_allowed(&role, "GetSystemState"));
410 assert!(action_allowed(&role, "ListUsers"));
411 assert!(action_allowed(&role, "ListInstalledFlatpaks"));
412 assert!(action_allowed(&role, "GetPendingUpdates"));
413 assert!(action_allowed(&role, "GetServiceStatus"));
414 assert!(action_allowed(&role, "ListTimers"));
415 }
416
417 #[test]
422 fn boot_can_call_everything() {
423 let role = CallerRole::Boot;
424 assert!(action_allowed(&role, "GetSystemState"));
426 assert!(action_allowed(&role, "ListDeployments"));
427 assert!(action_allowed(&role, "ListContainers"));
428 assert!(action_allowed(&role, "GetFirewallState"));
429 assert!(action_allowed(&role, "ListInstalledFlatpaks"));
430 assert!(action_allowed(&role, "GetPendingUpdates"));
431 assert!(action_allowed(&role, "GetServiceStatus"));
432 assert!(action_allowed(&role, "ListTimers"));
433 assert!(action_allowed(&role, "InstallFlatpak"));
434 assert!(action_allowed(&role, "UpdateFlatpak"));
435 assert!(action_allowed(&role, "CreateToolbox"));
436 assert!(action_allowed(&role, "StartService"));
437 assert!(action_allowed(&role, "ReloadService"));
438 assert!(action_allowed(&role, "ReloadDaemon"));
439 assert!(action_allowed(&role, "SetHostname"));
440 assert!(action_allowed(&role, "ConfigureWifi"));
441 assert!(action_allowed(&role, "CreateUser"));
442 assert!(action_allowed(&role, "UpdateSystem"));
443 assert!(action_allowed(&role, "RebaseSystem"));
444 assert!(action_allowed(&role, "RebootSystem"));
445 assert!(action_allowed(&role, "InstallPackages"));
446 assert!(action_allowed(&role, "RemoveBasePackage"));
447 assert!(action_allowed(&role, "AddUserToGroup"));
448 assert!(action_allowed(&role, "RemoveUserFromGroup"));
449 assert!(action_allowed(&role, "DeleteUser"));
450 assert!(action_allowed(&role, "AddAuthorizedKey"));
451 assert!(action_allowed(&role, "RemoveAuthorizedKey"));
452 }
453
454 #[test]
466 fn dev_cannot_create_user() {
467 assert!(!action_allowed(&CallerRole::Dev, "CreateUser"));
468 }
469
470 #[test]
471 fn dev_cannot_configure_firewall() {
472 assert!(!action_allowed(&CallerRole::Dev, "ConfigureFirewall"));
473 }
474
475 #[test]
476 fn dev_cannot_mask_service() {
477 assert!(!action_allowed(&CallerRole::Dev, "MaskService"));
478 }
479
480 #[test]
481 fn dev_cannot_add_package_repository() {
482 assert!(!action_allowed(&CallerRole::Dev, "AddPackageRepository"));
483 }
484
485 #[test]
486 fn dev_cannot_set_dns_servers() {
487 assert!(!action_allowed(&CallerRole::Dev, "SetDnsServers"));
488 }
489
490 #[test]
491 fn admin_can_create_user() {
492 assert!(action_allowed(&CallerRole::Admin, "CreateUser"));
493 }
494
495 #[test]
496 fn admin_can_configure_firewall() {
497 assert!(action_allowed(&CallerRole::Admin, "ConfigureFirewall"));
498 }
499
500 #[test]
501 fn admin_can_mask_service() {
502 assert!(action_allowed(&CallerRole::Admin, "MaskService"));
503 }
504
505 #[test]
506 fn admin_can_add_package_repository() {
507 assert!(action_allowed(&CallerRole::Admin, "AddPackageRepository"));
508 }
509
510 #[test]
511 fn admin_can_set_dns_servers() {
512 assert!(action_allowed(&CallerRole::Admin, "SetDnsServers"));
513 }
514
515 #[test]
520 fn unknown_action_denied_for_all_roles() {
521 assert!(!action_allowed(&CallerRole::Observer, "NonExistent"));
522 assert!(!action_allowed(&CallerRole::Dev, "NonExistent"));
523 assert!(!action_allowed(&CallerRole::Admin, "NonExistent"));
524 assert!(!action_allowed(&CallerRole::Boot, "NonExistent"));
525 }
526
527 #[test]
532 fn empty_policy_table_matches_baseline() {
533 let table = PolicyTable::empty();
534 assert_eq!(
535 table.min_role_for_action("InstallFlatpak"),
536 Some(CallerRole::Dev)
537 );
538 assert_eq!(
539 table.min_role_for_action("UpdateSystem"),
540 Some(CallerRole::Admin)
541 );
542 assert!(table.action_allowed(&CallerRole::Dev, "InstallFlatpak"));
543 assert!(!table.action_allowed(&CallerRole::Dev, "UpdateSystem"));
544 assert_eq!(table.override_count(), 0);
545 }
546
547 #[test]
548 fn override_raises_minimum_role() {
549 let mut raw = HashMap::new();
550 raw.insert("InstallFlatpak".to_string(), "High".to_string());
551 let table = PolicyTable::from_overrides(&raw).expect("valid raise");
552
553 assert_eq!(
554 table.min_role_for_action("InstallFlatpak"),
555 Some(CallerRole::Admin)
556 );
557 assert!(!table.action_allowed(&CallerRole::Dev, "InstallFlatpak"));
559 assert!(table.action_allowed(&CallerRole::Admin, "InstallFlatpak"));
560 assert_eq!(
562 table.min_role_for_action("GetSystemState"),
563 Some(CallerRole::Observer)
564 );
565 }
566
567 #[test]
568 fn override_at_baseline_is_noop() {
569 let mut raw = HashMap::new();
571 raw.insert("GetSystemState".to_string(), "Low".to_string());
572 let table = PolicyTable::from_overrides(&raw).expect("same-level override");
573 assert_eq!(
574 table.min_role_for_action("GetSystemState"),
575 Some(CallerRole::Observer)
576 );
577 }
578
579 #[test]
580 fn override_below_baseline_is_rejected() {
581 let mut raw = HashMap::new();
583 raw.insert("UpdateSystem".to_string(), "Medium".to_string());
584 let err = PolicyTable::from_overrides(&raw).expect_err("downgrade rejected");
585 match err {
586 PolicyValidationError::CannotLower {
587 action,
588 baseline,
589 attempted,
590 } => {
591 assert_eq!(action, "UpdateSystem");
592 assert_eq!(baseline, CallerRole::Admin);
593 assert_eq!(attempted, CallerRole::Dev);
594 }
595 other => panic!("wrong error variant: {other:?}"),
596 }
597 }
598
599 #[test]
600 fn override_unknown_action_is_rejected() {
601 let mut raw = HashMap::new();
602 raw.insert("DefinitelyNotAnAction".to_string(), "High".to_string());
603 let err = PolicyTable::from_overrides(&raw).expect_err("unknown action rejected");
604 match err {
605 PolicyValidationError::UnknownAction { action } => {
606 assert_eq!(action, "DefinitelyNotAnAction");
607 }
608 other => panic!("wrong error variant: {other:?}"),
609 }
610 }
611
612 #[test]
613 fn invalid_risk_level_is_rejected() {
614 let mut raw = HashMap::new();
615 raw.insert("InstallFlatpak".to_string(), "Critical".to_string());
616 let err = PolicyTable::from_overrides(&raw).expect_err("invalid level rejected");
617 match err {
618 PolicyValidationError::InvalidRiskLevel { action, value } => {
619 assert_eq!(action, "InstallFlatpak");
620 assert_eq!(value, "Critical");
621 }
622 other => panic!("wrong error variant: {other:?}"),
623 }
624 }
625
626 #[test]
627 fn risk_level_parsing_is_case_insensitive() {
628 let mut raw = HashMap::new();
629 raw.insert("InstallFlatpak".to_string(), "high".to_string());
630 let table = PolicyTable::from_overrides(&raw).expect("lowercase accepted");
631 assert_eq!(
632 table.min_role_for_action("InstallFlatpak"),
633 Some(CallerRole::Admin)
634 );
635
636 let mut raw2 = HashMap::new();
637 raw2.insert("InstallFlatpak".to_string(), " HIGH ".to_string());
638 let table2 = PolicyTable::from_overrides(&raw2).expect("trim + uppercase");
639 assert_eq!(
640 table2.min_role_for_action("InstallFlatpak"),
641 Some(CallerRole::Admin)
642 );
643 }
644
645 #[test]
646 fn active_overrides_returns_sorted_entries() {
647 let mut raw = HashMap::new();
648 raw.insert("InstallFlatpak".to_string(), "High".to_string());
649 raw.insert("CreateContainer".to_string(), "High".to_string());
650 let table = PolicyTable::from_overrides(&raw).unwrap();
651
652 let active = table.active_overrides();
653 assert_eq!(active.len(), 2);
654 assert_eq!(active[0].0, "CreateContainer");
655 assert_eq!(active[0].1, CallerRole::Admin);
656 assert_eq!(active[1].0, "InstallFlatpak");
657 assert_eq!(active[1].1, CallerRole::Admin);
658 }
659
660 #[test]
661 fn unknown_action_denied_under_override_table_too() {
662 let table = PolicyTable::empty();
663 assert!(!table.action_allowed(&CallerRole::Admin, "NonExistent"));
664 assert_eq!(table.min_role_for_action("NonExistent"), None);
665 }
666
667 #[test]
668 fn role_for_risk_level_mapping() {
669 assert_eq!(role_for_risk_level(RiskLevel::Low), CallerRole::Observer);
670 assert_eq!(role_for_risk_level(RiskLevel::Medium), CallerRole::Dev);
671 assert_eq!(role_for_risk_level(RiskLevel::High), CallerRole::Admin);
672 }
673
674 #[test]
675 fn dns_and_apparmor_mutations_require_admin() {
676 assert_eq!(
681 min_role_for_action("ResolvectlSetDns"),
682 Some(CallerRole::Admin)
683 );
684 assert_eq!(
685 min_role_for_action("SetDnsServers"),
686 min_role_for_action("ResolvectlSetDns"),
687 "ResolvectlSetDns must match SetDnsServers"
688 );
689 assert_eq!(
690 min_role_for_action("AppArmorComplain"),
691 Some(CallerRole::Admin)
692 );
693 assert_eq!(
694 min_role_for_action("AppArmorEnforce"),
695 min_role_for_action("AppArmorComplain"),
696 "AppArmorComplain must match AppArmorEnforce"
697 );
698 }
699
700 #[test]
701 fn group_create_is_dev_delete_is_admin_and_listening_ports_is_observer() {
702 assert_eq!(min_role_for_action("CreateGroup"), Some(CallerRole::Dev));
705 assert_eq!(min_role_for_action("DeleteGroup"), Some(CallerRole::Admin));
706 assert!(action_allowed(&CallerRole::Dev, "CreateGroup"));
707 assert!(!action_allowed(&CallerRole::Dev, "DeleteGroup"));
708 assert!(action_allowed(&CallerRole::Admin, "DeleteGroup"));
709 assert_eq!(
711 min_role_for_action("GetListeningPorts"),
712 Some(CallerRole::Observer)
713 );
714 assert!(action_allowed(&CallerRole::Observer, "GetListeningPorts"));
715 }
716
717 #[test]
718 fn process_and_account_control_require_admin() {
719 for action in [
720 "SignalProcess",
721 "LockUserAccount",
722 "UnlockUserAccount",
723 "SetSshdOption",
724 "ConfigureUnattendedUpgrades",
725 "CreateScheduledJob",
726 ] {
727 assert_eq!(
728 min_role_for_action(action),
729 Some(CallerRole::Admin),
730 "{action} must require Admin"
731 );
732 assert!(
733 !action_allowed(&CallerRole::Dev, action),
734 "{action} must reject Dev"
735 );
736 }
737 }
738}