Skip to content

Commit e25df00

Browse files
committed
selinux: add support for install
1 parent b955e8f commit e25df00

File tree

4 files changed

+120
-52
lines changed

4 files changed

+120
-52
lines changed

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ feat_acl = ["cp/feat_acl"]
4949
feat_selinux = [
5050
"cp/selinux",
5151
"id/selinux",
52+
"install/selinux",
5253
"ls/selinux",
5354
"mkdir/selinux",
5455
"mkfifo/selinux",

src/uu/install/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@ uucore = { workspace = true, features = [
3333
"process",
3434
] }
3535

36+
[features]
37+
selinux = ["uucore/selinux"]
38+
3639
[[bin]]
3740
name = "install"
3841
path = "src/main.rs"

src/uu/install/src/install.rs

Lines changed: 44 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@ use uucore::fs::dir_strip_dot_for_creation;
2525
use uucore::mode::get_umask;
2626
use uucore::perms::{Verbosity, VerbosityLevel, wrap_chown};
2727
use uucore::process::{getegid, geteuid};
28+
#[cfg(feature = "selinux")]
29+
use uucore::selinux::{contexts_differ, set_selinux_security_context};
2830
use uucore::{format_usage, help_about, help_usage, show, show_error, show_if_err};
2931

3032
#[cfg(unix)]
@@ -51,13 +53,12 @@ pub struct Behavior {
5153
create_leading: bool,
5254
target_dir: Option<String>,
5355
no_target_dir: bool,
56+
preserve_context: bool,
57+
context: Option<String>,
5458
}
5559

5660
#[derive(Error, Debug)]
5761
enum InstallError {
58-
#[error("Unimplemented feature: {0}")]
59-
Unimplemented(String),
60-
6162
#[error("{} with -d requires at least one argument.", uucore::util_name())]
6263
DirNeedsArg,
6364

@@ -108,14 +109,15 @@ enum InstallError {
108109

109110
#[error("extra operand {}\n{}", .0.quote(), .1.quote())]
110111
ExtraOperand(String, String),
112+
113+
#[cfg(feature = "selinux")]
114+
#[error("{}", .0)]
115+
SelinuxContextFailed(String),
111116
}
112117

113118
impl UError for InstallError {
114119
fn code(&self) -> i32 {
115-
match self {
116-
Self::Unimplemented(_) => 2,
117-
_ => 1,
118-
}
120+
1
119121
}
120122

121123
fn usage(&self) -> bool {
@@ -172,8 +174,6 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
172174
.map(|v| v.map(ToString::to_string).collect())
173175
.unwrap_or_default();
174176

175-
check_unimplemented(&matches)?;
176-
177177
let behavior = behavior(&matches)?;
178178

179179
match behavior.main_function {
@@ -295,21 +295,20 @@ pub fn uu_app() -> Command {
295295
.action(ArgAction::SetTrue),
296296
)
297297
.arg(
298-
// TODO implement flag
299298
Arg::new(OPT_PRESERVE_CONTEXT)
300299
.short('P')
301300
.long(OPT_PRESERVE_CONTEXT)
302-
.help("(unimplemented) preserve security context")
301+
.help("preserve security context")
303302
.action(ArgAction::SetTrue),
304303
)
305304
.arg(
306-
// TODO implement flag
307305
Arg::new(OPT_CONTEXT)
308306
.short('Z')
309307
.long(OPT_CONTEXT)
310-
.help("(unimplemented) set security context of files and directories")
308+
.help("set security context of files and directories")
311309
.value_name("CONTEXT")
312-
.action(ArgAction::SetTrue),
310+
.value_parser(clap::value_parser!(String))
311+
.num_args(0..=1),
313312
)
314313
.arg(
315314
Arg::new(ARG_FILES)
@@ -319,25 +318,6 @@ pub fn uu_app() -> Command {
319318
)
320319
}
321320

322-
/// Check for unimplemented command line arguments.
323-
///
324-
/// Either return the degenerate Ok value, or an Err with string.
325-
///
326-
/// # Errors
327-
///
328-
/// Error datum is a string of the unimplemented argument.
329-
///
330-
///
331-
fn check_unimplemented(matches: &ArgMatches) -> UResult<()> {
332-
if matches.get_flag(OPT_PRESERVE_CONTEXT) {
333-
Err(InstallError::Unimplemented(String::from("--preserve-context, -P")).into())
334-
} else if matches.get_flag(OPT_CONTEXT) {
335-
Err(InstallError::Unimplemented(String::from("--context, -Z")).into())
336-
} else {
337-
Ok(())
338-
}
339-
}
340-
341321
/// Determine behavior, given command line arguments.
342322
///
343323
/// If successful, returns a filled-out Behavior struct.
@@ -415,6 +395,8 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
415395
}
416396
};
417397

398+
let context = matches.get_one::<String>(OPT_CONTEXT).cloned();
399+
418400
Ok(Behavior {
419401
main_function,
420402
specified_mode,
@@ -435,6 +417,8 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
435417
create_leading: matches.get_flag(OPT_CREATE_LEADING),
436418
target_dir,
437419
no_target_dir,
420+
preserve_context: matches.get_flag(OPT_PRESERVE_CONTEXT),
421+
context,
438422
})
439423
}
440424

@@ -485,6 +469,10 @@ fn directory(paths: &[String], b: &Behavior) -> UResult<()> {
485469
}
486470

487471
show_if_err!(chown_optional_user_group(path, b));
472+
473+
// Set SELinux context for directory if needed
474+
#[cfg(feature = "selinux")]
475+
show_if_err!(set_selinux_context(path, b));
488476
}
489477
// If the exit code was set, or show! has been called at least once
490478
// (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<()> {
941929
preserve_timestamps(from, to)?;
942930
}
943931

