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 = [
|
feat_selinux = [
|
||||||
"cp/selinux",
|
"cp/selinux",
|
||||||
"id/selinux",
|
"id/selinux",
|
||||||
|
"install/selinux",
|
||||||
"ls/selinux",
|
"ls/selinux",
|
||||||
"mkdir/selinux",
|
"mkdir/selinux",
|
||||||
"mkfifo/selinux",
|
"mkfifo/selinux",
|
||||||
|
|
|
@ -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"
|
||||||
|
|
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue