mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
phase3: implement groupadd, groupdel, groupmod, grpck, chfn, chsh, newgrp
All 14 shadow-utils tools are now implemented. groupadd: create groups with auto GID allocation, system groups, force mode. groupdel: delete groups, checks for primary group usage. groupmod: modify GID, rename, password changes. grpck: verify /etc/group and /etc/gshadow integrity. chfn: change GECOS sub-fields (name, room, phone). chsh: change login shell with /etc/shells validation. newgrp: change effective group with crypt(3) password verification. 449 tests, zero clippy warnings, all 14 tools in multicall binary.
This commit is contained in:
+39
-1
@@ -19,6 +19,13 @@ members = [
|
||||
"src/uu/useradd",
|
||||
"src/uu/userdel",
|
||||
"src/uu/usermod",
|
||||
"src/uu/groupadd",
|
||||
"src/uu/groupdel",
|
||||
"src/uu/groupmod",
|
||||
"src/uu/grpck",
|
||||
"src/uu/chfn",
|
||||
"src/uu/chsh",
|
||||
"src/uu/newgrp",
|
||||
"src/uu/chpasswd",
|
||||
"src/uu/chage",
|
||||
]
|
||||
@@ -63,9 +70,17 @@ userdel = { optional = true, version = "0.0.1", package = "uu_userdel", path = "
|
||||
usermod = { optional = true, version = "0.0.1", package = "uu_usermod", path = "src/uu/usermod" }
|
||||
chpasswd = { optional = true, version = "0.0.1", package = "uu_chpasswd", path = "src/uu/chpasswd" }
|
||||
chage = { optional = true, version = "0.0.1", package = "uu_chage", path = "src/uu/chage" }
|
||||
groupadd = { optional = true, version = "0.0.1", package = "uu_groupadd", path = "src/uu/groupadd" }
|
||||
groupdel = { optional = true, version = "0.0.1", package = "uu_groupdel", path = "src/uu/groupdel" }
|
||||
groupmod = { optional = true, version = "0.0.1", package = "uu_groupmod", path = "src/uu/groupmod" }
|
||||
grpck = { optional = true, version = "0.0.1", package = "uu_grpck", path = "src/uu/grpck" }
|
||||
chfn = { optional = true, version = "0.0.1", package = "uu_chfn", path = "src/uu/chfn" }
|
||||
chsh = { optional = true, version = "0.0.1", package = "uu_chsh", path = "src/uu/chsh" }
|
||||
newgrp = { optional = true, version = "0.0.1", package = "uu_newgrp", path = "src/uu/newgrp" }
|
||||
|
||||
[features]
|
||||
default = ["passwd", "pwck", "useradd", "userdel", "usermod", "chpasswd", "chage"]
|
||||
default = ["passwd", "pwck", "useradd", "userdel", "usermod", "chpasswd", "chage",
|
||||
"groupadd", "groupdel", "groupmod", "grpck", "chfn", "chsh", "newgrp"]
|
||||
|
||||
# Individual tools
|
||||
feat_passwd = ["passwd"]
|
||||
@@ -75,10 +90,18 @@ feat_userdel = ["userdel"]
|
||||
feat_usermod = ["usermod"]
|
||||
feat_chpasswd = ["chpasswd"]
|
||||
feat_chage = ["chage"]
|
||||
feat_groupadd = ["groupadd"]
|
||||
feat_groupdel = ["groupdel"]
|
||||
feat_groupmod = ["groupmod"]
|
||||
feat_grpck = ["grpck"]
|
||||
feat_chfn = ["chfn"]
|
||||
feat_chsh = ["chsh"]
|
||||
feat_newgrp = ["newgrp"]
|
||||
|
||||
# Grouped features
|
||||
feat_phase1 = ["feat_passwd", "feat_pwck"]
|
||||
feat_phase2 = ["feat_useradd", "feat_userdel", "feat_usermod", "feat_chpasswd", "feat_chage"]
|
||||
feat_phase3 = ["feat_groupadd", "feat_groupdel", "feat_groupmod", "feat_grpck", "feat_chfn", "feat_chsh", "feat_newgrp"]
|
||||
feat_common = ["feat_phase1", "feat_phase2"]
|
||||
|
||||
[[bin]]
|
||||
@@ -97,9 +120,24 @@ path = "tests/by-util/test_chpasswd.rs"
|
||||
name = "test_passwd"
|
||||
path = "tests/by-util/test_passwd.rs"
|
||||
|
||||
[[test]]
|
||||
name = "test_chfn"
|
||||
path = "tests/by-util/test_chfn.rs"
|
||||
|
||||
[[test]]
|
||||
name = "test_chsh"
|
||||
path = "tests/by-util/test_chsh.rs"
|
||||
|
||||
[[test]]
|
||||
name = "test_newgrp"
|
||||
path = "tests/by-util/test_newgrp.rs"
|
||||
|
||||
[dev-dependencies]
|
||||
chage = { version = "0.0.1", package = "uu_chage", path = "src/uu/chage" }
|
||||
chfn = { version = "0.0.1", package = "uu_chfn", path = "src/uu/chfn" }
|
||||
chpasswd = { version = "0.0.1", package = "uu_chpasswd", path = "src/uu/chpasswd" }
|
||||
chsh = { version = "0.0.1", package = "uu_chsh", path = "src/uu/chsh" }
|
||||
newgrp = { version = "0.0.1", package = "uu_newgrp", path = "src/uu/newgrp" }
|
||||
shadow-core = { path = "src/shadow-core", features = ["shadow"] }
|
||||
nix = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
|
||||
@@ -54,13 +54,13 @@ default-in-Ubuntu in under 3 years. shadow-rs follows that playbook.
|
||||
| `usermod` | **Implemented.** Modify all properties, group membership, lock/unlock. |
|
||||
| `chpasswd` | **Implemented.** Batch password change from stdin. |
|
||||
| `chage` | **Implemented.** Password aging management, `-l` list mode. |
|
||||
| `groupadd` | Planned (Phase 3) |
|
||||
| `groupdel` | Planned (Phase 3) |
|
||||
| `groupmod` | Planned (Phase 3) |
|
||||
| `grpck` | Planned (Phase 3) |
|
||||
| `chfn` | Planned (Phase 3) |
|
||||
| `chsh` | Planned (Phase 3) |
|
||||
| `newgrp` | Planned (Phase 3) |
|
||||
| `groupadd` | **Implemented.** Auto GID allocation, system groups, force mode. |
|
||||
| `groupdel` | **Implemented.** Primary group usage check. |
|
||||
| `groupmod` | **Implemented.** GID change, rename, password. |
|
||||
| `grpck` | **Implemented.** Group/gshadow integrity verification. |
|
||||
| `chfn` | **Implemented.** GECOS sub-field modification. |
|
||||
| `chsh` | **Implemented.** Shell change with /etc/shells validation. |
|
||||
| `newgrp` | **Implemented.** Effective group change with crypt verification. |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -8,6 +8,11 @@
|
||||
//! Dispatches to the appropriate utility based on `argv[0]`.
|
||||
//! When invoked as `shadow-rs <util>`, uses the first argument instead.
|
||||
|
||||
// newgrp uses crypt(3) from libcrypt — ensure the linker includes it.
|
||||
#[cfg(feature = "newgrp")]
|
||||
#[link(name = "crypt")]
|
||||
extern "C" {}
|
||||
|
||||
use std::path::Path;
|
||||
|
||||
fn main() {
|
||||
@@ -54,12 +59,32 @@ fn dispatch(name: &str, args: &[std::ffi::OsString]) -> Option<i32> {
|
||||
match name {
|
||||
#[cfg(feature = "chage")]
|
||||
"chage" => Some(chage::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "chfn")]
|
||||
"chfn" => Some(chfn::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "chpasswd")]
|
||||
"chpasswd" => Some(chpasswd::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "chsh")]
|
||||
"chsh" => Some(chsh::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "groupadd")]
|
||||
"groupadd" => Some(groupadd::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "groupdel")]
|
||||
"groupdel" => Some(groupdel::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "groupmod")]
|
||||
"groupmod" => Some(groupmod::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "grpck")]
|
||||
"grpck" => Some(grpck::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "newgrp")]
|
||||
"newgrp" => Some(newgrp::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "passwd")]
|
||||
"passwd" => Some(passwd::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "pwck")]
|
||||
"pwck" => Some(pwck::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "useradd")]
|
||||
"useradd" => Some(useradd::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "userdel")]
|
||||
"userdel" => Some(userdel::uumain(args.iter().cloned())),
|
||||
#[cfg(feature = "usermod")]
|
||||
"usermod" => Some(usermod::uumain(args.iter().cloned())),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -69,10 +94,30 @@ fn print_available_utils() {
|
||||
|
||||
#[cfg(feature = "chage")]
|
||||
println!(" chage");
|
||||
#[cfg(feature = "chfn")]
|
||||
println!(" chfn");
|
||||
#[cfg(feature = "chpasswd")]
|
||||
println!(" chpasswd");
|
||||
#[cfg(feature = "chsh")]
|
||||
println!(" chsh");
|
||||
#[cfg(feature = "groupadd")]
|
||||
println!(" groupadd");
|
||||
#[cfg(feature = "groupdel")]
|
||||
println!(" groupdel");
|
||||
#[cfg(feature = "groupmod")]
|
||||
println!(" groupmod");
|
||||
#[cfg(feature = "grpck")]
|
||||
println!(" grpck");
|
||||
#[cfg(feature = "newgrp")]
|
||||
println!(" newgrp");
|
||||
#[cfg(feature = "passwd")]
|
||||
println!(" passwd");
|
||||
#[cfg(feature = "pwck")]
|
||||
println!(" pwck");
|
||||
#[cfg(feature = "useradd")]
|
||||
println!(" useradd");
|
||||
#[cfg(feature = "userdel")]
|
||||
println!(" userdel");
|
||||
#[cfg(feature = "usermod")]
|
||||
println!(" usermod");
|
||||
}
|
||||
|
||||
@@ -84,6 +84,12 @@ impl SysRoot {
|
||||
pub fn skel_path(&self) -> PathBuf {
|
||||
self.resolve("/etc/skel")
|
||||
}
|
||||
|
||||
/// Path to `/etc/shells`.
|
||||
#[must_use]
|
||||
pub fn shells_path(&self) -> PathBuf {
|
||||
self.resolve("/etc/shells")
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SysRoot {
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "uu_chfn"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "chfn command (shadow-rs)"
|
||||
|
||||
[lib]
|
||||
path = "src/chfn.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "chfn"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true }
|
||||
uucore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
uucore::bin!(uu_chfn);
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "uu_chsh"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "chsh command (shadow-rs)"
|
||||
|
||||
[lib]
|
||||
path = "src/chsh.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "chsh"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true }
|
||||
uucore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
uucore::bin!(uu_chsh);
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "uu_groupadd"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "groupadd command (shadow-rs)"
|
||||
|
||||
[lib]
|
||||
path = "src/groupadd.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "groupadd"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
|
||||
uucore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
uucore::bin!(uu_groupadd);
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "uu_groupdel"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "groupdel command (shadow-rs)"
|
||||
|
||||
[lib]
|
||||
path = "src/groupdel.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "groupdel"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
|
||||
uucore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,374 @@
|
||||
// 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.
|
||||
// spell-checker:ignore groupdel gshadow nscd sysroot
|
||||
|
||||
//! `groupdel` -- delete a group.
|
||||
//!
|
||||
//! Drop-in replacement for GNU shadow-utils `groupdel(8)`.
|
||||
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use clap::{Arg, Command};
|
||||
use uucore::error::{UError, UResult};
|
||||
|
||||
use shadow_core::atomic;
|
||||
use shadow_core::group::{self, GroupEntry};
|
||||
use shadow_core::gshadow::{self, GshadowEntry};
|
||||
use shadow_core::lock::FileLock;
|
||||
use shadow_core::nscd;
|
||||
use shadow_core::passwd;
|
||||
use shadow_core::sysroot::SysRoot;
|
||||
|
||||
mod options {
|
||||
pub const GROUP: &str = "GROUP";
|
||||
pub const ROOT: &str = "root";
|
||||
pub const PREFIX: &str = "prefix";
|
||||
}
|
||||
|
||||
mod exit_codes {
|
||||
pub const BAD_SYNTAX: i32 = 2;
|
||||
pub const GROUP_NOT_FOUND: i32 = 6;
|
||||
pub const PRIMARY_GROUP: i32 = 8;
|
||||
pub const CANT_UPDATE: i32 = 10;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GroupdelError {
|
||||
BadSyntax(String),
|
||||
GroupNotFound(String),
|
||||
PrimaryGroup(String),
|
||||
CantUpdate(String),
|
||||
AlreadyPrinted(i32),
|
||||
}
|
||||
|
||||
impl fmt::Display for GroupdelError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::BadSyntax(msg)
|
||||
| Self::GroupNotFound(msg)
|
||||
| Self::PrimaryGroup(msg)
|
||||
| Self::CantUpdate(msg) => f.write_str(msg),
|
||||
Self::AlreadyPrinted(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for GroupdelError {}
|
||||
|
||||
impl UError for GroupdelError {
|
||||
fn code(&self) -> i32 {
|
||||
match self {
|
||||
Self::BadSyntax(_) => exit_codes::BAD_SYNTAX,
|
||||
Self::GroupNotFound(_) => exit_codes::GROUP_NOT_FOUND,
|
||||
Self::PrimaryGroup(_) => exit_codes::PRIMARY_GROUP,
|
||||
Self::CantUpdate(_) => exit_codes::CANT_UPDATE,
|
||||
Self::AlreadyPrinted(c) => *c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Security hardening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn suppress_core_dumps() {
|
||||
let _ = nix::sys::resource::setrlimit(nix::sys::resource::Resource::RLIMIT_CORE, 0, 0);
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// SAFETY: prctl with PR_SET_DUMPABLE is a simple flag set, no pointers.
|
||||
unsafe {
|
||||
libc::prctl(libc::PR_SET_DUMPABLE, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raise_file_size_limit() {
|
||||
let _ = nix::sys::resource::setrlimit(
|
||||
nix::sys::resource::Resource::RLIMIT_FSIZE,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
);
|
||||
}
|
||||
|
||||
fn sanitize_env() {
|
||||
let saved: Vec<(String, String)> = std::env::vars()
|
||||
.filter(|(k, _)| k == "TERM" || k == "LANG" || k.starts_with("LC_"))
|
||||
.collect();
|
||||
|
||||
let keys: Vec<std::ffi::OsString> = std::env::vars_os().map(|(k, _)| k).collect();
|
||||
for key in keys {
|
||||
std::env::remove_var(&key);
|
||||
}
|
||||
|
||||
std::env::set_var("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
|
||||
|
||||
for (key, val) in saved {
|
||||
std::env::set_var(&key, &val);
|
||||
}
|
||||
}
|
||||
|
||||
fn caller_is_root() -> bool {
|
||||
nix::unistd::getuid().is_root()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[uucore::main]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
suppress_core_dumps();
|
||||
raise_file_size_limit();
|
||||
sanitize_env();
|
||||
|
||||
let matches = match uu_app().try_get_matches_from(args) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
e.print().ok();
|
||||
if !e.use_stderr() {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(GroupdelError::AlreadyPrinted(exit_codes::BAD_SYNTAX).into());
|
||||
}
|
||||
};
|
||||
|
||||
if !caller_is_root() {
|
||||
uucore::show_error!("Permission denied.");
|
||||
return Err(GroupdelError::AlreadyPrinted(1).into());
|
||||
}
|
||||
|
||||
let group_name = matches
|
||||
.get_one::<String>(options::GROUP)
|
||||
.ok_or_else(|| GroupdelError::BadSyntax("group name required".into()))?;
|
||||
|
||||
let prefix = matches.get_one::<String>(options::PREFIX).map(Path::new);
|
||||
let root_dir = matches.get_one::<String>(options::ROOT).map(Path::new);
|
||||
let root = SysRoot::new(prefix.or(root_dir));
|
||||
|
||||
// Read existing groups to find the target.
|
||||
let group_path = root.group_path();
|
||||
let group_lock = FileLock::acquire(&group_path).map_err(|e| {
|
||||
GroupdelError::CantUpdate(format!("cannot lock {}: {e}", group_path.display()))
|
||||
})?;
|
||||
|
||||
let entries = group::read_group_file(&group_path).map_err(|e| {
|
||||
GroupdelError::CantUpdate(format!("cannot read {}: {e}", group_path.display()))
|
||||
})?;
|
||||
|
||||
let target = entries
|
||||
.iter()
|
||||
.find(|g| g.name == *group_name)
|
||||
.ok_or_else(|| {
|
||||
GroupdelError::GroupNotFound(format!("group '{group_name}' does not exist"))
|
||||
})?;
|
||||
|
||||
let target_gid = target.gid;
|
||||
|
||||
// Check that no user has this group as their primary group.
|
||||
let passwd_path = root.passwd_path();
|
||||
if passwd_path.exists() {
|
||||
let passwd_entries = passwd::read_passwd_file(&passwd_path).map_err(|e| {
|
||||
GroupdelError::CantUpdate(format!("cannot read {}: {e}", passwd_path.display()))
|
||||
})?;
|
||||
|
||||
if let Some(user) = passwd_entries.iter().find(|u| u.gid == target_gid) {
|
||||
drop(group_lock);
|
||||
return Err(GroupdelError::PrimaryGroup(format!(
|
||||
"cannot remove the primary group of user '{}'",
|
||||
user.name
|
||||
))
|
||||
.into());
|
||||
}
|
||||
}
|
||||
|
||||
// Remove the group entry.
|
||||
let new_entries: Vec<GroupEntry> = entries
|
||||
.into_iter()
|
||||
.filter(|g| g.name != *group_name)
|
||||
.collect();
|
||||
|
||||
atomic::atomic_write(&group_path, |f| group::write_group(&new_entries, f)).map_err(|e| {
|
||||
GroupdelError::CantUpdate(format!("cannot write {}: {e}", group_path.display()))
|
||||
})?;
|
||||
|
||||
drop(group_lock);
|
||||
|
||||
// Remove from /etc/gshadow.
|
||||
let gshadow_path = root.gshadow_path();
|
||||
if gshadow_path.exists() {
|
||||
let gs_lock = FileLock::acquire(&gshadow_path).map_err(|e| {
|
||||
GroupdelError::CantUpdate(format!("cannot lock {}: {e}", gshadow_path.display()))
|
||||
})?;
|
||||
|
||||
let gs_entries = gshadow::read_gshadow_file(&gshadow_path).map_err(|e| {
|
||||
GroupdelError::CantUpdate(format!("cannot read {}: {e}", gshadow_path.display()))
|
||||
})?;
|
||||
|
||||
let new_gs: Vec<GshadowEntry> = gs_entries
|
||||
.into_iter()
|
||||
.filter(|g| g.name != *group_name)
|
||||
.collect();
|
||||
|
||||
// Only write if we actually had gshadow entries to begin with.
|
||||
if !new_gs.is_empty() {
|
||||
atomic::atomic_write(&gshadow_path, |f| gshadow::write_gshadow(&new_gs, f)).map_err(
|
||||
|e| {
|
||||
GroupdelError::CantUpdate(format!(
|
||||
"cannot write {}: {e}",
|
||||
gshadow_path.display()
|
||||
))
|
||||
},
|
||||
)?;
|
||||
}
|
||||
|
||||
drop(gs_lock);
|
||||
}
|
||||
|
||||
nscd::invalidate_cache("group");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn uu_app() -> Command {
|
||||
Command::new("groupdel")
|
||||
.about("Delete a group")
|
||||
.override_usage("groupdel [options] GROUP")
|
||||
.arg(
|
||||
Arg::new(options::ROOT)
|
||||
.short('R')
|
||||
.long("root")
|
||||
.value_name("CHROOT_DIR")
|
||||
.help("Apply changes in the CHROOT_DIR directory"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::PREFIX)
|
||||
.short('P')
|
||||
.long("prefix")
|
||||
.value_name("PREFIX_DIR")
|
||||
.help("Directory prefix"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::GROUP)
|
||||
.required(true)
|
||||
.index(1)
|
||||
.help("Name of the group to delete"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn test_app_builds() {
|
||||
uu_app().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_required() {
|
||||
assert!(uu_app().try_get_matches_from(["groupdel"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_prefix_flag() {
|
||||
let m = uu_app()
|
||||
.try_get_matches_from(["groupdel", "-P", "/mnt", "testgrp"])
|
||||
.expect("valid args");
|
||||
assert_eq!(
|
||||
m.get_one::<String>(options::PREFIX).map(String::as_str),
|
||||
Some("/mnt")
|
||||
);
|
||||
}
|
||||
|
||||
fn skip_unless_root() -> bool {
|
||||
!nix::unistd::geteuid().is_root()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_group() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(
|
||||
etc.join("group"),
|
||||
"root:x:0:\ntestgrp:x:1000:\nother:x:1001:\n",
|
||||
)
|
||||
.expect("write group");
|
||||
std::fs::write(etc.join("passwd"), "root:x:0:0:root:/root:/bin/bash\n")
|
||||
.expect("write passwd");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupdel".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"testgrp".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_eq!(code, 0);
|
||||
|
||||
let content = std::fs::read_to_string(etc.join("group")).expect("read group");
|
||||
assert!(!content.contains("testgrp"));
|
||||
assert!(content.contains("root:x:0:"));
|
||||
assert!(content.contains("other:x:1001:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_nonexistent_group() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(etc.join("group"), "root:x:0:\n").expect("write group");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupdel".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"nogrp".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_ne!(code, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cannot_delete_primary_group() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(etc.join("group"), "testgrp:x:1000:\n").expect("write group");
|
||||
std::fs::write(
|
||||
etc.join("passwd"),
|
||||
"testuser:x:1000:1000::/home/testuser:/bin/bash\n",
|
||||
)
|
||||
.expect("write passwd");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupdel".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"testgrp".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_ne!(code, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
uucore::bin!(uu_groupdel);
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "uu_groupmod"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "groupmod command (shadow-rs)"
|
||||
|
||||
[lib]
|
||||
path = "src/groupmod.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "groupmod"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
|
||||
uucore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
@@ -0,0 +1,471 @@
|
||||
// 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.
|
||||
// spell-checker:ignore groupmod gshadow nscd sysroot
|
||||
|
||||
//! `groupmod` -- modify a group definition.
|
||||
//!
|
||||
//! Drop-in replacement for GNU shadow-utils `groupmod(8)`.
|
||||
|
||||
use std::fmt;
|
||||
use std::path::Path;
|
||||
|
||||
use clap::{Arg, ArgAction, Command};
|
||||
use uucore::error::{UError, UResult};
|
||||
|
||||
use shadow_core::atomic;
|
||||
use shadow_core::group::{self};
|
||||
use shadow_core::gshadow::{self};
|
||||
use shadow_core::lock::FileLock;
|
||||
use shadow_core::nscd;
|
||||
use shadow_core::sysroot::SysRoot;
|
||||
|
||||
mod options {
|
||||
pub const GROUP: &str = "GROUP";
|
||||
pub const GID: &str = "gid";
|
||||
pub const NEW_NAME: &str = "new-name";
|
||||
pub const NON_UNIQUE: &str = "non-unique";
|
||||
pub const PASSWORD: &str = "password";
|
||||
pub const ROOT: &str = "root";
|
||||
pub const PREFIX: &str = "prefix";
|
||||
}
|
||||
|
||||
mod exit_codes {
|
||||
pub const BAD_SYNTAX: i32 = 2;
|
||||
pub const BAD_ARGUMENT: i32 = 3;
|
||||
pub const GID_IN_USE: i32 = 4;
|
||||
pub const GROUP_NOT_FOUND: i32 = 6;
|
||||
pub const NAME_IN_USE: i32 = 9;
|
||||
pub const CANT_UPDATE: i32 = 10;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum GroupmodError {
|
||||
BadSyntax(String),
|
||||
BadArgument(String),
|
||||
GidInUse(String),
|
||||
GroupNotFound(String),
|
||||
NameInUse(String),
|
||||
CantUpdate(String),
|
||||
AlreadyPrinted(i32),
|
||||
}
|
||||
|
||||
impl fmt::Display for GroupmodError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::BadSyntax(msg)
|
||||
| Self::BadArgument(msg)
|
||||
| Self::GidInUse(msg)
|
||||
| Self::GroupNotFound(msg)
|
||||
| Self::NameInUse(msg)
|
||||
| Self::CantUpdate(msg) => f.write_str(msg),
|
||||
Self::AlreadyPrinted(_) => Ok(()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for GroupmodError {}
|
||||
|
||||
impl UError for GroupmodError {
|
||||
fn code(&self) -> i32 {
|
||||
match self {
|
||||
Self::BadSyntax(_) => exit_codes::BAD_SYNTAX,
|
||||
Self::BadArgument(_) => exit_codes::BAD_ARGUMENT,
|
||||
Self::GidInUse(_) => exit_codes::GID_IN_USE,
|
||||
Self::GroupNotFound(_) => exit_codes::GROUP_NOT_FOUND,
|
||||
Self::NameInUse(_) => exit_codes::NAME_IN_USE,
|
||||
Self::CantUpdate(_) => exit_codes::CANT_UPDATE,
|
||||
Self::AlreadyPrinted(c) => *c,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Security hardening
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
fn suppress_core_dumps() {
|
||||
let _ = nix::sys::resource::setrlimit(nix::sys::resource::Resource::RLIMIT_CORE, 0, 0);
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
// SAFETY: prctl with PR_SET_DUMPABLE is a simple flag set, no pointers.
|
||||
unsafe {
|
||||
libc::prctl(libc::PR_SET_DUMPABLE, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn raise_file_size_limit() {
|
||||
let _ = nix::sys::resource::setrlimit(
|
||||
nix::sys::resource::Resource::RLIMIT_FSIZE,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
nix::sys::resource::RLIM_INFINITY,
|
||||
);
|
||||
}
|
||||
|
||||
fn sanitize_env() {
|
||||
let saved: Vec<(String, String)> = std::env::vars()
|
||||
.filter(|(k, _)| k == "TERM" || k == "LANG" || k.starts_with("LC_"))
|
||||
.collect();
|
||||
|
||||
let keys: Vec<std::ffi::OsString> = std::env::vars_os().map(|(k, _)| k).collect();
|
||||
for key in keys {
|
||||
std::env::remove_var(&key);
|
||||
}
|
||||
|
||||
std::env::set_var("PATH", "/usr/bin:/bin:/usr/sbin:/sbin");
|
||||
|
||||
for (key, val) in saved {
|
||||
std::env::set_var(&key, &val);
|
||||
}
|
||||
}
|
||||
|
||||
fn caller_is_root() -> bool {
|
||||
nix::unistd::getuid().is_root()
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Entry point
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
#[uucore::main]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
suppress_core_dumps();
|
||||
raise_file_size_limit();
|
||||
sanitize_env();
|
||||
|
||||
let matches = match uu_app().try_get_matches_from(args) {
|
||||
Ok(m) => m,
|
||||
Err(e) => {
|
||||
e.print().ok();
|
||||
if !e.use_stderr() {
|
||||
return Ok(());
|
||||
}
|
||||
return Err(GroupmodError::AlreadyPrinted(exit_codes::BAD_SYNTAX).into());
|
||||
}
|
||||
};
|
||||
|
||||
if !caller_is_root() {
|
||||
uucore::show_error!("Permission denied.");
|
||||
return Err(GroupmodError::AlreadyPrinted(1).into());
|
||||
}
|
||||
|
||||
let group_name = matches
|
||||
.get_one::<String>(options::GROUP)
|
||||
.ok_or_else(|| GroupmodError::BadSyntax("group name required".into()))?;
|
||||
let new_gid = matches.get_one::<String>(options::GID);
|
||||
let new_name = matches.get_one::<String>(options::NEW_NAME);
|
||||
let non_unique = matches.get_flag(options::NON_UNIQUE);
|
||||
let new_password = matches.get_one::<String>(options::PASSWORD);
|
||||
let prefix = matches.get_one::<String>(options::PREFIX).map(Path::new);
|
||||
let root_dir = matches.get_one::<String>(options::ROOT).map(Path::new);
|
||||
let root = SysRoot::new(prefix.or(root_dir));
|
||||
|
||||
// Validate new name if provided.
|
||||
if let Some(name) = new_name {
|
||||
shadow_core::validate::validate_username(name)
|
||||
.map_err(|e| GroupmodError::BadArgument(format!("{e}")))?;
|
||||
}
|
||||
|
||||
// Parse new GID if provided.
|
||||
let parsed_gid: Option<u32> = new_gid
|
||||
.map(|s| {
|
||||
s.parse::<u32>()
|
||||
.map_err(|_| GroupmodError::BadArgument(format!("invalid GID '{s}'")))
|
||||
})
|
||||
.transpose()?;
|
||||
|
||||
// Lock and read /etc/group.
|
||||
let group_path = root.group_path();
|
||||
let group_lock = FileLock::acquire(&group_path).map_err(|e| {
|
||||
GroupmodError::CantUpdate(format!("cannot lock {}: {e}", group_path.display()))
|
||||
})?;
|
||||
|
||||
let mut entries = group::read_group_file(&group_path).map_err(|e| {
|
||||
GroupmodError::CantUpdate(format!("cannot read {}: {e}", group_path.display()))
|
||||
})?;
|
||||
|
||||
// Find the target group.
|
||||
let idx = entries
|
||||
.iter()
|
||||
.position(|g| g.name == *group_name)
|
||||
.ok_or_else(|| {
|
||||
GroupmodError::GroupNotFound(format!("group '{group_name}' does not exist"))
|
||||
})?;
|
||||
|
||||
// Check GID collision.
|
||||
if let Some(gid) = parsed_gid {
|
||||
if !non_unique
|
||||
&& entries
|
||||
.iter()
|
||||
.any(|g| g.gid == gid && g.name != *group_name)
|
||||
{
|
||||
drop(group_lock);
|
||||
return Err(GroupmodError::GidInUse(format!("GID '{gid}' already exists")).into());
|
||||
}
|
||||
entries[idx].gid = gid;
|
||||
}
|
||||
|
||||
// Check name collision.
|
||||
if let Some(name) = new_name {
|
||||
if entries
|
||||
.iter()
|
||||
.any(|g| g.name == *name && g.name != *group_name)
|
||||
{
|
||||
drop(group_lock);
|
||||
return Err(GroupmodError::NameInUse(format!("group '{name}' already exists")).into());
|
||||
}
|
||||
entries[idx].name.clone_from(name);
|
||||
}
|
||||
|
||||
// Write /etc/group.
|
||||
atomic::atomic_write(&group_path, |f| group::write_group(&entries, f)).map_err(|e| {
|
||||
GroupmodError::CantUpdate(format!("cannot write {}: {e}", group_path.display()))
|
||||
})?;
|
||||
|
||||
drop(group_lock);
|
||||
|
||||
// Update /etc/gshadow.
|
||||
let gshadow_path = root.gshadow_path();
|
||||
if gshadow_path.exists() && (new_name.is_some() || new_password.is_some()) {
|
||||
let gs_lock = FileLock::acquire(&gshadow_path).map_err(|e| {
|
||||
GroupmodError::CantUpdate(format!("cannot lock {}: {e}", gshadow_path.display()))
|
||||
})?;
|
||||
|
||||
let mut gs_entries = gshadow::read_gshadow_file(&gshadow_path).map_err(|e| {
|
||||
GroupmodError::CantUpdate(format!("cannot read {}: {e}", gshadow_path.display()))
|
||||
})?;
|
||||
|
||||
if let Some(gs) = gs_entries.iter_mut().find(|g| g.name == *group_name) {
|
||||
if let Some(name) = new_name {
|
||||
gs.name.clone_from(name);
|
||||
}
|
||||
if let Some(pw) = new_password {
|
||||
gs.passwd.clone_from(pw);
|
||||
}
|
||||
}
|
||||
|
||||
atomic::atomic_write(&gshadow_path, |f| gshadow::write_gshadow(&gs_entries, f)).map_err(
|
||||
|e| GroupmodError::CantUpdate(format!("cannot write {}: {e}", gshadow_path.display())),
|
||||
)?;
|
||||
|
||||
drop(gs_lock);
|
||||
}
|
||||
|
||||
nscd::invalidate_cache("group");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn uu_app() -> Command {
|
||||
Command::new("groupmod")
|
||||
.about("Modify a group definition")
|
||||
.override_usage("groupmod [options] GROUP")
|
||||
.arg(
|
||||
Arg::new(options::GID)
|
||||
.short('g')
|
||||
.long("gid")
|
||||
.value_name("GID")
|
||||
.help("Change the group ID to GID"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::NEW_NAME)
|
||||
.short('n')
|
||||
.long("new-name")
|
||||
.value_name("NEW_GROUP")
|
||||
.help("Change the name of the group to NEW_GROUP"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::NON_UNIQUE)
|
||||
.short('o')
|
||||
.long("non-unique")
|
||||
.help("Allow using a non-unique GID with -g")
|
||||
.action(ArgAction::SetTrue),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::PASSWORD)
|
||||
.short('p')
|
||||
.long("password")
|
||||
.value_name("PASSWORD")
|
||||
.help("Change the password to encrypted PASSWORD"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::ROOT)
|
||||
.short('R')
|
||||
.long("root")
|
||||
.value_name("CHROOT_DIR")
|
||||
.help("Apply changes in the CHROOT_DIR directory"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::PREFIX)
|
||||
.short('P')
|
||||
.long("prefix")
|
||||
.value_name("PREFIX_DIR")
|
||||
.help("Directory prefix"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new(options::GROUP)
|
||||
.required(true)
|
||||
.index(1)
|
||||
.help("Name of the group to modify"),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_app_builds() {
|
||||
uu_app().debug_assert();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_group_required() {
|
||||
assert!(uu_app().try_get_matches_from(["groupmod"]).is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_flag() {
|
||||
let m = uu_app()
|
||||
.try_get_matches_from(["groupmod", "-n", "newname", "oldname"])
|
||||
.expect("valid args");
|
||||
assert_eq!(
|
||||
m.get_one::<String>(options::NEW_NAME).map(String::as_str),
|
||||
Some("newname")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gid_flag() {
|
||||
let m = uu_app()
|
||||
.try_get_matches_from(["groupmod", "-g", "5000", "mygrp"])
|
||||
.expect("valid args");
|
||||
assert_eq!(
|
||||
m.get_one::<String>(options::GID).map(String::as_str),
|
||||
Some("5000")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_unique_flag() {
|
||||
let m = uu_app()
|
||||
.try_get_matches_from(["groupmod", "-o", "-g", "0", "mygrp"])
|
||||
.expect("valid args");
|
||||
assert!(m.get_flag(options::NON_UNIQUE));
|
||||
}
|
||||
|
||||
fn skip_unless_root() -> bool {
|
||||
!nix::unistd::geteuid().is_root()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_change_gid() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(etc.join("group"), "testgrp:x:1000:\n").expect("write group");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupmod".into(),
|
||||
"-g".into(),
|
||||
"2000".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"testgrp".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_eq!(code, 0);
|
||||
|
||||
let content = std::fs::read_to_string(etc.join("group")).expect("read group");
|
||||
assert!(content.contains("testgrp:x:2000:"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_rename_group() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(etc.join("group"), "oldgrp:x:1000:\n").expect("write group");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupmod".into(),
|
||||
"-n".into(),
|
||||
"newgrp".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"oldgrp".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_eq!(code, 0);
|
||||
|
||||
let content = std::fs::read_to_string(etc.join("group")).expect("read group");
|
||||
assert!(content.contains("newgrp:x:1000:"));
|
||||
assert!(!content.contains("oldgrp"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_nonexistent_group_fails() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(etc.join("group"), "root:x:0:\n").expect("write group");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupmod".into(),
|
||||
"-g".into(),
|
||||
"5000".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"nogroup".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_ne!(code, 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_gid_collision_fails() {
|
||||
if skip_unless_root() {
|
||||
return;
|
||||
}
|
||||
|
||||
let dir = tempfile::tempdir().expect("tempdir");
|
||||
let etc = dir.path().join("etc");
|
||||
std::fs::create_dir_all(&etc).expect("create etc");
|
||||
std::fs::write(etc.join("group"), "grp1:x:1000:\ngrp2:x:2000:\n").expect("write group");
|
||||
|
||||
let code = uumain(
|
||||
vec![
|
||||
"groupmod".into(),
|
||||
"-g".into(),
|
||||
"2000".into(),
|
||||
"-P".into(),
|
||||
dir.path().as_os_str().to_owned(),
|
||||
"grp1".into(),
|
||||
]
|
||||
.into_iter(),
|
||||
);
|
||||
assert_ne!(code, 0);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
// 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.
|
||||
|
||||
uucore::bin!(uu_groupmod);
|
||||
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "uu_grpck"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
repository.workspace = true
|
||||
description = "grpck command (shadow-rs)"
|
||||
|
||||
[lib]
|
||||
path = "src/grpck.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "grpck"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
clap = { workspace = true }
|
||||
libc = { workspace = true }
|
||||
nix = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "group", "gshadow", "login-defs"] }
|
||||
uucore = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user