From 7010dfd9390f0a97434b1d6785e9df7bbbc2ded5 Mon Sep 17 00:00:00 2001 From: Koutheir Attouchi Date: Thu, 19 Aug 2021 21:38:57 -0400 Subject: [PATCH] runcon: added implementation and tests. --- .../cspell.dictionaries/shell.wordlist.txt | 1 + .../workspace.wordlist.txt | 1 + Cargo.lock | 22 +- Cargo.toml | 4 +- GNUmakefile | 4 +- README.md | 5 +- src/uu/runcon/Cargo.toml | 27 ++ src/uu/runcon/src/errors.rs | 73 +++ src/uu/runcon/src/main.rs | 1 + src/uu/runcon/src/runcon.rs | 450 ++++++++++++++++++ tests/by-util/test_runcon.rs | 151 ++++++ 11 files changed, 731 insertions(+), 8 deletions(-) create mode 100644 src/uu/runcon/Cargo.toml create mode 100644 src/uu/runcon/src/errors.rs create mode 100644 src/uu/runcon/src/main.rs create mode 100644 src/uu/runcon/src/runcon.rs create mode 100644 tests/by-util/test_runcon.rs diff --git a/.vscode/cspell.dictionaries/shell.wordlist.txt b/.vscode/cspell.dictionaries/shell.wordlist.txt index 88ecee35b..07c2364ac 100644 --- a/.vscode/cspell.dictionaries/shell.wordlist.txt +++ b/.vscode/cspell.dictionaries/shell.wordlist.txt @@ -91,6 +91,7 @@ rerast rollup sed selinuxenabled +sestatus wslpath xargs diff --git a/.vscode/cspell.dictionaries/workspace.wordlist.txt b/.vscode/cspell.dictionaries/workspace.wordlist.txt index b506f6847..9c4b1c82f 100644 --- a/.vscode/cspell.dictionaries/workspace.wordlist.txt +++ b/.vscode/cspell.dictionaries/workspace.wordlist.txt @@ -162,6 +162,7 @@ blocksize canonname chroot dlsym +execvp fdatasync freeaddrinfo getaddrinfo diff --git a/Cargo.lock b/Cargo.lock index b41affd7f..2a5b46153 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -125,9 +125,9 @@ checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" [[package]] name = "bitflags" -version = "1.2.1" +version = "1.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitvec" @@ -384,6 +384,7 @@ dependencies = [ "uu_relpath", "uu_rm", "uu_rmdir", + "uu_runcon", "uu_seq", "uu_shred", "uu_shuf", @@ -1675,9 +1676,9 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "selinux" -version = "0.2.1" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa2f705dd871c2eb90888bb2d44b13218b34f5c7318c3971df62f799d0143eb" +checksum = "1cf704a543fe60d898f3253f1cc37655d0f0e9cdb68ef6230557e0e031b80608" dependencies = [ "bitflags", "libc", @@ -2795,6 +2796,19 @@ dependencies = [ "uucore_procs", ] +[[package]] +name = "uu_runcon" +version = "0.0.7" +dependencies = [ + "clap", + "fts-sys", + "libc", + "selinux", + "thiserror", + "uucore", + "uucore_procs", +] + [[package]] name = "uu_seq" version = "0.0.7" diff --git a/Cargo.toml b/Cargo.toml index a7e191554..dd500214f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -189,6 +189,7 @@ feat_require_unix_utmpx = [ # "feat_require_selinux" == set of utilities depending on SELinux. feat_require_selinux = [ "chcon", + "runcon", ] ## (alternate/newer/smaller platforms) feature sets # "feat_os_unix_fuchsia" == set of utilities which can be built/run on the "Fuchsia" OS (refs: ; ) @@ -241,7 +242,7 @@ clap = { version = "2.33", features = ["wrap_help"] } lazy_static = { version="1.3" } textwrap = { version="0.14", features=["terminal_size"] } uucore = { version=">=0.0.9", package="uucore", path="src/uucore" } -selinux = { version="0.2.1", optional = true } +selinux = { version="0.2.3", optional = true } # * uutils uu_test = { optional=true, version="0.0.7", package="uu_test", path="src/uu/test" } # @@ -313,6 +314,7 @@ realpath = { optional=true, version="0.0.7", package="uu_realpath", path="src/uu relpath = { optional=true, version="0.0.7", package="uu_relpath", path="src/uu/relpath" } rm = { optional=true, version="0.0.7", package="uu_rm", path="src/uu/rm" } rmdir = { optional=true, version="0.0.7", package="uu_rmdir", path="src/uu/rmdir" } +runcon = { optional=true, version="0.0.7", package="uu_runcon", path="src/uu/runcon" } seq = { optional=true, version="0.0.7", package="uu_seq", path="src/uu/seq" } shred = { optional=true, version="0.0.7", package="uu_shred", path="src/uu/shred" } shuf = { optional=true, version="0.0.7", package="uu_shuf", path="src/uu/shuf" } diff --git a/GNUmakefile b/GNUmakefile index 4c550dadc..367568ca8 100644 --- a/GNUmakefile +++ b/GNUmakefile @@ -157,7 +157,8 @@ UNIX_PROGS := \ who SELINUX_PROGS := \ - chcon + chcon \ + runcon ifneq ($(OS),Windows_NT) PROGS := $(PROGS) $(UNIX_PROGS) @@ -216,6 +217,7 @@ TEST_PROGS := \ realpath \ rm \ rmdir \ + runcon \ seq \ sort \ split \ diff --git a/README.md b/README.md index bc3cc0a98..7e420bb33 100644 --- a/README.md +++ b/README.md @@ -365,8 +365,8 @@ To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). | Done | Semi-Done | To Do | |-----------|-----------|--------| -| arch | cp | runcon | -| base32 | date | stty | +| arch | cp | stty | +| base32 | date | | | base64 | dd | | | basename | df | | | basenc | expr | | @@ -426,6 +426,7 @@ To contribute to uutils, please see [CONTRIBUTING](CONTRIBUTING.md). | relpath | | | | rm | | | | rmdir | | | +| runcon | | | | seq | | | | shred | | | | shuf | | | diff --git a/src/uu/runcon/Cargo.toml b/src/uu/runcon/Cargo.toml new file mode 100644 index 000000000..4e4c0bed6 --- /dev/null +++ b/src/uu/runcon/Cargo.toml @@ -0,0 +1,27 @@ +[package] +name = "uu_runcon" +version = "0.0.7" +authors = ["uutils developers"] +license = "MIT" +description = "runcon ~ (uutils) run command with specified security context" +homepage = "https://github.com/uutils/coreutils" +repository = "https://github.com/uutils/coreutils/tree/master/src/uu/runcon" +keywords = ["coreutils", "uutils", "cli", "utility"] +categories = ["command-line-utilities"] +edition = "2018" + +[lib] +path = "src/runcon.rs" + +[dependencies] +clap = { version = "2.33", features = ["wrap_help"] } +uucore = { version = ">=0.0.9", package="uucore", path="../../uucore", features=["entries", "fs", "perms"] } +uucore_procs = { version = ">=0.0.6", package="uucore_procs", path="../../uucore_procs" } +selinux = { version = "0.2" } +fts-sys = { version = "0.2" } +thiserror = { version = "1.0" } +libc = { version = "0.2" } + +[[bin]] +name = "runcon" +path = "src/main.rs" diff --git a/src/uu/runcon/src/errors.rs b/src/uu/runcon/src/errors.rs new file mode 100644 index 000000000..bc10a2f3e --- /dev/null +++ b/src/uu/runcon/src/errors.rs @@ -0,0 +1,73 @@ +use std::ffi::OsString; +use std::fmt::Write; +use std::io; +use std::str::Utf8Error; + +pub(crate) type Result = std::result::Result; + +#[derive(thiserror::Error, Debug)] +pub(crate) enum Error { + #[error("No command is specified")] + MissingCommand, + + #[error("SELinux is not enabled")] + SELinuxNotEnabled, + + #[error(transparent)] + NotUTF8(#[from] Utf8Error), + + #[error(transparent)] + CommandLine(#[from] clap::Error), + + #[error("{operation} failed")] + SELinux { + operation: &'static str, + source: selinux::errors::Error, + }, + + #[error("{operation} failed")] + Io { + operation: &'static str, + source: io::Error, + }, + + #[error("{operation} failed on '{}'", .operand1.to_string_lossy())] + Io1 { + operation: &'static str, + operand1: OsString, + source: io::Error, + }, +} + +impl Error { + pub(crate) fn from_io(operation: &'static str, source: io::Error) -> Self { + Self::Io { operation, source } + } + + pub(crate) fn from_io1( + operation: &'static str, + operand1: impl Into, + source: io::Error, + ) -> Self { + Self::Io1 { + operation, + operand1: operand1.into(), + source, + } + } + + pub(crate) fn from_selinux(operation: &'static str, source: selinux::errors::Error) -> Self { + Self::SELinux { operation, source } + } +} + +pub(crate) fn report_full_error(mut err: &dyn std::error::Error) -> String { + let mut desc = String::with_capacity(256); + write!(&mut desc, "{}", err).unwrap(); + while let Some(source) = err.source() { + err = source; + write!(&mut desc, ": {}", err).unwrap(); + } + desc.push('.'); + desc +} diff --git a/src/uu/runcon/src/main.rs b/src/uu/runcon/src/main.rs new file mode 100644 index 000000000..86aae54e5 --- /dev/null +++ b/src/uu/runcon/src/main.rs @@ -0,0 +1 @@ +uucore_procs::main!(uu_runcon); diff --git a/src/uu/runcon/src/runcon.rs b/src/uu/runcon/src/runcon.rs new file mode 100644 index 000000000..b2f1468bd --- /dev/null +++ b/src/uu/runcon/src/runcon.rs @@ -0,0 +1,450 @@ +// spell-checker:ignore (vars) RFILE + +use uucore::{show_error, show_usage_error}; + +use clap::{App, Arg}; +use selinux::{OpaqueSecurityContext, SecurityClass, SecurityContext}; + +use std::borrow::Cow; +use std::ffi::{CStr, CString, OsStr, OsString}; +use std::os::raw::c_char; +use std::os::unix::ffi::OsStrExt; +use std::{io, ptr}; + +mod errors; + +use errors::{report_full_error, Error, Result}; + +const VERSION: &str = env!("CARGO_PKG_VERSION"); +const ABOUT: &str = "Run command with specified security context."; +const DESCRIPTION: &str = "Run COMMAND with completely-specified CONTEXT, or with current or \ + transitioned security context modified by one or more of \ + LEVEL, ROLE, TYPE, and USER.\n\n\ + If none of --compute, --type, --user, --role or --range is specified, \ + then the first argument is used as the complete context.\n\n\ + Note that only carefully-chosen contexts are likely to successfully run.\n\n\ + With neither CONTEXT nor COMMAND are specified, \ + then this prints the current security context."; + +pub mod options { + pub const COMPUTE: &str = "compute"; + + pub const USER: &str = "user"; + pub const ROLE: &str = "role"; + pub const TYPE: &str = "type"; + pub const RANGE: &str = "range"; +} + +// This list is NOT exhaustive. This command might perform an `execvp()` to run +// a different program. When that happens successfully, the exit status of this +// process will be the exit status of that program. +mod error_exit_status { + pub const SUCCESS: i32 = libc::EXIT_SUCCESS; + pub const NOT_FOUND: i32 = 127; + pub const COULD_NOT_EXECUTE: i32 = 126; + pub const ANOTHER_ERROR: i32 = libc::EXIT_FAILURE; +} + +fn get_usage() -> String { + format!( + "{0} [CONTEXT COMMAND [ARG...]]\n \ + {0} [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] COMMAND [ARG...]", + uucore::execution_phrase() + ) +} + +pub fn uumain(args: impl uucore::Args) -> i32 { + let usage = get_usage(); + + let config = uu_app().usage(usage.as_ref()); + + let options = match parse_command_line(config, args) { + Ok(r) => r, + Err(r) => { + if let Error::CommandLine(ref r) = r { + match r.kind { + clap::ErrorKind::HelpDisplayed | clap::ErrorKind::VersionDisplayed => { + println!("{}", r); + return error_exit_status::SUCCESS; + } + _ => {} + } + } + + show_usage_error!("{}.\n", r); + return error_exit_status::ANOTHER_ERROR; + } + }; + + match &options.mode { + CommandLineMode::Print => { + if let Err(r) = print_current_context() { + show_error!("{}", report_full_error(&r)); + return error_exit_status::ANOTHER_ERROR; + } + } + + CommandLineMode::PlainContext { context, command } => { + let (exit_status, err) = + if let Err(err) = get_plain_context(context).and_then(set_next_exec_context) { + (error_exit_status::ANOTHER_ERROR, err) + } else { + // On successful execution, the following call never returns, + // and this process image is replaced. + execute_command(command, &options.arguments) + }; + + show_error!("{}", report_full_error(&err)); + return exit_status; + } + + CommandLineMode::CustomContext { + compute_transition_context, + user, + role, + the_type, + range, + command, + } => { + if let Some(command) = command { + let (exit_status, err) = if let Err(err) = get_custom_context( + *compute_transition_context, + user.as_deref(), + role.as_deref(), + the_type.as_deref(), + range.as_deref(), + command, + ) + .and_then(set_next_exec_context) + { + (error_exit_status::ANOTHER_ERROR, err) + } else { + // On successful execution, the following call never returns, + // and this process image is replaced. + execute_command(command, &options.arguments) + }; + + show_error!("{}", report_full_error(&err)); + return exit_status; + } else if let Err(r) = print_current_context() { + show_error!("{}", report_full_error(&r)); + return error_exit_status::ANOTHER_ERROR; + } + } + } + + error_exit_status::SUCCESS +} + +pub fn uu_app() -> App<'static, 'static> { + App::new(uucore::util_name()) + .version(VERSION) + .about(ABOUT) + .after_help(DESCRIPTION) + .arg( + Arg::with_name(options::COMPUTE) + .short("c") + .long(options::COMPUTE) + .takes_value(false) + .help("Compute process transition context before modifying."), + ) + .arg( + Arg::with_name(options::USER) + .short("u") + .long(options::USER) + .takes_value(true) + .value_name("USER") + .help("Set user USER in the target security context."), + ) + .arg( + Arg::with_name(options::ROLE) + .short("r") + .long(options::ROLE) + .takes_value(true) + .value_name("ROLE") + .help("Set role ROLE in the target security context."), + ) + .arg( + Arg::with_name(options::TYPE) + .short("t") + .long(options::TYPE) + .takes_value(true) + .value_name("TYPE") + .help("Set type TYPE in the target security context."), + ) + .arg( + Arg::with_name(options::RANGE) + .short("l") + .long(options::RANGE) + .takes_value(true) + .value_name("RANGE") + .help("Set range RANGE in the target security context."), + ) + .arg(Arg::with_name("ARG").multiple(true)) + // Once "ARG" is parsed, everything after that belongs to it. + // + // This is not how POSIX does things, but this is how the GNU implementation + // parses its command line. + .setting(clap::AppSettings::TrailingVarArg) +} + +#[derive(Debug)] +enum CommandLineMode { + Print, + + PlainContext { + context: OsString, + command: OsString, + }, + + CustomContext { + /// Compute process transition context before modifying. + compute_transition_context: bool, + + /// Use the current context with the specified user. + user: Option, + + /// Use the current context with the specified role. + role: Option, + + /// Use the current context with the specified type. + the_type: Option, + + /// Use the current context with the specified range. + range: Option, + + // `command` can be `None`, in which case we're dealing with this syntax: + // runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] + // + // This syntax is undocumented, but it is accepted by the GNU implementation, + // so we do the same for compatibility. + command: Option, + }, +} + +#[derive(Debug)] +struct Options { + mode: CommandLineMode, + arguments: Vec, +} + +fn parse_command_line(config: App, args: impl uucore::Args) -> Result { + let matches = config.get_matches_from_safe(args)?; + + let compute_transition_context = matches.is_present(options::COMPUTE); + + let mut args = matches + .values_of_os("ARG") + .unwrap_or_default() + .map(OsString::from); + + if compute_transition_context + || matches.is_present(options::USER) + || matches.is_present(options::ROLE) + || matches.is_present(options::TYPE) + || matches.is_present(options::RANGE) + { + // runcon [-c] [-u USER] [-r ROLE] [-t TYPE] [-l RANGE] [COMMAND [args]] + + let mode = CommandLineMode::CustomContext { + compute_transition_context, + user: matches.value_of_os(options::USER).map(Into::into), + role: matches.value_of_os(options::ROLE).map(Into::into), + the_type: matches.value_of_os(options::TYPE).map(Into::into), + range: matches.value_of_os(options::RANGE).map(Into::into), + command: args.next(), + }; + + Ok(Options { + mode, + arguments: args.collect(), + }) + } else if let Some(context) = args.next() { + // runcon CONTEXT COMMAND [args] + + args.next() + .ok_or(Error::MissingCommand) + .map(move |command| Options { + mode: CommandLineMode::PlainContext { context, command }, + arguments: args.collect(), + }) + } else { + // runcon + + Ok(Options { + mode: CommandLineMode::Print, + arguments: Vec::default(), + }) + } +} + +fn print_current_context() -> Result<()> { + let op = "Getting security context of the current process"; + let context = SecurityContext::current(false).map_err(|r| Error::from_selinux(op, r))?; + + let context = context + .to_c_string() + .map_err(|r| Error::from_selinux(op, r))?; + + if let Some(context) = context { + let context = context.as_ref().to_str()?; + println!("{}", context); + } else { + println!(); + } + Ok(()) +} + +fn set_next_exec_context(context: OpaqueSecurityContext) -> Result<()> { + let c_context = context + .to_c_string() + .map_err(|r| Error::from_selinux("Creating new context", r))?; + + let sc = SecurityContext::from_c_str(&c_context, false); + + if sc.check() != Some(true) { + let ctx = OsStr::from_bytes(c_context.as_bytes()); + let err = io::ErrorKind::InvalidInput.into(); + return Err(Error::from_io1("Checking security context", ctx, err)); + } + + sc.set_for_next_exec() + .map_err(|r| Error::from_selinux("Setting new security context", r)) +} + +fn get_plain_context(context: &OsStr) -> Result { + if selinux::kernel_support() == selinux::KernelSupport::Unsupported { + return Err(Error::SELinuxNotEnabled); + } + + let c_context = os_str_to_c_string(context)?; + + OpaqueSecurityContext::from_c_str(&c_context) + .map_err(|r| Error::from_selinux("Creating new context", r)) +} + +fn get_transition_context(command: &OsStr) -> Result { + // Generate context based on process transition. + let sec_class = SecurityClass::from_name("process") + .map_err(|r| Error::from_selinux("Getting process security class", r))?; + + // Get context of file to be executed. + let file_context = match SecurityContext::of_path(command, true, false) { + Ok(Some(context)) => context, + + Ok(None) => { + let err = io::Error::from_raw_os_error(libc::ENODATA); + return Err(Error::from_io1("getfilecon", command, err)); + } + + Err(r) => { + let op = "Getting security context of command file"; + return Err(Error::from_selinux(op, r)); + } + }; + + let process_context = SecurityContext::current(false) + .map_err(|r| Error::from_selinux("Getting security context of the current process", r))?; + + // Compute result of process transition. + process_context + .of_labeling_decision(&file_context, sec_class, "") + .map_err(|r| Error::from_selinux("Computing result of process transition", r)) +} + +fn get_initial_custom_opaque_context( + compute_transition_context: bool, + command: &OsStr, +) -> Result { + let context = if compute_transition_context { + get_transition_context(command)? + } else { + SecurityContext::current(false).map_err(|r| { + Error::from_selinux("Getting security context of the current process", r) + })? + }; + + let c_context = context + .to_c_string() + .map_err(|r| Error::from_selinux("Getting security context", r))? + .unwrap_or_else(|| Cow::Owned(CString::default())); + + OpaqueSecurityContext::from_c_str(c_context.as_ref()) + .map_err(|r| Error::from_selinux("Creating new context", r)) +} + +fn get_custom_context( + compute_transition_context: bool, + user: Option<&OsStr>, + role: Option<&OsStr>, + the_type: Option<&OsStr>, + range: Option<&OsStr>, + command: &OsStr, +) -> Result { + use OpaqueSecurityContext as OSC; + type SetNewValueProc = fn(&OSC, &CStr) -> selinux::errors::Result<()>; + + if selinux::kernel_support() == selinux::KernelSupport::Unsupported { + return Err(Error::SELinuxNotEnabled); + } + + let osc = get_initial_custom_opaque_context(compute_transition_context, command)?; + + let list: &[(Option<&OsStr>, SetNewValueProc, &'static str)] = &[ + (user, OSC::set_user, "Setting security context user"), + (role, OSC::set_role, "Setting security context role"), + (the_type, OSC::set_type, "Setting security context type"), + (range, OSC::set_range, "Setting security context range"), + ]; + + for &(new_value, method, op) in list { + if let Some(new_value) = new_value { + let c_new_value = os_str_to_c_string(new_value)?; + method(&osc, &c_new_value).map_err(|r| Error::from_selinux(op, r))?; + } + } + Ok(osc) +} + +/// The actual return type of this function should be `Result` +/// However, until the *never* type is stabilized, one way to indicate to the +/// compiler the only valid return type is to say "if this returns, it will +/// always return an error". +fn execute_command(command: &OsStr, arguments: &[OsString]) -> (i32, Error) { + let c_command = match os_str_to_c_string(command) { + Ok(v) => v, + Err(r) => return (error_exit_status::ANOTHER_ERROR, r), + }; + + let argv_storage: Vec = match arguments + .iter() + .map(AsRef::as_ref) + .map(os_str_to_c_string) + .collect::>() + { + Ok(v) => v, + Err(r) => return (error_exit_status::ANOTHER_ERROR, r), + }; + + let mut argv: Vec<*const c_char> = Vec::with_capacity(arguments.len().saturating_add(2)); + argv.push(c_command.as_ptr()); + argv.extend(argv_storage.iter().map(AsRef::as_ref).map(CStr::as_ptr)); + argv.push(ptr::null()); + + unsafe { libc::execvp(c_command.as_ptr(), argv.as_ptr()) }; + + let err = io::Error::last_os_error(); + let exit_status = if err.kind() == io::ErrorKind::NotFound { + error_exit_status::NOT_FOUND + } else { + error_exit_status::COULD_NOT_EXECUTE + }; + + let err = Error::from_io1("Executing command", command, err); + (exit_status, err) +} + +fn os_str_to_c_string(s: &OsStr) -> Result { + CString::new(s.as_bytes()) + .map_err(|_r| Error::from_io("CString::new()", io::ErrorKind::InvalidInput.into())) +} diff --git a/tests/by-util/test_runcon.rs b/tests/by-util/test_runcon.rs new file mode 100644 index 000000000..047ce5769 --- /dev/null +++ b/tests/by-util/test_runcon.rs @@ -0,0 +1,151 @@ +// spell-checker:ignore (jargon) xattributes + +#![cfg(feature = "feat_selinux")] + +use crate::common::util::*; + +// TODO: Check the implementation of `--compute` somehow. + +#[test] +fn version() { + new_ucmd!().arg("--version").succeeds(); + new_ucmd!().arg("-V").succeeds(); +} + +#[test] +fn help() { + new_ucmd!().arg("--help").succeeds(); + new_ucmd!().arg("-h").succeeds(); +} + +#[test] +fn print() { + new_ucmd!().succeeds(); + + for &flag in &["-c", "--compute"] { + new_ucmd!().arg(flag).succeeds(); + } + + for &flag in &[ + "-t", "--type", "-u", "--user", "-r", "--role", "-l", "--range", + ] { + new_ucmd!().args(&[flag, "example"]).succeeds(); + new_ucmd!().args(&[flag, "example1,example2"]).succeeds(); + } +} + +#[test] +fn invalid() { + new_ucmd!().arg("invalid").fails().code_is(1); + + let args = &[ + "unconfined_u:unconfined_r:unconfined_t:s0", + "inexistent-file", + ]; + new_ucmd!().args(args).fails().code_is(127); + + let args = &["invalid", "/bin/true"]; + new_ucmd!().args(args).fails().code_is(1); + + let args = &["--compute", "inexistent-file"]; + new_ucmd!().args(args).fails().code_is(1); + + let args = &["--compute", "--compute"]; + new_ucmd!().args(args).fails().code_is(1); + + // clap has an issue that makes this test fail: https://github.com/clap-rs/clap/issues/1543 + // TODO: Enable this code once the issue is fixed in the clap version we're using. + //new_ucmd!().arg("--compute=example").fails().code_is(1); + + for &flag in &[ + "-t", "--type", "-u", "--user", "-r", "--role", "-l", "--range", + ] { + new_ucmd!().arg(flag).fails().code_is(1); + + let args = &[flag, "example", flag, "example"]; + new_ucmd!().args(args).fails().code_is(1); + } +} + +#[test] +fn plain_context() { + let ctx = "unconfined_u:unconfined_r:unconfined_t:s0-s0"; + new_ucmd!().args(&[ctx, "/bin/true"]).succeeds(); + new_ucmd!().args(&[ctx, "/bin/false"]).fails().code_is(1); + + let output = new_ucmd!().args(&[ctx, "sestatus", "-v"]).succeeds(); + let r = get_sestatus_context(output.stdout()); + assert_eq!(r, "unconfined_u:unconfined_r:unconfined_t:s0"); + + let ctx = "system_u:unconfined_r:unconfined_t:s0-s0"; + new_ucmd!().args(&[ctx, "/bin/true"]).succeeds(); + + let ctx = "system_u:system_r:unconfined_t:s0"; + let output = new_ucmd!().args(&[ctx, "sestatus", "-v"]).succeeds(); + assert_eq!(get_sestatus_context(output.stdout()), ctx); +} + +#[test] +fn custom_context() { + let t_ud = "unconfined_t"; + let u_ud = "unconfined_u"; + let r_ud = "unconfined_r"; + + new_ucmd!().args(&["--compute", "/bin/true"]).succeeds(); + + let args = &["--compute", "/bin/false"]; + new_ucmd!().args(args).fails().code_is(1); + + let args = &["--type", t_ud, "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--compute", "--type", t_ud, "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--user=system_u", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--compute", "--user=system_u", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--role=system_r", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + let args = &["--compute", "--role=system_r", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + new_ucmd!().args(&["--range=s0", "/bin/true"]).succeeds(); + + let args = &["--compute", "--range=s0", "/bin/true"]; + new_ucmd!().args(args).succeeds(); + + for &(ctx, u, r) in &[ + ("unconfined_u:unconfined_r:unconfined_t:s0", u_ud, r_ud), + ("system_u:unconfined_r:unconfined_t:s0", "system_u", r_ud), + ("unconfined_u:system_r:unconfined_t:s0", u_ud, "system_r"), + ("system_u:system_r:unconfined_t:s0", "system_u", "system_r"), + ] { + let args = &["-t", t_ud, "-u", u, "-r", r, "-l", "s0", "sestatus", "-v"]; + + let output = new_ucmd!().args(args).succeeds(); + assert_eq!(get_sestatus_context(output.stdout()), ctx); + } +} + +fn get_sestatus_context(output: &[u8]) -> &str { + let re = regex::bytes::Regex::new(r#"Current context:\s*(\S+)\s*"#) + .expect("Invalid regular expression"); + + output + .split(|&b| b == b'\n') + .find(|&b| b.starts_with(b"Current context:")) + .and_then(|line| { + re.captures_iter(line) + .next() + .and_then(|c| c.get(1)) + .as_ref() + .map(regex::bytes::Match::as_bytes) + }) + .and_then(|bytes| std::str::from_utf8(bytes).ok()) + .expect("Output of sestatus is unexpected") +}