Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,10 @@ development tool.

shadow-rs is distributed under the terms of the [MIT License](LICENSE).

Acceptable dependency licenses: MIT, Apache-2.0, ISC, BSD-2-Clause,
BSD-3-Clause, CC0-1.0, Unicode-3.0, Zlib, MPL-2.0.
Acceptable dependency licences are exactly the ones the dependency graph uses
today: **MIT, Apache-2.0, BSD-3-Clause, Unicode-3.0**. The list is deliberately
no wider than the graph, so a crate arriving under a new licence is a decision
someone records in `deny.toml` rather than something that passes silently.

**No GPL or LGPL dependencies, ever.**
**No GPL or LGPL dependencies, ever**, and no MPL-2.0 either: it is file-level
copyleft and this project ships MIT only.
13 changes: 13 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

12 changes: 11 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ members = [
"src/uu/newgrp",
"src/uu/chpasswd",
"src/uu/chage",
"src/uu/gpasswd",
]

[workspace.package]
Expand Down Expand Up @@ -90,10 +91,11 @@ grpck = { optional = true, version = "0.3.1", package = "uu_grpck", path = "src/
chfn = { optional = true, version = "0.3.1", package = "uu_chfn", path = "src/uu/chfn" }
chsh = { optional = true, version = "0.3.1", package = "uu_chsh", path = "src/uu/chsh" }
newgrp = { optional = true, version = "0.3.1", package = "uu_newgrp", path = "src/uu/newgrp" }
gpasswd = { optional = true, version = "0.3.1", package = "uu_gpasswd", path = "src/uu/gpasswd" }

[features]
default = ["passwd", "pwck", "useradd", "userdel", "usermod", "chpasswd", "chage",
"groupadd", "groupdel", "groupmod", "grpck", "chfn", "chsh", "newgrp"]
"groupadd", "groupdel", "groupmod", "grpck", "chfn", "chsh", "newgrp", "gpasswd"]

# PAM authentication (requires libpam-dev). The `?` matters: without it,
# asking for PAM would drag in the three applets that can use it even when the
Expand Down Expand Up @@ -171,5 +173,13 @@ must_use_candidate = { level = "allow", priority = 1 }
module_name_repetitions = { level = "allow", priority = 1 }
cargo_common_metadata = { level = "allow", priority = 1 }

# The print macros panic when the stream is closed, which a setuid-root tool
# must not do part way through changing an account file. Every tool uses
# `writeln!` or `uucore::show_error!` instead; this makes the rule a build
# failure rather than something a reviewer has to remember. Tests are exempt:
# a panic there is a failed test, which is the point.
print_stdout = { level = "deny", priority = 1 }
print_stderr = { level = "deny", priority = 1 }

[lints]
workspace = true
12 changes: 6 additions & 6 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ BINDIR ?= $(PREFIX)/bin
SBINDIR ?= $(PREFIX)/sbin

# Tools that need setuid-root to allow non-root callers (change own password,
# GECOS, shell, effective group).
SETUID_TOOLS = passwd chfn chsh newgrp
# GECOS, shell, effective group, or administer a group as a group admin).
SETUID_TOOLS = passwd chfn chsh newgrp gpasswd

# Root-only tools (no setuid; fail at getuid() check for non-root callers).
ROOT_TOOLS = useradd userdel usermod chpasswd \
Expand Down Expand Up @@ -83,7 +83,7 @@ test:
test-gnu-compat:
bash tests/gnu-compat.sh

# Default install: 14 standalone per-tool binaries, with the setuid layout and
# Default install: 15 standalone per-tool binaries, with the setuid layout and
# the bin/sbin split GNU shadow-utils uses. Only passwd/chfn/chsh/newgrp are
# setuid.
install: build
Expand All @@ -100,9 +100,9 @@ install: build
@echo " $(DESTDIR)$(SBINDIR)/ root (0755): $(ROOT_TOOLS)"

# Opt-in install: single multicall binary with symlinks. Smaller footprint.
# The binary is installed setuid-root for passwd/chfn/chsh/newgrp; the other
# applets drop back to the caller's uid before running, so the privilege model
# matches the per-tool layout. Intended for container/embedded use.
# The binary is installed setuid-root for passwd/chfn/chsh/newgrp/gpasswd; the
# other applets drop back to the caller's uid before running, so the privilege
# model matches the per-tool layout. Intended for container/embedded use.
install-multicall: build-multicall
install -Dm4755 target/release/shadow-rs $(DESTDIR)$(SBINDIR)/shadow-rs
@install -d $(DESTDIR)$(BINDIR)
Expand Down
9 changes: 5 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ default-in-Ubuntu in under 3 years. This project follows that playbook.
| `chfn` | **Implemented.** GECOS sub-field modification. |
| `chsh` | **Implemented.** Shell change with /etc/shells validation. |
| `newgrp` | **Implemented.** Effective group change with crypt verification. |
| `gpasswd` | **Implemented.** Group membership, administrators, and group password. |

