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:
parent
71af6d2089
commit
38861cc767
4 changed files with 120 additions and 52 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(())
|
||||
}
|
||||
|
|
|
@ -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