diff --git a/src/uu/mv/Cargo.toml b/src/uu/mv/Cargo.toml index 995862609..329bb78ba 100644 --- a/src/uu/mv/Cargo.toml +++ b/src/uu/mv/Cargo.toml @@ -41,6 +41,9 @@ windows-sys = { workspace = true, features = [ [target.'cfg(unix)'.dependencies] libc = { workspace = true } +[features] +selinux = ["uucore/selinux"] + [[bin]] name = "mv" path = "src/main.rs" diff --git a/src/uu/mv/locales/en-US.ftl b/src/uu/mv/locales/en-US.ftl index 07d1155b8..9f3f5fd08 100644 --- a/src/uu/mv/locales/en-US.ftl +++ b/src/uu/mv/locales/en-US.ftl @@ -48,6 +48,8 @@ mv-help-verbose = explain what is being done mv-help-progress = Display a progress bar. Note: this feature is not supported by GNU coreutils. mv-help-debug = explain how a file is copied. Implies -v +mv-help-selinux = set SELinux security context of destination file to default type +mv-help-context = like -Z, or if CTX is specified then set the SELinux security context to CTX # Verbose messages mv-verbose-renamed = renamed {$from} -> {$to} diff --git a/src/uu/mv/src/mv.rs b/src/uu/mv/src/mv.rs index 6576d9717..bcc8de2c3 100644 --- a/src/uu/mv/src/mv.rs +++ b/src/uu/mv/src/mv.rs @@ -44,6 +44,8 @@ use uucore::fs::{ }; #[cfg(all(unix, not(any(target_os = "macos", target_os = "redox"))))] use uucore::fsxattr; +#[cfg(feature = "selinux")] +use uucore::selinux::set_selinux_security_context; use uucore::translate; use uucore::update_control; @@ -99,6 +101,9 @@ pub struct Options { /// `--debug` pub debug: bool, + + /// `-Z, --context` + pub context: Option, } impl Default for Options { @@ -114,6 +119,7 @@ impl Default for Options { strip_slashes: false, progress_bar: false, debug: false, + context: None, } } } @@ -140,6 +146,8 @@ static OPT_VERBOSE: &str = "verbose"; static OPT_PROGRESS: &str = "progress"; static ARG_FILES: &str = "files"; static OPT_DEBUG: &str = "debug"; +static OPT_CONTEXT: &str = "context"; +static OPT_SELINUX: &str = "selinux"; #[uucore::main] pub fn uumain(args: impl uucore::Args) -> UResult<()> { @@ -189,6 +197,15 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { } } + // Handle -Z and --context options + // If -Z is used, use the default context (empty string) + // If --context=value is used, use that specific value + let context = if matches.get_flag(OPT_SELINUX) { + Some(String::new()) + } else { + matches.get_one::(OPT_CONTEXT).cloned() + }; + let opts = Options { overwrite: overwrite_mode, backup: backup_mode, @@ -200,6 +217,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> { strip_slashes: matches.get_flag(OPT_STRIP_TRAILING_SLASHES), progress_bar: matches.get_flag(OPT_PROGRESS), debug: matches.get_flag(OPT_DEBUG), + context, }; mv(&files[..], &opts) @@ -282,6 +300,22 @@ pub fn uu_app() -> Command { .help(translate!("mv-help-progress")) .action(ArgAction::SetTrue), ) + .arg( + Arg::new(OPT_SELINUX) + .short('Z') + .help(translate!("mv-help-selinux")) + .action(ArgAction::SetTrue), + ) + .arg( + Arg::new(OPT_CONTEXT) + .long(OPT_CONTEXT) + .value_name("CTX") + .value_parser(clap::value_parser!(String)) + .help(translate!("mv-help-context")) + .num_args(0..=1) + .require_equals(true) + .default_missing_value(""), + ) .arg( Arg::new(ARG_FILES) .action(ArgAction::Append) @@ -733,6 +767,12 @@ fn rename( rename_with_fallback(from, to, multi_progress, None, None)?; } + #[cfg(feature = "selinux")] + if let Some(ref context) = opts.context { + set_selinux_security_context(to, Some(context)) + .map_err(|e| io::Error::other(e.to_string()))?; + } + if opts.verbose { let message = match backup_path { Some(path) => { diff --git a/src/uucore/src/lib/features/selinux.rs b/src/uucore/src/lib/features/selinux.rs index 0b260dd75..939210ae8 100644 --- a/src/uucore/src/lib/features/selinux.rs +++ b/src/uucore/src/lib/features/selinux.rs @@ -328,6 +328,58 @@ pub fn preserve_security_context(from_path: &Path, to_path: &Path) -> Result<(), set_selinux_security_context(to_path, Some(&context)) } +/// Gets the SELinux security context for a file using getfattr. +/// +/// This function is primarily used for testing purposes to verify that SELinux +/// contexts have been properly set on files. It uses the `getfattr` command +/// to retrieve the security.selinux extended attribute. +/// +/// # Arguments +/// +/// * `f` - The file path as a string. +/// +/// # Returns +/// +/// Returns the SELinux context string extracted from the getfattr output. +/// If the context cannot be retrieved, the function will panic. +/// +/// # Panics +/// +/// This function will panic if: +/// - The `getfattr` command fails to execute +/// - The `getfattr` command returns a non-zero exit status +/// +/// # Examples +/// +/// ```no_run +/// use uucore::selinux::get_getfattr_output; +/// +/// let context = get_getfattr_output("/path/to/file"); +/// println!("SELinux context: {}", context); +/// ``` +pub fn get_getfattr_output(f: &str) -> String { + use std::process::Command; + + let getfattr_output = Command::new("getfattr") + .arg(f) + .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) + ); + + String::from_utf8_lossy(&getfattr_output.stdout) + .split('"') + .nth(1) + .unwrap_or("") + .to_string() +} + #[cfg(test)] mod tests { use super::*; diff --git a/tests/by-util/test_cp.rs b/tests/by-util/test_cp.rs index 037273652..c8d50b048 100644 --- a/tests/by-util/test_cp.rs +++ b/tests/by-util/test_cp.rs @@ -6,6 +6,8 @@ // spell-checker:ignore (flags) reflink (fs) tmpfs (linux) rlimit Rlim NOFILE clob btrfs neve ROOTDIR USERDIR outfile uufs xattrs // spell-checker:ignore bdfl hlsl IRWXO IRWXG nconfined matchpathcon libselinux-devel prwx doesnotexist use uucore::display::Quotable; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::util::TestScenario; use uutests::{at_and_ucmd, new_ucmd, path_concat, util_name}; @@ -6372,30 +6374,6 @@ fn test_cp_from_stream_permission() { assert_eq!(at.metadata(target).permissions().mode(), 0o100_777); } -#[cfg(feature = "feat_selinux")] -fn get_getfattr_output(f: &str) -> String { - use std::process::Command; - - let getfattr_output = Command::new("getfattr") - .arg(f) - .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) - ); - - String::from_utf8_lossy(&getfattr_output.stdout) - .split('"') - .nth(1) - .unwrap_or("") - .to_string() -} - #[test] #[cfg(feature = "feat_selinux")] fn test_cp_selinux() { diff --git a/tests/by-util/test_install.rs b/tests/by-util/test_install.rs index 436e5a424..900e88a32 100644 --- a/tests/by-util/test_install.rs +++ b/tests/by-util/test_install.rs @@ -13,6 +13,8 @@ use std::process::Command; #[cfg(any(target_os = "linux", target_os = "android"))] use std::thread::sleep; use uucore::process::{getegid, geteuid}; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::at_and_ucmd; use uutests::new_ucmd; use uutests::util::{TestScenario, is_ci, run_ucmd_as_root}; @@ -2235,8 +2237,6 @@ fn test_install_no_target_basic() { #[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"; @@ -2265,29 +2265,19 @@ fn test_selinux() { result.success().stdout_contains("orig' -> '"); - let getfattr_output = Command::new("getfattr") - .arg(at.plus_as_string(dest)) - .arg("-n") - .arg("security.selinux") - .output(); + // Try to get SELinux context, skip test if getfattr is not available + let context_value = + std::panic::catch_unwind(|| get_getfattr_output(&at.plus_as_string(dest))); - // Skip test if getfattr is not available - let Ok(getfattr_output) = getfattr_output else { - println!("Skipping SELinux test: getfattr not available"); + let Ok(context_value) = context_value else { + println!("Skipping SELinux test: getfattr not available or failed"); at.remove(&at.plus_as_string(dest)); continue; }; - 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}" + context_value.contains("unconfined_u"), + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}" ); at.remove(&at.plus_as_string(dest)); } diff --git a/tests/by-util/test_mkdir.rs b/tests/by-util/test_mkdir.rs index e6bd6b2c1..1c734449f 100644 --- a/tests/by-util/test_mkdir.rs +++ b/tests/by-util/test_mkdir.rs @@ -11,6 +11,8 @@ use libc::mode_t; #[cfg(not(windows))] use std::os::unix::fs::PermissionsExt; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; #[cfg(not(windows))] use uutests::at_and_ucmd; use uutests::new_ucmd; @@ -390,8 +392,6 @@ fn test_empty_argument() { #[test] #[cfg(feature = "feat_selinux")] fn test_selinux() { - use std::process::Command; - let scene = TestScenario::new(util_name!()); let at = &scene.fixtures; let dest = "test_dir_a"; @@ -404,25 +404,12 @@ fn test_selinux() { .succeeds() .stdout_contains("created directory"); - 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"); - + let context_value = get_getfattr_output(&at.plus_as_string(dest)); 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"), + context_value.contains("unconfined_u"), "Expected '{}' not found in getfattr output:\n{}", "unconfined_u", - stdout + context_value ); at.rmdir(dest); } diff --git a/tests/by-util/test_mkfifo.rs b/tests/by-util/test_mkfifo.rs index 6bc8f3dd4..7ed97b3dd 100644 --- a/tests/by-util/test_mkfifo.rs +++ b/tests/by-util/test_mkfifo.rs @@ -5,6 +5,8 @@ // spell-checker:ignore nconfined +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util_name; @@ -108,7 +110,6 @@ fn test_create_fifo_with_umask() { #[test] #[cfg(feature = "feat_selinux")] fn test_mkfifo_selinux() { - use std::process::Command; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; let dest = "test_file"; @@ -121,23 +122,10 @@ fn test_mkfifo_selinux() { ts.ucmd().arg(arg).arg(dest).succeeds(); assert!(at.is_fifo("test_file")); - 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:?}"); + let context_value = get_getfattr_output(&at.plus_as_string(dest)); 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}" + context_value.contains("unconfined_u"), + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}" ); at.remove(&at.plus_as_string(dest)); } diff --git a/tests/by-util/test_mknod.rs b/tests/by-util/test_mknod.rs index 1c558dfd3..34136b828 100644 --- a/tests/by-util/test_mknod.rs +++ b/tests/by-util/test_mknod.rs @@ -7,6 +7,8 @@ use std::os::unix::fs::PermissionsExt; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::util::run_ucmd_as_root; @@ -155,7 +157,6 @@ fn test_mknod_mode_permissions() { #[test] #[cfg(feature = "feat_selinux")] fn test_mknod_selinux() { - use std::process::Command; let ts = TestScenario::new(util_name!()); let at = &ts.fixtures; let dest = "test_file"; @@ -175,25 +176,10 @@ fn test_mknod_selinux() { assert!(ts.fixtures.is_fifo("test_file")); assert!(ts.fixtures.metadata("test_file").permissions().readonly()); - 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:?}"); + let context_value = get_getfattr_output(&at.plus_as_string(dest)); 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 '{}' not found in getfattr output:\n{}", - "foo", - stdout + context_value.contains("unconfined_u"), + "Expected 'unconfined_u' not found in getfattr output:\n{context_value}" ); at.remove(&at.plus_as_string(dest)); } diff --git a/tests/by-util/test_mv.rs b/tests/by-util/test_mv.rs index 62c12c1d2..8da94e864 100644 --- a/tests/by-util/test_mv.rs +++ b/tests/by-util/test_mv.rs @@ -10,6 +10,8 @@ use rstest::rstest; use std::io::Write; #[cfg(not(windows))] use std::path::Path; +#[cfg(feature = "feat_selinux")] +use uucore::selinux::get_getfattr_output; use uutests::new_ucmd; use uutests::util::TestScenario; use uutests::{at_and_ucmd, util_name}; @@ -2488,3 +2490,60 @@ fn test_mv_cross_device_permission_denied() { set_permissions(other_fs_tempdir.path(), PermissionsExt::from_mode(0o755)) .expect("Unable to restore directory permissions"); } + +#[test] +#[cfg(feature = "selinux")] +fn test_mv_selinux_context() { + let test_cases = [ + ("-Z", None), + ( + "--context=unconfined_u:object_r:user_tmp_t:s0", + Some("unconfined_u"), + ), + ]; + + for (arg, expected_context) in test_cases { + let scene = TestScenario::new(util_name!()); + let at = &scene.fixtures; + let src = "source.txt"; + let dest = "dest.txt"; + + at.touch(src); + + let mut cmd = scene.ucmd(); + cmd.arg(arg); + + let result = cmd + .arg(at.plus_as_string(src)) + .arg(at.plus_as_string(dest)) + .run(); + + // Skip test if SELinux is not enabled + if result + .stderr_str() + .contains("SELinux is not enabled on this system") + { + println!("Skipping SELinux test: SELinux is not enabled"); + return; + } + + result.success(); + assert!(at.file_exists(dest)); + assert!(!at.file_exists(src)); + + // Verify SELinux context was set using getfattr + let context_value = get_getfattr_output(&at.plus_as_string(dest)); + if !context_value.is_empty() { + if let Some(expected) = expected_context { + assert!( + context_value.contains(expected), + "Expected context to contain '{expected}', got: {context_value}" + ); + } + } + + // Clean up files + let _ = std::fs::remove_file(at.plus_as_string(dest)); + let _ = std::fs::remove_file(at.plus_as_string(src)); + } +}