comtrya_lib/atoms/command/
exec.rs1use crate::atoms::Outcome;
2
3use super::super::Atom;
4use crate::utilities;
5use anyhow::anyhow;
6use tracing::debug;
7
8#[derive(Default)]
9pub struct Exec {
10 pub command: String,
11 pub arguments: Vec<String>,
12 pub working_dir: Option<String>,
13 pub environment: Vec<(String, String)>,
14 pub privileged: bool,
15 pub privilege_provider: String,
16 pub(crate) status: ExecStatus,
17}
18
19#[derive(Default)]
20pub(crate) struct ExecStatus {
21 code: i32,
22 stdout: String,
23 stderr: String,
24}
25
26#[allow(dead_code)]
27pub fn new_run_command(command: String) -> Exec {
28 Exec {
29 command,
30 ..Default::default()
31 }
32}
33
34impl Exec {
35 fn elevate_if_required(&self) -> (String, Vec<String>) {
36 let privilege_provider = self.privilege_provider.clone();
40
41 match (self.privileged, whoami::username().as_str()) {
42 (false, _) => (self.command.clone(), self.arguments.clone()),
44
45 (true, "root") => (self.command.clone(), self.arguments.clone()),
47
48 (true, _) => (
50 privilege_provider,
51 [vec![self.command.clone()], self.arguments.clone()].concat(),
52 ),
53 }
54 }
55
56 fn elevate(&mut self) -> anyhow::Result<()> {
57 tracing::info!(
58 "Privilege elevation required to run `{} {}`. Validating privileges ...",
59 &self.command,
60 &self.arguments.join(" ")
61 );
62
63 let privilege_provider = utilities::get_binary_path(&self.privilege_provider)?;
64
65 match std::process::Command::new(privilege_provider)
66 .stdin(std::process::Stdio::inherit())
67 .stdout(std::process::Stdio::inherit())
68 .stderr(std::process::Stdio::inherit())
69 .arg("--validate")
70 .output()
71 {
72 Ok(std::process::Output { status, .. }) if status.success() => Ok(()),
73
74 Ok(std::process::Output { stderr, .. }) => Err(anyhow!(
75 "Command requires privilege escalation, but couldn't elevate privileges: {}",
76 String::from_utf8(stderr)?
77 )),
78
79 Err(err) => Err(anyhow!(
80 "Command requires privilege escalation, but couldn't elevate privileges: {}",
81 err
82 )),
83 }
84 }
85}
86
87impl std::fmt::Display for Exec {
88 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
89 write!(
90 f,
91 "CommandExec with: privileged={}: {} {}",
92 self.privileged,
93 self.command,
94 self.arguments.join(" ")
95 )
96 }
97}
98
99impl Atom for Exec {
100 fn plan(&self) -> anyhow::Result<Outcome> {
101 Ok(Outcome {
102 side_effects: vec![],
107 should_run: true,
110 })
111 }
112
113 fn execute(&mut self) -> anyhow::Result<()> {
114 let (command, arguments) = self.elevate_if_required();
115
116 let command = utilities::get_binary_path(&command)
117 .or_else(|_| Err(anyhow!("Command `{}` not found in path", command)))?;
118
119 if command.eq("doas") || command.eq("sudo") || command.eq("run0") {
122 match self.elevate() {
123 Ok(_) => (),
124 Err(err) => {
125 return Err(anyhow!(err));
126 }
127 }
128 }
129
130 match std::process::Command::new(&command)
131 .envs(self.environment.clone())
132 .args(&arguments)
133 .current_dir(&self.working_dir.clone().unwrap_or_else(|| {
134 std::env::current_dir()
135 .map(|current_dir| current_dir.display().to_string())
136 .expect("Failed to get current directory")
137 }))
138 .output()
139 {
140 Ok(output) if output.status.success() => {
141 self.status.stdout = String::from_utf8(output.stdout)?;
142 self.status.stderr = String::from_utf8(output.stderr)?;
143
144 debug!("stdout: {}", &self.status.stdout);
145
146 Ok(())
147 }
148
149 Ok(output) => {
150 self.status.stdout = String::from_utf8(output.stdout)?;
151 self.status.stderr = String::from_utf8(output.stderr)?;
152
153 debug!("exit code: {}", &self.status.code);
154 debug!("stdout: {}", &self.status.stdout);
155 debug!("stderr: {}", &self.status.stderr);
156
157 Err(anyhow!(
158 "Command failed with exit code: {}",
159 output.status.code().unwrap_or(1)
160 ))
161 }
162
163 Err(err) => Err(anyhow!(err)),
164 }
165 }
166
167 fn output_string(&self) -> String {
168 self.status.stdout.clone()
169 }
170
171 fn error_message(&self) -> String {
172 self.status.stderr.clone()
173 }
174}
175
176#[cfg(test)]
177mod tests {
178 use super::*;
179 use crate::contexts::privilege::Privilege;
180 use pretty_assertions::assert_eq;
181
182 #[test]
183 fn defaults() {
184 let command_run = Exec {
185 ..Default::default()
186 };
187
188 assert_eq!(String::from(""), command_run.command);
189 assert_eq!(0, command_run.arguments.len());
190 assert_eq!(None, command_run.working_dir);
191 assert_eq!(0, command_run.environment.len());
192 assert_eq!(false, command_run.privileged);
193
194 let command_run = new_run_command(String::from("echo"));
195
196 assert_eq!(String::from("echo"), command_run.command);
197 assert_eq!(0, command_run.arguments.len());
198 assert_eq!(None, command_run.working_dir);
199 assert_eq!(0, command_run.environment.len());
200 assert_eq!(false, command_run.privileged);
201 }
202
203 #[test]
204 fn elevate() {
205 let mut command_run = new_run_command(String::from("echo"));
206 command_run.arguments = vec![String::from("Hello, world!")];
207 let (command, args) = command_run.elevate_if_required();
208
209 assert_eq!(String::from("echo"), command);
210 assert_eq!(vec![String::from("Hello, world!")], args);
211
212 let mut command_run = new_run_command(String::from("echo"));
213 command_run.arguments = vec![String::from("Hello, world!")];
214 command_run.privileged = true;
215 command_run.privilege_provider = Privilege::Sudo.to_string();
216 let (command, args) = command_run.elevate_if_required();
217
218 assert_eq!(String::from("sudo"), command);
219 assert_eq!(
220 vec![String::from("echo"), String::from("Hello, world!")],
221 args
222 );
223 }
224
225 #[test]
226 fn elevate_doas() {
227 let mut command_run = new_run_command(String::from("echo"));
228 command_run.arguments = vec![String::from("Hello, world!")];
229 let (command, args) = command_run.elevate_if_required();
230
231 assert_eq!(String::from("echo"), command);
232 assert_eq!(vec![String::from("Hello, world!")], args);
233
234 let mut command_run = new_run_command(String::from("echo"));
235 command_run.arguments = vec![String::from("Hello, world!")];
236 command_run.privileged = true;
237 command_run.privilege_provider = Privilege::Doas.to_string();
238 let (command, args) = command_run.elevate_if_required();
239
240 assert_eq!(String::from("doas"), command);
241 assert_eq!(
242 vec![String::from("echo"), String::from("Hello, world!")],
243 args
244 );
245 }
246 #[test]
247 fn elevate_run0() {
248 let mut command_run = new_run_command(String::from("echo"));
249 command_run.arguments = vec![String::from("Hello, world!")];
250 let (command, args) = command_run.elevate_if_required();
251
252 assert_eq!(String::from("echo"), command);
253 assert_eq!(vec![String::from("Hello, world!")], args);
254
255 let mut command_run = new_run_command(String::from("echo"));
256 command_run.arguments = vec![String::from("Hello, world!")];
257 command_run.privileged = true;
258 command_run.privilege_provider = Privilege::Run0.to_string();
259 let (command, args) = command_run.elevate_if_required();
260
261 assert_eq!(String::from("run0"), command);
262 assert_eq!(
263 vec![String::from("echo"), String::from("Hello, world!")],
264 args
265 );
266 }
267
268 #[test]
269 fn error_propagation() {
270 let mut command_run = new_run_command(String::from("non-existant-command"));
271 command_run.execute().expect_err("Command should fail");
272 }
273}