## Building

Expand All @@ -87,9 +88,9 @@ docker compose run --rm debian cargo build --release

### Install

Default install: 14 standalone per-tool binaries with least-privilege setuid
layout matching GNU shadow-utils. Only `passwd`, `chfn`, `chsh`, `newgrp` are
installed setuid-root; the other 10 are plain `0755`.
Default install: 15 standalone per-tool binaries with least-privilege setuid
layout matching GNU shadow-utils. Only `passwd`, `chfn`, `chsh`, `newgrp`,
`gpasswd` are installed setuid-root; the other 10 are plain `0755`.

```shell
sudo make install PREFIX=/usr/local
Expand Down Expand Up @@ -140,7 +141,7 @@ would:
tar xzf uu_shadow-x86_64-unknown-linux-gnu.tar.gz # or the -musl-static one
sudo install -o root -g root -m 4755 \
uu_shadow-*/shadow-rs /usr/local/bin/shadow-rs
for tool in passwd chfn chsh newgrp chage chpasswd groupadd groupdel \
for tool in passwd chfn chsh newgrp gpasswd chage chpasswd groupadd groupdel \
groupmod grpck pwck useradd userdel usermod; do
sudo ln -sf shadow-rs "/usr/local/bin/$tool"
done
Expand Down
2 changes: 1 addition & 1 deletion docs/PLATFORM-SUPPORT.md
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ tools, which are root-only anyway and reach `/etc/shadow` directly.
### 2. No NSS

`shadow_core::process` resolves the calling user through `getpwuid_r`, used by
`passwd`, `chfn`, `chsh`, `chage` and `newgrp`.
`passwd`, `chfn`, `chsh`, `chage`, `newgrp` and `gpasswd`.

glibc answers such lookups through its NSS module system, so it sees users from
LDAP, SSSD, Active Directory or systemd-userdb. musl has no NSS module system
Expand Down
107 changes: 107 additions & 0 deletions docs/man/gpasswd.1.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# gpasswd(1) - administer /etc/group and /etc/gshadow

## NAME

gpasswd - administer /etc/group and /etc/gshadow

## SYNOPSIS

**gpasswd** [*options*] *group*

## DESCRIPTION

The **gpasswd** command administers `/etc/group` and `/etc/gshadow`.
Every group can have administrators, members, and a password.

System administrators can use the **-A** option to define group
administrator(s) and the **-M** option to define members. They have
all rights of group administrators and members.

**gpasswd** called by a group administrator with a group name only
prompts for the new password of the *group*.

If a password is set the members can still use **newgrp**(1) without a
password, and non-members must supply the password.

This tool is installed setuid-root so that a group administrator (a
user named in the gshadow administrators field) can add and remove
members and change the group password without being root. **-A** and
**-M** remain root-only.

### Notes about group passwords

Group passwords are an inherent security problem since more than one
person is permitted to know the password. However, groups are a useful
tool for permitting co-operation between different users.

## OPTIONS

Except for the **-A** and **-M** options, the options cannot be combined.

**-a**, **--add** *USER*
: Add *USER* to the named group.

**-d**, **--delete** *USER*
: Remove *USER* from the named group.

**-A**, **--administrators** *USER,...*
: Set the list of administrative users. Root only. Requires
`/etc/gshadow`. An empty list clears the administrators.

**-M**, **--members** *USER,...*
: Set the list of group members. Root only. An empty list clears
the members.

**-r**, **--remove-password**
: Remove the password from the named group. The group password
will be empty. Only group members will be allowed to use
**newgrp** to join the named group.

**-R**, **--restrict**
: Restrict access to the named group. The group password is set
to "!". Only group members will be allowed to use **newgrp** to
join the named group.

**-Q**, **--root** *CHROOT_DIR*
: Locate the system files under *CHROOT_DIR* instead of `/`. Only
absolute paths are supported. Root only.

**-P**, **--prefix** *PREFIX_DIR*
: Use *PREFIX_DIR* as a prefix for system file paths. Root only.

## EXIT STATUS

**0**
: Success.

**1**
: Permission denied.

**2**
: Invalid command syntax.

**3**
: Invalid argument to option, or specified group doesn't exist.

**10**
: Can't update group file.

**17**
: Shadow group file required for **-A**.

## FILES

/etc/group
: Group account information.

/etc/gshadow
: Secure group account information.

/etc/login.defs
: Shadow password suite configuration (`ENCRYPT_METHOD`,
`SHA_CRYPT_MIN_ROUNDS`, `SHA_CRYPT_MAX_ROUNDS`).

## SEE ALSO

newgrp(1), groupadd(8), groupdel(8), groupmod(8), grpck(8), group(5),
gshadow(5)
4 changes: 4 additions & 0 deletions src/bin/completions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@ fn get_tool_app(name: &str) -> Option<Command> {
"groupdel" => Some(groupdel::uu_app()),
#[cfg(feature = "groupmod")]
"groupmod" => Some(groupmod::uu_app()),
#[cfg(feature = "gpasswd")]
"gpasswd" => Some(gpasswd::uu_app()),
#[cfg(feature = "grpck")]
"grpck" => Some(grpck::uu_app()),
#[cfg(feature = "newgrp")]
Expand Down Expand Up @@ -72,6 +74,8 @@ fn all_tool_names() -> Vec<&'static str> {
names.push("groupdel");
#[cfg(feature = "groupmod")]
names.push("groupmod");
#[cfg(feature = "gpasswd")]
names.push("gpasswd");
#[cfg(feature = "grpck")]
names.push("grpck");
#[cfg(feature = "newgrp")]
Expand Down
14 changes: 8 additions & 6 deletions src/bin/shadow-rs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,9 @@ use std::process::ExitCode;

type Applet = fn(&[OsString]) -> i32;

/// Applets that keep euid 0 for an unprivileged caller: the same four that
/// Applets that keep euid 0 for an unprivileged caller: the same five that
/// `make install` marks setuid.
const SETUID_APPLETS: [&str; 4] = ["passwd", "chfn", "chsh", "newgrp"];
const SETUID_APPLETS: [&str; 5] = ["passwd", "chfn", "chsh", "newgrp", "gpasswd"];

/// Every applet compiled into this binary, by name, in `--list` order.
// `#[cfg]` is not accepted on the elements of a `vec![]` literal, so the
Expand All @@ -45,6 +45,8 @@ fn applets() -> Vec<(&'static str, Applet)> {
table.push(("chpasswd", |a| chpasswd::uumain(a.iter().cloned())));
#[cfg(feature = "chsh")]
table.push(("chsh", |a| chsh::uumain(a.iter().cloned())));
#[cfg(feature = "gpasswd")]
table.push(("gpasswd", |a| gpasswd::uumain(a.iter().cloned())));
#[cfg(feature = "groupadd")]
table.push(("groupadd", |a| groupadd::uumain(a.iter().cloned())));
#[cfg(feature = "groupdel")]
Expand Down Expand Up @@ -223,9 +225,9 @@ fn print_available_utils() {
mod tests {
use super::*;

const ALL_TOOLS: [&str; 14] = [
"chage", "chfn", "chpasswd", "chsh", "groupadd", "groupdel", "groupmod", "grpck", "newgrp",
"passwd", "pwck", "useradd", "userdel", "usermod",
const ALL_TOOLS: [&str; 15] = [
"chage", "chfn", "chpasswd", "chsh", "gpasswd", "groupadd", "groupdel", "groupmod",
"grpck", "newgrp", "passwd", "pwck", "useradd", "userdel", "usermod",
];

// The table drives both dispatch and `--list`, so it must contain only
Expand All @@ -247,7 +249,7 @@ mod tests {
for tool in ALL_TOOLS {
assert_eq!(
keeps_privilege(tool),
matches!(tool, "passwd" | "chfn" | "chsh" | "newgrp"),
matches!(tool, "passwd" | "chfn" | "chsh" | "newgrp" | "gpasswd"),
"{tool}"
);
}
Expand Down
36 changes: 36 additions & 0 deletions src/uu/gpasswd/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
[package]
name = "uu_gpasswd"
version.workspace = true
edition.workspace = true
license.workspace = true
repository.workspace = true
homepage.workspace = true
keywords.workspace = true
categories.workspace = true
rust-version.workspace = true
description = "gpasswd ~ (shadow-rs) administer /etc/group and /etc/gshadow"

[lib]
path = "src/gpasswd.rs"

[[bin]]
name = "gpasswd"
path = "src/main.rs"

[dependencies]
clap = { workspace = true }
rustix = { workspace = true }
zeroize = { workspace = true }
shadow-core = { workspace = true, features = ["crypt"] }
uucore = { workspace = true }

[dev-dependencies]
tempfile = { workspace = true }

[lints]
workspace = true

# Distributed via the `shadow-rs` multicall binary in the workspace root
# package, not as a standalone archive (see dist-workspace.toml, issue #207).
[package.metadata.dist]
dist = false
2 changes: 2 additions & 0 deletions src/uu/gpasswd/locales/en-US.ftl
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
gpasswd-about = Administer group membership and the group password
gpasswd-usage = gpasswd [options] group
Loading