1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
//! Scoped sudoers.d management.
//!
//! `GetSudoGrants` lists the SysKnife-managed drop-ins (read-only). `GrantSudoAccess`
//! and `RevokeSudoAccess` create/remove a drop-in under `/etc/sudoers.d/` via the
//! root-owned helper `/usr/lib/sysknife/sudoers-edit`, which validates the rule
//! with `visudo -cf` on a temp file BEFORE installing it — so a malformed rule can
//! never land in `/etc/sudoers.d/` and break sudo. This is the highest-privilege
//! action family (it configures privilege escalation itself), hence Admin/High with
//! exact-approval previews.
use super::{command_mechanism, ActionSpec};
use sysknife_types::RiskLevel;
const HELPER: &str = "/usr/lib/sysknife/sudoers-edit";
pub fn specs() -> Vec<ActionSpec> {
vec![
get_sudo_grants(),
// `nopasswd: false` on purpose. This example is what the catalogue
// shows and what `docs/action-reference.md` publishes, and it used to
// read `--commands /usr/bin/systemctl --nopasswd`, which is a standing
// passwordless root credential: `systemctl edit` opens an editor,
// `systemctl link` takes an arbitrary unit path. The daemon refuses
// that combination now, so the published example was also an example
// that no longer runs. Keeping the realistic command and dropping the
// passwordless half is the accurate version of the same story.
grant_sudo_access(
"deploy-restart",
"deploy",
"/usr/bin/systemctl",
None,
false,
),
revoke_sudo_access("deploy-restart"),
]
}
/// List SysKnife-managed sudoers drop-ins (`sudoers-edit --op list`). Read-only.
///
/// Run directly (no `sudo`): reading `/etc/sudoers.d/` needs root and the daemon
/// already runs as root, matching the `GetLvmReport` pattern.
pub fn get_sudo_grants() -> ActionSpec {
ActionSpec {
action_name: "GetSudoGrants",
mechanism: command_mechanism(HELPER, ["--op", "list"]),
risk_level: RiskLevel::Low,
reboot_required: false,
rollback_available: false,
}
}
/// Grant a scoped sudo rule via the visudo-validated helper.
///
/// `commands` is either `"ALL"` or a comma-separated list of absolute command
/// paths. `runas` defaults to `root` when `None`.
pub fn grant_sudo_access(
name: &str,
user: &str,
commands: &str,
runas: Option<&str>,
nopasswd: bool,
) -> ActionSpec {
let mut args = vec![
HELPER.to_string(),
"--op".to_string(),
"grant".to_string(),
"--name".to_string(),
name.to_string(),
"--user".to_string(),
user.to_string(),
"--commands".to_string(),
commands.to_string(),
];
if let Some(r) = runas {
args.push("--runas".to_string());
args.push(r.to_string());
}
if nopasswd {
args.push("--nopasswd".to_string());
}
ActionSpec {
action_name: "GrantSudoAccess",
mechanism: command_mechanism("sudo", args),
risk_level: RiskLevel::High,
reboot_required: false,
rollback_available: false,
}
}
/// Remove a SysKnife-managed sudoers drop-in (`sudoers-edit --op revoke`).
pub fn revoke_sudo_access(name: &str) -> ActionSpec {
ActionSpec {
action_name: "RevokeSudoAccess",
mechanism: command_mechanism("sudo", [HELPER, "--op", "revoke", "--name", name]),
risk_level: RiskLevel::High,
reboot_required: false,
rollback_available: false,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::actions::ActionMechanism;
fn args_of(spec: &ActionSpec) -> (&'static str, Vec<String>) {
match &spec.mechanism {
ActionMechanism::Command { program, args } => (program, args.clone()),
other => panic!("expected Command, got {other:?}"),
}
}
#[test]
fn list_is_read_only_bare_helper() {
let (program, args) = args_of(&get_sudo_grants());
assert_eq!(program, HELPER);
assert_eq!(args, vec!["--op", "list"]);
assert_eq!(get_sudo_grants().risk_level, RiskLevel::Low);
}
#[test]
fn grant_builds_flags_including_optional_runas_and_nopasswd() {
let (program, args) = args_of(&grant_sudo_access(
"ci",
"ci",
"/usr/bin/systemctl",
Some("root"),
true,
));
assert_eq!(program, "sudo");
assert_eq!(
args,
vec![
HELPER,
"--op",
"grant",
"--name",
"ci",
"--user",
"ci",
"--commands",
"/usr/bin/systemctl",
"--runas",
"root",
"--nopasswd"
]
);
// no runas, no nopasswd
let (_, minimal) = args_of(&grant_sudo_access("ci", "ci", "ALL", None, false));
assert!(!minimal.iter().any(|a| a == "--runas" || a == "--nopasswd"));
assert_eq!(
grant_sudo_access("a", "b", "ALL", None, false).risk_level,
RiskLevel::High
);
}
#[test]
fn revoke_shape() {
let (program, args) = args_of(&revoke_sudo_access("ci"));
assert_eq!(program, "sudo");
assert_eq!(args, vec![HELPER, "--op", "revoke", "--name", "ci"]);
}
}