From ead107cca37c0e1f98d9e0e75d61b210a2a2a72d Mon Sep 17 00:00:00 2001 From: Jorge Aparicio Date: Tue, 7 Mar 2023 19:21:33 +0100 Subject: [PATCH] stdin support --- .../sudo-compliance-tests/src/lib.rs | 18 +++--- test-framework/sudo-test/src/docker.rs | 56 ++++++++++++++----- test-framework/sudo-test/src/helpers.rs | 19 +++++-- test-framework/sudo-test/src/lib.rs | 33 ++++++----- 4 files changed, 85 insertions(+), 41 deletions(-) diff --git a/test-framework/sudo-compliance-tests/src/lib.rs b/test-framework/sudo-compliance-tests/src/lib.rs index d38a03256..aaccf368e 100644 --- a/test-framework/sudo-compliance-tests/src/lib.rs +++ b/test-framework/sudo-compliance-tests/src/lib.rs @@ -35,7 +35,7 @@ fn parse_env_output(env_output: &str) -> Result> { fn cannot_sudo_with_empty_sudoers_file() -> Result<()> { let env = EnvBuilder::default().build()?; - let output = env.exec(&["sudo", "true"], As::Root)?; + let output = env.exec(&["sudo", "true"], As::Root, None)?; assert_eq!(Some(1), output.status.code()); assert_contains!(output.stderr, "root is not in the sudoers file"); @@ -46,7 +46,7 @@ fn cannot_sudo_with_empty_sudoers_file() -> Result<()> { fn cannot_sudo_if_sudoers_file_is_world_writable() -> Result<()> { let env = EnvBuilder::default().sudoers_chmod("446").build()?; - let output = env.exec(&["sudo", "true"], As::Root)?; + let output = env.exec(&["sudo", "true"], As::Root, None)?; assert_eq!(Some(1), output.status.code()); assert_contains!(output.stderr, "/etc/sudoers is world writable"); @@ -54,12 +54,12 @@ fn cannot_sudo_if_sudoers_file_is_world_writable() -> Result<()> { } #[test] -fn can_sudo_if_user_is_in_sudoers_file() -> Result<()> { +fn can_sudo_as_root_if_root_is_in_sudoers_file() -> Result<()> { let env = EnvBuilder::default() .sudoers("root ALL=(ALL:ALL) ALL") .build()?; - let output = env.exec(&["sudo", "true"], As::Root)?; + let output = env.exec(&["sudo", "true"], As::Root, None)?; assert!(output.status.success(), "{}", output.stderr); Ok(()) @@ -75,7 +75,7 @@ fn can_sudo_if_users_group_is_in_sudoers_file() -> Result<()> { .user(username, &[groupname]) .build()?; - let output = env.exec(&["sudo", "true"], As::User { name: username })?; + let output = env.exec(&["sudo", "true"], As::User { name: username }, None)?; assert!(output.status.success(), "{}", output.stderr); Ok(()) @@ -85,7 +85,7 @@ fn can_sudo_if_users_group_is_in_sudoers_file() -> Result<()> { fn cannot_sudo_if_sudoers_has_invalid_syntax() -> Result<()> { let env = EnvBuilder::default().sudoers("invalid syntax").build()?; - let output = env.exec(&["sudo", "true"], As::Root)?; + let output = env.exec(&["sudo", "true"], As::Root, None)?; assert!(!output.status.success()); assert_eq!(Some(1), output.status.code()); assert_contains!(output.stderr, "syntax error"); @@ -93,7 +93,7 @@ fn cannot_sudo_if_sudoers_has_invalid_syntax() -> Result<()> { Ok(()) } -// see 'envirnoment' section in`man sudo` +// see 'environment' section in`man sudo` // see 'command environment' section in`man sudoers` #[test] fn vars_set_by_sudo_in_env_reset_mode() -> Result<()> { @@ -102,11 +102,11 @@ fn vars_set_by_sudo_in_env_reset_mode() -> Result<()> { .sudoers("root ALL=(ALL:ALL) ALL") .build()?; - let stdout = env.stdout(&["env"], As::Root)?; + let stdout = env.stdout(&["env"], As::Root, None)?; let normal_env = parse_env_output(&stdout)?; // run sudo in an empty environment - let stdout = env.stdout(&["env", "-i", "sudo", "/usr/bin/env"], As::Root)?; + let stdout = env.stdout(&["env", "-i", "sudo", "/usr/bin/env"], As::Root, None)?; let mut sudo_env = parse_env_output(&stdout)?; // # man sudo diff --git a/test-framework/sudo-test/src/docker.rs b/test-framework/sudo-test/src/docker.rs index 7b980ea67..92d3e6cf7 100644 --- a/test-framework/sudo-test/src/docker.rs +++ b/test-framework/sudo-test/src/docker.rs @@ -18,24 +18,32 @@ impl Container { pub fn new(image: &str) -> Result { let mut cmd = Command::new("docker"); cmd.args(["run", "-d", "--rm", image]).args(DEFAULT_COMMAND); - let id = helpers::stdout(&mut cmd)?; + let id = helpers::stdout(&mut cmd, None)?; validate_docker_id(&id, &cmd)?; Ok(Container { id }) } - pub fn exec(&self, cmd: &[impl AsRef], user: As) -> Result { - helpers::run(&mut self.docker_cmd(cmd, user)) + pub fn exec( + &self, + cmd: &[impl AsRef], + user: As, + stdin: Option<&str>, + ) -> Result { + helpers::run(&mut self.docker_cmd(cmd, user, stdin.is_some()), stdin) } /// Returns `$cmd`'s stdout if it successfully exists - pub fn stdout(&self, cmd: &[impl AsRef], user: As) -> Result { - helpers::stdout(&mut self.docker_cmd(cmd, user)) + pub fn stdout(&self, cmd: &[impl AsRef], user: As, stdin: Option<&str>) -> Result { + helpers::stdout(&mut self.docker_cmd(cmd, user, stdin.is_some()), stdin) } - fn docker_cmd(&self, cmd: &[impl AsRef], user: As) -> Command { + fn docker_cmd(&self, cmd: &[impl AsRef], user: As, with_stdin: bool) -> Command { let mut docker_cmd = Command::new("docker"); docker_cmd.arg("exec"); + if with_stdin { + docker_cmd.arg("-i"); + } if let Some(user) = user.as_string() { docker_cmd.arg("--user"); docker_cmd.arg(user); @@ -54,7 +62,10 @@ impl Container { let src_path = temp_file.path().display().to_string(); let dest_path = format!("{}:{path_in_container}", self.id); - helpers::stdout(Command::new("docker").args(["cp", &src_path, &dest_path]))?; + helpers::stdout( + Command::new("docker").args(["cp", &src_path, &dest_path]), + None, + )?; Ok(()) } @@ -121,14 +132,14 @@ mod tests { check_cmd.args(["ps", "--all", "--quiet", "--filter"]); check_cmd.arg(format!("id={}", docker.id)); - let matches = helpers::stdout(&mut check_cmd)?; + let matches = helpers::stdout(&mut check_cmd, None)?; assert_eq!(1, matches.lines().count()); drop(docker); // wait for a bit until `stop` and `--rm` have done their work thread::sleep(Duration::from_secs(15)); - let matches = helpers::stdout(&mut check_cmd)?; + let matches = helpers::stdout(&mut check_cmd, None)?; assert_eq!(0, matches.lines().count()); Ok(()) @@ -137,9 +148,9 @@ mod tests { #[test] fn exec_as_root_works() -> Result<()> { let docker = Container::new(IMAGE)?; - let output = docker.exec(&["true"], As::Root)?; + let output = docker.exec(&["true"], As::Root, None)?; assert!(output.status.success()); - let output = docker.exec(&["false"], As::Root)?; + let output = docker.exec(&["false"], As::Root, None)?; assert_eq!(Some(1), output.status.code()); Ok(()) } @@ -147,7 +158,7 @@ mod tests { #[test] fn exec_as_user_named_root_works() -> Result<()> { let docker = Container::new(IMAGE)?; - let output = docker.exec(&["true"], As::User { name: "root" })?; + let output = docker.exec(&["true"], As::User { name: "root" }, None)?; assert!(output.status.success()); Ok(()) } @@ -156,9 +167,9 @@ mod tests { fn exec_as_non_root_user_works() -> Result<()> { let docker = Container::new(IMAGE)?; let username = "ferris"; - let output = docker.exec(&["useradd", username], As::Root)?; + let output = docker.exec(&["useradd", username], As::Root, None)?; assert!(output.status.success()); - let output = docker.exec(&["true"], As::User { name: username })?; + let output = docker.exec(&["true"], As::User { name: username }, None)?; assert!(output.status.success()); Ok(()) } @@ -168,8 +179,23 @@ mod tests { let docker = Container::new(IMAGE)?; let expected = "Hello, world!"; docker.cp("/tmp/file", expected)?; - let actual = docker.stdout(&["cat", "/tmp/file"], As::Root)?; + let actual = docker.stdout(&["cat", "/tmp/file"], As::Root, None)?; + assert_eq!(expected, actual); + Ok(()) + } + + #[test] + fn stdin_works() -> Result<()> { + let expected = "Hello, root!"; + + let docker = Container::new(IMAGE)?; + + docker.stdout(&["tee", "greeting"], As::Root, Some(expected))?; + + let actual = docker.stdout(&["cat", "greeting"], As::Root, None)?; + assert_eq!(expected, actual); + Ok(()) } } diff --git a/test-framework/sudo-test/src/helpers.rs b/test-framework/sudo-test/src/helpers.rs index 66eb76d99..3d40033d4 100644 --- a/test-framework/sudo-test/src/helpers.rs +++ b/test-framework/sudo-test/src/helpers.rs @@ -1,8 +1,19 @@ -use std::process::Command; +use std::{ + io::{Seek, Write}, + process::{Command, Stdio}, +}; use crate::{docker::ExecOutput, Result}; -pub fn run(cmd: &mut Command) -> Result { +pub fn run(cmd: &mut Command, stdin: Option<&str>) -> Result { + let mut temp_file; + if let Some(stdin) = stdin { + temp_file = tempfile::tempfile()?; + temp_file.write_all(stdin.as_bytes())?; + temp_file.seek(std::io::SeekFrom::Start(0))?; + cmd.stdin(Stdio::from(temp_file)); + } + let output = cmd.output()?; let mut stderr = String::from_utf8(output.stderr)?; @@ -24,8 +35,8 @@ pub fn run(cmd: &mut Command) -> Result { }) } -pub fn stdout(cmd: &mut Command) -> Result { - let output = run(cmd)?; +pub fn stdout(cmd: &mut Command, stdin: Option<&str>) -> Result { + let output = run(cmd, stdin)?; if !output.status.success() { let reason = if let Some(code) = output.status.code() { diff --git a/test-framework/sudo-test/src/lib.rs b/test-framework/sudo-test/src/lib.rs index 3bae6efe2..fa09f6a27 100644 --- a/test-framework/sudo-test/src/lib.rs +++ b/test-framework/sudo-test/src/lib.rs @@ -108,6 +108,7 @@ impl EnvBuilder { path, ], As::Root, + None, )?; container.stdout( @@ -119,12 +120,13 @@ impl EnvBuilder { path, ], As::Root, + None, )?; for user_groups in self.username_to_groups.values() { for user_group in user_groups { if !groups.contains(user_group) { - container.stdout(&["groupadd", user_group], As::Root)?; + container.stdout(&["groupadd", user_group], As::Root, None)?; groups.insert(user_group.to_string()); } @@ -138,7 +140,7 @@ impl EnvBuilder { group_list = user_groups.iter().cloned().collect::>().join(","); cmd.extend_from_slice(&["-G", &group_list]); } - container.stdout(&cmd, As::Root)?; + container.stdout(&cmd, As::Root, None)?; users.insert(username.to_string()); groups.insert(username.to_string()); @@ -191,7 +193,7 @@ fn build_base_image() -> Result<()> { } } - helpers::stdout(&mut cmd)?; + helpers::stdout(&mut cmd, None)?; Ok(()) } @@ -204,7 +206,7 @@ fn repo_root() -> PathBuf { } fn get_groups(container: &Container) -> Result> { - let stdout = container.stdout(&["getent", "group"], As::Root)?; + let stdout = container.stdout(&["getent", "group"], As::Root, None)?; let mut groups = HashSet::new(); for line in stdout.lines() { if let Some((name, _rest)) = line.split_once(':') { @@ -216,7 +218,7 @@ fn get_groups(container: &Container) -> Result> { } fn get_users(container: &Container) -> Result> { - let stdout = container.stdout(&["getent", "passwd"], As::Root)?; + let stdout = container.stdout(&["getent", "passwd"], As::Root, None)?; let mut users = HashSet::new(); for line in stdout.lines() { if let Some((name, _rest)) = line.split_once(':') { @@ -234,7 +236,12 @@ pub struct Env { } impl Env { - pub fn exec(&self, cmd: &[impl AsRef], user: As) -> Result { + pub fn exec( + &self, + cmd: &[impl AsRef], + user: As, + stdin: Option<&str>, + ) -> Result { if let As::User { name } = user { assert!( self.users.contains(name), @@ -242,10 +249,10 @@ impl Env { ); } - self.container.exec(cmd, user) + self.container.exec(cmd, user, stdin) } - pub fn stdout(&self, cmd: &[impl AsRef], user: As) -> Result { + pub fn stdout(&self, cmd: &[impl AsRef], user: As, stdin: Option<&str>) -> Result { if let As::User { name } = user { assert!( self.users.contains(name), @@ -253,7 +260,7 @@ impl Env { ); } - self.container.stdout(cmd, user) + self.container.stdout(cmd, user, stdin) } } @@ -289,7 +296,7 @@ mod tests { let new_user = "ferris"; let env = EnvBuilder::default().user(new_user, &[]).build()?; - let output = env.exec(&["sh", "-c", "[ -d /home/ferris ]"], As::Root)?; + let output = env.exec(&["sh", "-c", "[ -d /home/ferris ]"], As::Root, None)?; assert!(output.status.success()); Ok(()) @@ -300,7 +307,7 @@ mod tests { let new_user = "ferris"; let env = EnvBuilder::default().user(new_user, &[]).build()?; - let output = env.exec(&["groups"], As::User { name: new_user })?; + let output = env.exec(&["groups"], As::User { name: new_user }, None)?; assert!(output.status.success()); let groups = output.stdout.split(' ').collect::>(); @@ -315,7 +322,7 @@ mod tests { let group = "users"; let env = EnvBuilder::default().user(user, &[group]).build()?; - let output = env.exec(&["groups"], As::User { name: user })?; + let output = env.exec(&["groups"], As::User { name: user }, None)?; assert!(output.status.success()); let user_groups = output.stdout.split(' ').collect::>(); @@ -330,7 +337,7 @@ mod tests { let expected = "Hello, root!"; let env = EnvBuilder::default().sudoers(expected).build()?; - let output = env.exec(&["cat", "/etc/sudoers"], As::Root)?; + let output = env.exec(&["cat", "/etc/sudoers"], As::Root, None)?; assert!(output.status.success()); let actual = output.stdout;