runcon: added implementation and tests.

This commit is contained in:
Koutheir Attouchi 2021-08-19 21:38:57 -04:00 committed by Michael Debertol
parent 4ef35d4a96
commit 7010dfd939
11 changed files with 731 additions and 8 deletions

View file

@ -91,6 +91,7 @@ rerast
rollup
sed
selinuxenabled
sestatus
wslpath
xargs

View file

@ -162,6 +162,7 @@ blocksize
canonname
chroot
dlsym
execvp
fdatasync
freeaddrinfo
getaddrinfo

22
Cargo.lock generated
View file

@ -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"

View file

@ -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: <https://fuchsia.dev>; <https://en.wikipedia.org/wiki/Google_Fuchsia>)
@ -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" }

View file

@ -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 \

View file

@ -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 | | |

27
src/uu/runcon/Cargo.toml Normal file
View file

@ -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"

View file

@ -0,0 +1,73 @@
use std::ffi::OsString;
use std::fmt::Write;
use std::io;
use std::str::Utf8Error;
pub(crate) type Result<T> = std::result::Result<T, Error>;
#[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<OsString>,
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
}

View file

@ -0,0 +1 @@
uucore_procs::main!(uu_runcon);

450
src/uu/runcon/src/runcon.rs Normal file
View file

@ -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<OsString>,
/// Use the current context with the specified role.
role: Option<OsString>,
/// Use the current context with the specified type.
the_type: Option<OsString>,
/// Use the current context with the specified range.
range: Option<OsString>,
// `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<OsString>,
},
}
#[derive(Debug)]
struct Options {
mode: CommandLineMode,
arguments: Vec<OsString>,
}
fn parse_command_line(config: App, args: impl uucore::Args) -> Result<Options> {
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<OpaqueSecurityContext> {
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<SecurityContext> {
// 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<OpaqueSecurityContext> {
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<OpaqueSecurityContext> {
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<!, (i32, Error)>`
/// 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<CString> = match arguments
.iter()
.map(AsRef::as_ref)
.map(os_str_to_c_string)
.collect::<Result<_>>()
{
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> {
CString::new(s.as_bytes())
.map_err(|_r| Error::from_io("CString::new()", io::ErrorKind::InvalidInput.into()))
}

View file

@ -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")
}