mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
docs/fixes: man pages, benchmarks, BSD research, Copilot findings
Fixes #56 — 14 man pages in docs/man/ (markdown format). Fixes #67 — 8 Copilot findings fixed: - useradd: proper date validation (Feb 31 rejected) - userdel/usermod: --root wired to SysRoot - usermod: --expiredate validates input - usermod: --login validates name + updates shadow/group - userdel: -f separated from -r behavior - useradd: home dir resolved through SysRoot - skel: preserves directory permissions Fixes #69 — benchmark script (benches/benchmark.sh) Fixes #70 — FreeBSD/NetBSD reference (docs/FREEBSD-NETBSD-REFERENCE.md) 456 tests, zero clippy warnings.
This commit is contained in:
Executable
+270
@@ -0,0 +1,270 @@
|
||||
#!/usr/bin/env bash
|
||||
# spell-checker:ignore timeformat tempdir ldd passwd pwck
|
||||
#
|
||||
# Benchmark script for shadow-rs vs GNU shadow-utils.
|
||||
#
|
||||
# Compares performance, binary size, and shared library dependencies
|
||||
# between shadow-rs (Rust) and GNU shadow-utils (C) implementations.
|
||||
#
|
||||
# Usage:
|
||||
# docker compose run --rm debian bash benches/benchmark.sh
|
||||
#
|
||||
# Requirements:
|
||||
# - Must run inside Docker (needs both GNU shadow-utils and shadow-rs)
|
||||
# - Must run as root (passwd -S and pwck require root or shadow access)
|
||||
#
|
||||
# The script builds shadow-rs in release mode, locates GNU shadow-utils
|
||||
# binaries, and runs comparative benchmarks.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Configuration
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
PASSWD_STATUS_ITERS=1000
|
||||
PWCK_ITERS=100
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log() { printf '\033[1;34m==> %s\033[0m\n' "$*"; }
|
||||
warn() { printf '\033[1;33mWARN: %s\033[0m\n' "$*"; }
|
||||
err() { printf '\033[1;31mERROR: %s\033[0m\n' "$*"; exit 1; }
|
||||
|
||||
# Print a separator line.
|
||||
separator() { printf '%.0s-' {1..72}; printf '\n'; }
|
||||
|
||||
# Format bytes as human-readable.
|
||||
human_size() {
|
||||
local bytes=$1
|
||||
if [ "$bytes" -ge 1048576 ]; then
|
||||
printf '%.1f MiB' "$(echo "$bytes / 1048576" | bc -l)"
|
||||
elif [ "$bytes" -ge 1024 ]; then
|
||||
printf '%.1f KiB' "$(echo "$bytes / 1024" | bc -l)"
|
||||
else
|
||||
printf '%d B' "$bytes"
|
||||
fi
|
||||
}
|
||||
|
||||
# Time a command N times, report wall-clock total in seconds.
|
||||
# Usage: bench_command <label> <iterations> <command...>
|
||||
bench_command() {
|
||||
local label=$1
|
||||
local iters=$2
|
||||
shift 2
|
||||
|
||||
log "Benchmarking: $label ($iters iterations)"
|
||||
|
||||
local start end elapsed
|
||||
start=$(date +%s%N)
|
||||
for ((i = 0; i < iters; i++)); do
|
||||
"$@" >/dev/null 2>&1 || true
|
||||
done
|
||||
end=$(date +%s%N)
|
||||
|
||||
elapsed=$(echo "scale=3; ($end - $start) / 1000000000" | bc)
|
||||
local per_iter
|
||||
per_iter=$(echo "scale=6; $elapsed / $iters" | bc)
|
||||
|
||||
printf ' %-30s total: %8ss per-iter: %ss\n' "$label" "$elapsed" "$per_iter"
|
||||
echo "$elapsed"
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Locate binaries
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
find_gnu_binary() {
|
||||
local name=$1
|
||||
# GNU shadow-utils binaries are typically in /usr/sbin or /usr/bin.
|
||||
for path in /usr/sbin/"$name" /usr/bin/"$name" /sbin/"$name" /bin/"$name"; do
|
||||
if [ -x "$path" ]; then
|
||||
# Make sure it is NOT our shadow-rs binary (check for ELF or "shadow-rs").
|
||||
if ! file "$path" 2>/dev/null | grep -q 'statically linked\|shadow-rs'; then
|
||||
echo "$path"
|
||||
return 0
|
||||
fi
|
||||
fi
|
||||
done
|
||||
return 1
|
||||
}
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Build shadow-rs
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log "Building shadow-rs in release mode..."
|
||||
cargo build --release --quiet 2>&1
|
||||
|
||||
SHADOW_RS_DIR="$(cargo metadata --format-version=1 --no-deps 2>/dev/null | \
|
||||
python3 -c 'import sys,json; print(json.load(sys.stdin)["target_directory"])' 2>/dev/null || \
|
||||
echo "target")/release"
|
||||
|
||||
# The multicall binary.
|
||||
SHADOW_RS_BIN="$SHADOW_RS_DIR/shadow-rs"
|
||||
|
||||
if [ ! -x "$SHADOW_RS_BIN" ]; then
|
||||
# Try individual binaries.
|
||||
SHADOW_RS_BIN=""
|
||||
fi
|
||||
|
||||
# Individual tool binaries (built by cargo as separate bins).
|
||||
RS_PASSWD="$SHADOW_RS_DIR/passwd"
|
||||
RS_PWCK="$SHADOW_RS_DIR/pwck"
|
||||
|
||||
# Fall back to the multicall binary if individual bins are not found.
|
||||
if [ ! -x "$RS_PASSWD" ] && [ -x "$SHADOW_RS_DIR/shadow-rs" ]; then
|
||||
RS_PASSWD="$SHADOW_RS_DIR/shadow-rs"
|
||||
fi
|
||||
if [ ! -x "$RS_PWCK" ] && [ -x "$SHADOW_RS_DIR/shadow-rs" ]; then
|
||||
RS_PWCK="$SHADOW_RS_DIR/shadow-rs"
|
||||
fi
|
||||
|
||||
# Locate GNU binaries.
|
||||
GNU_PASSWD=$(find_gnu_binary passwd) || GNU_PASSWD=""
|
||||
GNU_PWCK=$(find_gnu_binary pwck) || GNU_PWCK=""
|
||||
|
||||
separator
|
||||
log "Binary locations:"
|
||||
printf ' %-20s %s\n' "shadow-rs passwd:" "${RS_PASSWD:-NOT FOUND}"
|
||||
printf ' %-20s %s\n' "shadow-rs pwck:" "${RS_PWCK:-NOT FOUND}"
|
||||
printf ' %-20s %s\n' "GNU passwd:" "${GNU_PASSWD:-NOT FOUND}"
|
||||
printf ' %-20s %s\n' "GNU pwck:" "${GNU_PWCK:-NOT FOUND}"
|
||||
separator
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Binary size comparison
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log "Binary size comparison"
|
||||
printf '\n'
|
||||
printf ' %-35s %12s\n' "Binary" "Size"
|
||||
separator
|
||||
|
||||
report_size() {
|
||||
local label=$1
|
||||
local path=$2
|
||||
if [ -x "$path" ]; then
|
||||
local size
|
||||
size=$(stat --format='%s' "$path" 2>/dev/null || stat -f '%z' "$path" 2>/dev/null || echo 0)
|
||||
printf ' %-35s %12s (%d bytes)\n' "$label" "$(human_size "$size")" "$size"
|
||||
else
|
||||
printf ' %-35s %12s\n' "$label" "N/A"
|
||||
fi
|
||||
}
|
||||
|
||||
report_size "shadow-rs multicall" "$SHADOW_RS_DIR/shadow-rs"
|
||||
report_size "shadow-rs passwd" "$RS_PASSWD"
|
||||
report_size "shadow-rs pwck" "$RS_PWCK"
|
||||
report_size "GNU passwd" "$GNU_PASSWD"
|
||||
report_size "GNU pwck" "$GNU_PWCK"
|
||||
printf '\n'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Shared library dependencies
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log "Shared library dependencies"
|
||||
printf '\n'
|
||||
|
||||
report_deps() {
|
||||
local label=$1
|
||||
local path=$2
|
||||
if [ -x "$path" ] && command -v ldd >/dev/null 2>&1; then
|
||||
local count
|
||||
count=$(ldd "$path" 2>/dev/null | grep -c '=>' || echo 0)
|
||||
printf ' %-35s %d shared libraries\n' "$label" "$count"
|
||||
ldd "$path" 2>/dev/null | sed 's/^/ /'
|
||||
elif [ -x "$path" ]; then
|
||||
printf ' %-35s (ldd not available)\n' "$label"
|
||||
else
|
||||
printf ' %-35s N/A\n' "$label"
|
||||
fi
|
||||
printf '\n'
|
||||
}
|
||||
|
||||
report_deps "shadow-rs passwd" "$RS_PASSWD"
|
||||
report_deps "GNU passwd" "$GNU_PASSWD"
|
||||
separator
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Performance benchmarks
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
log "Performance benchmarks"
|
||||
printf '\n'
|
||||
printf ' Test: passwd -S root (%d iterations)\n' "$PASSWD_STATUS_ITERS"
|
||||
printf ' Test: pwck -r (%d iterations)\n' "$PWCK_ITERS"
|
||||
printf '\n'
|
||||
separator
|
||||
|
||||
# -- passwd -S root --
|
||||
|
||||
rs_passwd_time=""
|
||||
gnu_passwd_time=""
|
||||
|
||||
if [ -x "$RS_PASSWD" ]; then
|
||||
rs_passwd_time=$(bench_command "shadow-rs: passwd -S root" "$PASSWD_STATUS_ITERS" "$RS_PASSWD" -S root)
|
||||
fi
|
||||
|
||||
if [ -x "$GNU_PASSWD" ]; then
|
||||
gnu_passwd_time=$(bench_command "GNU: passwd -S root" "$PASSWD_STATUS_ITERS" "$GNU_PASSWD" -S root)
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
|
||||
# -- pwck -r --
|
||||
|
||||
rs_pwck_time=""
|
||||
gnu_pwck_time=""
|
||||
|
||||
if [ -x "$RS_PWCK" ]; then
|
||||
rs_pwck_time=$(bench_command "shadow-rs: pwck -r" "$PWCK_ITERS" "$RS_PWCK" -r)
|
||||
fi
|
||||
|
||||
if [ -x "$GNU_PWCK" ]; then
|
||||
gnu_pwck_time=$(bench_command "GNU: pwck -r" "$PWCK_ITERS" "$GNU_PWCK" -r)
|
||||
fi
|
||||
|
||||
printf '\n'
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Summary table
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
separator
|
||||
log "SUMMARY"
|
||||
printf '\n'
|
||||
printf ' %-30s %12s %12s %12s\n' "Benchmark" "shadow-rs" "GNU" "Ratio"
|
||||
separator
|
||||
|
||||
compute_ratio() {
|
||||
local rs=$1
|
||||
local gnu=$2
|
||||
if [ -n "$rs" ] && [ -n "$gnu" ] && [ "$(echo "$gnu > 0" | bc)" -eq 1 ]; then
|
||||
printf '%.2fx' "$(echo "$rs / $gnu" | bc -l)"
|
||||
else
|
||||
echo "N/A"
|
||||
fi
|
||||
}
|
||||
|
||||
ratio_passwd=$(compute_ratio "${rs_passwd_time:-}" "${gnu_passwd_time:-}")
|
||||
ratio_pwck=$(compute_ratio "${rs_pwck_time:-}" "${gnu_pwck_time:-}")
|
||||
|
||||
printf ' %-30s %11ss %11ss %12s\n' \
|
||||
"passwd -S root (${PASSWD_STATUS_ITERS}x)" \
|
||||
"${rs_passwd_time:-N/A}" \
|
||||
"${gnu_passwd_time:-N/A}" \
|
||||
"$ratio_passwd"
|
||||
|
||||
printf ' %-30s %11ss %11ss %12s\n' \
|
||||
"pwck -r (${PWCK_ITERS}x)" \
|
||||
"${rs_pwck_time:-N/A}" \
|
||||
"${gnu_pwck_time:-N/A}" \
|
||||
"$ratio_pwck"
|
||||
|
||||
printf '\n'
|
||||
separator
|
||||
log "Benchmark complete."
|
||||
@@ -0,0 +1,63 @@
|
||||
# FreeBSD/NetBSD Security Reference for shadow-rs
|
||||
|
||||
Analysis of FreeBSD's `pw` and NetBSD's user management implementations.
|
||||
Both BSD-2-Clause licensed — safe to reference.
|
||||
|
||||
## FreeBSD `pw` Patterns
|
||||
|
||||
### What FreeBSD Does Differently
|
||||
|
||||
| Pattern | FreeBSD | shadow-rs | Action |
|
||||
|---------|---------|-----------|--------|
|
||||
| Username allows trailing `$` | Yes (Samba compat) | No | Consider adding for Samba/AD |
|
||||
| Salt generation | `arc4random_uniform()` | PAM handles | N/A (PAM delegates hashing) |
|
||||
| Password fd input (`-h FD`) | Yes | No | Low priority — niche use case |
|
||||
| Password buffers not zeroed | Vulnerable | Fixed (zeroize) | We're ahead |
|
||||
| No mlock() on passwords | Vulnerable | Not implemented | Future work |
|
||||
| Selective config override | Sentinel values (-1) | Login.defs defaults | Already implemented |
|
||||
|
||||
### Key Takeaway
|
||||
|
||||
FreeBSD's `pw` is less hardened than OpenBSD's `passwd` — no explicit memory
|
||||
zeroing, no mlock, no pledge/unveil equivalent. Our implementation with
|
||||
`zeroize`, core dump suppression, and environment sanitization is already
|
||||
ahead of FreeBSD's security posture.
|
||||
|
||||
### Patterns Worth Adopting
|
||||
|
||||
1. **Samba-compatible usernames**: Allow trailing `$` in usernames for
|
||||
Active Directory machine accounts. This is a common real-world need.
|
||||
|
||||
2. **Password input via fd**: The `-h FD` pattern allows passing passwords
|
||||
from a pipe without command-line exposure. Lower priority but useful for
|
||||
automation.
|
||||
|
||||
3. **mlock() for password buffers**: Neither FreeBSD nor our implementation
|
||||
uses `mlock()` to prevent password data from being swapped to disk.
|
||||
OpenBSD doesn't either (they rely on encrypted swap). Consider adding
|
||||
as defense-in-depth.
|
||||
|
||||
## NetBSD Patterns
|
||||
|
||||
NetBSD's user management follows similar patterns to FreeBSD. Key
|
||||
differences:
|
||||
|
||||
- Uses `vipw(8)` for direct passwd editing (different approach)
|
||||
- Stricter POSIX compliance in username validation
|
||||
- Similar lack of memory hardening
|
||||
|
||||
## Recommendations for shadow-rs
|
||||
|
||||
All high-value items from BSD review are already tracked:
|
||||
|
||||
- **mlock()**: Future work (docs/SECURITY-HARDENING.md)
|
||||
- **Samba usernames**: Could add `--badname` flag (matches GNU `useradd --badname`)
|
||||
- **Password fd input**: Low priority feature
|
||||
|
||||
No critical security gaps found relative to FreeBSD/NetBSD implementations.
|
||||
shadow-rs is already more hardened than both.
|
||||
|
||||
## References
|
||||
|
||||
- FreeBSD pw: https://cgit.freebsd.org/src/tree/usr.sbin/pw/
|
||||
- NetBSD user management: https://cvsweb.netbsd.org/bsdweb.cgi/src/usr.sbin/user/
|
||||
@@ -0,0 +1,70 @@
|
||||
# chage(1) - change user password expiry information
|
||||
|
||||
## NAME
|
||||
|
||||
chage - change user password expiry information
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**chage** [*options*] *LOGIN*
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **chage** command changes the number of days between password changes
|
||||
and the date of the last password change. This information is used by
|
||||
the system to determine when a user must change their password.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-d**, **--lastday** *LAST_DAY*
|
||||
: Set the date of the last password change. The date may be expressed
|
||||
as a date (YYYY-MM-DD) or as the number of days since January 1, 1970.
|
||||
A value of -1 removes the last-change date requirement.
|
||||
|
||||
**-E**, **--expiredate** *EXPIRE_DATE*
|
||||
: Set the account expiration date. The date may be expressed as a date
|
||||
(YYYY-MM-DD) or as the number of days since January 1, 1970.
|
||||
A value of -1 removes the expiration date.
|
||||
|
||||
**-I**, **--inactive** *INACTIVE*
|
||||
: Set the number of days of inactivity after a password has expired
|
||||
before the account is locked. A value of -1 removes the inactivity
|
||||
requirement.
|
||||
|
||||
**-l**, **--list**
|
||||
: Show account aging information.
|
||||
|
||||
**-m**, **--mindays** *MIN_DAYS*
|
||||
: Set the minimum number of days between password changes. A value
|
||||
of -1 removes the minimum days requirement.
|
||||
|
||||
**-M**, **--maxdays** *MAX_DAYS*
|
||||
: Set the maximum number of days during which a password is valid.
|
||||
A value of -1 removes the maximum days requirement.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-W**, **--warndays** *WARN_DAYS*
|
||||
: Set the number of days of warning before a password change is
|
||||
required. A value of -1 removes the warning.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Permission denied.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
passwd(1), passwd(5), shadow(5)
|
||||
@@ -0,0 +1,60 @@
|
||||
# chfn(1) - change user finger information
|
||||
|
||||
## NAME
|
||||
|
||||
chfn - change real user name and information
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**chfn** [*options*] [*LOGIN*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **chfn** command changes the user finger information stored in the
|
||||
GECOS field of /etc/passwd. This information is typically displayed by
|
||||
the **finger**(1) program and includes the user's full name, office room
|
||||
number, and phone numbers.
|
||||
|
||||
A normal user may only change their own finger information; the superuser
|
||||
may change the information for any user. Only the superuser may change
|
||||
the "other" field.
|
||||
|
||||
At least one option flag (**-f**, **-r**, **-w**, **-h**, or **-o**) must
|
||||
be specified.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-f**, **--full-name** *FULL_NAME*
|
||||
: Change the user's full name.
|
||||
|
||||
**-h**, **--home-phone** *HOME_PHONE*
|
||||
: Change the user's home phone number.
|
||||
|
||||
**-o**, **--other** *OTHER*
|
||||
: Change the user's other GECOS information. Only root may set this field.
|
||||
|
||||
**-r**, **--room** *ROOM*
|
||||
: Change the user's room number.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-w**, **--work-phone** *WORK_PHONE*
|
||||
: Change the user's office phone number.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Permission denied or operation failed.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
chsh(1), finger(1), passwd(5)
|
||||
@@ -0,0 +1,56 @@
|
||||
# chpasswd(8) - update passwords in batch mode
|
||||
|
||||
## NAME
|
||||
|
||||
chpasswd - update passwords in batch mode
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**chpasswd** [*options*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **chpasswd** command reads a list of username:password pairs from
|
||||
standard input and uses this information to update a group of existing
|
||||
users. Each line is of the format:
|
||||
|
||||
username:password
|
||||
|
||||
By default the password is expected to be in cleartext (not yet
|
||||
supported in shadow-rs; use **-e**). With the **-e** flag, the password
|
||||
is expected to be already encrypted (pre-hashed).
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-c**, **--crypt-method** *METHOD*
|
||||
: Use the specified crypt method. Supported values: SHA256, SHA512,
|
||||
YESCRYPT, DES, MD5.
|
||||
|
||||
**-e**, **--encrypted**
|
||||
: Supplied passwords are already encrypted (pre-hashed).
|
||||
|
||||
**-m**, **--md5**
|
||||
: Use MD5 encryption for cleartext passwords (deprecated).
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-s**, **--sha-rounds** *ROUNDS*
|
||||
: Use the specified number of rounds for SHA256/SHA512 encryption.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Permission denied, invalid input, file busy, or unexpected failure.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
passwd(1), passwd(5), shadow(5)
|
||||
@@ -0,0 +1,50 @@
|
||||
# chsh(1) - change login shell
|
||||
|
||||
## NAME
|
||||
|
||||
chsh - change login shell
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**chsh** [*options*] [*LOGIN*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **chsh** command changes the user login shell. This determines the
|
||||
name of the user's initial login command. A normal user may only change
|
||||
the login shell for their own account; the superuser may change the login
|
||||
shell for any account.
|
||||
|
||||
The new shell must be listed in /etc/shells unless the caller is root.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-l**, **--list-shells**
|
||||
: Print the list of shells listed in /etc/shells and exit.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-s**, **--shell** *SHELL*
|
||||
: Set the login shell to *SHELL*. The shell must be an absolute path
|
||||
and must be listed in /etc/shells (unless the caller is root).
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Permission denied or operation failed.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
/etc/shells
|
||||
: List of valid login shells.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
chfn(1), login(1), passwd(5), shells(5)
|
||||
@@ -0,0 +1,80 @@
|
||||
# groupadd(8) - create a new group
|
||||
|
||||
## NAME
|
||||
|
||||
groupadd - create a new group
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**groupadd** [*options*] *GROUP*
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **groupadd** command creates a new group account using the values
|
||||
specified on the command line plus the default values from the system.
|
||||
The new group will be entered into the system files (/etc/group and
|
||||
/etc/gshadow) as needed.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-f**, **--force**
|
||||
: Exit successfully if the group already exists, and cancel **-g** if
|
||||
the GID is already used (a new GID will be allocated instead).
|
||||
|
||||
**-g**, **--gid** *GID*
|
||||
: Use *GID* for the new group.
|
||||
|
||||
**-K**, **--key** *KEY=VALUE*
|
||||
: Override /etc/login.defs defaults (GID_MIN, GID_MAX, SYS_GID_MIN,
|
||||
SYS_GID_MAX). Can be specified multiple times.
|
||||
|
||||
**-o**, **--non-unique**
|
||||
: Allow creating a group with a non-unique (duplicate) GID.
|
||||
|
||||
**-p**, **--password** *PASSWORD*
|
||||
: Set the encrypted password for the new group.
|
||||
|
||||
**-P**, **--prefix** *PREFIX_DIR*
|
||||
: Use *PREFIX_DIR* as a prefix for system file paths.
|
||||
|
||||
**-r**, **--system**
|
||||
: Create a system group (allocated from the system GID range defined
|
||||
in /etc/login.defs).
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**3**
|
||||
: Invalid argument to option.
|
||||
|
||||
**4**
|
||||
: GID already in use (and no **-o** or **-f**).
|
||||
|
||||
**9**
|
||||
: Group name already in use.
|
||||
|
||||
**10**
|
||||
: Cannot update group file.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information.
|
||||
|
||||
/etc/login.defs
|
||||
: Shadow password suite configuration.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
groupdel(8), groupmod(8), login.defs(5)
|
||||
@@ -0,0 +1,54 @@
|
||||
# groupdel(8) - delete a group
|
||||
|
||||
## NAME
|
||||
|
||||
groupdel - delete a group
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**groupdel** [*options*] *GROUP*
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **groupdel** command modifies the system account files, deleting all
|
||||
entries that refer to *GROUP*. The named group must exist.
|
||||
|
||||
You may not remove the primary group of any existing user. You must
|
||||
remove the user before you remove the group.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-P**, **--prefix** *PREFIX_DIR*
|
||||
: Use *PREFIX_DIR* as a prefix for system file paths.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**6**
|
||||
: Group does not exist.
|
||||
|
||||
**8**
|
||||
: Cannot remove a user's primary group.
|
||||
|
||||
**10**
|
||||
: Cannot update group file.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
groupadd(8), groupmod(8), userdel(8)
|
||||
@@ -0,0 +1,70 @@
|
||||
# groupmod(8) - modify a group definition
|
||||
|
||||
## NAME
|
||||
|
||||
groupmod - modify a group definition
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**groupmod** [*options*] *GROUP*
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **groupmod** command modifies the definition of the specified
|
||||
*GROUP* by modifying the appropriate entries in the group and gshadow
|
||||
databases.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-g**, **--gid** *GID*
|
||||
: Change the group ID to *GID*.
|
||||
|
||||
**-n**, **--new-name** *NEW_GROUP*
|
||||
: Change the name of the group to *NEW_GROUP*.
|
||||
|
||||
**-o**, **--non-unique**
|
||||
: Allow using a non-unique (duplicate) GID when used with **-g**.
|
||||
|
||||
**-p**, **--password** *PASSWORD*
|
||||
: Change the group password to the encrypted *PASSWORD*.
|
||||
|
||||
**-P**, **--prefix** *PREFIX_DIR*
|
||||
: Use *PREFIX_DIR* as a prefix for system file paths.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**3**
|
||||
: Invalid argument to option.
|
||||
|
||||
**4**
|
||||
: GID already in use.
|
||||
|
||||
**6**
|
||||
: Group does not exist.
|
||||
|
||||
**9**
|
||||
: Group name already in use.
|
||||
|
||||
**10**
|
||||
: Cannot update group file.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
groupadd(8), groupdel(8)
|
||||
@@ -0,0 +1,68 @@
|
||||
# grpck(8) - verify integrity of group files
|
||||
|
||||
## NAME
|
||||
|
||||
grpck - verify integrity of group files
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**grpck** [*options*] [*group* [*gshadow*]]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **grpck** command verifies the integrity of the group information.
|
||||
It checks that all entries in /etc/group and (optionally) /etc/gshadow
|
||||
have the proper format and contain valid data.
|
||||
|
||||
Checks performed include:
|
||||
|
||||
- Correct number of fields
|
||||
- Unique group names
|
||||
- Valid GID values
|
||||
- Matching group/gshadow entries
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-q**, **--quiet**
|
||||
: Report only errors, suppress warnings.
|
||||
|
||||
**-r**, **--read-only**
|
||||
: Display errors and warnings but do not modify files.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-s**, **--sort**
|
||||
: Sort entries by GID.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**2**
|
||||
: One or more bad group entries.
|
||||
|
||||
**3**
|
||||
: Cannot open files.
|
||||
|
||||
**4**
|
||||
: Cannot lock files.
|
||||
|
||||
**5**
|
||||
: Cannot update files.
|
||||
|
||||
**6**
|
||||
: Cannot sort files.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
groupadd(8), groupdel(8), groupmod(8), pwck(8)
|
||||
@@ -0,0 +1,50 @@
|
||||
# newgrp(1) - log in to a new group
|
||||
|
||||
## NAME
|
||||
|
||||
newgrp - log in to a new group
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**newgrp** [*group*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **newgrp** command is used to change the current group ID during a
|
||||
login session. If the optional *group* argument is given, the effective
|
||||
group ID is changed to that group; otherwise the effective group ID is
|
||||
changed to the user's primary group from /etc/passwd.
|
||||
|
||||
If the user is not a member of the specified group, and the group has a
|
||||
password set in /etc/gshadow, the user will be prompted for the group
|
||||
password. Root always has access to any group without a password prompt.
|
||||
|
||||
A new shell is started with the changed group ID. The shell is
|
||||
determined by the **SHELL** environment variable, falling back to
|
||||
/bin/sh.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
None. Only an optional positional group name argument is accepted.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success (though note that **newgrp** replaces the current process
|
||||
with a new shell via **execv**(2), so exit status 0 is not normally
|
||||
returned to the caller).
|
||||
|
||||
**1**
|
||||
: Permission denied, group not found, or unable to execute shell.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information (for group passwords).
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
groups(1), id(1), login(1), sg(1), group(5), gshadow(5)
|
||||
@@ -0,0 +1,104 @@
|
||||
# passwd(1) - change user password
|
||||
|
||||
## NAME
|
||||
|
||||
passwd - change user password
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**passwd** [*options*] [*LOGIN*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **passwd** command changes passwords for user accounts. A normal user
|
||||
may only change the password for their own account; the superuser may
|
||||
change the password for any account. **passwd** also changes the account
|
||||
or associated password validity period.
|
||||
|
||||
When invoked without a LOGIN argument, **passwd** changes the password for
|
||||
the current user.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-a**, **--all**
|
||||
: Report password status on all accounts. Requires **-S**.
|
||||
|
||||
**-d**, **--delete**
|
||||
: Delete the password for the named account. This makes the account
|
||||
passwordless.
|
||||
|
||||
**-e**, **--expire**
|
||||
: Immediately expire the password for the named account. This forces
|
||||
the user to change their password at next login.
|
||||
|
||||
**-i**, **--inactive** *INACTIVE*
|
||||
: Set the number of days of inactivity after a password has expired
|
||||
before the account is locked.
|
||||
|
||||
**-k**, **--keep-tokens**
|
||||
: Change password only if expired.
|
||||
|
||||
**-l**, **--lock**
|
||||
: Lock the password of the named account. This prepends a '!' to the
|
||||
encrypted password, effectively disabling the password.
|
||||
|
||||
**-n**, **--mindays** *MIN_DAYS*
|
||||
: Set the minimum number of days between password changes.
|
||||
|
||||
**-q**, **--quiet**
|
||||
: Quiet mode.
|
||||
|
||||
**-r**, **--repository** *REPOSITORY*
|
||||
: Change password in the named repository.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory and use the configuration
|
||||
files from the *CHROOT_DIR* directory.
|
||||
|
||||
**-P**, **--prefix** *PREFIX_DIR*
|
||||
: Use *PREFIX_DIR* as a prefix for system file paths.
|
||||
|
||||
**-S**, **--status**
|
||||
: Display account status information. The status information consists
|
||||
of 7 fields: login name, password status (L=locked, NP=no password,
|
||||
P=usable password), date of last password change, minimum age,
|
||||
maximum age, warning period, and inactivity period.
|
||||
|
||||
**-s**, **--stdin**
|
||||
: Read the new password token from standard input.
|
||||
|
||||
**-u**, **--unlock**
|
||||
: Unlock the password of the named account. This removes the '!' prefix
|
||||
from the encrypted password.
|
||||
|
||||
**-w**, **--warndays** *WARN_DAYS*
|
||||
: Set the number of days of warning before a password change is required.
|
||||
|
||||
**-x**, **--maxdays** *MAX_DAYS*
|
||||
: Set the maximum number of days a password remains valid.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Permission denied or operation failed.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**5**
|
||||
: Password file busy.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
chage(1), chpasswd(8), login.defs(5), shadow(5), pwck(8)
|
||||
@@ -0,0 +1,74 @@
|
||||
# pwck(8) - verify integrity of password files
|
||||
|
||||
## NAME
|
||||
|
||||
pwck - verify integrity of password files
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**pwck** [*options*] [*passwd* [*shadow*]]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **pwck** command verifies the integrity of the system authentication
|
||||
information. It checks that all entries in /etc/passwd and /etc/shadow
|
||||
have the proper format and contain valid data.
|
||||
|
||||
Checks performed include:
|
||||
|
||||
- Correct number of fields
|
||||
- Unique and valid user names
|
||||
- Valid user and group identifiers
|
||||
- Valid primary group
|
||||
- Valid home directory
|
||||
- Valid login shell
|
||||
- Matching passwd/shadow entries
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-q**, **--quiet**
|
||||
: Report only errors, suppress warnings.
|
||||
|
||||
**-r**, **--read-only**
|
||||
: Display errors and warnings but do not modify files.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-s**, **--sort**
|
||||
: Sort entries by UID.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Invalid command syntax.
|
||||
|
||||
**2**
|
||||
: One or more bad password entries.
|
||||
|
||||
**3**
|
||||
: Cannot open files.
|
||||
|
||||
**4**
|
||||
: Cannot lock files.
|
||||
|
||||
**5**
|
||||
: Cannot update files.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
passwd(5), shadow(5), grpck(8)
|
||||
@@ -0,0 +1,136 @@
|
||||
# useradd(8) - create a new user
|
||||
|
||||
## NAME
|
||||
|
||||
useradd - create a new user or update default new user information
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**useradd** [*options*] *LOGIN*
|
||||
**useradd** **-D** [*options*]
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **useradd** command creates a new user account using the values
|
||||
specified on the command line plus the default values from the system.
|
||||
The new user account will be entered into the system files as needed,
|
||||
the home directory will be created, and initial files copied, depending
|
||||
on the command line options.
|
||||
|
||||
When invoked with the **-D** flag, **useradd** displays or updates the
|
||||
default values used for creating new accounts.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-c**, **--comment** *COMMENT*
|
||||
: Set the GECOS field of the new account.
|
||||
|
||||
**-d**, **--home-dir** *HOME_DIR*
|
||||
: Set the home directory of the new account.
|
||||
|
||||
**-D**, **--defaults**
|
||||
: Print or change default useradd configuration.
|
||||
|
||||
**-e**, **--expiredate** *EXPIRE_DATE*
|
||||
: Set the expiration date of the new account (YYYY-MM-DD).
|
||||
|
||||
**-f**, **--inactive** *INACTIVE*
|
||||
: Set the password inactivity period of the new account.
|
||||
|
||||
**-g**, **--gid** *GROUP*
|
||||
: Set the name or numeric ID of the primary group of the new account.
|
||||
|
||||
**-G**, **--groups** *GROUPS*
|
||||
: Set the list of supplementary groups of the new account (comma-separated).
|
||||
|
||||
**-k**, **--skel** *SKEL_DIR*
|
||||
: Specify the skeleton directory (default: /etc/skel).
|
||||
|
||||
**-m**, **--create-home**
|
||||
: Create the user's home directory if it does not exist.
|
||||
|
||||
**-M**, **--no-create-home**
|
||||
: Do not create the user's home directory.
|
||||
|
||||
**-N**, **--no-user-group**
|
||||
: Do not create a group with the same name as the user.
|
||||
|
||||
**-o**, **--non-unique**
|
||||
: Allow creating users with duplicate (non-unique) UIDs. Requires **-u**.
|
||||
|
||||
**-p**, **--password** *PASSWORD*
|
||||
: Set the encrypted password of the new account.
|
||||
|
||||
**-r**, **--system**
|
||||
: Create a system account.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-s**, **--shell** *SHELL*
|
||||
: Set the login shell of the new account.
|
||||
|
||||
**-u**, **--uid** *UID*
|
||||
: Set the user ID of the new account.
|
||||
|
||||
**-U**, **--user-group**
|
||||
: Create a group with the same name as the user (default behavior).
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Cannot update password file.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**3**
|
||||
: Invalid argument to option.
|
||||
|
||||
**4**
|
||||
: UID already in use (and no **-o**).
|
||||
|
||||
**6**
|
||||
: Specified group does not exist.
|
||||
|
||||
**9**
|
||||
: Username already in use.
|
||||
|
||||
**10**
|
||||
: Cannot update group file.
|
||||
|
||||
**12**
|
||||
: Cannot create home directory.
|
||||
|
||||
**14**
|
||||
: Cannot update SELinux user mapping.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information.
|
||||
|
||||
/etc/login.defs
|
||||
: Shadow password suite configuration.
|
||||
|
||||
/etc/default/useradd
|
||||
: Default values for account creation.
|
||||
|
||||
/etc/skel
|
||||
: Directory containing default files.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
userdel(8), usermod(8), groupadd(8), login.defs(5)
|
||||
@@ -0,0 +1,67 @@
|
||||
# userdel(8) - delete a user account
|
||||
|
||||
## NAME
|
||||
|
||||
userdel - delete a user account and related files
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**userdel** [*options*] *LOGIN*
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **userdel** command modifies the system account files, deleting all
|
||||
entries that refer to the user name *LOGIN*. The named user must exist.
|
||||
|
||||
The user's entry is removed from /etc/passwd and /etc/shadow. The user
|
||||
is also removed from membership lists in /etc/group and /etc/gshadow.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-f**, **--force**
|
||||
: Force removal of the account even if the user is still logged in.
|
||||
Also forces removal of the home directory and mail spool.
|
||||
|
||||
**-P**, **--prefix** *PREFIX_DIR*
|
||||
: Use *PREFIX_DIR* as a prefix for system file paths.
|
||||
|
||||
**-r**, **--remove**
|
||||
: Remove the user's home directory and mail spool.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Cannot update password file.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**10**
|
||||
: Cannot update group file.
|
||||
|
||||
**12**
|
||||
: Cannot remove home directory.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
/etc/gshadow
|
||||
: Secure group account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
useradd(8), usermod(8), groupdel(8)
|
||||
@@ -0,0 +1,93 @@
|
||||
# usermod(8) - modify a user account
|
||||
|
||||
## NAME
|
||||
|
||||
usermod - modify a user account
|
||||
|
||||
## SYNOPSIS
|
||||
|
||||
**usermod** [*options*] *LOGIN*
|
||||
|
||||
## DESCRIPTION
|
||||
|
||||
The **usermod** command modifies the system account files to reflect the
|
||||
changes that are specified on the command line.
|
||||
|
||||
## OPTIONS
|
||||
|
||||
**-a**, **--append**
|
||||
: Append the user to the supplementary group(s) specified by **-G**.
|
||||
Use only with the **-G** option.
|
||||
|
||||
**-c**, **--comment** *COMMENT*
|
||||
: Set the new value of the user's GECOS field.
|
||||
|
||||
**-d**, **--home** *HOME_DIR*
|
||||
: Set the new home directory for the user.
|
||||
|
||||
**-e**, **--expiredate** *EXPIRE_DATE*
|
||||
: Set the account expiration date.
|
||||
|
||||
**-f**, **--inactive** *INACTIVE*
|
||||
: Set the password inactive period.
|
||||
|
||||
**-g**, **--gid** *GROUP*
|
||||
: Set the new primary group ID (numeric).
|
||||
|
||||
**-G**, **--groups** *GROUPS*
|
||||
: Set the list of supplementary groups (comma-separated). If the **-a**
|
||||
option is not used, the user is removed from all groups not listed.
|
||||
|
||||
**-l**, **--login** *NEW_LOGIN*
|
||||
: Change the user's login name.
|
||||
|
||||
**-L**, **--lock**
|
||||
: Lock the user's password by prepending a '!' to the shadow password.
|
||||
|
||||
**-P**, **--prefix** *PREFIX_DIR*
|
||||
: Use *PREFIX_DIR* as a prefix for system file paths.
|
||||
|
||||
**-R**, **--root** *CHROOT_DIR*
|
||||
: Apply changes in the *CHROOT_DIR* directory.
|
||||
|
||||
**-s**, **--shell** *SHELL*
|
||||
: Set the new login shell.
|
||||
|
||||
**-u**, **--uid** *UID*
|
||||
: Set the new numeric user ID.
|
||||
|
||||
**-U**, **--unlock**
|
||||
: Unlock the user's password by removing the '!' prefix from the
|
||||
shadow password.
|
||||
|
||||
## EXIT STATUS
|
||||
|
||||
**0**
|
||||
: Success.
|
||||
|
||||
**1**
|
||||
: Cannot update password file.
|
||||
|
||||
**2**
|
||||
: Invalid command syntax.
|
||||
|
||||
**4**
|
||||
: UID already in use.
|
||||
|
||||
**6**
|
||||
: User does not exist.
|
||||
|
||||
## FILES
|
||||
|
||||
/etc/passwd
|
||||
: User account information.
|
||||
|
||||
/etc/shadow
|
||||
: Secure user account information.
|
||||
|
||||
/etc/group
|
||||
: Group account information.
|
||||
|
||||
## SEE ALSO
|
||||
|
||||
useradd(8), userdel(8), groupmod(8), passwd(1)
|
||||
@@ -44,6 +44,12 @@ fn copy_dir_recursive(src: &Path, dst: &Path, uid: u32, gid: u32) -> Result<(),
|
||||
if file_type.is_dir() {
|
||||
std::fs::create_dir_all(&dst_path)
|
||||
.map_err(|e| ShadowError::IoPath(e, dst_path.clone()))?;
|
||||
// Preserve the source directory's permissions.
|
||||
let src_perms = std::fs::metadata(&src_path)
|
||||
.map_err(|e| ShadowError::IoPath(e, src_path.clone()))?
|
||||
.permissions();
|
||||
std::fs::set_permissions(&dst_path, src_perms)
|
||||
.map_err(|e| ShadowError::IoPath(e, dst_path.clone()))?;
|
||||
copy_dir_recursive(&src_path, &dst_path, uid, gid)?;
|
||||
} else if file_type.is_symlink() {
|
||||
let target = std::fs::read_link(&src_path)
|
||||
|
||||
@@ -203,12 +203,19 @@ fn parse_expire_date(s: &str) -> Result<Option<i64>, UseraddError> {
|
||||
UseraddError::BadArgument(format!("invalid date '{s}' (expected YYYY-MM-DD)"))
|
||||
})?;
|
||||
|
||||
if !(1..=12).contains(&month) || !(1..=31).contains(&day) || year < 1970 {
|
||||
if !(1..=12).contains(&month) || year < 1970 {
|
||||
return Err(UseraddError::BadArgument(format!(
|
||||
"invalid date '{s}' (expected YYYY-MM-DD with valid ranges)"
|
||||
)));
|
||||
}
|
||||
|
||||
let max_day = days_in_month(year, month);
|
||||
if !(1..=max_day).contains(&day) {
|
||||
return Err(UseraddError::BadArgument(format!(
|
||||
"invalid date '{s}' (day {day} out of range for month {month})"
|
||||
)));
|
||||
}
|
||||
|
||||
// Convert to days since epoch using a simple calendar calculation.
|
||||
// This is sufficient for the date ranges used by shadow-utils.
|
||||
let days = days_since_epoch(year, month, day);
|
||||
@@ -229,6 +236,28 @@ fn days_since_epoch(year: i64, month: i64, day: i64) -> i64 {
|
||||
era * 146_097 + doe - 719_468
|
||||
}
|
||||
|
||||
/// Whether `year` is a leap year in the Gregorian calendar.
|
||||
fn is_leap_year(year: i64) -> bool {
|
||||
(year % 4 == 0 && year % 100 != 0) || year % 400 == 0
|
||||
}
|
||||
|
||||
/// Number of days in a given month (1-indexed) for `year`.
|
||||
fn days_in_month(year: i64, month: i64) -> i64 {
|
||||
match month {
|
||||
1 | 3 | 5 | 7 | 8 | 10 | 12 => 31,
|
||||
4 | 6 | 9 | 11 => 30,
|
||||
2 => {
|
||||
if is_leap_year(year) {
|
||||
29
|
||||
} else {
|
||||
28
|
||||
}
|
||||
}
|
||||
// Month range is already validated before calling this function.
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Current date as days since epoch.
|
||||
#[allow(clippy::cast_possible_wrap)]
|
||||
fn today_days_since_epoch() -> i64 {
|
||||
@@ -574,7 +603,9 @@ fn do_useradd(opts: &UseraddOptions) -> UResult<()> {
|
||||
|
||||
// Step 15: Create home directory and copy skel.
|
||||
if opts.create_home {
|
||||
create_home_directory(&home_dir, &opts.skel_dir, uid, gid)?;
|
||||
let resolved_home = opts.root.resolve(&home_dir);
|
||||
let resolved_skel = opts.root.resolve(&opts.skel_dir);
|
||||
create_home_directory(&resolved_home, &resolved_skel, uid, gid)?;
|
||||
}
|
||||
|
||||
// Step 16: Invalidate nscd caches.
|
||||
@@ -809,22 +840,23 @@ fn add_to_supplementary_groups(
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Create the home directory and copy skeleton files.
|
||||
fn create_home_directory(home_dir: &str, skel_dir: &str, uid: u32, gid: u32) -> UResult<()> {
|
||||
let home_path = Path::new(home_dir);
|
||||
|
||||
///
|
||||
/// Paths must already be resolved through `SysRoot` by the caller.
|
||||
fn create_home_directory(home_path: &Path, skel_path: &Path, uid: u32, gid: u32) -> UResult<()> {
|
||||
// Use create_dir (not create_dir_all) to avoid TOCTOU between exists() and mkdir().
|
||||
match std::fs::create_dir(home_path) {
|
||||
Ok(()) => {}
|
||||
Err(e) if e.kind() == std::io::ErrorKind::AlreadyExists => {
|
||||
uucore::show_warning!(
|
||||
"home directory '{}' already exists -- not copying from skel directory",
|
||||
home_dir
|
||||
home_path.display()
|
||||
);
|
||||
return Ok(());
|
||||
}
|
||||
Err(e) => {
|
||||
return Err(UseraddError::CannotCreateHome(format!(
|
||||
"cannot create directory '{home_dir}': {e}"
|
||||
"cannot create directory '{}': {e}",
|
||||
home_path.display()
|
||||
))
|
||||
.into());
|
||||
}
|
||||
@@ -832,19 +864,26 @@ fn create_home_directory(home_dir: &str, skel_dir: &str, uid: u32, gid: u32) ->
|
||||
|
||||
// Set permissions to 0700 (home directories should be private by default).
|
||||
std::fs::set_permissions(home_path, std::fs::Permissions::from_mode(0o700)).map_err(|e| {
|
||||
UseraddError::CannotCreateHome(format!("cannot set permissions on '{home_dir}': {e}"))
|
||||
UseraddError::CannotCreateHome(format!(
|
||||
"cannot set permissions on '{}': {e}",
|
||||
home_path.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
// Set ownership.
|
||||
std::os::unix::fs::chown(home_path, Some(uid), Some(gid)).map_err(|e| {
|
||||
UseraddError::CannotCreateHome(format!("cannot set ownership on '{home_dir}': {e}"))
|
||||
UseraddError::CannotCreateHome(format!(
|
||||
"cannot set ownership on '{}': {e}",
|
||||
home_path.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
// Copy skeleton directory contents.
|
||||
let skel_path = Path::new(skel_dir);
|
||||
skel::copy_skel(skel_path, home_path, uid, gid).map_err(|e| {
|
||||
UseraddError::CannotCreateHome(format!(
|
||||
"cannot copy skel '{skel_dir}' to '{home_dir}': {e}"
|
||||
"cannot copy skel '{}' to '{}': {e}",
|
||||
skel_path.display(),
|
||||
home_path.display()
|
||||
))
|
||||
})?;
|
||||
|
||||
@@ -1250,6 +1289,48 @@ mod tests {
|
||||
assert!(parse_expire_date("1969-12-31").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expire_date_feb_31() {
|
||||
assert!(parse_expire_date("2025-02-31").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expire_date_feb_29_non_leap() {
|
||||
assert!(parse_expire_date("2025-02-29").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expire_date_feb_29_leap() {
|
||||
assert!(parse_expire_date("2024-02-29").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expire_date_apr_31() {
|
||||
assert!(parse_expire_date("2025-04-31").is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_expire_date_apr_30() {
|
||||
assert!(parse_expire_date("2025-04-30").is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_leap_year() {
|
||||
assert!(is_leap_year(2000));
|
||||
assert!(is_leap_year(2024));
|
||||
assert!(!is_leap_year(1900));
|
||||
assert!(!is_leap_year(2023));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_in_month_values() {
|
||||
assert_eq!(days_in_month(2025, 1), 31);
|
||||
assert_eq!(days_in_month(2025, 2), 28);
|
||||
assert_eq!(days_in_month(2024, 2), 29);
|
||||
assert_eq!(days_in_month(2025, 4), 30);
|
||||
assert_eq!(days_in_month(2025, 12), 31);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_days_since_epoch_known_dates() {
|
||||
// 1970-01-01 = day 0
|
||||
@@ -1605,12 +1686,14 @@ mod tests {
|
||||
let home = dir.path().join("home/testuser");
|
||||
let skel = dir.path().join("skel");
|
||||
|
||||
// Parent of home must exist (create_dir is intentionally used, not create_dir_all).
|
||||
fs::create_dir_all(dir.path().join("home")).expect("create home parent");
|
||||
|
||||
// Create skeleton directory with a file.
|
||||
fs::create_dir_all(&skel).expect("create skel");
|
||||
fs::write(skel.join(".bashrc"), "# bashrc\n").expect("write bashrc");
|
||||
|
||||
create_home_directory(&home.to_string_lossy(), &skel.to_string_lossy(), 1000, 1000)
|
||||
.expect("create home");
|
||||
create_home_directory(&home, &skel, 1000, 1000).expect("create home");
|
||||
|
||||
assert!(home.exists());
|
||||
assert!(home.join(".bashrc").exists());
|
||||
@@ -1632,7 +1715,7 @@ mod tests {
|
||||
fs::create_dir_all(&home).expect("create home");
|
||||
|
||||
// Should succeed with a warning, not copy skel.
|
||||
create_home_directory(&home.to_string_lossy(), "/nonexistent/skel", 1000, 1000)
|
||||
create_home_directory(&home, Path::new("/nonexistent/skel"), 1000, 1000)
|
||||
.expect("should succeed for existing home");
|
||||
}
|
||||
|
||||
|
||||
@@ -85,8 +85,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let login = matches
|
||||
.get_one::<String>(options::LOGIN)
|
||||
.expect("LOGIN is required");
|
||||
let remove_home = matches.get_flag(options::REMOVE) || matches.get_flag(options::FORCE);
|
||||
let prefix = matches.get_one::<String>(options::PREFIX).map(Path::new);
|
||||
let remove_home = matches.get_flag(options::REMOVE);
|
||||
let prefix = matches
|
||||
.get_one::<String>(options::PREFIX)
|
||||
.or_else(|| matches.get_one::<String>(options::ROOT))
|
||||
.map(Path::new);
|
||||
let root = SysRoot::new(prefix);
|
||||
|
||||
// Must be root.
|
||||
|
||||
@@ -19,7 +19,7 @@ use shadow_core::lock::FileLock;
|
||||
use shadow_core::passwd::{self};
|
||||
use shadow_core::shadow::{self};
|
||||
use shadow_core::sysroot::SysRoot;
|
||||
use shadow_core::{atomic, nscd};
|
||||
use shadow_core::{atomic, nscd, validate};
|
||||
|
||||
mod options {
|
||||
pub const COMMENT: &str = "comment";
|
||||
@@ -88,7 +88,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let login = matches
|
||||
.get_one::<String>(options::USER)
|
||||
.expect("USER is required");
|
||||
let prefix = matches.get_one::<String>(options::PREFIX).map(Path::new);
|
||||
let prefix = matches
|
||||
.get_one::<String>(options::PREFIX)
|
||||
.or_else(|| matches.get_one::<String>(options::ROOT))
|
||||
.map(Path::new);
|
||||
let root = SysRoot::new(prefix);
|
||||
|
||||
if !nix::unistd::getuid().is_root() {
|
||||
@@ -134,8 +137,11 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
if let Some(&gid) = matches.get_one::<u32>(options::GID) {
|
||||
entries[idx].gid = gid;
|
||||
}
|
||||
if let Some(new_login) = matches.get_one::<String>(options::LOGIN) {
|
||||
entries[idx].name.clone_from(new_login);
|
||||
let new_login = matches.get_one::<String>(options::LOGIN);
|
||||
if let Some(new_name) = new_login {
|
||||
validate::validate_username(new_name)
|
||||
.map_err(|e| UsermodError::CantUpdate(format!("invalid login name: {e}")))?;
|
||||
entries[idx].name.clone_from(new_name);
|
||||
}
|
||||
|
||||
let new_uid = entries[idx].uid;
|
||||
@@ -160,7 +166,10 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
let expire = matches.get_one::<String>(options::EXPIREDATE);
|
||||
let inactive = matches.get_one::<i64>(options::INACTIVE);
|
||||
|
||||
if shadow_path.exists() && (do_lock || do_unlock || expire.is_some() || inactive.is_some()) {
|
||||
let login_changing = new_login.is_some();
|
||||
if shadow_path.exists()
|
||||
&& (do_lock || do_unlock || expire.is_some() || inactive.is_some() || login_changing)
|
||||
{
|
||||
let slock = FileLock::acquire(&shadow_path)
|
||||
.map_err(|e| UsermodError::CantUpdate(format!("cannot lock shadow: {e}")))?;
|
||||
|
||||
@@ -175,11 +184,22 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
s.unlock();
|
||||
}
|
||||
if let Some(exp) = expire {
|
||||
s.expire_date = if exp == "-1" { None } else { exp.parse().ok() };
|
||||
s.expire_date = if exp == "-1" || exp.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(exp.parse::<i64>().map_err(|_| {
|
||||
UsermodError::CantUpdate(format!(
|
||||
"invalid expire date '{exp}' (expected days since epoch)"
|
||||
))
|
||||
})?)
|
||||
};
|
||||
}
|
||||
if let Some(&i) = inactive {
|
||||
s.inactive_days = if i < 0 { None } else { Some(i) };
|
||||
}
|
||||
if let Some(new_name) = new_login {
|
||||
s.name.clone_from(new_name);
|
||||
}
|
||||
}
|
||||
|
||||
atomic::atomic_write(&shadow_path, |f| shadow::write_shadow(&se, f))
|
||||
@@ -187,6 +207,32 @@ pub fn uumain(args: impl uucore::Args) -> UResult<()> {
|
||||
drop(slock);
|
||||
}
|
||||
|
||||
// Rename user in group membership lists when --login changes the name.
|
||||
if let Some(new_name) = new_login {
|
||||
let group_path = root.group_path();
|
||||
if group_path.exists() {
|
||||
let glock = FileLock::acquire(&group_path)
|
||||
.map_err(|e| UsermodError::CantUpdate(format!("cannot lock group: {e}")))?;
|
||||
|
||||
let mut ge = group::read_group_file(&group_path)
|
||||
.map_err(|e| UsermodError::CantUpdate(format!("{e}")))?;
|
||||
|
||||
let mut changed = false;
|
||||
for g in &mut ge {
|
||||
if let Some(m) = g.members.iter_mut().find(|m| **m == *login) {
|
||||
m.clone_from(new_name);
|
||||
changed = true;
|
||||
}
|
||||
}
|
||||
|
||||
if changed {
|
||||
atomic::atomic_write(&group_path, |f| group::write_group(&ge, f))
|
||||
.map_err(|e| UsermodError::CantUpdate(format!("{e}")))?;
|
||||
}
|
||||
drop(glock);
|
||||
}
|
||||
}
|
||||
|
||||
// Group modifications.
|
||||
if let Some(groups_str) = matches.get_one::<String>(options::GROUPS) {
|
||||
let group_path = root.group_path();
|
||||
|
||||
Reference in New Issue
Block a user