Skip to content

Commit

Permalink
stdin support
Browse files Browse the repository at this point in the history
  • Loading branch information
japaric committed Mar 7, 2023
1 parent ff7fc94 commit ead107c
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 41 deletions.
18 changes: 9 additions & 9 deletions test-framework/sudo-compliance-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ fn parse_env_output(env_output: &str) -> Result<HashMap<&str, &str>> {
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");

Expand All @@ -46,20 +46,20 @@ 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");

Ok(())
}

#[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(())
Expand All @@ -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(())
Expand All @@ -85,15 +85,15 @@ 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");

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<()> {
Expand All @@ -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
Expand Down
56 changes: 41 additions & 15 deletions test-framework/sudo-test/src/docker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -18,24 +18,32 @@ impl Container {
pub fn new(image: &str) -> Result<Self> {
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<str>], user: As) -> Result<ExecOutput> {
helpers::run(&mut self.docker_cmd(cmd, user))
pub fn exec(
&self,
cmd: &[impl AsRef<str>],
user: As,
stdin: Option<&str>,
) -> Result<ExecOutput> {
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<str>], user: As) -> Result<String> {
helpers::stdout(&mut self.docker_cmd(cmd, user))
pub fn stdout(&self, cmd: &[impl AsRef<str>], user: As, stdin: Option<&str>) -> Result<String> {
helpers::stdout(&mut self.docker_cmd(cmd, user, stdin.is_some()), stdin)
}

fn docker_cmd(&self, cmd: &[impl AsRef<str>], user: As) -> Command {
fn docker_cmd(&self, cmd: &[impl AsRef<str>], 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);
Expand All @@ -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(())
}
Expand Down Expand Up @@ -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(())
Expand All @@ -137,17 +148,17 @@ 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(())
}

#[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(())
}
Expand All @@ -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(())
}
Expand All @@ -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(())
}
}
19 changes: 15 additions & 4 deletions test-framework/sudo-test/src/helpers.rs
Original file line number Diff line number Diff line change
@@ -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<ExecOutput> {
pub fn run(cmd: &mut Command, stdin: Option<&str>) -> Result<ExecOutput> {
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)?;
Expand All @@ -24,8 +35,8 @@ pub fn run(cmd: &mut Command) -> Result<ExecOutput> {
})
}

pub fn stdout(cmd: &mut Command) -> Result<String> {
let output = run(cmd)?;
pub fn stdout(cmd: &mut Command, stdin: Option<&str>) -> Result<String> {
let output = run(cmd, stdin)?;

if !output.status.success() {
let reason = if let Some(code) = output.status.code() {
Expand Down
33 changes: 20 additions & 13 deletions test-framework/sudo-test/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ impl EnvBuilder {
path,
],
As::Root,
None,
)?;

container.stdout(
Expand All @@ -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());
}
Expand All @@ -138,7 +140,7 @@ impl EnvBuilder {
group_list = user_groups.iter().cloned().collect::<Vec<_>>().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());
Expand Down Expand Up @@ -191,7 +193,7 @@ fn build_base_image() -> Result<()> {
}
}

helpers::stdout(&mut cmd)?;
helpers::stdout(&mut cmd, None)?;

Ok(())
}
Expand All @@ -204,7 +206,7 @@ fn repo_root() -> PathBuf {
}

fn get_groups(container: &Container) -> Result<HashSet<String>> {
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(':') {
Expand All @@ -216,7 +218,7 @@ fn get_groups(container: &Container) -> Result<HashSet<String>> {
}

fn get_users(container: &Container) -> Result<HashSet<String>> {
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(':') {
Expand All @@ -234,26 +236,31 @@ pub struct Env {
}

impl Env {
pub fn exec(&self, cmd: &[impl AsRef<str>], user: As) -> Result<ExecOutput> {
pub fn exec(
&self,
cmd: &[impl AsRef<str>],
user: As,
stdin: Option<&str>,
) -> Result<ExecOutput> {
if let As::User { name } = user {
assert!(
self.users.contains(name),
"tried to exec as non-existing user"
);
}

self.container.exec(cmd, user)
self.container.exec(cmd, user, stdin)
}

pub fn stdout(&self, cmd: &[impl AsRef<str>], user: As) -> Result<String> {
pub fn stdout(&self, cmd: &[impl AsRef<str>], user: As, stdin: Option<&str>) -> Result<String> {
if let As::User { name } = user {
assert!(
self.users.contains(name),
"tried to exec as non-existing user"
);
}

self.container.stdout(cmd, user)
self.container.stdout(cmd, user, stdin)
}
}

Expand Down Expand Up @@ -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(())
Expand All @@ -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::<HashSet<_>>();
Expand All @@ -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::<HashSet<_>>();
Expand All @@ -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;
Expand Down

0 comments on commit ead107c

Please sign in to comment.