mirror of
https://github.com/uutils/coreutils.git
synced 2026-06-10 15:48:22 -07:00
install: add -U (unprivileged) option
This commit is contained in:
@@ -204,3 +204,7 @@ With `-U`/`--no-utf8`, you can interpret input files as 8-bit ASCII rather than
|
||||
## `expand`
|
||||
|
||||
`expand` also offers the `-U`/`--no-utf8` option to interpret input files as 8-bit ASCII instead of UTF-8.
|
||||
|
||||
## `install`
|
||||
|
||||
`install` offers FreeBSD's `-U` unprivileged option to not change the owner, the group, or the file flags of the destination.
|
||||
|
||||
@@ -19,6 +19,7 @@ install-help-verbose = explain what is being done
|
||||
install-help-preserve-context = preserve security context
|
||||
install-help-context = set security context of files and directories
|
||||
install-help-default-context = set SELinux security context of destination file and each created directory to default type
|
||||
install-help-unprivileged = do not require elevated privileges to change the owner, the group, or the file flags of the destination
|
||||
|
||||
# Error messages
|
||||
install-error-dir-needs-arg = { $util_name } with -d requires at least one argument.
|
||||
|
||||
@@ -19,6 +19,7 @@ install-help-verbose = expliquer ce qui est fait
|
||||
install-help-preserve-context = préserver le contexte de sécurité
|
||||
install-help-context = définir le contexte de sécurité des fichiers et répertoires
|
||||
install-help-default-context = définir le contexte de sécurité SELinux du fichier de destination et de chaque répertoire créé au type par défaut
|
||||
install-help-unprivileged = ne pas nécessiter de privilèges élevés pour changer le propriétaire, le groupe ou les attributs du fichier de destination
|
||||
|
||||
# Messages d'erreur
|
||||
install-error-dir-needs-arg = { $util_name } avec -d nécessite au moins un argument.
|
||||
|
||||
@@ -62,6 +62,7 @@ pub struct Behavior {
|
||||
preserve_context: bool,
|
||||
context: Option<String>,
|
||||
default_context: bool,
|
||||
unprivileged: bool,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
@@ -163,6 +164,7 @@ static OPT_VERBOSE: &str = "verbose";
|
||||
static OPT_PRESERVE_CONTEXT: &str = "preserve-context";
|
||||
static OPT_CONTEXT: &str = "context";
|
||||
static OPT_DEFAULT_CONTEXT: &str = "default-context";
|
||||
static OPT_UNPRIVILEGED: &str = "unprivileged";
|
||||
|
||||
static ARG_FILES: &str = "files";
|
||||
|
||||
@@ -317,6 +319,13 @@ pub fn uu_app() -> Command {
|
||||
.value_hint(clap::ValueHint::AnyPath)
|
||||
.value_parser(clap::value_parser!(OsString)),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(OPT_UNPRIVILEGED)
|
||||
.short('U')
|
||||
.long(OPT_UNPRIVILEGED)
|
||||
.help(translate!("install-help-unprivileged"))
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
}
|
||||
|
||||
/// Determine behavior, given command line arguments.
|
||||
@@ -416,6 +425,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
|
||||
|
||||
let context = matches.get_one::<String>(OPT_CONTEXT).cloned();
|
||||
let default_context = matches.get_flag(OPT_DEFAULT_CONTEXT);
|
||||
let unprivileged = matches.get_flag(OPT_UNPRIVILEGED);
|
||||
|
||||
Ok(Behavior {
|
||||
main_function,
|
||||
@@ -439,6 +449,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
|
||||
preserve_context: matches.get_flag(OPT_PRESERVE_CONTEXT),
|
||||
context,
|
||||
default_context,
|
||||
unprivileged,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -479,7 +490,7 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
|
||||
|
||||
// Set SELinux context for all created directories if needed
|
||||
#[cfg(feature = "selinux")]
|
||||
if b.context.is_some() || b.default_context {
|
||||
if should_set_selinux_context(b) {
|
||||
let context = get_context_for_selinux(b);
|
||||
set_selinux_context_for_directories_install(path_to_create.as_path(), context);
|
||||
}
|
||||
@@ -498,15 +509,17 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
|
||||
continue;
|
||||
}
|
||||
|
||||
show_if_err!(chown_optional_user_group(path, b));
|
||||
if !b.unprivileged {
|
||||
show_if_err!(chown_optional_user_group(path, b));
|
||||
|
||||
// Set SELinux context for directory if needed
|
||||
#[cfg(feature = "selinux")]
|
||||
if b.default_context {
|
||||
show_if_err!(set_selinux_default_context(path));
|
||||
} else if b.context.is_some() {
|
||||
let context = get_context_for_selinux(b);
|
||||
show_if_err!(set_selinux_security_context(path, context));
|
||||
// Set SELinux context for directory if needed
|
||||
#[cfg(feature = "selinux")]
|
||||
if b.default_context {
|
||||
show_if_err!(set_selinux_default_context(path));
|
||||
} else if b.context.is_some() {
|
||||
let context = get_context_for_selinux(b);
|
||||
show_if_err!(set_selinux_security_context(path, context));
|
||||
}
|
||||
}
|
||||
}
|
||||
// If the exit code was set, or show! has been called at least once
|
||||
@@ -628,7 +641,7 @@ fn standard(mut paths: Vec<OsString>, b: &Behavior) -> UResult<()> {
|
||||
|
||||
// Set SELinux context for all created directories if needed
|
||||
#[cfg(feature = "selinux")]
|
||||
if b.context.is_some() || b.default_context {
|
||||
if should_set_selinux_context(b) {
|
||||
let context = get_context_for_selinux(b);
|
||||
set_selinux_context_for_directories_install(to_create, context);
|
||||
}
|
||||
@@ -918,7 +931,9 @@ fn set_ownership_and_permissions(to: &Path, b: &Behavior) -> UResult<()> {
|
||||
return Err(InstallError::ChmodFailed(to.to_path_buf()).into());
|
||||
}
|
||||
|
||||
chown_optional_user_group(to, b)?;
|
||||
if !b.unprivileged {
|
||||
chown_optional_user_group(to, b)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -984,16 +999,18 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
|
||||
}
|
||||
|
||||
#[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.default_context {
|
||||
set_selinux_default_context(to)
|
||||
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
|
||||
} else if b.context.is_some() {
|
||||
let context = get_context_for_selinux(b);
|
||||
set_selinux_security_context(to, context)
|
||||
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
|
||||
if !b.unprivileged {
|
||||
if b.preserve_context {
|
||||
uucore::selinux::preserve_security_context(from, to)
|
||||
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
|
||||
} else if b.default_context {
|
||||
set_selinux_default_context(to)
|
||||
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
|
||||
} else if b.context.is_some() {
|
||||
let context = get_context_for_selinux(b);
|
||||
set_selinux_security_context(to, context)
|
||||
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
if b.verbose {
|
||||
@@ -1022,6 +1039,11 @@ fn get_context_for_selinux(b: &Behavior) -> Option<&String> {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
fn should_set_selinux_context(b: &Behavior) -> bool {
|
||||
!b.unprivileged && (b.context.is_some() || b.default_context)
|
||||
}
|
||||
|
||||
/// Check if a file needs to be copied due to ownership differences when no explicit group is specified.
|
||||
/// Returns true if the destination file's ownership would differ from what it should be after installation.
|
||||
fn needs_copy_for_ownership(to: &Path, to_meta: &fs::Metadata) -> bool {
|
||||
@@ -1113,7 +1135,7 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
|
||||
}
|
||||
|
||||
#[cfg(feature = "selinux")]
|
||||
if b.preserve_context && contexts_differ(from, to) {
|
||||
if !b.unprivileged && b.preserve_context && contexts_differ(from, to) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1121,17 +1143,17 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
|
||||
|
||||
// Check if the owner ID is specified and differs from the destination file's owner.
|
||||
if let Some(owner_id) = b.owner_id {
|
||||
if owner_id != to_meta.uid() {
|
||||
if !b.unprivileged && owner_id != to_meta.uid() {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// Check if the group ID is specified and differs from the destination file's group.
|
||||
if let Some(group_id) = b.group_id {
|
||||
if group_id != to_meta.gid() {
|
||||
if !b.unprivileged && group_id != to_meta.gid() {
|
||||
return true;
|
||||
}
|
||||
} else if needs_copy_for_ownership(to, &to_meta) {
|
||||
} else if !b.unprivileged && needs_copy_for_ownership(to, &to_meta) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -2489,3 +2489,35 @@ fn test_install_non_utf8_paths() {
|
||||
|
||||
ucmd.arg("-D").arg(source_file).arg(&target_path).succeeds();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_install_unprivileged_option_u_skips_chown() {
|
||||
// This test only makes sense when not running as root.
|
||||
if geteuid() == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
let scene = TestScenario::new(util_name!());
|
||||
let at = &scene.fixtures;
|
||||
|
||||
let src = "source_file";
|
||||
let dst_fail = "target_fail";
|
||||
let dst_ok = "target_ok";
|
||||
at.touch(src);
|
||||
|
||||
// Without -U, attempting to chown to root should fail for an unprivileged user.
|
||||
let res = scene.ucmd().args(&["--owner=root", src, dst_fail]).run();
|
||||
|
||||
res.failure();
|
||||
|
||||
// With -U, install should not require elevated privileges for owner/group changes,
|
||||
// meaning it should succeed and leave ownership as the current user.
|
||||
scene
|
||||
.ucmd()
|
||||
.args(&["-U", "--owner=root", src, dst_ok])
|
||||
.succeeds()
|
||||
.no_stderr();
|
||||
|
||||
assert!(at.file_exists(dst_ok));
|
||||
assert_eq!(at.metadata(dst_ok).uid(), geteuid());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user