change_user_run/
command.rs

1//! Running of commands as a different user.
2
3use std::{
4    collections::HashMap,
5    env::vars,
6    fmt::Display,
7    io::Write,
8    process::{Command, ExitStatus, Stdio},
9    str::from_utf8,
10};
11
12use log::debug;
13
14use crate::{Error, get_command};
15
16/// Data on a command that has been executed.
17///
18/// Tracks the command that has been executed, its stdout, stderr and status code.
19#[derive(Debug)]
20pub struct CommandOutput {
21    /// The command that has been executed.
22    pub command: String,
23
24    /// Status code of [`Command`].
25    pub status: ExitStatus,
26
27    /// Standard output of [`Command`].
28    pub stdout: String,
29
30    /// Standard error of [`Command`].
31    pub stderr: String,
32}
33
34impl Display for CommandOutput {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        writeln!(f, "{}", self.command)?;
37        writeln!(
38            f,
39            "⤷ status: {}",
40            self.status
41                .code()
42                .map(|code| code.to_string())
43                .unwrap_or("n/a".to_string())
44        )?;
45        writeln!(f, "⤷ stdout:\n{}", self.stdout)?;
46        writeln!(f, "⤷ stderr:\n{}", self.stderr)?;
47        Ok(())
48    }
49}
50
51/// Runs `command` with `command_args` and optional `command_input` as `user`.
52///
53/// Uses [runuser] to run the `command` as the specific `user`.
54///
55/// An `env_list` can be passed in to pass on a specific set of environment variables to the
56/// environment of the `user` when calling `command`.
57/// An optional [`HashMap`] of `envs` can be passed in to provide specific overrides of environment
58/// variables passed in to the environment of the `user` when calling `command`.
59///
60/// # Note
61///
62/// Running as another user is a privileged action which requires calling this function as root.
63///
64/// # Errors
65///
66/// Returns an error if
67///
68/// - [runuser] cannot be found,
69/// - `command` cannot be found,
70/// - `command` cannot be run in background,
71/// - `command_input` is provided, but stdin cannot be attached or written to,
72/// - `command` cannot be executed,
73/// - or a UTF-8 error occurred while converting stdout or stderr of the command to string.
74///
75/// # Examples
76///
77/// ```no_run
78/// use std::collections::HashMap;
79///
80/// use change_user_run::run_command_as_user;
81///
82/// # fn main() -> testresult::TestResult {
83/// // Run `whoami` as the user `test`.
84/// run_command_as_user("whoami", &[], None, &[], None, "test");
85///
86/// // Run `example` as the user `test`, pass in environment variables relevant for `cargo-llvm-cov`.
87/// // Here, we assume that `example` has been compiled with required coverage instrumentation.
88/// let env_list = [
89///     "LLVM_PROFILE_FILE",
90///     "CARGO_LLVM_COV",
91///     "CARGO_LLVM_COV_SHOW_ENV",
92///     "CARGO_LLVM_COV_TARGET_DIR",
93///     "RUSTFLAGS",
94///     "RUSTDOCFLAGS",
95/// ];
96/// let mut envs: HashMap<String, String> = HashMap::new();
97/// // Note: This instructs relevant .profraw data to be written to /tmp.
98/// envs.insert(
99///     "LLVM_PROFILE_FILE".to_string(),
100///     "/tmp/project-%p-%16m.profraw".to_string(),
101/// );
102/// run_command_as_user("example", &["--help"], None, &env_list, Some(envs), "test");
103/// # Ok(())
104/// # }
105/// ```
106///
107/// [runuser]: https://man.archlinux.org/man/runuser.1
108pub fn run_command_as_user(
109    cmd: &str,
110    cmd_args: &[&str],
111    cmd_input: Option<&[u8]>,
112    env_list: &[&str],
113    envs: Option<HashMap<String, String>>,
114    user: &str,
115) -> Result<CommandOutput, Error> {
116    let runuser_command = get_command("runuser")?;
117    debug!("Checking availability of command {cmd}");
118    get_command(cmd)?;
119
120    // Prepare environment variables to pass in.
121    let mut envs_to_pass: HashMap<String, String> = HashMap::new();
122    if !env_list.is_empty() {
123        for env_var in vars().filter(|(key, _)| env_list.contains(&key.as_str())) {
124            envs_to_pass.insert(env_var.0, env_var.1);
125        }
126    }
127    if let Some(envs) = envs {
128        envs_to_pass.extend(envs);
129    }
130
131    // Run command as user.
132    let mut command = Command::new(runuser_command);
133    command.arg("--user").arg(user);
134
135    // Allow and pass in the prepared environment variables.
136    if !envs_to_pass.is_empty() {
137        command
138            .arg(format!(
139                "--whitelist-environment={}",
140                envs_to_pass
141                    .keys()
142                    .map(|key| key.as_str())
143                    .collect::<Vec<&str>>()
144                    .join(",")
145            ))
146            .envs(&envs_to_pass);
147    }
148
149    // Add command (and arguments) to run as user.
150    command.arg("--").arg(cmd);
151    for cmd_arg in cmd_args {
152        command.arg(cmd_arg);
153    }
154
155    command
156        .stdout(Stdio::piped())
157        .stderr(Stdio::piped())
158        .stdin(if cmd_input.is_none() {
159            Stdio::null()
160        } else {
161            Stdio::piped()
162        });
163
164    debug!("Running command {command:?}");
165    let mut command_child = command.spawn().map_err(|source| Error::CommandBackground {
166        command: format!("{command:?}"),
167        source,
168    })?;
169
170    if let Some(input) = cmd_input {
171        command_child
172            .stdin
173            .take()
174            .ok_or(Error::CommandAttachToStdin {
175                command: format!("{command:?}"),
176            })?
177            .write_all(input)
178            .map_err(|source| Error::CommandWriteToStdin {
179                command: format!("{command:?}"),
180                source,
181            })?;
182    }
183
184    let command_output = command_child
185        .wait_with_output()
186        .map_err(|source| Error::CommandExec {
187            command: format!("{command:?}"),
188            source,
189        })?;
190
191    Ok(CommandOutput {
192        status: command_output.status,
193        stdout: from_utf8(&command_output.stdout)
194            .map_err(|source| Error::Utf8String {
195                context: format!("processing the stdout of {command:?}"),
196                source,
197            })?
198            .to_string(),
199        stderr: from_utf8(&command_output.stderr)
200            .map_err(|source| Error::Utf8String {
201                context: format!("processing the stderr of {command:?}"),
202                source,
203            })?
204            .to_string(),
205        command: format!("{command:?}"),
206    })
207}