mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
92783375aa
Security fixes: - newgrp: add initgroups() before execv to reset supplementary groups (H-3) - pam: use Zeroizing<String> for password buffers on all exit paths (M-5) - atomic: retry-once on stale tmp file from crashed run (M-3) - atomic: UmaskGuard opts out of Send/Sync via PhantomData (L-2) - lock: flush + fsync in write_pid_file before hard_link (L-7) Deduplication (~300 lines removed): - Centralize caller_is_root(), current_username(), SignalBlocker in shadow_core::hardening — remove 10+5+5 private copies across tools - Add SignalBlocker to all 6 remaining mutating tools (M-4) - Remove dead CantSort variant from grpck, fix pwck allow (M-8) - Remove dead read_passwd from test_chfn (M-8) Binary cleanup: - shadow-rs multicall: use ExitCode instead of process::exit (M-6) - completions: use ExitCode + Result pattern, remove unwrap/exit (H-3) Documentation: - crypt: thread-safety warning on public API (H-1) - shadow: clarify unlock() behavior for * password (H-2) - validate: document $ rejection deviation from GNU (L-4) - selinux/audit: note shell-out implementation status (L-6) - chpasswd/chage tests: TODO for --prefix integration (M-8) - userdel: document inline skip_unless_root reason (L-9) Testing: - Add fuzz targets for group and gshadow parsers (M-7) - skel: use create_dir for tighter error detection (L-8) - crypt: compile-time modulo bias assertion (L-1)
166 lines
5.7 KiB
Rust
166 lines
5.7 KiB
Rust
// 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 chpasswd
|
|
|
|
//! Integration tests for the `chpasswd` utility.
|
|
//!
|
|
//! Tests that require root are guarded by `common::skip_unless_root()` and run inside
|
|
//! Docker CI containers. Non-root tests exercise clap parsing and error paths
|
|
//! that do not need privilege.
|
|
|
|
use std::ffi::OsString;
|
|
|
|
#[path = "../common/mod.rs"]
|
|
mod common;
|
|
|
|
/// Run `uumain` with the given args, returning the exit code.
|
|
fn run(args: &[&str]) -> i32 {
|
|
let os_args: Vec<OsString> = args.iter().map(|s| (*s).into()).collect();
|
|
chpasswd::uumain(os_args.into_iter())
|
|
}
|
|
|
|
/// Helper to create a temp dir with an `etc/shadow` file.
|
|
fn setup_shadow(shadow_content: &str) -> tempfile::TempDir {
|
|
let dir = tempfile::tempdir().expect("failed to create temp dir");
|
|
let etc = dir.path().join("etc");
|
|
std::fs::create_dir_all(&etc).expect("failed to create etc dir");
|
|
std::fs::write(etc.join("shadow"), shadow_content).expect("failed to write shadow file");
|
|
dir
|
|
}
|
|
|
|
/// Read the shadow file content back from a prefix dir.
|
|
fn read_shadow(dir: &tempfile::TempDir) -> String {
|
|
std::fs::read_to_string(dir.path().join("etc/shadow")).expect("failed to read shadow file")
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Non-root tests — exercise clap parsing and error paths
|
|
// ---------------------------------------------------------------------------
|
|
|
|
#[test]
|
|
fn test_help_exits_zero() {
|
|
let code = run(&["chpasswd", "--help"]);
|
|
assert_eq!(code, 0, "--help should exit 0");
|
|
}
|
|
|
|
#[test]
|
|
fn test_invalid_crypt_method_exits_error() {
|
|
let code = run(&["chpasswd", "-c", "BOGUS"]);
|
|
// clap error for invalid value
|
|
assert_ne!(code, 0, "invalid crypt method should exit non-zero");
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Root-only tests — exercise real operations via shadow-core
|
|
// ---------------------------------------------------------------------------
|
|
//
|
|
// TODO(#integration): These tests directly manipulate shadow-core data
|
|
// structures instead of calling chpasswd::uumain(). Full end-to-end
|
|
// integration via uumain() is not yet feasible because chpasswd only supports
|
|
// --root (which performs a real chroot(2) and requires root), not --prefix
|
|
// (path-prefix without chroot). Once chpasswd gains a --prefix flag, replace
|
|
// these tests with uumain() calls using run(&["chpasswd", "--prefix", ...,
|
|
// "-e", ...]) with synthetic files.
|
|
|
|
#[test]
|
|
fn test_batch_password_update() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir =
|
|
setup_shadow("alice:$6$oldhash:19500:0:99999:7:::\nbob:$6$oldhash:19500:0:99999:7:::\n");
|
|
let shadow_path = dir.path().join("etc/shadow");
|
|
|
|
// Simulate what chpasswd -e does: update password hashes.
|
|
let mut entries =
|
|
shadow_core::shadow::read_shadow_file(&shadow_path).expect("failed to read shadow");
|
|
|
|
for entry in &mut entries {
|
|
if entry.name == "alice" {
|
|
entry.passwd = "$6$newhash_alice".to_string();
|
|
} else if entry.name == "bob" {
|
|
entry.passwd = "$6$newhash_bob".to_string();
|
|
}
|
|
}
|
|
|
|
let file = std::fs::File::create(&shadow_path).expect("failed to create shadow file");
|
|
shadow_core::shadow::write_shadow(&entries, file).expect("failed to write shadow");
|
|
|
|
let content = read_shadow(&dir);
|
|
assert!(
|
|
content.contains("alice:$6$newhash_alice:"),
|
|
"alice's hash should be updated, got: {content}"
|
|
);
|
|
assert!(
|
|
content.contains("bob:$6$newhash_bob:"),
|
|
"bob's hash should be updated, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_preserves_other_fields() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_shadow("testuser:$6$oldhash:19500:5:180:14:30:20000:\n");
|
|
let shadow_path = dir.path().join("etc/shadow");
|
|
|
|
let mut entries =
|
|
shadow_core::shadow::read_shadow_file(&shadow_path).expect("failed to read shadow");
|
|
|
|
let entry = entries
|
|
.iter_mut()
|
|
.find(|e| e.name == "testuser")
|
|
.expect("testuser not found");
|
|
entry.passwd = "$6$newhash".to_string();
|
|
|
|
let file = std::fs::File::create(&shadow_path).expect("failed to create shadow file");
|
|
shadow_core::shadow::write_shadow(&entries, file).expect("failed to write shadow");
|
|
|
|
let content = read_shadow(&dir);
|
|
// Verify aging fields are preserved.
|
|
assert!(
|
|
content.contains(":5:180:14:30:20000:"),
|
|
"aging fields should be preserved, got: {content}"
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn test_multiple_users_atomic() {
|
|
if common::skip_unless_root() {
|
|
return;
|
|
}
|
|
|
|
let dir = setup_shadow(
|
|
"user1:$6$old1:19500:0:99999:7:::\nuser2:$6$old2:19500:0:99999:7:::\nuser3:$6$old3:19500:0:99999:7:::\n",
|
|
);
|
|
let shadow_path = dir.path().join("etc/shadow");
|
|
|
|
let mut entries =
|
|
shadow_core::shadow::read_shadow_file(&shadow_path).expect("failed to read shadow");
|
|
|
|
// Update only user1 and user3, leave user2 alone.
|
|
for entry in &mut entries {
|
|
match entry.name.as_str() {
|
|
"user1" => entry.passwd = "$6$new1".to_string(),
|
|
"user3" => entry.passwd = "$6$new3".to_string(),
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
let file = std::fs::File::create(&shadow_path).expect("failed to create shadow file");
|
|
shadow_core::shadow::write_shadow(&entries, file).expect("failed to write shadow");
|
|
|
|
let content = read_shadow(&dir);
|
|
assert!(content.contains("user1:$6$new1:"));
|
|
assert!(
|
|
content.contains("user2:$6$old2:"),
|
|
"user2 should be unchanged"
|
|
);
|
|
assert!(content.contains("user3:$6$new3:"));
|
|
}
|