Skip to content

Commit 55846ef

Browse files
authored
Implement --preserve-env-var=VAR (#1167)
2 parents c5b334c + b20da66 commit 55846ef

File tree

5 files changed

+123
-91
lines changed

5 files changed

+123
-91
lines changed

src/sudo/cli/mod.rs

Lines changed: 12 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -355,7 +355,7 @@ pub struct SudoRunOptions {
355355
// -B
356356
pub bell: bool,
357357
// -E
358-
pub preserve_env: PreserveEnv,
358+
/* ignored, part of env_var_list */
359359
// -k
360360
pub reset_timestamp: bool,
361361
// -n
@@ -384,7 +384,6 @@ impl TryFrom<SudoOptions> for SudoRunOptions {
384384

385385
fn try_from(mut opts: SudoOptions) -> Result<Self, Self::Error> {
386386
let bell = mem::take(&mut opts.bell);
387-
let preserve_env = mem::take(&mut opts.preserve_env);
388387
let reset_timestamp = mem::take(&mut opts.reset_timestamp);
389388
let non_interactive = mem::take(&mut opts.non_interactive);
390389
let stdin = mem::take(&mut opts.stdin);
@@ -424,7 +423,6 @@ impl TryFrom<SudoOptions> for SudoRunOptions {
424423

425424
Ok(Self {
426425
bell,
427-
preserve_env,
428426
reset_timestamp,
429427
non_interactive,
430428
stdin,
@@ -455,7 +453,7 @@ struct SudoOptions {
455453
// -U
456454
other_user: Option<SudoString>,
457455
// -E
458-
preserve_env: PreserveEnv,
456+
/* ignored, part of env_var_list */
459457
// -s
460458
shell: bool,
461459
// -S
@@ -488,29 +486,6 @@ struct SudoOptions {
488486
positional_args: Vec<String>,
489487
}
490488

491-
#[derive(Default, Debug, Clone, PartialEq)]
492-
pub enum PreserveEnv {
493-
#[default]
494-
Nothing,
495-
Everything,
496-
Only(Vec<String>),
497-
}
498-
499-
impl PreserveEnv {
500-
#[cfg(test)]
501-
pub fn try_into_only(self) -> Result<Vec<String>, Self> {
502-
if let Self::Only(v) = self {
503-
Ok(v)
504-
} else {
505-
Err(self)
506-
}
507-
}
508-
509-
pub fn is_nothing(&self) -> bool {
510-
matches!(self, Self::Nothing)
511-
}
512-
}
513-
514489
#[derive(Debug, Clone, PartialEq)]
515490
pub enum List {
516491
Once,
@@ -666,7 +641,9 @@ impl SudoOptions {
666641
options.bell = true;
667642
}
668643
"-E" | "--preserve-env" => {
669-
options.preserve_env = PreserveEnv::Everything;
644+
eprintln_ignore_io_error!(
645+
"warning: preserving the entire environment is not supported, `{flag}` is ignored"
646+
)
670647
}
671648
"-e" | "--edit" => {
672649
options.edit = true;
@@ -716,15 +693,13 @@ impl SudoOptions {
716693
options.chdir = Some(SudoPath::from_cli_string(value));
717694
}
718695
"-E" | "--preserve-env" => {
719-
let split_value = || value.split(',').map(str::to_string);
720-
match &mut options.preserve_env {
721-
PreserveEnv::Nothing => {
722-
options.preserve_env = PreserveEnv::Only(split_value().collect())
723-
}
724-
PreserveEnv::Everything => {}
725-
PreserveEnv::Only(list) => list.extend(split_value()),
726-
}
727-
// options.preserve_env = value.split(',').map(str::to_string).collect()
696+
options
697+
.env_var_list
698+
.extend(value.split(',').filter_map(|var| {
699+
std::env::var(var)
700+
.ok()
701+
.map(|value| (var.to_string(), value))
702+
}));
728703
}
729704
"-g" | "--group" => {
730705
options.group = Some(SudoString::from_cli_string(value));
@@ -787,12 +762,6 @@ impl<T> IsAbsent for Vec<T> {
787762
}
788763
}
789764

790-
impl IsAbsent for PreserveEnv {
791-
fn is_absent(&self) -> bool {
792-
self.is_nothing()
793-
}
794-
}
795-
796765
fn ensure_is_absent(context: &str, thing: &dyn IsAbsent, name: &str) -> Result<(), String> {
797766
if thing.is_absent() {
798767
Ok(())
@@ -822,7 +791,6 @@ fn reject_all(context: &str, opts: SudoOptions) -> Result<(), String> {
822791
login,
823792
non_interactive,
824793
other_user,
825-
preserve_env,
826794
shell,
827795
stdin,
828796
prompt,
@@ -848,7 +816,6 @@ fn reject_all(context: &str, opts: SudoOptions) -> Result<(), String> {
848816
tuple!(login),
849817
tuple!(non_interactive),
850818
tuple!(other_user),
851-
tuple!(preserve_env),
852819
tuple!(remove_timestamp),
853820
tuple!(reset_timestamp),
854821
tuple!(shell),

src/sudo/cli/tests.rs

Lines changed: 23 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
11
use crate::common::SudoPath;
22

3-
use crate::sudo::cli::PreserveEnv;
4-
53
use super::{SudoAction, SudoOptions};
64
use pretty_assertions::assert_eq;
75

@@ -19,71 +17,56 @@ fn short_preserve_env_with_var_fails() {
1917
/// Passing '--preserve-env' with an argument fills 'preserve_env', 'short_preserve_env' stays 'false'
2018
#[test]
2119
fn preserve_env_with_var() {
22-
let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env=some_argument"]).unwrap();
20+
let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env=HOME"]).unwrap();
2321
assert_eq!(
24-
["some_argument"],
25-
cmd.preserve_env.try_into_only().unwrap().as_slice(),
22+
[("HOME".to_string(), std::env::var("HOME").unwrap())],
23+
cmd.env_var_list.as_slice()
2624
);
2725
}
2826

2927
/// Passing '--preserve-env' with several arguments fills 'preserve_env', 'short_preserve_env' stays 'false'
3028
#[test]
3129
fn preserve_env_with_several_vars() {
32-
let cmd = SudoOptions::try_parse_from([
33-
"sudo",
34-
"--preserve-env=some_argument,another_argument,a_third_one",
35-
])
36-
.unwrap();
30+
let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env=PATH,HOME"]).unwrap();
3731
assert_eq!(
38-
["some_argument", "another_argument", "a_third_one"],
39-
cmd.preserve_env.try_into_only().unwrap().as_slice(),
32+
[
33+
("PATH".to_string(), std::env::var("PATH").unwrap()),
34+
("HOME".to_string(), std::env::var("HOME").unwrap()),
35+
],
36+
cmd.env_var_list.as_slice()
4037
);
4138
}
4239

43-
#[test]
44-
fn preserve_env_boolean() {
45-
let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env"]).unwrap();
46-
assert_eq!(cmd.preserve_env, PreserveEnv::Everything);
47-
}
48-
4940
#[test]
5041
fn preserve_env_boolean_and_list() {
51-
let expected = PreserveEnv::Everything;
5242
let argss = [
53-
["sudo", "--preserve-env", "--preserve-env=some_argument"],
54-
["sudo", "--preserve-env=some_argument", "--preserve-env"],
43+
["sudo", "--preserve-env", "--preserve-env=HOME"],
44+
["sudo", "--preserve-env=HOME", "--preserve-env"],
5545
];
5646

5747
for args in argss {
5848
let cmd = SudoOptions::try_parse_from(args).unwrap();
59-
assert_eq!(expected, cmd.preserve_env);
49+
assert_eq!(
50+
[("HOME".to_string(), std::env::var("HOME").unwrap())],
51+
cmd.env_var_list.as_slice()
52+
);
6053
}
6154
}
6255

6356
#[test]
6457
fn preserve_env_repeated() {
65-
let cmd = SudoOptions::try_parse_from([
66-
"sudo",
67-
"--preserve-env=some_argument",
68-
"--preserve-env=another_argument",
69-
])
70-
.unwrap();
58+
let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env=PATH", "--preserve-env=HOME"])
59+
.unwrap();
7160
assert_eq!(
72-
["some_argument", "another_argument"],
73-
cmd.preserve_env.try_into_only().unwrap().as_slice()
61+
["PATH", "HOME"],
62+
cmd.env_var_list
63+
.into_iter()
64+
.map(|x| x.0)
65+
.collect::<Vec<_>>()
66+
.as_slice()
7467
);
7568
}
7669

77-
// `--preserve-env` only accepts a value with the syntax `--preserve-env=varname`
78-
// so this `--preserve-env` is acting like a boolean flag
79-
#[test]
80-
fn preserve_env_space() {
81-
let cmd = SudoOptions::try_parse_from(["sudo", "--preserve-env", "true"]).unwrap();
82-
83-
assert_eq!(PreserveEnv::Everything, cmd.preserve_env);
84-
assert_eq!(["true"], cmd.positional_args.as_slice());
85-
}
86-
8770
/// Catch env variable that is given without hyphens in 'VAR=value' form in env_var_list.
8871
/// external_args stay empty.
8972
#[test]

src/sudo/pipeline.rs

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -59,12 +59,6 @@ pub fn run(mut cmd_opts: SudoRunOptions) -> Result<(), Error> {
5959

6060
let user_requested_env_vars = std::mem::take(&mut cmd_opts.env_var_list);
6161

62-
if !cmd_opts.preserve_env.is_nothing() {
63-
eprintln_ignore_io_error!(
64-
"warning: `--preserve-env` has not yet been implemented and will be ignored"
65-
)
66-
}
67-
6862
let mut context = Context::from_run_opts(cmd_opts, &mut policy)?;
6963

7064
let policy = judge(policy, &context)?;

test-framework/sudo-compliance-tests/src/sudo.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ mod flag_help;
99
mod flag_list;
1010
mod flag_login;
1111
mod flag_non_interactive;
12+
mod flag_preserve_environment;
1213
mod flag_prompt;
1314
mod flag_shell;
1415
mod flag_user;
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
use sudo_test::{Command, Env};
2+
3+
use crate::{helpers, SUDOERS_ALL_ALL_NOPASSWD};
4+
5+
#[test]
6+
fn env_var_is_preserved() {
7+
let name = "SHOULD_BE_PRESERVED";
8+
let value = "42";
9+
let env = Env([SUDOERS_ALL_ALL_NOPASSWD, "Defaults setenv"]).build();
10+
11+
let stdout = Command::new("env")
12+
.arg(format!("{name}={value}"))
13+
.args(["sudo", &format!("--preserve-env={name}"), "env"])
14+
.output(&env)
15+
.stdout();
16+
let sudo_env = helpers::parse_env_output(&stdout);
17+
18+
assert_eq!(Some(value), sudo_env.get(name).copied());
19+
}
20+
21+
#[test]
22+
fn preserve_and_env_var_can_coexist() {
23+
let name = "SHOULD_BE_PRESERVED";
24+
let value = "42";
25+
let other_name = "ALSO_PRESERVED";
26+
let other_value = "37";
27+
let env = Env([SUDOERS_ALL_ALL_NOPASSWD, "Defaults setenv"]).build();
28+
29+
let stdout = Command::new("env")
30+
.arg(format!("{name}={value}"))
31+
.arg(format!("{other_name}={other_value}"))
32+
.args([
33+
"sudo",
34+
&format!("{name}={value}"),
35+
&format!("--preserve-env={other_name}"),
36+
"env",
37+
])
38+
.output(&env)
39+
.stdout();
40+
let sudo_env = helpers::parse_env_output(&stdout);
41+
42+
assert_eq!(Some(value), sudo_env.get(name).copied());
43+
assert_eq!(Some(other_value), sudo_env.get(other_name).copied());
44+
}
45+
#[test]
46+
fn env_var_overrides_preserve() {
47+
let name = "SHOULD_BE_PRESERVED";
48+
let value = "42";
49+
let other_value = "37";
50+
let env = Env([SUDOERS_ALL_ALL_NOPASSWD, "Defaults setenv"]).build();
51+
52+
let stdout = Command::new("env")
53+
.arg(format!("{name}={value}"))
54+
.args([
55+
"sudo",
56+
&format!("--preserve-env={name}"),
57+
&format!("{name}={other_value}"),
58+
"env",
59+
])
60+
.output(&env)
61+
.stdout();
62+
let sudo_env = helpers::parse_env_output(&stdout);
63+
64+
assert_eq!(Some(other_value), sudo_env.get(name).copied());
65+
}
66+
67+
#[test]
68+
fn preserve_overrides_env_var() {
69+
let name = "SHOULD_BE_PRESERVED";
70+
let value = "42";
71+
let other_value = "37";
72+
let env = Env([SUDOERS_ALL_ALL_NOPASSWD, "Defaults setenv"]).build();
73+
74+
let stdout = Command::new("env")
75+
.arg(format!("{name}={value}"))
76+
.args([
77+
"sudo",
78+
&format!("{name}={other_value}"),
79+
&format!("--preserve-env={name}"),
80+
"env",
81+
])
82+
.output(&env)
83+
.stdout();
84+
let sudo_env = helpers::parse_env_output(&stdout);
85+
86+
assert_eq!(Some(value), sudo_env.get(name).copied());
87+
}

0 commit comments

Comments
 (0)