Skip to content

Commit 8c10759

Browse files
authored
Merge pull request #9526 from alexandrefresnais/feature/install_unprivileged
install: add FreeBSD's -U (unprivileged) option
2 parents 2d718f2 + 8c7434c commit 8c10759

File tree

5 files changed

+85
-25
lines changed

5 files changed

+85
-25
lines changed

docs/src/extensions.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,3 +200,7 @@ With `-U`/`--no-utf8`, you can interpret input files as 8-bit ASCII rather than
200200
## `expand`
201201

202202
`expand` also offers the `-U`/`--no-utf8` option to interpret input files as 8-bit ASCII instead of UTF-8.
203+
204+
## `install`
205+
206+
`install` offers FreeBSD's `-U` unprivileged option to not change the owner, the group, or the file flags of the destination.

src/uu/install/locales/en-US.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ install-help-verbose = explain what is being done
1919
install-help-preserve-context = preserve security context
2020
install-help-context = set security context of files and directories
2121
install-help-default-context = set SELinux security context of destination file and each created directory to default type
22+
install-help-unprivileged = do not require elevated privileges to change the owner, the group, or the file flags of the destination
2223
2324
# Error messages
2425
install-error-dir-needs-arg = { $util_name } with -d requires at least one argument.

src/uu/install/locales/fr-FR.ftl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ install-help-verbose = expliquer ce qui est fait
1919
install-help-preserve-context = préserver le contexte de sécurité
2020
install-help-context = définir le contexte de sécurité des fichiers et répertoires
2121
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
22+
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
2223
2324
# Messages d'erreur
2425
install-error-dir-needs-arg = { $util_name } avec -d nécessite au moins un argument.

src/uu/install/src/install.rs

Lines changed: 47 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ pub struct Behavior {
6262
preserve_context: bool,
6363
context: Option<String>,
6464
default_context: bool,
65+
unprivileged: bool,
6566
}
6667

6768
#[derive(Error, Debug)]
@@ -163,6 +164,7 @@ static OPT_VERBOSE: &str = "verbose";
163164
static OPT_PRESERVE_CONTEXT: &str = "preserve-context";
164165
static OPT_CONTEXT: &str = "context";
165166
static OPT_DEFAULT_CONTEXT: &str = "default-context";
167+
static OPT_UNPRIVILEGED: &str = "unprivileged";
166168

167169
static ARG_FILES: &str = "files";
168170

@@ -317,6 +319,13 @@ pub fn uu_app() -> Command {
317319
.value_hint(clap::ValueHint::AnyPath)
318320
.value_parser(clap::value_parser!(OsString)),
319321
)
322+
.arg(
323+
Arg::new(OPT_UNPRIVILEGED)
324+
.short('U')
325+
.long(OPT_UNPRIVILEGED)
326+
.help(translate!("install-help-unprivileged"))
327+
.action(ArgAction::SetTrue),
328+
)
320329
}
321330

322331
/// Determine behavior, given command line arguments.
@@ -416,6 +425,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
416425

417426
let context = matches.get_one::<String>(OPT_CONTEXT).cloned();
418427
let default_context = matches.get_flag(OPT_DEFAULT_CONTEXT);
428+
let unprivileged = matches.get_flag(OPT_UNPRIVILEGED);
419429

420430
Ok(Behavior {
421431
main_function,
@@ -439,6 +449,7 @@ fn behavior(matches: &ArgMatches) -> UResult<Behavior> {
439449
preserve_context: matches.get_flag(OPT_PRESERVE_CONTEXT),
440450
context,
441451
default_context,
452+
unprivileged,
442453
})
443454
}
444455

@@ -479,7 +490,7 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
479490

