comtrya_lib/atoms/command/
exec.rs

1use 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        // Depending on the priviledged flag and who who the current user is
37        // we can determine if we need to prepend sudo to the command
38
39        let privilege_provider = self.privilege_provider.clone();
40
41        match (self.privileged, whoami::username().as_str()) {
42            // Hasn't requested priviledged, so never try to elevate
43            (false, _) => (self.command.clone(), self.arguments.clone()),
44
45            // Requested priviledged, but is already root
46            (true, "root") => (self.command.clone(), self.arguments.clone()),
47
48            // Requested priviledged, but is not root
49            (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            // Commands may have side-effects, but none that can be "known"
103            // without some sandboxed operations to detect filesystem and network
104            // affects.
105            // Maybe we'll look into this one day?
106            side_effects: vec![],
107            // Commands should always run, we have no cache-key based
108            // determinism atm the moment.
109            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 we require root, we need to use sudo with inherited IO
120        // to ensure the user can respond if prompted for a password
121        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}