932+
#[cfg(feature = "selinux")]
933+
if b.preserve_context {
934+
uucore::selinux::preserve_security_context(from, to)
935+
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
936+
} else if b.context.is_some() {
937+
set_selinux_context(to, b)?;
938+
}
939+
944940
if b.verbose {
945941
print!("{} -> {}", from.quote(), to.quote());
946942
match backup_path {
@@ -1012,6 +1008,11 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> UResult<bool> {
10121008
return Ok(true);
10131009
}
10141010

1011+
#[cfg(feature = "selinux")]
1012+
if b.preserve_context && contexts_differ(from, to) {
1013+
return Ok(true);
1014+
}
1015+
10151016
// TODO: if -P (#1809) and from/to contexts mismatch, return true.
10161017

10171018
// 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> {
10421043

10431044
Ok(false)
10441045
}
1046+
1047+
#[cfg(feature = "selinux")]
1048+
fn set_selinux_context(path: &Path, behavior: &Behavior) -> UResult<()> {
1049+
if !behavior.preserve_context && behavior.context.is_some() {
1050+
// Use the provided context set by -Z/--context
1051+
set_selinux_security_context(path, behavior.context.as_ref())
1052+
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?
1053+
}
1054+
Ok(())
1055+
}

tests/by-util/test_install.rs

Lines changed: 72 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
//
33
// For the full copyright and license information, please view the LICENSE
44
// file that was distributed with this source code.
5-
// spell-checker:ignore (words) helloworld nodir objdump n'source
5+
// spell-checker:ignore (words) helloworld nodir objdump n'source nconfined
66

77
#[cfg(not(target_os = "openbsd"))]
88
use filetime::FileTime;
@@ -70,24 +70,6 @@ fn test_install_failing_not_dir() {
7070
.stderr_contains("not a directory");
7171
}
7272

73-
#[test]
74-
fn test_install_unimplemented_arg() {
75-
let (at, mut ucmd) = at_and_ucmd!();
76-
let dir = "target_dir";
77-
let file = "source_file";
78-
let context_arg = "--context";
79-
80-
at.touch(file);
81-
at.mkdir(dir);
82-
ucmd.arg(context_arg)
83-
.arg(file)
84-
.arg(dir)
85-
.fails()
86-
.stderr_contains("Unimplemented");
87-
88-
assert!(!at.file_exists(format!("{dir}/{file}")));
89-
}
90-
9173
#[test]
9274
fn test_install_ancestors_directories() {
9375
let (at, mut ucmd) = at_and_ucmd!();
@@ -1964,3 +1946,74 @@ fn test_install_no_target_basic() {
19641946
assert!(at.file_exists(file));
19651947
assert!(at.file_exists(format!("{dir}/{file}")));
19661948
}
1949+
1950+
#[test]
1951+
#[cfg(feature = "feat_selinux")]
1952+
fn test_selinux() {
1953+
use std::process::Command;
1954+
1955+
let scene = TestScenario::new(util_name!());
1956+
let at = &scene.fixtures;
1957+
let src = "orig";
1958+
at.touch(src);
1959+
1960+
let dest = "orig.2";
1961+
1962+
let args = ["-Z", "--context=unconfined_u:object_r:user_tmp_t:s0"];
1963+
for arg in args {
1964+
new_ucmd!()
1965+
.arg(arg)
1966+
.arg("-v")
1967+
.arg(at.plus_as_string(src))
1968+
.arg(at.plus_as_string(dest))
1969+
.succeeds()
1970+
.stdout_contains("orig' -> '");
1971+
1972+
let getfattr_output = Command::new("getfattr")
1973+
.arg(at.plus_as_string(dest))
1974+
.arg("-n")
1975+
.arg("security.selinux")
1976+
.output()
1977+
.expect("Failed to run `getfattr` on the destination file");
1978+
println!("{:?}", getfattr_output);
1979+
assert!(
1980+
getfattr_output.status.success(),
1981+
"getfattr did not run successfully: {}",
1982+
String::from_utf8_lossy(&getfattr_output.stderr)
1983+
);
1984+
1985+
let stdout = String::from_utf8_lossy(&getfattr_output.stdout);
1986+
assert!(
1987+
stdout.contains("unconfined_u"),
1988+
"Expected 'foo' not found in getfattr output:\n{stdout}"
1989+
);
1990+
at.remove(&at.plus_as_string(dest));
1991+
}
1992+
}
1993+
1994+
#[test]
1995+
#[cfg(feature = "feat_selinux")]
1996+
fn test_selinux_invalid_args() {
1997+
let scene = TestScenario::new(util_name!());
1998+
let at = &scene.fixtures;
1999+
let src = "orig";
2000+
at.touch(src);
2001+
let dest = "orig.2";
2002+
2003+
let args = [
2004+
"--context=a",
2005+
"--context=unconfined_u:object_r:user_tmp_t:s0:a",
2006+
"--context=nconfined_u:object_r:user_tmp_t:s0",
2007+
];
2008+
for arg in args {
2009+
new_ucmd!()
2010+
.arg(arg)
2011+
.arg("-v")
2012+
.arg(at.plus_as_string(src))
2013+
.arg(at.plus_as_string(dest))
2014+
.fails()
2015+
.stderr_contains("failed to set default file creation");
2016+
2017+
at.remove(&at.plus_as_string(dest));
2018+
}
2019+
}

0 commit comments

Comments
 (0)