Skip to content

Commit

Permalink
refactor test framework (#103)
Browse files Browse the repository at this point in the history
* sudo-test: revamp API to make it more like std::process::Command

* applease clippy

* update compliance tests

* sudo-test: revamp EnvBuilder API to better adhere to the builder pattern

* update compliance tests

* add regression test for #81

* apply workaround for #102

* refactor some literals into constants
  • Loading branch information
japaric authored Mar 21, 2023
1 parent e939538 commit 53e4fd6
Show file tree
Hide file tree
Showing 14 changed files with 936 additions and 849 deletions.
44 changes: 18 additions & 26 deletions test-framework/sudo-compliance-tests/src/child_process.rs
Original file line number Diff line number Diff line change
@@ -1,53 +1,45 @@
use pretty_assertions::assert_eq;
use sudo_test::{As, EnvBuilder};
use sudo_test::{Command, Env};

use crate::{Result, SUDOERS_ROOT_ALL_NOPASSWD};

#[test]
fn sudo_forwards_childs_exit_code() -> Result<()> {
let env = EnvBuilder::default()
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;

let expected = 42;
let output = env.exec(
&["sudo", "sh", "-c", &format!("exit {expected}")],
As::Root,
None,
)?;
assert_eq!(Some(expected), output.status.code());
let output = Command::new("sudo")
.args(["sh", "-c"])
.arg(format!("exit {expected}"))
.exec(&env)?;
assert_eq!(Some(expected), output.status().code());

Ok(())
}

#[test]
fn sudo_forwards_childs_stdout() -> Result<()> {
let env = EnvBuilder::default()
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;

let expected = "hello";
let output = env.exec(&["sudo", "echo", expected], As::Root, None)?;
assert_eq!(expected, output.stdout);
assert!(output.stderr.is_empty());
let output = Command::new("sudo").args(["echo", expected]).exec(&env)?;
assert!(output.stderr().is_empty());
assert_eq!(expected, output.stdout()?);

Ok(())
}

#[test]
fn sudo_forwards_childs_stderr() -> Result<()> {
let env = EnvBuilder::default()
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;

let expected = "hello";
let output = env.exec(
&["sudo", "sh", "-c", &format!(">&2 echo {expected}")],
As::Root,
None,
)?;
assert_eq!(expected, output.stderr);
assert!(output.stdout.is_empty());
let output = Command::new("sudo")
.args(["sh", "-c"])
.arg(format!(">&2 echo {expected}"))
.exec(&env)?;
assert_eq!(expected, output.stderr());
assert!(output.stdout()?.is_empty());

Ok(())
}
52 changes: 27 additions & 25 deletions test-framework/sudo-compliance-tests/src/env_reset.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use std::collections::HashMap;

use pretty_assertions::assert_eq;
use sudo_test::{As, EnvBuilder};
use sudo_test::{Command, Env};

use crate::{Result, SUDOERS_ROOT_ALL_NOPASSWD};

Expand All @@ -11,22 +11,24 @@ use crate::{Result, SUDOERS_ROOT_ALL_NOPASSWD};
// see 'command environment' section in`man sudoers`
#[test]
fn vars_set_by_sudo_in_env_reset_mode() -> Result<()> {
let env = EnvBuilder::default()
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;

let stdout = env.stdout(&["env"], As::Root, None)?;
let stdout = Command::new("env").exec(&env)?.stdout()?;
let normal_env = parse_env_output(&stdout)?;

let sudo_abs_path = env.stdout(&["which", "sudo"], As::Root, None)?;
let env_abs_path = env.stdout(&["which", "env"], As::Root, None)?;
let sudo_abs_path = Command::new("which").arg("sudo").exec(&env)?.stdout()?;
let env_abs_path = Command::new("which").arg("env").exec(&env)?.stdout()?;

// run sudo in an empty environment
let stdout = env.stdout(
&["env", "-i", "SUDO_RS_IS_UNSTABLE=I accept that my system may break unexpectedly", &sudo_abs_path, &env_abs_path],
As::Root,
None,
)?;
let stdout = Command::new("env")
.args([
"-i",
"SUDO_RS_IS_UNSTABLE=I accept that my system may break unexpectedly",
&sudo_abs_path,
&env_abs_path,
])
.exec(&env)?
.stdout()?;
let mut sudo_env = parse_env_output(&stdout)?;

// # man sudo
Expand Down Expand Up @@ -78,27 +80,25 @@ fn vars_set_by_sudo_in_env_reset_mode() -> Result<()> {

#[test]
fn env_reset_mode_clears_env_vars() -> Result<()> {
let env = EnvBuilder::default()
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).build()?;

let varname = "SHOULD_BE_REMOVED";
let set_env_var = format!("export {varname}=1");

// sanity check that `set_env_var` makes `varname` visible to `env` program
let stdout = env.stdout(
&["sh", "-c", &format!("{set_env_var}; env")],
As::Root,
None,
)?;
let stdout = Command::new("sh")
.arg("-c")
.arg(format!("{set_env_var}; env"))
.exec(&env)?
.stdout()?;
let env_vars = parse_env_output(&stdout)?;
assert!(env_vars.contains_key(varname));

let stdout = env.stdout(
&["sh", "-c", &format!("{set_env_var}; sudo env")],
As::Root,
None,
)?;
let stdout = Command::new("sh")
.arg("-c")
.arg(format!("{set_env_var}; sudo env"))
.exec(&env)?
.stdout()?;
let env_vars = parse_env_output(&stdout)?;
assert!(!env_vars.contains_key(varname));

Expand All @@ -110,6 +110,8 @@ fn parse_env_output(env_output: &str) -> Result<HashMap<&str, &str>> {
for line in env_output.lines() {
if let Some((key, value)) = line.split_once('=') {
env.insert(key, value);
} else {
return Err(format!("invalid env syntax: {line}").into());
}
}

Expand Down
97 changes: 62 additions & 35 deletions test-framework/sudo-compliance-tests/src/flag_user.rs
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
use pretty_assertions::assert_eq;
use sudo_test::{As, EnvBuilder};
use sudo_test::{Command, Env, User};

use crate::{Result, SUDOERS_FERRIS_ALL_NOPASSWD, SUDOERS_ROOT_ALL_NOPASSWD};
use crate::{Result, GROUPNAME, SUDOERS_ROOT_ALL_NOPASSWD, SUDOERS_USER_ALL_NOPASSWD, USERNAME};

#[test]
fn root_can_become_another_user_by_name() -> Result<()> {
let username = "ferris";
let env = EnvBuilder::default()
.user(username, &[])
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).user(USERNAME).build()?;

let expected = env.stdout(&["id"], As::User { name: username }, None)?;
let actual = env.stdout(&["sudo", "-u", username, "id"], As::Root, None)?;
let expected = Command::new("id").as_user(USERNAME).exec(&env)?.stdout()?;
let actual = Command::new("sudo")
.args(["-u", USERNAME, "id"])
.exec(&env)?
.stdout()?;

assert_eq!(expected, actual);

Expand All @@ -21,44 +20,72 @@ fn root_can_become_another_user_by_name() -> Result<()> {

#[test]
fn root_can_become_another_user_by_uid() -> Result<()> {
let username = "ferris";
let env = EnvBuilder::default()
.user(username, &[])
.sudoers(SUDOERS_ROOT_ALL_NOPASSWD)
.build()?;
let env = Env(SUDOERS_ROOT_ALL_NOPASSWD).user(USERNAME).build()?;

let uid = env
.stdout(&["id", "-u"], As::User { name: username }, None)?
let uid = Command::new("id")
.arg("-u")
.as_user(USERNAME)
.exec(&env)?
.stdout()?
.parse::<u32>()?;
let expected = env.stdout(&["id"], As::User { name: username }, None)?;
let actual = env.stdout(&["sudo", "-u", &format!("#{uid}"), "id"], As::Root, None)?;
let expected = Command::new("id").as_user(USERNAME).exec(&env)?.stdout()?;
let actual = Command::new("sudo")
.arg("-u")
.arg(format!("#{uid}"))
.arg("id")
.exec(&env)?
.stdout()?;

assert_eq!(expected, actual);

Ok(())
}

#[ignore]
#[test]
fn user_can_become_another_user() -> Result<()> {
let env = EnvBuilder::default()
.user("ferris", &[])
.user("someone_else", &[])
.sudoers(SUDOERS_FERRIS_ALL_NOPASSWD)
let invoking_user = USERNAME;
let another_user = "another_user";
let env = Env(SUDOERS_USER_ALL_NOPASSWD)
.user(invoking_user)
.user(another_user)
.build()?;

let expected = Command::new("id")
.as_user(another_user)
.exec(&env)?
.stdout()?;
let actual = Command::new("sudo")
.args(["-u", another_user, "id"])
.as_user(USERNAME)
.exec(&env)?
.stdout()?;

assert_eq!(expected, actual);

Ok(())
}

// regression test for memorysafety/sudo-rs#81
#[test]
#[ignore]
fn invoking_user_groups_are_lost_when_becoming_another_user() -> Result<()> {
let invoking_user = USERNAME;
let another_user = "another_user";
let env = Env(SUDOERS_USER_ALL_NOPASSWD)
.group(GROUPNAME)
.user(User(invoking_user).group(GROUPNAME))
.user(another_user)
.build()?;

let expected = env.stdout(
&["id"],
As::User {
name: "someone_else",
},
None,
)?;
let actual = env.stdout(
&["sudo", "-u", "someone_else", "id"],
As::User { name: "ferris" },
None,
)?;
let expected = Command::new("id")
.as_user(another_user)
.exec(&env)?
.stdout()?;
let actual = Command::new("sudo")
.args(["-u", another_user, "id"])
.as_user(invoking_user)
.exec(&env)?
.stdout()?;

assert_eq!(expected, actual);

Expand Down
8 changes: 6 additions & 2 deletions test-framework/sudo-compliance-tests/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@ mod sudoers;
type Error = Box<dyn std::error::Error>;
type Result<T> = core::result::Result<T, Error>;

const SUDOERS_FERRIS_ALL_NOPASSWD: &str = "ferris ALL=(ALL:ALL) NOPASSWD: ALL";
const USERNAME: &str = "ferris";
const GROUPNAME: &str = "rustaceans";
const PASSWORD: &str = "strong-password";

const SUDOERS_ROOT_ALL: &str = "root ALL=(ALL:ALL) ALL";
const SUDOERS_ROOT_ALL_NOPASSWD: &str = "root ALL=(ALL:ALL) NOPASSWD: ALL";
const SUDOERS_ROOT_ALL_NOPASSWD: &str = "root ALL=(ALL:ALL) NOPASSWD: ALL";
const SUDOERS_USER_ALL_NOPASSWD: &str = "ferris ALL=(ALL:ALL) NOPASSWD: ALL";
49 changes: 21 additions & 28 deletions test-framework/sudo-compliance-tests/src/nopasswd.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
//! Scenarios where a password does not need to be provided
use sudo_test::{As, EnvBuilder};
use sudo_test::{Command, Env};

use crate::{Result, SUDOERS_ROOT_ALL};
use crate::{Result, SUDOERS_ROOT_ALL, USERNAME};

// NOTE all these tests assume that the invoking user passes the sudoers file 'User_List' criteria

Expand All @@ -11,46 +11,39 @@ use crate::{Result, SUDOERS_ROOT_ALL};
#[ignore]
#[test]
fn user_is_root() -> Result<()> {
let env = EnvBuilder::default().sudoers(SUDOERS_ROOT_ALL).build()?;
let env = Env(SUDOERS_ROOT_ALL).build()?;

let output = env.exec(&["sudo", "true"], As::Root, None)?;
assert!(output.status.success(), "{}", output.stderr);

Ok(())
Command::new("sudo")
.arg("true")
.exec(&env)?
.assert_success()
}

// man sudoers > User Authentication:
// "A password is not required if (..) the target user is the same as the invoking user"
#[ignore]
#[test]
fn user_as_themselves() -> Result<()> {
let username = "ferris";
let env = EnvBuilder::default()
.user(username, &[])
.sudoers(&format!("{username} ALL=(ALL:ALL) ALL"))
let env = Env(format!("{USERNAME} ALL=(ALL:ALL) ALL"))
.user(USERNAME)
.build()?;

let output = env.exec(
&["sudo", "-u", username, "true"],
As::User { name: username },
None,
)?;

assert!(output.status.success(), "{}", output.stderr);

Ok(())
Command::new("sudo")
.args(["-u", USERNAME, "true"])
.as_user(USERNAME)
.exec(&env)?
.assert_success()
}

#[test]
fn nopasswd_tag() -> Result<()> {
let username = "ferris";
let env = EnvBuilder::default()
.sudoers(&format!("{username} ALL=(ALL:ALL) NOPASSWD: ALL"))
.user(username, &[])
let env = Env(format!("{USERNAME} ALL=(ALL:ALL) NOPASSWD: ALL"))
.user(USERNAME)
.build()?;

let output = env.exec(&["sudo", "true"], As::User { name: username }, None)?;
assert!(output.status.success(), "{}", output.stderr);

Ok(())
Command::new("sudo")
.arg("true")
.as_user(USERNAME)
.exec(&env)?
.assert_success()
}
Loading

0 comments on commit 53e4fd6

Please sign in to comment.