mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
shadow-core, tools: source the EACCES message from the OS (#159)
The manual root-privilege guards across the account tools printed a hardcoded "Permission denied." string. Route that text through libc's strerror instead (via std::io::Error::from_raw_os_error), so the wording comes from the host OS and is translated by glibc on localized systems — the same way GNU coreutils renders system errors (e.g. cat: Is a directory). This also keeps the string out of our source tree, shrinking the residual clean-room surface that motivated #159. Add shadow_core::os_error with strerror(errno) and permission_denied(), and convert the 13 caller_is_root() guards in chage, chpasswd, groupadd, groupdel, groupmod, passwd, useradd, userdel, usermod to use it. Scope: this covers the errno-mappable class only. True I/O errors already render the OS strerror via ShadowError::Io/IoPath. Domain errors (e.g. "group already exists") have no OS equivalent and stay in-tree. The gettext-catalog idea was evaluated and declined in #159 (the English msgid must remain in source as the lookup key, so it does not remove the exposure). Note: output changes from "Permission denied." to the OS text "Permission denied" (no trailing period; translated off-English). No tests asserted the old literal.
This commit is contained in:
@@ -10,6 +10,7 @@
|
||||
|
||||
pub mod cli;
|
||||
pub mod error;
|
||||
pub mod os_error;
|
||||
pub mod passwd;
|
||||
pub mod validate;
|
||||
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of the shadow-rs package.
|
||||
//
|
||||
// For the full copyright and license information, please view the LICENSE
|
||||
// file that was distributed with this source code.
|
||||
|
||||
//! Error text sourced from the operating system instead of hardcoded.
|
||||
//!
|
||||
//! Wording for conditions that map to a libc `errno` is taken from
|
||||
//! `strerror` (via [`std::io::Error`]) rather than carried as a string
|
||||
//! literal in our tree. This keeps the text matching the host OS and lets
|
||||
//! glibc translate it on localized systems — the same way GNU coreutils
|
||||
//! renders system errors (e.g. `cat: /tmp: Is a directory`). See issue #159.
|
||||
|
||||
/// The operating system's message for a raw `errno`.
|
||||
///
|
||||
/// For example `EACCES` renders as "Permission denied" on English locales
|
||||
/// and the translated equivalent elsewhere. On targets whose libc does not
|
||||
/// translate (musl), this is the untranslated English text.
|
||||
#[must_use]
|
||||
pub fn strerror(errno: i32) -> String {
|
||||
std::io::Error::from_raw_os_error(errno).to_string()
|
||||
}
|
||||
|
||||
/// The OS message for `EACCES` ("Permission denied"), sourced from libc.
|
||||
#[must_use]
|
||||
pub fn permission_denied() -> String {
|
||||
strerror(libc::EACCES)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn permission_denied_is_nonempty_and_os_sourced() {
|
||||
// We assert the shape, not the exact text: the wording comes from the
|
||||
// host libc and may be localized, so hardcoding it would defeat the
|
||||
// purpose of this module.
|
||||
let msg = permission_denied();
|
||||
assert!(!msg.is_empty());
|
||||
// Same value libc would give for EACCES directly.
|
||||
assert_eq!(msg, strerror(libc::EACCES));
|
||||
}
|
||||
}
|
||||
@@ -274,7 +274,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let current_user = shadow_core::hardening::current_username()
|
||||
.map_err(|e| ChageError::UnexpectedFailure(e.to_string()))?;
|
||||
if current_user != *login {
|
||||
return Err(ChageError::PermissionDenied("Permission denied.".into()).into());
|
||||
return Err(ChageError::PermissionDenied(
|
||||
shadow_core::os_error::permission_denied(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
return cmd_list(&root, login);
|
||||
@@ -282,7 +285,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
// All modification flags require root.
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
return Err(ChageError::PermissionDenied("Permission denied.".into()).into());
|
||||
return Err(
|
||||
ChageError::PermissionDenied(shadow_core::os_error::permission_denied()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
if !has_modifications {
|
||||
|
||||
@@ -186,7 +186,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
// chpasswd always requires root.
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
return Err(ChpasswdError::PermissionDenied("Permission denied.".into()).into());
|
||||
return Err(
|
||||
ChpasswdError::PermissionDenied(shadow_core::os_error::permission_denied()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
let is_encrypted = matches.get_flag(options::ENCRYPTED);
|
||||
|
||||
@@ -109,7 +109,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
};
|
||||
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
uucore::show_error!("Permission denied.");
|
||||
uucore::show_error!("{}", shadow_core::os_error::permission_denied());
|
||||
return Err(GroupaddError::AlreadyPrinted(1).into());
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
};
|
||||
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
uucore::show_error!("Permission denied.");
|
||||
uucore::show_error!("{}", shadow_core::os_error::permission_denied());
|
||||
return Err(GroupdelError::AlreadyPrinted(1).into());
|
||||
}
|
||||
|
||||
|
||||
@@ -109,7 +109,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
};
|
||||
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
uucore::show_error!("Permission denied.");
|
||||
uucore::show_error!("{}", shadow_core::os_error::permission_denied());
|
||||
return Err(GroupmodError::AlreadyPrinted(1).into());
|
||||
}
|
||||
|
||||
|
||||
@@ -188,12 +188,18 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
// Non-root users can only view their own status.
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
if show_all {
|
||||
return Err(PasswdError::PermissionDenied("Permission denied.".into()).into());
|
||||
return Err(PasswdError::PermissionDenied(
|
||||
shadow_core::os_error::permission_denied(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
let current_user = shadow_core::hardening::current_username()
|
||||
.map_err(|e| PasswdError::UnexpectedFailure(e.to_string()))?;
|
||||
if current_user != target_user {
|
||||
return Err(PasswdError::PermissionDenied("Permission denied.".into()).into());
|
||||
return Err(PasswdError::PermissionDenied(
|
||||
shadow_core::os_error::permission_denied(),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,7 +224,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
// caller to be root. Non-root users can only change their own password
|
||||
// (the default PAM path below).
|
||||
if (has_mutation || has_aging) && !shadow_core::hardening::caller_is_root() {
|
||||
return Err(PasswdError::PermissionDenied("Permission denied.".into()).into());
|
||||
return Err(
|
||||
PasswdError::PermissionDenied(shadow_core::os_error::permission_denied()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
// When a mutation flag and aging flags are both present, apply both in a
|
||||
|
||||
@@ -290,7 +290,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
// Only root can add users.
|
||||
if !shadow_core::hardening::caller_is_root() {
|
||||
uucore::show_error!("Permission denied.");
|
||||
uucore::show_error!("{}", shadow_core::os_error::permission_denied());
|
||||
return Err(UseraddError::AlreadyPrinted(1).into());
|
||||
}
|
||||
|
||||
|
||||
@@ -97,7 +97,9 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
|
||||
// Must be root.
|
||||
if !rustix::process::getuid().is_root() {
|
||||
return Err(UserdelError::CantUpdatePasswd("Permission denied.".into()).into());
|
||||
return Err(
|
||||
UserdelError::CantUpdatePasswd(shadow_core::os_error::permission_denied()).into(),
|
||||
);
|
||||
}
|
||||
|
||||
// Read the user's home directory and UID from /etc/passwd BEFORE removing
|
||||
|
||||
@@ -99,7 +99,7 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let root = SysRoot::new(prefix);
|
||||
|
||||
if !rustix::process::getuid().is_root() {
|
||||
return Err(UsermodError::CantUpdate("Permission denied.".into()).into());
|
||||
return Err(UsermodError::CantUpdate(shadow_core::os_error::permission_denied()).into());
|
||||
}
|
||||
|
||||
// Block signals for the passwd lock→write critical section only.
|
||||
|
||||
Reference in New Issue
Block a user