diff --git a/Cargo.toml b/Cargo.toml index 67c9e4f79..7d53404a8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ feat_acl = ["cp/feat_acl"] feat_selinux = [ "cp/selinux", "id/selinux", + "install/selinux", "ls/selinux", "mkdir/selinux", "mkfifo/selinux", diff --git a/src/uu/install/Cargo.toml b/src/uu/install/Cargo.toml index 5e7c5c5df..a715902cb 100644 --- a/src/uu/install/Cargo.toml +++ b/src/uu/install/Cargo.toml @@ -33,6 +33,9 @@ uucore = { workspace = true, features = [ "process", ] } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "install" path = "src/main.rs" diff --git a/src/uu/install/src/install.rs b/src/uu/install/src/install.rs index 4cad5d1fb..c4590240b 100644 --- a/src/uu/install/src/install.rs +++ b/src/uu/install/src/install.rs @@ -25,6 +25,8 @@ use uucore::fs::dir_strip_dot_for_creation; use uucore::mode::get_umask; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::process::{getegid, geteuid}; +#[cfg(feature = "selinux")] +use uucore::selinux::{contexts_differ, set_selinux_security_context}; use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err}; #[cfg(unix)] @@ -51,13 +53,12 @@ pub struct Behavior { create_leading: bool, target_dir: Option, no_target_dir: bool, + preserve_context: bool, + context: Option, } #[derive(Error, Debug)] enum InstallError { - #[error("Unimplemented feature: {0}")] - Unimplemented(String), - #[error("{} with -d requires at least one argument.", uucore::util_name())] DirNeedsArg, @@ -108,14 +109,15 @@ enum InstallError { #[error("extra operand {}\n{}", .0.quote(), .1.quote())] ExtraOperand(String, String), + + #[cfg(feature = "selinux")] + #[error("{}", .0)] + SelinuxContextFailed(String), } impl UError for InstallError { fn code(&self) -> i32 { - match self { - Self::Unimplemented(_) => 2, - _ => 1, - } + 1 } fn usage(&self) -> bool { @@ -172,8 +174,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { .map(|v| v.map(ToString::to_string).collect()) .unwrap_or_default(); - check_unimplemented(&matches)?; - let behavior = behavior(&matches)?; match behavior.main_function { @@ -295,21 +295,20 @@ pub fn uu_app() -> Command { .action(ArgAction::SetTrue), ) .arg( - // TODO implement flag Arg::new(OPT_PRESERVE_CONTEXT) .short('P') .long(OPT_PRESERVE_CONTEXT) - .help("(unimplemented) preserve security context") + .help("preserve security context") .action(ArgAction::SetTrue), ) .arg( - // TODO implement flag Arg::new(OPT_CONTEXT) .short('Z') .long(OPT_CONTEXT) - .help("(unimplemented) set security context of files and directories") + .help("set security context of files and directories") .value_name("CONTEXT") - .action(ArgAction::SetTrue), + .value_parser(clap::value_parser!(String)) + .num_args(0..=1), ) .arg( Arg::new(ARG_FILES) @@ -319,25 +318,6 @@ pub fn uu_app() -> Command { ) } -/// Check for unimplemented command line arguments. -/// -/// Either return the degenerate Ok value, or an Err with string. -/// -/// # Errors -/// -/// Error datum is a string of the unimplemented argument. -/// -/// -fn check_unimplemented(matches: &ArgMatches) -> UResult<()> { - if matches.get_flag(OPT_PRESERVE_CONTEXT) { - Err(InstallError::Unimplemented(String::from("--preserve-context, -P")).into()) - } else if matches.get_flag(OPT_CONTEXT) { - Err(InstallError::Unimplemented(String::from("--context, -Z")).into()) - } else { - Ok(()) - } -} - /// Determine behavior, given command line arguments. /// /// If successful, returns a filled-out Behavior struct. @@ -415,6 +395,8 @@ fn behavior(matches: &ArgMatches) -> UResult { } }; + let context = matches.get_one::(OPT_CONTEXT).cloned(); + Ok(Behavior { main_function, specified_mode, @@ -435,6 +417,8 @@ fn behavior(matches: &ArgMatches) -> UResult { create_leading: matches.get_flag(OPT_CREATE_LEADING), target_dir, no_target_dir, + preserve_context: matches.get_flag(OPT_PRESERVE_CONTEXT), + context, }) } @@ -485,6 +469,10 @@ fn directory(paths: &[String], b: &Behavior) -> UResult<()> { } show_if_err!(chown_optional_user_group(path, b)); + + // Set SELinux context for directory if needed + #[cfg(feature = "selinux")] + show_if_err!(set_selinux_context(path, b)); } // If the exit code was set, or show! has been called at least once // (which sets the exit code as well), function execution will end after @@ -941,6 +929,14 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> { preserve_timestamps(from, to)?; } + #[cfg(feature = "selinux")] + if b.preserve_context { + uucore::selinux::preserve_security_context(from, to) + .map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?; + } else if b.context.is_some() { + set_selinux_context(to, b)?; + } + if b.verbose { print!("{} -> {}", from.quote(), to.quote()); match backup_path { @@ -1012,6 +1008,11 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult { return Ok(true); } + #[cfg(feature = "selinux")] + if b.preserve_context && contexts_differ(from, to) { + return Ok(true); + } + // TODO: if -P (#1809) and from/to contexts mismatch, return true. // Check if the owner ID is specified and differs from the destination file's owner. @@ -1042,3 +1043,13 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult { Ok(false) } + +#[cfg(feature = "selinux")] +fn set_selinux_context(path: &Path, behavior: &Behavior) -> UResult<()> { + if !behavior.preserve_context && behavior.context.is_some() { + // Use the provided context set by -Z/--context + set_selinux_security_context(path, behavior.context.as_ref()) + .map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?; + } + Ok(()) +} diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index fdb66639f..c402f3537 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -2,7 +2,7 @@ // // For the full copyright and license information, please view the LICENSE // file that was distributed with this source code. -// spell-checker:ignore (words) helloworld nodir objdump n'source +// spell-checker:ignore (words) helloworld nodir objdump n'source nconfined #[cfg(not(target_os = "openbsd"))] use filetime::FileTime; @@ -70,24 +70,6 @@ fn test_install_failing_not_dir() { .stderr_contains("not a directory"); } -#[test] -fn test_install_unimplemented_arg() { - let (at, mut ucmd) = at_and_ucmd!(); - let dir = "target_dir"; - let file = "source_file"; - let context_arg = "--context"; - - at.touch(file); - at.mkdir(dir); - ucmd.arg(context_arg) - .arg(file) - .arg(dir) - .fails() - .stderr_contains("Unimplemented"); - - assert!(!at.file_exists(format!("{dir}/{file}"))); -} - #[test] fn test_install_ancestors_directories() { let (at, mut ucmd) = at_and_ucmd!(); @@ -1964,3 +1946,74 @@ fn test_install_no_target_basic() { assert!(at.file_exists(file)); assert!(at.file_exists(format!("{dir}/{file}"))); } + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_selinux() { + use std::process::Command; + + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "orig"; + at.touch(src); + + let dest = "orig.2"; + + let args = ["-Z", "--context=unconfined_u:object_r:user_tmp_t:s0"]; + for arg in args { + new_ucmd!() + .arg(arg) + .arg("-v") + .arg(at.plus_as_string(src)) + .arg(at.plus_as_string(dest)) + .succeeds() + .stdout_contains("orig' -> '"); + + let getfattr_output = Command::new("getfattr") + .arg(at.plus_as_string(dest)) + .arg("-n") + .arg("security.selinux") + .output() + .expect("Failed to run `getfattr` on the destination file"); + println!("{:?}", getfattr_output); + assert!( + getfattr_output.status.success(), + "getfattr did not run successfully: {}", + String::from_utf8_lossy(&getfattr_output.stderr) + ); + + let stdout = String::from_utf8_lossy(&getfattr_output.stdout); + assert!( + stdout.contains("unconfined_u"), + "Expected 'foo' not found in getfattr output:\n{stdout}" + ); + at.remove(&at.plus_as_string(dest)); + } +} + +#[test] +#[cfg(feature = "feat_selinux")] +fn test_selinux_invalid_args() { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "orig"; + at.touch(src); + let dest = "orig.2"; + + let args = [ + "--context=a", + "--context=unconfined_u:object_r:user_tmp_t:s0:a", + "--context=nconfined_u:object_r:user_tmp_t:s0", + ]; + for arg in args { + new_ucmd!() + .arg(arg) + .arg("-v") + .arg(at.plus_as_string(src)) + .arg(at.plus_as_string(dest)) + .fails() + .stderr_contains("failed to set default file creation"); + + at.remove(&at.plus_as_string(dest)); + } +}