480491
// Set SELinux context for all created directories if needed
481492
#[cfg(feature = "selinux")]
482-
if b.context.is_some() || b.default_context {
493+
if should_set_selinux_context(b) {
483494
let context = get_context_for_selinux(b);
484495
set_selinux_context_for_directories_install(path_to_create.as_path(), context);
485496
}
@@ -498,15 +509,17 @@ fn directory(paths: &[OsString], b: &Behavior) -> UResult<()> {
498509
continue;
499510
}
500511

501-
show_if_err!(chown_optional_user_group(path, b));
512+
if !b.unprivileged {
513+
show_if_err!(chown_optional_user_group(path, b));
502514

503-
// Set SELinux context for directory if needed
504-
#[cfg(feature = "selinux")]
505-
if b.default_context {
506-
show_if_err!(set_selinux_default_context(path));
507-
} else if b.context.is_some() {
508-
let context = get_context_for_selinux(b);
509-
show_if_err!(set_selinux_security_context(path, context));
515+
// Set SELinux context for directory if needed
516+
#[cfg(feature = "selinux")]
517+
if b.default_context {
518+
show_if_err!(set_selinux_default_context(path));
519+
} else if b.context.is_some() {
520+
let context = get_context_for_selinux(b);
521+
show_if_err!(set_selinux_security_context(path, context));
522+
}
510523
}
511524
}
512525
// 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<()> {
628641

629642
// Set SELinux context for all created directories if needed
630643
#[cfg(feature = "selinux")]
631-
if b.context.is_some() || b.default_context {
644+
if should_set_selinux_context(b) {
632645
let context = get_context_for_selinux(b);
633646
set_selinux_context_for_directories_install(to_create, context);
634647
}
@@ -918,7 +931,9 @@ fn set_ownership_and_permissions(to: &Path, b: &Behavior) -> UResult<()> {
918931
return Err(InstallError::ChmodFailed(to.to_path_buf()).into());
919932
}
920933

921-
chown_optional_user_group(to, b)?;
934+
if !b.unprivileged {
935+
chown_optional_user_group(to, b)?;
936+
}
922937

923938
Ok(())
924939
}
@@ -984,16 +999,18 @@ fn copy(from: &Path, to: &Path, b: &Behavior) -> UResult<()> {
984999
}
9851000

9861001
#[cfg(feature = "selinux")]
987-
if b.preserve_context {
988-
uucore::selinux::preserve_security_context(from, to)
989-
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
990-
} else if b.default_context {
991-
set_selinux_default_context(to)
992-
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
993-
} else if b.context.is_some() {
994-
let context = get_context_for_selinux(b);
995-
set_selinux_security_context(to, context)
996-
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
1002+
if !b.unprivileged {
1003+
if b.preserve_context {
1004+
uucore::selinux::preserve_security_context(from, to)
1005+
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
1006+
} else if b.default_context {
1007+
set_selinux_default_context(to)
1008+
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
1009+
} else if b.context.is_some() {
1010+
let context = get_context_for_selinux(b);
1011+
set_selinux_security_context(to, context)
1012+
.map_err(|e| InstallError::SelinuxContextFailed(e.to_string()))?;
1013+
}
9971014
}
9981015

9991016
if b.verbose {
@@ -1022,6 +1039,11 @@ fn get_context_for_selinux(b: &Behavior) -> Option<&String> {
10221039
}
10231040
}
10241041

1042+
#[cfg(feature = "selinux")]
1043+
fn should_set_selinux_context(b: &Behavior) -> bool {
1044+
!b.unprivileged && (b.context.is_some() || b.default_context)
1045+
}
1046+
10251047
/// Check if a file needs to be copied due to ownership differences when no explicit group is specified.
10261048
/// Returns true if the destination file's ownership would differ from what it should be after installation.
10271049
fn needs_copy_for_ownership(to: &Path, to_meta: &fs::Metadata) -> bool {
@@ -1113,25 +1135,25 @@ fn need_copy(from: &Path, to: &Path, b: &Behavior) -> bool {
11131135
}
11141136

11151137
#[cfg(feature = "selinux")]
1116-
if b.preserve_context && contexts_differ(from, to) {
1138+
if !b.unprivileged && b.preserve_context && contexts_differ(from, to) {
11171139
return true;
11181140
}
11191141

11201142
// TODO: if -P (#1809) and from/to contexts mismatch, return true.
11211143

11221144
// Check if the owner ID is specified and differs from the destination file's owner.
11231145
if let Some(owner_id) = b.owner_id {
1124-
if owner_id != to_meta.uid() {
1146+
if !b.unprivileged && owner_id != to_meta.uid() {
11251147
return true;
11261148
}
11271149
}
11281150

11291151
// Check if the group ID is specified and differs from the destination file's group.
11301152
if let Some(group_id) = b.group_id {
1131-
if group_id != to_meta.gid() {
1153+
if !b.unprivileged && group_id != to_meta.gid() {
11321154
return true;
11331155
}
1134-
} else if needs_copy_for_ownership(to, &to_meta) {
1156+
} else if !b.unprivileged && needs_copy_for_ownership(to, &to_meta) {
11351157
return true;
11361158
}
11371159

tests/by-util/test_install.rs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2513,3 +2513,35 @@ fn test_install_non_utf8_paths() {
25132513

25142514
ucmd.arg("-D").arg(source_file).arg(&target_path).succeeds();
25152515
}
2516+
2517+
#[test]
2518+
fn test_install_unprivileged_option_u_skips_chown() {
2519+
// This test only makes sense when not running as root.
2520+
if geteuid() == 0 {
2521+
return;
2522+
}
2523+
2524+
let scene = TestScenario::new(util_name!());
2525+
let at = &scene.fixtures;
2526+
2527+
let src = "source_file";
2528+
let dst_fail = "target_fail";
2529+
let dst_ok = "target_ok";
2530+
at.touch(src);
2531+
2532+
// Without -U, attempting to chown to root should fail for an unprivileged user.
2533+
let res = scene.ucmd().args(&["--owner=root", src, dst_fail]).run();
2534+
2535+
res.failure();
2536+
2537+
// With -U, install should not require elevated privileges for owner/group changes,
2538+
// meaning it should succeed and leave ownership as the current user.
2539+
scene
2540+
.ucmd()
2541+
.args(&["-U", "--owner=root", src, dst_ok])
2542+
.succeeds()
2543+
.no_stderr();
2544+
2545+
assert!(at.file_exists(dst_ok));
2546+
assert_eq!(at.metadata(dst_ok).uid(), geteuid());
2547+
}

0 commit comments

Comments
 (0)