mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
Merge pull request #8333 from sylvestre/selinux-mv
mv: add support for selinux and add minor refactor
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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<String>,
|
||||
}
|
||||
|
||||
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::<String>(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) => {
|
||||
|
||||
@@ -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::*;
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
|
||||
@@ -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));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user