mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-27 19:17:43 +00:00
Merge pull request #7908 from sylvestre/selinux-installa
selinux: start the support of install
This commit is contained in:
commit
3a05fa92d6
5 changed files with 350 additions and 70 deletions
|
@ -49,6 +49,7 @@ feat_acl = ["cp/feat_acl"]
|
|||
feat_selinux = [
|
||||
"cp/selinux",
|
||||
"id/selinux",
|
||||
"install/selinux",
|
||||
"ls/selinux",
|
||||
"mkdir/selinux",
|
||||
"mkfifo/selinux",
|
||||
|
|
|
@ -33,6 +33,9 @@ uucore = { workspace = true, features = [
|
|||
"process",
|
||||
] }
|
||||
|
||||
[features]
|
||||
selinux = ["uucore/selinux"]
|
||||
|
||||
[[bin]]
|
||||
name = "install"
|
||||
path = "src/main.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<String>,
|
||||
no_target_dir: bool,
|
||||
preserve_context: bool,
|
||||
context: Option<String>,
|
||||
}
|
||||
|
||||
#[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<Behavior> {
|
|||
}
|
||||
};
|
||||
|
||||
let context = matches.get_one::<String>(OPT_CONTEXT).cloned();
|
||||
|
||||
Ok(Behavior {
|
||||
main_function,
|
||||
specified_mode,
|
||||
|
@ -435,6 +417,8 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
|
|||
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<bool> {
|
|||
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<bool> {
|
|||
|
||||
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(())
|
||||
}
|
||||
|
|
|
@ -228,6 +228,106 @@ pub fn get_selinux_security_context(path: &Path) -> Result<String, SeLinuxError>
|
|||
}
|
||||
}
|
||||
|
||||
/// Compares SELinux security contexts of two filesystem paths.
|
||||
///
|
||||
/// This function retrieves and compares the SELinux security contexts of two paths.
|
||||
/// If the contexts differ or an error occurs during retrieval, it returns true.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `from_path` - Source filesystem path.
|
||||
/// * `to_path` - Destination filesystem path.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `true` - If contexts differ, cannot be retrieved, or if SELinux is not enabled.
|
||||
/// * `false` - If contexts are the same.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use uucore::selinux::contexts_differ;
|
||||
///
|
||||
/// // Check if contexts differ between two files
|
||||
/// let differ = contexts_differ(Path::new("/path/to/source"), Path::new("/path/to/destination"));
|
||||
/// if differ {
|
||||
/// println!("Files have different SELinux contexts");
|
||||
/// } else {
|
||||
/// println!("Files have the same SELinux context");
|
||||
/// }
|
||||
/// ```
|
||||
pub fn contexts_differ(from_path: &Path, to_path: &Path) -> bool {
|
||||
if !is_selinux_enabled() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Check if SELinux contexts differ
|
||||
match (
|
||||
selinux::SecurityContext::of_path(from_path, false, false),
|
||||
selinux::SecurityContext::of_path(to_path, false, false),
|
||||
) {
|
||||
(Ok(Some(from_ctx)), Ok(Some(to_ctx))) => {
|
||||
// Convert contexts to CString and compare
|
||||
match (from_ctx.to_c_string(), to_ctx.to_c_string()) {
|
||||
(Ok(Some(from_c_str)), Ok(Some(to_c_str))) => {
|
||||
from_c_str.to_string_lossy() != to_c_str.to_string_lossy()
|
||||
}
|
||||
// If contexts couldn't be converted to CString or were None, consider them different
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
// If either context is None or an error occurred, assume contexts differ
|
||||
_ => true,
|
||||
}
|
||||
}
|
||||
|
||||
/// Preserves the SELinux security context from one filesystem path to another.
|
||||
///
|
||||
/// This function copies the security context from the source path to the destination path.
|
||||
/// If SELinux is not enabled, or if the source has no context, the function returns success
|
||||
/// without making any changes.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `from_path` - Source filesystem path from which to copy the SELinux context.
|
||||
/// * `to_path` - Destination filesystem path to which the context should be applied.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` - If the context was successfully preserved or if SELinux is not enabled.
|
||||
/// * `Err(SeLinuxError)` - If an error occurred during context retrieval or application.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use uucore::selinux::preserve_security_context;
|
||||
///
|
||||
/// // Preserve the SELinux context from source to destination
|
||||
/// match preserve_security_context(Path::new("/path/to/source"), Path::new("/path/to/destination")) {
|
||||
/// Ok(_) => println!("Context preserved successfully (or SELinux is not enabled)"),
|
||||
/// Err(err) => eprintln!("Failed to preserve context: {}", err),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn preserve_security_context(from_path: &Path, to_path: &Path) -> Result<(), SeLinuxError> {
|
||||
// If SELinux is not enabled, return success without doing anything
|
||||
if !is_selinux_enabled() {
|
||||
return Err(SeLinuxError::SELinuxNotEnabled);
|
||||
}
|
||||
|
||||
// Get context from the source path
|
||||
let context = get_selinux_security_context(from_path)?;
|
||||
|
||||
// If no context was found, just return success (nothing to preserve)
|
||||
if context.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Apply the context to the destination path
|
||||
set_selinux_security_context(to_path, Some(&context))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -295,18 +395,6 @@ mod tests {
|
|||
let result = set_selinux_security_context(path, Some(&invalid_context));
|
||||
|
||||
assert!(result.is_err());
|
||||
if let Err(err) = result {
|
||||
match err {
|
||||
SeLinuxError::ContextConversionFailure(ctx, msg) => {
|
||||
assert_eq!(ctx, "invalid\0context");
|
||||
assert!(
|
||||
msg.contains("nul byte"),
|
||||
"Error message should mention nul byte"
|
||||
);
|
||||
}
|
||||
_ => panic!("Expected ContextConversionFailure error but got: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -402,15 +490,139 @@ mod tests {
|
|||
let result = get_selinux_security_context(path);
|
||||
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_contexts_differ() {
|
||||
let file1 = NamedTempFile::new().expect("Failed to create first tempfile");
|
||||
let file2 = NamedTempFile::new().expect("Failed to create second tempfile");
|
||||
let path1 = file1.path();
|
||||
let path2 = file2.path();
|
||||
|
||||
std::fs::write(path1, b"content for file 1").expect("Failed to write to first tempfile");
|
||||
std::fs::write(path2, b"content for file 2").expect("Failed to write to second tempfile");
|
||||
|
||||
if !is_selinux_enabled() {
|
||||
assert!(
|
||||
contexts_differ(path1, path2),
|
||||
"contexts_differ should return true when SELinux is not enabled"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let test_context = String::from("system_u:object_r:tmp_t:s0");
|
||||
let result1 = set_selinux_security_context(path1, Some(&test_context));
|
||||
let result2 = set_selinux_security_context(path2, Some(&test_context));
|
||||
|
||||
if result1.is_ok() && result2.is_ok() {
|
||||
assert!(
|
||||
!contexts_differ(path1, path2),
|
||||
"Contexts should not differ when the same context is set on both files"
|
||||
);
|
||||
|
||||
let different_context = String::from("system_u:object_r:user_tmp_t:s0");
|
||||
if set_selinux_security_context(path2, Some(&different_context)).is_ok() {
|
||||
assert!(
|
||||
contexts_differ(path1, path2),
|
||||
"Contexts should differ when different contexts are set"
|
||||
);
|
||||
}
|
||||
} else {
|
||||
println!(
|
||||
"Note: Couldn't set SELinux contexts to test differences. This is expected if the test doesn't have sufficient permissions."
|
||||
);
|
||||
assert!(
|
||||
contexts_differ(path1, path2),
|
||||
"Contexts should differ when different contexts are set"
|
||||
);
|
||||
}
|
||||
|
||||
let nonexistent_path = Path::new("/nonexistent/file/path");
|
||||
assert!(
|
||||
contexts_differ(path1, nonexistent_path),
|
||||
"contexts_differ should return true when one path doesn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_security_context() {
|
||||
let source_file = NamedTempFile::new().expect("Failed to create source tempfile");
|
||||
let dest_file = NamedTempFile::new().expect("Failed to create destination tempfile");
|
||||
let source_path = source_file.path();
|
||||
let dest_path = dest_file.path();
|
||||
|
||||
std::fs::write(source_path, b"source content").expect("Failed to write to source tempfile");
|
||||
std::fs::write(dest_path, b"destination content")
|
||||
.expect("Failed to write to destination tempfile");
|
||||
|
||||
if !is_selinux_enabled() {
|
||||
let result = preserve_security_context(source_path, dest_path);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"preserve_security_context should fail when SELinux is not enabled"
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let source_context = String::from("system_u:object_r:tmp_t:s0");
|
||||
let result = set_selinux_security_context(source_path, Some(&source_context));
|
||||
|
||||
if result.is_ok() {
|
||||
let preserve_result = preserve_security_context(source_path, dest_path);
|
||||
assert!(
|
||||
preserve_result.is_ok(),
|
||||
"Failed to preserve context: {:?}",
|
||||
preserve_result.err()
|
||||
);
|
||||
|
||||
assert!(
|
||||
!contexts_differ(source_path, dest_path),
|
||||
"Contexts should be the same after preserving"
|
||||
);
|
||||
} else {
|
||||
println!(
|
||||
"Note: Couldn't set SELinux context on source file to test preservation. This is expected if the test doesn't have sufficient permissions."
|
||||
);
|
||||
|
||||
let preserve_result = preserve_security_context(source_path, dest_path);
|
||||
assert!(preserve_result.is_err());
|
||||
}
|
||||
|
||||
let nonexistent_path = Path::new("/nonexistent/file/path");
|
||||
let result = preserve_security_context(nonexistent_path, dest_path);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"preserve_security_context should fail when source file doesn't exist"
|
||||
);
|
||||
|
||||
let result = preserve_security_context(source_path, nonexistent_path);
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"preserve_security_context should fail when destination file doesn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preserve_security_context_empty_context() {
|
||||
let source_file = NamedTempFile::new().expect("Failed to create source tempfile");
|
||||
let dest_file = NamedTempFile::new().expect("Failed to create destination tempfile");
|
||||
let source_path = source_file.path();
|
||||
let dest_path = dest_file.path();
|
||||
|
||||
if !is_selinux_enabled() {
|
||||
return;
|
||||
}
|
||||
|
||||
let result = preserve_security_context(source_path, dest_path);
|
||||
|
||||
if let Err(err) = result {
|
||||
match err {
|
||||
SeLinuxError::FileOpenFailure(e) => {
|
||||
assert!(
|
||||
e.contains("No such file"),
|
||||
"Error should mention file not found"
|
||||
);
|
||||
SeLinuxError::ContextSetFailure(_, _) => {
|
||||
println!("Note: Could not set context due to permissions: {}", err);
|
||||
}
|
||||
unexpected => {
|
||||
panic!("Unexpected error: {}", unexpected);
|
||||
}
|
||||
_ => panic!("Expected FileOpenFailure error but got: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue