1
Fork 0
mirror of https://github.com/RGBCube/uutils-coreutils synced 2025-07-28 03:27:44 +00:00

selinux: add support for install

This commit is contained in:
Sylvestre Ledru 2025-05-09 22:17:21 +02:00 committed by Daniel Hofstetter
parent 71af6d2089
commit 38861cc767
4 changed files with 120 additions and 52 deletions

View file

@ -49,6 +49,7 @@ feat_acl = ["cp/feat_acl"]
feat_selinux = [ feat_selinux = [
"cp/selinux", "cp/selinux",
"id/selinux", "id/selinux",
"install/selinux",
"ls/selinux", "ls/selinux",
"mkdir/selinux", "mkdir/selinux",
"mkfifo/selinux", "mkfifo/selinux",

View file

@ -33,6 +33,9 @@ uucore = { workspace = true, features = [
"process", "process",
] } ] }
[features]
selinux = ["uucore/selinux"]
[[bin]] [[bin]]
name = "install" name = "install"
path = "src/main.rs" path = "src/main.rs"

View file

@ -25,6 +25,8 @@ use uucore::fs::dir_strip_dot_for_creation;
use uucore::mode::get_umask; use uucore::mode::get_umask;
use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown}; use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown};
use uucore::process::{getegid, geteuid}; 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}; use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err};
#[cfg(unix)] #[cfg(unix)]
@ -51,13 +53,12 @@ pub struct Behavior {
create_leading: bool, create_leading: bool,
target_dir: Option<String>, target_dir: Option<String>,
no_target_dir: bool, no_target_dir: bool,
preserve_context: bool,
context: Option<String>,
} }
#[derive(Error, Debug)] #[derive(Error, Debug)]
enum InstallError { enum InstallError {
#[error("Unimplemented feature: {0}")]
Unimplemented(String),
#[error("{} with -d requires at least one argument.", uucore::util_name())] #[error("{} with -d requires at least one argument.", uucore::util_name())]
DirNeedsArg, DirNeedsArg,
@ -108,14 +109,15 @@ enum InstallError {
#[error("extra operand {}\n{}", .0.quote(), .1.quote())] #[error("extra operand {}\n{}", .0.quote(), .1.quote())]
ExtraOperand(String, String), ExtraOperand(String, String),
#[cfg(feature = "selinux")]
#[error("{}", .0)]
SelinuxContextFailed(String),
} }
impl UError for InstallError { impl UError for InstallError {
fn code(&self) -> i32 { fn code(&self) -> i32 {
match self { 1
Self::Unimplemented(_) => 2,
_ => 1,
}
} }
fn usage(&self) -> bool { fn usage(&self) -> bool {
@ -172,8 +174,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
.map(|v| v.map(ToString::to_string).collect()) .map(|v| v.map(ToString::to_string).collect())
.unwrap_or_default(); .unwrap_or_default();
check_unimplemented(&matches)?;
let behavior = behavior(&matches)?; let behavior = behavior(&matches)?;
match behavior.main_function { match behavior.main_function {
@ -295,21 +295,20 @@ pub fn uu_app() -> Command {
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
// TODO implement flag
Arg::new(OPT_PRESERVE_CONTEXT) Arg::new(OPT_PRESERVE_CONTEXT)
.short('P') .short('P')
.long(OPT_PRESERVE_CONTEXT) .long(OPT_PRESERVE_CONTEXT)
.help("(unimplemented) preserve security context") .help("preserve security context")
.action(ArgAction::SetTrue), .action(ArgAction::SetTrue),
) )
.arg( .arg(
// TODO implement flag
Arg::new(OPT_CONTEXT) Arg::new(OPT_CONTEXT)
.short('Z') .short('Z')
.long(OPT_CONTEXT) .long(OPT_CONTEXT)
.help("(unimplemented) set security context of files and directories") .help("set security context of files and directories")
.value_name("CONTEXT") .value_name("CONTEXT")
.action(ArgAction::SetTrue), .value_parser(clap::value_parser!(String))
.num_args(0..=1),
) )
.arg( .arg(
Arg::new(ARG_FILES) 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. /// Determine behavior, given command line arguments.
/// ///
/// If successful, returns a filled-out Behavior struct. /// 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 { Ok(Behavior {
main_function, main_function,
specified_mode, specified_mode,
@ -435,6 +417,8 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
create_leading: matches.get_flag(OPT_CREATE_LEADING), create_leading: matches.get_flag(OPT_CREATE_LEADING),
target_dir, target_dir,
no_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)); 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 // 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 // (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)?; 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 { if b.verbose {
print!("{} -> {}", from.quote(), to.quote()); print!("{} -> {}", from.quote(), to.quote());
match backup_path { match backup_path {
@ -1012,6 +1008,11 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult<bool> {
return Ok(true); 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. // 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. // 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) 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(())
}

View file

@ -2,7 +2,7 @@
// //
// For the full copyright and license information, please view the LICENSE // For the full copyright and license information, please view the LICENSE
// file that was distributed with this source code. // 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"))] #[cfg(not(target_os = "openbsd"))]
use filetime::FileTime; use filetime::FileTime;
@ -70,24 +70,6 @@ fn test_install_failing_not_dir() {
.stderr_contains("not a directory"); .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] #[test]
fn test_install_ancestors_directories() { fn test_install_ancestors_directories() {
let (at, mut ucmd) = at_and_ucmd!(); 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(file));
assert!(at.file_exists(format!("{dir}/{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));
}
}