mirror of
https://github.com/uutils/shadow.git
synced 2026-06-10 16:14:57 -07:00
passwd: add PAM integration, --root, --quiet, comprehensive tests
shadow-core/pam: complete PAM FFI bindings (feature-gated): - pam_start/pam_end/pam_authenticate/pam_acct_mgmt/pam_chauthtok - Conversation function with terminal (echo disable) and stdin modes - Safe PamContext wrapper with RAII cleanup - All unsafe blocks have SAFETY comments passwd tool — all 17 flags now implemented: - Default password change via PAM (authenticate + chauthtok) - --keep-tokens (-k): PAM_CHANGE_EXPIRED_AUTHTOK flag - --stdin (-s): read password from stdin instead of /dev/tty - --repository (-r): passed through to PAM - --root (-R): chroot into directory before operations - --quiet (-q): suppress action messages on stderr shadow-core/shadow: 15 new ShadowEntry helper tests: - is_locked, has_no_password, lock, unlock edge cases (double lock, unlock-only-bang, unlock-double-bang) - delete_password, expire, status_char passwd tool: 29 new tests (43 total in tool, 92 workspace-wide): - Clap validation (all conflict groups, flag parsing, value requirements) - Integration tests with --prefix (lock/unlock/delete/expire lifecycle, multi-user isolation, nonexistent user, missing file, quiet mode) - Full lifecycle test: lock→L→unlock→P→delete→NP→expire All 92 tests pass on Debian/Alpine/Fedora, zero clippy warnings.
This commit is contained in:
+1
-1
@@ -35,7 +35,7 @@ clap = { version = "4", features = ["derive", "wrap_help"] }
|
||||
thiserror = "2"
|
||||
|
||||
# Unix/Linux
|
||||
nix = { version = "0.29", features = ["user", "fs", "process", "signal"] }
|
||||
nix = { version = "0.29", features = ["user", "fs", "process", "signal", "term"] }
|
||||
libc = "0.2"
|
||||
|
||||
# Testing
|
||||
|
||||
+882
-2
File diff suppressed because it is too large
Load Diff
@@ -241,4 +241,258 @@ mod tests {
|
||||
assert_eq!(entry.inactive_days, None);
|
||||
assert_eq!(entry.expire_date, None);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------
|
||||
// ShadowEntry helper method tests
|
||||
// -------------------------------------------------------------------
|
||||
|
||||
#[test]
|
||||
fn test_is_locked_with_bang() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "!$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(entry.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_is_locked_without_bang() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(!entry.is_locked());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_no_password_empty() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: String::new(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(entry.has_no_password());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_has_no_password_with_hash() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(!entry.has_no_password());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lock_adds_bang() {
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
entry.lock();
|
||||
assert_eq!(entry.passwd, "!$6$hash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lock_already_locked_adds_another() {
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "!$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
entry.lock();
|
||||
assert_eq!(entry.passwd, "!!$6$hash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unlock_removes_bang() {
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "!$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(entry.unlock());
|
||||
assert_eq!(entry.passwd, "$6$hash");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unlock_not_locked_returns_false() {
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(!entry.unlock());
|
||||
assert_eq!(entry.passwd, "$6$hash", "should be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unlock_only_bang_returns_false() {
|
||||
// "!" alone cannot be unlocked — would result in empty password.
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "!".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(!entry.unlock());
|
||||
assert_eq!(entry.passwd, "!", "should be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_unlock_double_bang_returns_false() {
|
||||
// "!!" — removing one '!' leaves "!" which is still invalid.
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "!!".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert!(!entry.unlock());
|
||||
assert_eq!(entry.passwd, "!!", "should be unchanged");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_delete_password_clears() {
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
entry.delete_password();
|
||||
assert_eq!(entry.passwd, "");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_expire_sets_zero() {
|
||||
let mut entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: Some(19500),
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
entry.expire();
|
||||
assert_eq!(entry.last_change, Some(0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_char_locked() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "!$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert_eq!(entry.status_char(), "L");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_char_no_password() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: String::new(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert_eq!(entry.status_char(), "NP");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_status_char_usable() {
|
||||
let entry = ShadowEntry {
|
||||
name: "u".to_string(),
|
||||
passwd: "$6$hash".to_string(),
|
||||
last_change: None,
|
||||
min_age: None,
|
||||
max_age: None,
|
||||
warn_days: None,
|
||||
inactive_days: None,
|
||||
expire_date: None,
|
||||
reserved: String::new(),
|
||||
};
|
||||
assert_eq!(entry.status_char(), "P");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,6 +21,10 @@ nix = { workspace = true }
|
||||
shadow-core = { workspace = true, features = ["shadow", "login-defs"] }
|
||||
thiserror = { workspace = true }
|
||||
|
||||
[features]
|
||||
default = []
|
||||
pam = ["shadow-core/pam"]
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = { workspace = true }
|
||||
|
||||
|
||||
+498
-127
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user