diff --git a/pve-rs/src/tfa.rs b/pve-rs/src/tfa.rs index e879cec..6f028a5 100644 --- a/pve-rs/src/tfa.rs +++ b/pve-rs/src/tfa.rs @@ -27,6 +27,7 @@ pub(self) use proxmox_tfa::api::{ #[perlmod::package(name = "PVE::RS::TFA")] mod export { + use std::collections::HashMap; use std::convert::TryInto; use std::sync::Mutex; @@ -492,6 +493,56 @@ mod export { userid, )?) } + + #[derive(serde::Serialize)] + #[serde(rename_all = "kebab-case")] + struct TfaLockStatus { + /// Once a user runs into a TOTP limit they get locked out of TOTP until they successfully use + /// a recovery key. + #[serde(skip_serializing_if = "bool_is_false", default)] + totp_locked: bool, + + /// If a user hits too many 2nd factor failures, they get completely blocked for a while. + #[serde(skip_serializing_if = "Option::is_none", default)] + #[serde(deserialize_with = "filter_expired_timestamp")] + tfa_locked_until: Option, + } + + impl From<&proxmox_tfa::api::TfaUserData> for TfaLockStatus { + fn from(data: &proxmox_tfa::api::TfaUserData) -> Self { + Self { + totp_locked: data.totp_locked, + tfa_locked_until: data.tfa_locked_until, + } + } + } + + fn bool_is_false(b: &bool) -> bool { + !*b + } + + #[export] + fn tfa_lock_status( + #[try_from_ref] this: &Tfa, + userid: Option<&str>, + ) -> Result, Error> { + let this = this.inner.lock().unwrap(); + if let Some(userid) = userid { + if let Some(user) = this.users.get(userid) { + Ok(Some(perlmod::to_value(&TfaLockStatus::from(user))?)) + } else { + Ok(None) + } + } else { + Ok(Some(perlmod::to_value( + &HashMap::::from_iter( + this.users + .iter() + .map(|(uid, data)| (uid.clone(), TfaLockStatus::from(data))), + ), + )?)) + } + } } /// Version 1 format of `/etc/pve/priv/tfa.cfg`