swarmctl: add user reset-password
authelia's file backend has no self-service reset (no SMTP notifier), so the only way a human account got a new password after the old one was forgotten was hand-editing users.yml as root. `user add` already hashes a password into the file; this verb does the same for an existing user instead of refusing on the name. Mirrors `user add`'s UX exactly: no password flag, authelia generates and hashes it (never crosses argv), and it's printed once and never stored. Refuses on an unknown user before ever invoking authelia. Same publish path as add/update, so the same atomic write and no-restart (authelia watches the file) behaviour apply. Split the digest-replacement into users::reset_password so it's testable without a command line or a running authelia, same pattern as apply_update.
This commit is contained in:
parent
ccb5bd3b38
commit
69ae23f801
4 changed files with 151 additions and 1 deletions
|
|
@ -287,6 +287,18 @@ pub fn apply_update(user: &mut User, update: &UserUpdate) -> Result<Vec<String>>
|
|||
Ok(changes)
|
||||
}
|
||||
|
||||
/// Replace a user's password digest, leaving every other field untouched.
|
||||
///
|
||||
/// A one-line function, but pulled out for the same reason [`apply_update`]
|
||||
/// is: the caller has already looked the user up (and reports "no such
|
||||
/// user" itself, the same way `user update` does), so what's left here is
|
||||
/// exactly the part that's testable without a command line or a running
|
||||
/// authelia — the digest comes from generating a real password, which
|
||||
/// isn't.
|
||||
pub fn reset_password(user: &mut User, digest: String) {
|
||||
user.password = digest;
|
||||
}
|
||||
|
||||
/// Group list for a message, so an empty one reads as a word rather than
|
||||
/// as a missing value.
|
||||
pub fn fmt_groups(groups: &[String]) -> String {
|
||||
|
|
@ -632,6 +644,31 @@ users:
|
|||
assert_eq!(u.displayname, "Test User", "the user must be untouched");
|
||||
}
|
||||
|
||||
/// The whole point of resetting one user's credential: the other
|
||||
/// entries in the store, including their own passwords, must come out
|
||||
/// exactly as they went in.
|
||||
#[test]
|
||||
fn reset_password_changes_only_the_targeted_users_digest() {
|
||||
let mut store = UserStore::default();
|
||||
store
|
||||
.users
|
||||
.insert("mara".to_owned(), user("$argon2id$old-mara"));
|
||||
store
|
||||
.users
|
||||
.insert("atlas".to_owned(), user("$argon2id$old-atlas"));
|
||||
|
||||
reset_password(
|
||||
store.users.get_mut("mara").expect("mara is in the store"),
|
||||
"$argon2id$new-mara".to_owned(),
|
||||
);
|
||||
|
||||
assert_eq!(store.users["mara"].password, "$argon2id$new-mara");
|
||||
assert_eq!(
|
||||
store.users["atlas"].password, "$argon2id$old-atlas",
|
||||
"an unrelated user's password must not change"
|
||||
);
|
||||
}
|
||||
|
||||
/// Replaces `only_the_untouched_seed_reads_as_takeable`. That test
|
||||
/// pinned the seed check, which existed to decide whether overwriting
|
||||
/// `users.yml` was safe — a question that only arose because a *second*
|
||||
|
|
|
|||
Loading…
Reference in a new issue