mirror of
https://github.com/RGBCube/uutils-coreutils
synced 2025-07-27 19:17:43 +00:00
Merge pull request #7635 from sylvestre/temp-selinux-impl
mkdir: add the selinux support
This commit is contained in:
commit
80b6a2155c
10 changed files with 315 additions and 23 deletions
2
.github/workflows/CICD.yml
vendored
2
.github/workflows/CICD.yml
vendored
|
@ -1188,7 +1188,7 @@ jobs:
|
|||
- run: rsync -v -a -e ssh . lima-default:~/work/
|
||||
- name: Setup Rust and other build deps in VM
|
||||
run: |
|
||||
lima sudo dnf install gcc g++ git rustup libselinux-devel clang-devel -y
|
||||
lima sudo dnf install gcc g++ git rustup libselinux-devel clang-devel attr -y
|
||||
lima rustup-init -y --default-toolchain stable
|
||||
- name: Verify SELinux Status
|
||||
run: |
|
||||
|
|
1
Cargo.lock
generated
1
Cargo.lock
generated
|
@ -3585,6 +3585,7 @@ dependencies = [
|
|||
"number_prefix",
|
||||
"os_display",
|
||||
"regex",
|
||||
"selinux",
|
||||
"sha1",
|
||||
"sha2",
|
||||
"sha3",
|
||||
|
|
|
@ -50,6 +50,7 @@ feat_selinux = [
|
|||
"cp/selinux",
|
||||
"id/selinux",
|
||||
"ls/selinux",
|
||||
"mkdir/selinux",
|
||||
"selinux",
|
||||
"feat_require_selinux",
|
||||
]
|
||||
|
|
|
@ -21,6 +21,8 @@ path = "src/mkdir.rs"
|
|||
clap = { workspace = true }
|
||||
uucore = { workspace = true, features = ["fs", "mode", "fsxattr"] }
|
||||
|
||||
[features]
|
||||
selinux = ["uucore/selinux"]
|
||||
|
||||
[[bin]]
|
||||
name = "mkdir"
|
||||
|
|
|
@ -29,6 +29,26 @@ mod options {
|
|||
pub const PARENTS: &str = "parents";
|
||||
pub const VERBOSE: &str = "verbose";
|
||||
pub const DIRS: &str = "dirs";
|
||||
pub const SELINUX: &str = "z";
|
||||
pub const CONTEXT: &str = "context";
|
||||
}
|
||||
|
||||
/// Configuration for directory creation.
|
||||
pub struct Config<'a> {
|
||||
/// Create parent directories as needed.
|
||||
pub recursive: bool,
|
||||
|
||||
/// File permissions (octal).
|
||||
pub mode: u32,
|
||||
|
||||
/// Print message for each created directory.
|
||||
pub verbose: bool,
|
||||
|
||||
/// Set SELinux security context.
|
||||
pub set_selinux_context: bool,
|
||||
|
||||
/// Specific SELinux context.
|
||||
pub context: Option<&'a String>,
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
|
@ -91,8 +111,21 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
|||
let verbose = matches.get_flag(options::VERBOSE);
|
||||
let recursive = matches.get_flag(options::PARENTS);
|
||||
|
||||
// Extract the SELinux related flags and options
|
||||
let set_selinux_context = matches.get_flag(options::SELINUX);
|
||||
let context = matches.get_one::<String>(options::CONTEXT);
|
||||
|
||||
match get_mode(&matches, mode_had_minus_prefix) {
|
||||
Ok(mode) => exec(dirs, recursive, mode, verbose),
|
||||
Ok(mode) => {
|
||||
let config = Config {
|
||||
recursive,
|
||||
mode,
|
||||
verbose,
|
||||
set_selinux_context: set_selinux_context || context.is_some(),
|
||||
context,
|
||||
};
|
||||
exec(dirs, &config)
|
||||
}
|
||||
Err(f) => Err(USimpleError::new(1, f)),
|
||||
}
|
||||
}
|
||||
|
@ -124,6 +157,15 @@ pub fn uu_app() -> Command {
|
|||
.help("print a message for each printed directory")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::SELINUX)
|
||||
.short('Z')
|
||||
.help("set SELinux security context of each created directory to the default type")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(Arg::new(options::CONTEXT).long(options::CONTEXT).value_name("CTX").help(
|
||||
"like -Z, or if CTX is specified then set the SELinux or SMACK security context to CTX",
|
||||
))
|
||||
.arg(
|
||||
Arg::new(options::DIRS)
|
||||
.action(ArgAction::Append)
|
||||
|
@ -137,12 +179,12 @@ pub fn uu_app() -> Command {
|
|||
/**
|
||||
* Create the list of new directories
|
||||
*/
|
||||
fn exec(dirs: ValuesRef<OsString>, recursive: bool, mode: u32, verbose: bool) -> UResult<()> {
|
||||
fn exec(dirs: ValuesRef<OsString>, config: &Config) -> UResult<()> {
|
||||
for dir in dirs {
|
||||
let path_buf = PathBuf::from(dir);
|
||||
let path = path_buf.as_path();
|
||||
|
||||
show_if_err!(mkdir(path, recursive, mode, verbose));
|
||||
show_if_err!(mkdir(path, config));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
@ -160,7 +202,7 @@ fn exec(dirs: ValuesRef<OsString>, recursive: bool, mode: u32, verbose: bool) ->
|
|||
///
|
||||
/// To match the GNU behavior, a path with the last directory being a single dot
|
||||
/// (like `some/path/to/.`) is created (with the dot stripped).
|
||||
pub fn mkdir(path: &Path, recursive: bool, mode: u32, verbose: bool) -> UResult<()> {
|
||||
pub fn mkdir(path: &Path, config: &Config) -> UResult<()> {
|
||||
if path.as_os_str().is_empty() {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
|
@ -173,7 +215,7 @@ pub fn mkdir(path: &Path, recursive: bool, mode: u32, verbose: bool) -> UResult<
|
|||
// std::fs::create_dir("foo/."); fails in pure Rust
|
||||
let path_buf = dir_strip_dot_for_creation(path);
|
||||
let path = path_buf.as_path();
|
||||
create_dir(path, recursive, verbose, false, mode)
|
||||
create_dir(path, false, config)
|
||||
}
|
||||
|
||||
#[cfg(any(unix, target_os = "redox"))]
|
||||
|
@ -194,15 +236,9 @@ fn chmod(_path: &Path, _mode: u32) -> UResult<()> {
|
|||
// Return true if the directory at `path` has been created by this call.
|
||||
// `is_parent` argument is not used on windows
|
||||
#[allow(unused_variables)]
|
||||
fn create_dir(
|
||||
path: &Path,
|
||||
recursive: bool,
|
||||
verbose: bool,
|
||||
is_parent: bool,
|
||||
mode: u32,
|
||||
) -> UResult<()> {
|
||||
fn create_dir(path: &Path, is_parent: bool, config: &Config) -> UResult<()> {
|
||||
let path_exists = path.exists();
|
||||
if path_exists && !recursive {
|
||||
if path_exists && !config.recursive {
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("{}: File exists", path.display()),
|
||||
|
@ -212,9 +248,9 @@ fn create_dir(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
if recursive {
|
||||
if config.recursive {
|
||||
match path.parent() {
|
||||
Some(p) => create_dir(p, recursive, verbose, true, mode)?,
|
||||
Some(p) => create_dir(p, true, config)?,
|
||||
None => {
|
||||
USimpleError::new(1, "failed to create whole tree");
|
||||
}
|
||||
|
@ -223,7 +259,7 @@ fn create_dir(
|
|||
|
||||
match std::fs::create_dir(path) {
|
||||
Ok(()) => {
|
||||
if verbose {
|
||||
if config.verbose {
|
||||
println!(
|
||||
"{}: created directory {}",
|
||||
uucore::util_name(),
|
||||
|
@ -233,7 +269,7 @@ fn create_dir(
|
|||
|
||||
#[cfg(all(unix, target_os = "linux"))]
|
||||
let new_mode = if path_exists {
|
||||
mode
|
||||
config.mode
|
||||
} else {
|
||||
// TODO: Make this macos and freebsd compatible by creating a function to get permission bits from
|
||||
// acl in extended attributes
|
||||
|
@ -242,19 +278,33 @@ fn create_dir(
|
|||
if is_parent {
|
||||
(!mode::get_umask() & 0o777) | 0o300 | acl_perm_bits
|
||||
} else {
|
||||
mode | acl_perm_bits
|
||||
config.mode | acl_perm_bits
|
||||
}
|
||||
};
|
||||
#[cfg(all(unix, not(target_os = "linux")))]
|
||||
let new_mode = if is_parent {
|
||||
(!mode::get_umask() & 0o777) | 0o300
|
||||
} else {
|
||||
mode
|
||||
config.mode
|
||||
};
|
||||
#[cfg(windows)]
|
||||
let new_mode = mode;
|
||||
let new_mode = config.mode;
|
||||
|
||||
chmod(path, new_mode)?;
|
||||
|
||||
// Apply SELinux context if requested
|
||||
#[cfg(feature = "selinux")]
|
||||
if config.set_selinux_context && uucore::selinux::check_selinux_enabled().is_ok() {
|
||||
if let Err(e) = uucore::selinux::set_selinux_security_context(path, config.context)
|
||||
{
|
||||
let _ = std::fs::remove_dir(path);
|
||||
return Err(USimpleError::new(
|
||||
1,
|
||||
format!("failed to set SELinux security context: {}", e),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
|
|
@ -58,6 +58,7 @@ crc32fast = { workspace = true, optional = true }
|
|||
regex = { workspace = true, optional = true }
|
||||
bigdecimal = { workspace = true, optional = true }
|
||||
num-traits = { workspace = true, optional = true }
|
||||
selinux = { workspace = true, optional = true }
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
walkdir = { workspace = true, optional = true }
|
||||
|
@ -105,13 +106,14 @@ format = [
|
|||
mode = ["libc"]
|
||||
perms = ["entries", "libc", "walkdir"]
|
||||
buf-copy = []
|
||||
parser = ["extendedbigdecimal", "glob", "num-traits"]
|
||||
pipes = []
|
||||
process = ["libc"]
|
||||
proc-info = ["tty", "walkdir"]
|
||||
quoting-style = []
|
||||
ranges = []
|
||||
ringbuffer = []
|
||||
parser = ["extendedbigdecimal", "glob", "num-traits"]
|
||||
selinux = ["dep:selinux"]
|
||||
signals = []
|
||||
sum = [
|
||||
"digest",
|
||||
|
|
|
@ -66,6 +66,8 @@ pub mod tty;
|
|||
|
||||
#[cfg(all(unix, feature = "fsxattr"))]
|
||||
pub mod fsxattr;
|
||||
#[cfg(all(target_os = "linux", feature = "selinux"))]
|
||||
pub mod selinux;
|
||||
#[cfg(all(unix, not(target_os = "fuchsia"), feature = "signals"))]
|
||||
pub mod signals;
|
||||
#[cfg(all(
|
||||
|
|
174
src/uucore/src/lib/features/selinux.rs
Normal file
174
src/uucore/src/lib/features/selinux.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
// This file is part of the uutils uucore package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
use selinux::SecurityContext;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
SELinuxNotEnabled,
|
||||
}
|
||||
|
||||
/// Checks if SELinux is enabled on the system.
|
||||
///
|
||||
/// This function verifies whether the kernel has SELinux support enabled.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `Ok(())` - If SELinux is enabled on the system.
|
||||
/// * `Err(Error::SELinuxNotEnabled)` - If SELinux is not enabled.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use uucore::selinux::check_selinux_enabled;
|
||||
///
|
||||
/// match check_selinux_enabled() {
|
||||
/// Ok(_) => println!("SELinux is enabled"),
|
||||
/// Err(_) => println!("SELinux is not enabled"),
|
||||
/// }
|
||||
/// ```
|
||||
pub fn check_selinux_enabled() -> Result<(), Error> {
|
||||
if selinux::kernel_support() == selinux::KernelSupport::Unsupported {
|
||||
Err(Error::SELinuxNotEnabled)
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Sets the SELinux security context for the given filesystem path.
|
||||
///
|
||||
/// If a specific context is provided, it attempts to set this context explicitly.
|
||||
/// Otherwise, it applies the default SELinux context for the provided path.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `path` - Filesystem path on which to set the SELinux context.
|
||||
/// * `context` - Optional SELinux context string to explicitly set.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Returns an error if:
|
||||
/// - SELinux is not enabled on the system.
|
||||
/// - The provided context is invalid or cannot be applied.
|
||||
/// - The default SELinux context cannot be set.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Setting default context:
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use uucore::selinux::set_selinux_security_context;
|
||||
///
|
||||
/// // Set the default SELinux context for a file
|
||||
/// let result = set_selinux_security_context(Path::new("/path/to/file"), None);
|
||||
/// if let Err(err) = result {
|
||||
/// eprintln!("Failed to set default context: {}", err);
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Setting specific context:
|
||||
/// ```
|
||||
/// use std::path::Path;
|
||||
/// use uucore::selinux::set_selinux_security_context;
|
||||
///
|
||||
/// // Set a specific SELinux context for a file
|
||||
/// let context = String::from("unconfined_u:object_r:user_home_t:s0");
|
||||
/// let result = set_selinux_security_context(Path::new("/path/to/file"), Some(&context));
|
||||
/// if let Err(err) = result {
|
||||
/// eprintln!("Failed to set context: {}", err);
|
||||
/// }
|
||||
/// ```
|
||||
pub fn set_selinux_security_context(path: &Path, context: Option<&String>) -> Result<(), String> {
|
||||
// Check if SELinux is enabled on the system
|
||||
check_selinux_enabled().map_err(|e| format!("{:?}", e))?;
|
||||
|
||||
if let Some(ctx_str) = context {
|
||||
// Create a CString from the provided context string
|
||||
let c_context = std::ffi::CString::new(ctx_str.as_str())
|
||||
.map_err(|_| "Invalid context string (contains null bytes)".to_string())?;
|
||||
|
||||
// Convert the CString into an SELinux security context
|
||||
let security_context = selinux::OpaqueSecurityContext::from_c_str(&c_context)
|
||||
.map_err(|e| format!("Failed to create security context: {}", e))?;
|
||||
|
||||
// Set the provided security context on the specified path
|
||||
SecurityContext::from_c_str(
|
||||
&security_context.to_c_string().map_err(|e| e.to_string())?,
|
||||
false,
|
||||
)
|
||||
.set_for_path(path, false, false)
|
||||
.map_err(|e| format!("Failed to set context: {}", e))
|
||||
} else {
|
||||
// If no context provided, set the default SELinux context for the path
|
||||
SecurityContext::set_default_for_path(path)
|
||||
.map_err(|e| format!("Failed to set default context: {}", e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
#[test]
|
||||
fn test_selinux_context_setting() {
|
||||
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
|
||||
let path = tmpfile.path();
|
||||
|
||||
let result = set_selinux_security_context(path, None);
|
||||
|
||||
if result.is_ok() {
|
||||
// SELinux enabled and successfully set default context
|
||||
assert!(true, "Successfully set SELinux context");
|
||||
} else {
|
||||
let err = result.unwrap_err();
|
||||
let valid_errors = [
|
||||
"SELinux is not enabled on this system",
|
||||
&format!(
|
||||
"Failed to set default context: selinux_lsetfilecon_default() failed on path '{}'",
|
||||
path.display()
|
||||
),
|
||||
];
|
||||
|
||||
assert!(
|
||||
valid_errors.contains(&err.as_str()),
|
||||
"Unexpected error message: {}",
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_context_string_error() {
|
||||
let tmpfile = NamedTempFile::new().expect("Failed to create tempfile");
|
||||
let path = tmpfile.path();
|
||||
|
||||
// Pass a context string containing a null byte to trigger CString::new error
|
||||
let invalid_context = String::from("invalid\0context");
|
||||
let result = set_selinux_security_context(path, Some(&invalid_context));
|
||||
|
||||
assert!(result.is_err());
|
||||
assert_eq!(
|
||||
result.unwrap_err(),
|
||||
"Invalid context string (contains null bytes)"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_selinux_enabled_runtime_behavior() {
|
||||
let result = check_selinux_enabled();
|
||||
|
||||
match selinux::kernel_support() {
|
||||
selinux::KernelSupport::Unsupported => {
|
||||
assert!(matches!(result, Err(Error::SELinuxNotEnabled)));
|
||||
}
|
||||
_ => {
|
||||
assert!(result.is_ok(), "Expected Ok(()) when SELinux is supported");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -103,6 +103,9 @@ pub use crate::features::fsext;
|
|||
#[cfg(all(unix, feature = "fsxattr"))]
|
||||
pub use crate::features::fsxattr;
|
||||
|
||||
#[cfg(all(target_os = "linux", feature = "selinux"))]
|
||||
pub use crate::features::selinux;
|
||||
|
||||
//## core functions
|
||||
|
||||
#[cfg(unix)]
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
// spell-checker:ignore bindgen
|
||||
// spell-checker:ignore bindgen getfattr testtest
|
||||
|
||||
#![allow(clippy::cast_sign_loss, clippy::cast_possible_truncation)]
|
||||
|
||||
|
@ -358,3 +358,60 @@ fn test_empty_argument() {
|
|||
.fails()
|
||||
.stderr_only("mkdir: cannot create directory '': No such file or directory\n");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "feat_selinux")]
|
||||
fn test_selinux() {
|
||||
use std::process::Command;
|
||||
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
let dest = "test_dir_a";
|
||||
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(dest))
|
||||
.succeeds()
|
||||
.stdout_contains("created directory");
|
||||
|
||||
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");
|
||||
|
||||
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 '{}' not found in getfattr output:\n{}",
|
||||
"unconfined_u",
|
||||
stdout
|
||||
);
|
||||
at.rmdir(dest);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(feature = "feat_selinux")]
|
||||
fn test_selinux_invalid() {
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
let dest = "test_dir_a";
|
||||
new_ucmd!()
|
||||
.arg("--context=testtest")
|
||||
.arg(at.plus_as_string(dest))
|
||||
.fails()
|
||||
.no_stdout()
|
||||
.stderr_contains("failed to set SELinux security context:");
|
||||
// invalid context, so, no directory
|
||||
assert!(!at.dir_exists(dest));
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue