From: Wolfgang Bumiller <w.bumiller@proxmox.com>
To: pmg-devel@lists.proxmox.com
Subject: [pmg-devel] [PATCH perl-rs 6/7] pmg: add tfa module
Date: Fri, 26 Nov 2021 14:55:17 +0100 [thread overview]
Message-ID: <20211126135524.117846-14-w.bumiller@proxmox.com> (raw)
In-Reply-To: <20211126135524.117846-1-w.bumiller@proxmox.com>
Signed-off-by: Wolfgang Bumiller <w.bumiller@proxmox.com>
---
pmg-rs/Cargo.toml | 5 +-
pmg-rs/Makefile | 5 +-
pmg-rs/debian/control | 9 +-
pmg-rs/src/lib.rs | 1 +
pmg-rs/src/tfa.rs | 603 ++++++++++++++++++++++++++++++++++++++++++
5 files changed, 618 insertions(+), 5 deletions(-)
create mode 100644 pmg-rs/src/tfa.rs
diff --git a/pmg-rs/Cargo.toml b/pmg-rs/Cargo.toml
index 659456c..1d66bb5 100644
--- a/pmg-rs/Cargo.toml
+++ b/pmg-rs/Cargo.toml
@@ -21,13 +21,16 @@ crate-type = [ "cdylib" ]
[dependencies]
anyhow = "1.0"
hex = "0.4"
+libc = "0.2"
+nix = "0.19"
openssl = "0.10.32"
serde = "1.0"
serde_bytes = "0.11.3"
serde_json = "1.0"
+url = "2"
perlmod = { version = "0.9", features = [ "exporter" ] }
proxmox-acme-rs = { version = "0.3.1", features = ["client"] }
-
proxmox-apt = "0.8.0"
+proxmox-tfa = { version = "2", features = ["api"] }
diff --git a/pmg-rs/Makefile b/pmg-rs/Makefile
index a290544..69b2798 100644
--- a/pmg-rs/Makefile
+++ b/pmg-rs/Makefile
@@ -18,9 +18,10 @@ PM_DIRS := \
PMG/RS/APT
PM_FILES := \
- PMG/RS/Acme.pm \
PMG/RS/APT/Repositories.pm \
- PMG/RS/CSR.pm
+ PMG/RS/Acme.pm \
+ PMG/RS/CSR.pm \
+ PMG/RS/TFA.pm
ifeq ($(BUILD_MODE), release)
CARGO_BUILD_ARGS += --release
diff --git a/pmg-rs/debian/control b/pmg-rs/debian/control
index be632ba..8c22fae 100644
--- a/pmg-rs/debian/control
+++ b/pmg-rs/debian/control
@@ -6,15 +6,20 @@ Build-Depends:
debhelper (>= 12),
librust-anyhow-1+default-dev,
librust-hex-0.4+default-dev,
+ librust-libc-0.2+default-dev,
+ librust-nix-0.19+default-dev,
librust-openssl-0.10+default-dev (>= 0.10.32-~~),
- librust-perlmod-0.8+default-dev,
- librust-perlmod-0.8+exporter-dev,
+ librust-perlmod-0.8+default-dev (>= 0.8.1-~~),
+ librust-perlmod-0.8+exporter-dev (>= 0.8.1-~~),
librust-proxmox-acme-rs-0.3+client-dev (>= 0.3.1-~~),
librust-proxmox-acme-rs-0.3+default-dev (>= 0.3.1-~~),
librust-proxmox-apt-0.8+default-dev,
+ librust-proxmox-tfa-2+api-dev,
+ librust-proxmox-tfa-2+default-dev,
librust-serde-1+default-dev,
librust-serde-bytes-0.11+default-dev (>= 0.11.3-~~),
librust-serde-json-1+default-dev,
+ librust-url-2+default-dev,
Standards-Version: 4.3.0
Homepage: https://www.proxmox.com
diff --git a/pmg-rs/src/lib.rs b/pmg-rs/src/lib.rs
index 47e61b5..e3c9593 100644
--- a/pmg-rs/src/lib.rs
+++ b/pmg-rs/src/lib.rs
@@ -1,3 +1,4 @@
pub mod acme;
pub mod apt;
pub mod csr;
+pub mod tfa;
diff --git a/pmg-rs/src/tfa.rs b/pmg-rs/src/tfa.rs
new file mode 100644
index 0000000..404ddb2
--- /dev/null
+++ b/pmg-rs/src/tfa.rs
@@ -0,0 +1,603 @@
+//! This implements the `tfa.cfg` parser & TFA API calls for PMG.
+//!
+//! The exported `PMG::RS::TFA` perl package provides access to rust's `TfaConfig`.
+//! Contrary to the PVE implementation, this does not need to provide any backward compatible
+//! entries.
+//!
+//! NOTE: In PMG the tfa config is behind `PVE::INotify`'s `ccache`, so PMG sets it to `noclone` in
+//! order to avoid losing the rust magic-ref.
+
+use std::fs::File;
+use std::io::{self, Read};
+use std::os::unix::fs::OpenOptionsExt;
+use std::os::unix::io::{AsRawFd, RawFd};
+use std::path::{Path, PathBuf};
+
+use anyhow::{bail, format_err, Error};
+use nix::errno::Errno;
+use nix::sys::stat::Mode;
+
+pub(self) use proxmox_tfa::api::{
+ RecoveryState, TfaChallenge, TfaConfig, TfaResponse, U2fConfig, WebauthnConfig,
+};
+
+#[perlmod::package(name = "PMG::RS::TFA")]
+mod export {
+ use std::convert::TryInto;
+ use std::sync::Mutex;
+
+ use anyhow::{bail, format_err, Error};
+ use serde_bytes::ByteBuf;
+ use url::Url;
+
+ use perlmod::Value;
+ use proxmox_tfa::api::methods;
+
+ use super::{TfaConfig, UserAccess};
+
+ perlmod::declare_magic!(Box<Tfa> : &Tfa as "PMG::RS::TFA");
+
+ /// A TFA Config instance.
+ pub struct Tfa {
+ inner: Mutex<TfaConfig>,
+ }
+
+ /// Prevent 'dclone'.
+ #[export(name = "STORABLE_freeze", raw_return)]
+ fn storable_freeze(#[try_from_ref] _this: &Tfa, _cloning: bool) -> Result<Value, Error> {
+ bail!("freezing TFA config not supported!");
+ }
+
+ /// Parse a TFA configuration.
+ #[export(raw_return)]
+ fn new(#[raw] class: Value, config: &[u8]) -> Result<Value, Error> {
+ let mut inner: TfaConfig = serde_json::from_slice(config)
+ .map_err(|err| format_err!("failed to parse TFA file: {}", err))?;
+
+ // PMG does not support U2F.
+ inner.u2f = None;
+ Ok(perlmod::instantiate_magic!(
+ &class, MAGIC => Box::new(Tfa { inner: Mutex::new(inner) })
+ ))
+ }
+
+ /// Write the configuration out into a JSON string.
+ #[export]
+ fn write(#[try_from_ref] this: &Tfa) -> Result<serde_bytes::ByteBuf, Error> {
+ let inner = this.inner.lock().unwrap();
+ Ok(ByteBuf::from(serde_json::to_vec(&*inner)?))
+ }
+
+ /// Debug helper: serialize the TFA user data into a perl value.
+ #[export]
+ fn to_perl(#[try_from_ref] this: &Tfa) -> Result<Value, Error> {
+ let inner = this.inner.lock().unwrap();
+ Ok(perlmod::to_value(&*inner)?)
+ }
+
+ /// Get a list of all the user names in this config.
+ /// PMG uses this to verify users and purge the invalid ones.
+ #[export]
+ fn users(#[try_from_ref] this: &Tfa) -> Result<Vec<String>, Error> {
+ Ok(this.inner.lock().unwrap().users.keys().cloned().collect())
+ }
+
+ /// Remove a user from the TFA configuration.
+ #[export]
+ fn remove_user(#[try_from_ref] this: &Tfa, userid: &str) -> Result<bool, Error> {
+ Ok(this.inner.lock().unwrap().users.remove(userid).is_some())
+ }
+
+ /// Get the TFA data for a specific user.
+ #[export(raw_return)]
+ fn get_user(#[try_from_ref] this: &Tfa, userid: &str) -> Result<Value, perlmod::Error> {
+ perlmod::to_value(&this.inner.lock().unwrap().users.get(userid))
+ }
+
+ /// Add a u2f registration. This modifies the config (adds the user to it), so it needs be
+ /// written out.
+ #[export]
+ fn add_u2f_registration(
+ #[raw] raw_this: Value,
+ //#[try_from_ref] this: &Tfa,
+ userid: &str,
+ description: String,
+ ) -> Result<String, Error> {
+ let this: &Tfa = (&raw_this).try_into()?;
+ let mut inner = this.inner.lock().unwrap();
+ inner.u2f_registration_challenge(UserAccess::new(&raw_this)?, userid, description)
+ }
+
+ /// Finish a u2f registration. This updates temporary data in `/run` and therefore the config
+ /// needs to be written out!
+ #[export]
+ fn finish_u2f_registration(
+ #[raw] raw_this: Value,
+ //#[try_from_ref] this: &Tfa,
+ userid: &str,
+ challenge: &str,
+ response: &str,
+ ) -> Result<String, Error> {
+ let this: &Tfa = (&raw_this).try_into()?;
+ let mut inner = this.inner.lock().unwrap();
+ inner.u2f_registration_finish(UserAccess::new(&raw_this)?, userid, challenge, response)
+ }
+
+ /// Check if a user has any TFA entries of a given type.
+ #[export]
+ fn has_type(#[try_from_ref] this: &Tfa, userid: &str, typename: &str) -> Result<bool, Error> {
+ Ok(match this.inner.lock().unwrap().users.get(userid) {
+ Some(user) => match typename {
+ "totp" | "oath" => !user.totp.is_empty(),
+ "u2f" => !user.u2f.is_empty(),
+ "webauthn" => !user.webauthn.is_empty(),
+ "yubico" => !user.yubico.is_empty(),
+ "recovery" => match &user.recovery {
+ Some(r) => r.count_available() > 0,
+ None => false,
+ },
+ _ => bail!("unrecognized TFA type {:?}", typename),
+ },
+ None => false,
+ })
+ }
+
+ /// Generates a space separated list of yubico keys of this account.
+ #[export]
+ fn get_yubico_keys(#[try_from_ref] this: &Tfa, userid: &str) -> Result<Option<String>, Error> {
+ Ok(this.inner.lock().unwrap().users.get(userid).map(|user| {
+ user.enabled_yubico_entries()
+ .fold(String::new(), |mut s, k| {
+ if !s.is_empty() {
+ s.push(' ');
+ }
+ s.push_str(k);
+ s
+ })
+ }))
+ }
+
+ #[export]
+ fn set_u2f_config(#[try_from_ref] this: &Tfa, config: Option<super::U2fConfig>) {
+ this.inner.lock().unwrap().u2f = config;
+ }
+
+ #[export]
+ fn set_webauthn_config(
+ #[try_from_ref] this: &Tfa,
+ config: Option<super::WebauthnConfig>,
+ ) -> Result<(), Error> {
+ this.inner.lock().unwrap().webauthn = config.map(TryInto::try_into).transpose()?;
+ Ok(())
+ }
+
+ #[export]
+ fn get_webauthn_config(
+ #[try_from_ref] this: &Tfa,
+ ) -> Result<(Option<String>, Option<super::WebauthnConfig>), Error> {
+ Ok(match this.inner.lock().unwrap().webauthn.clone() {
+ Some(config) => (Some(hex::encode(&config.digest())), Some(config.into())),
+ None => (None, None),
+ })
+ }
+
+ #[export]
+ fn has_webauthn_origin(#[try_from_ref] this: &Tfa) -> bool {
+ match &this.inner.lock().unwrap().webauthn {
+ Some(wa) => wa.origin.is_some(),
+ None => false,
+ }
+ }
+
+ /// Create an authentication challenge.
+ ///
+ /// Returns the challenge as a json string.
+ /// Returns `undef` if no second factor is configured.
+ #[export]
+ fn authentication_challenge(
+ #[raw] raw_this: Value,
+ //#[try_from_ref] this: &Tfa,
+ userid: &str,
+ origin: Option<Url>,
+ ) -> Result<Option<String>, Error> {
+ let this: &Tfa = (&raw_this).try_into()?;
+ let mut inner = this.inner.lock().unwrap();
+ match inner.authentication_challenge(
+ UserAccess::new(&raw_this)?,
+ userid,
+ origin.as_ref(),
+ )? {
+ Some(challenge) => Ok(Some(serde_json::to_string(&challenge)?)),
+ None => Ok(None),
+ }
+ }
+
+ /// Get the recovery state (suitable for a challenge object).
+ #[export]
+ fn recovery_state(#[try_from_ref] this: &Tfa, userid: &str) -> Option<super::RecoveryState> {
+ this.inner
+ .lock()
+ .unwrap()
+ .users
+ .get(userid)
+ .and_then(|user| {
+ let state = user.recovery_state();
+ state.is_available().then(move || state)
+ })
+ }
+
+ /// Takes the TFA challenge string (which is a json object) and verifies ther esponse against
+ /// it.
+ ///
+ /// NOTE: This returns a boolean whether the config data needs to be *saved* after this call
+ /// (to use up recovery keys!).
+ #[export]
+ fn authentication_verify(
+ #[raw] raw_this: Value,
+ //#[try_from_ref] this: &Tfa,
+ userid: &str,
+ challenge: &str, //super::TfaChallenge,
+ response: &str,
+ origin: Option<Url>,
+ ) -> Result<bool, Error> {
+ let this: &Tfa = (&raw_this).try_into()?;
+ let challenge: super::TfaChallenge = serde_json::from_str(challenge)?;
+ let response: super::TfaResponse = response.parse()?;
+ let mut inner = this.inner.lock().unwrap();
+ inner
+ .verify(
+ UserAccess::new(&raw_this)?,
+ userid,
+ &challenge,
+ response,
+ origin.as_ref(),
+ )
+ .map(|save| save.needs_saving())
+ }
+
+ /// DEBUG HELPER: Get the current TOTP value for a given TOTP URI.
+ #[export]
+ fn get_current_totp_value(otp_uri: &str) -> Result<String, Error> {
+ let totp: proxmox_tfa::totp::Totp = otp_uri.parse()?;
+ Ok(totp.time(std::time::SystemTime::now())?.to_string())
+ }
+
+ #[export]
+ fn api_list_user_tfa(
+ #[try_from_ref] this: &Tfa,
+ userid: &str,
+ ) -> Result<Vec<methods::TypedTfaInfo>, Error> {
+ methods::list_user_tfa(&this.inner.lock().unwrap(), userid)
+ }
+
+ #[export]
+ fn api_get_tfa_entry(
+ #[try_from_ref] this: &Tfa,
+ userid: &str,
+ id: &str,
+ ) -> Option<methods::TypedTfaInfo> {
+ methods::get_tfa_entry(&this.inner.lock().unwrap(), userid, id)
+ }
+
+ /// Returns `true` if the user still has other TFA entries left, `false` if the user has *no*
+ /// more tfa entries.
+ #[export]
+ fn api_delete_tfa(#[try_from_ref] this: &Tfa, userid: &str, id: String) -> Result<bool, Error> {
+ let mut this = this.inner.lock().unwrap();
+ match methods::delete_tfa(&mut this, userid, &id) {
+ Ok(has_entries_left) => Ok(has_entries_left),
+ Err(methods::EntryNotFound) => bail!("no such entry"),
+ }
+ }
+
+ #[export]
+ fn api_list_tfa(
+ #[try_from_ref] this: &Tfa,
+ authid: &str,
+ top_level_allowed: bool,
+ ) -> Result<Vec<methods::TfaUser>, Error> {
+ methods::list_tfa(&this.inner.lock().unwrap(), authid, top_level_allowed)
+ }
+
+ #[export]
+ fn api_add_tfa_entry(
+ #[raw] raw_this: Value,
+ //#[try_from_ref] this: &Tfa,
+ userid: &str,
+ description: Option<String>,
+ totp: Option<String>,
+ value: Option<String>,
+ challenge: Option<String>,
+ ty: methods::TfaType,
+ origin: Option<Url>,
+ ) -> Result<methods::TfaUpdateInfo, Error> {
+ let this: &Tfa = (&raw_this).try_into()?;
+ methods::add_tfa_entry(
+ &mut this.inner.lock().unwrap(),
+ UserAccess::new(&raw_this)?,
+ userid,
+ description,
+ totp,
+ value,
+ challenge,
+ ty,
+ origin.as_ref(),
+ )
+ }
+
+ /// Add a totp entry without validating it, used for user.cfg keys.
+ /// Returns the ID.
+ #[export]
+ fn add_totp_entry(
+ #[try_from_ref] this: &Tfa,
+ userid: &str,
+ description: String,
+ totp: String,
+ ) -> Result<String, Error> {
+ Ok(this
+ .inner
+ .lock()
+ .unwrap()
+ .add_totp(userid, description, totp.parse()?))
+ }
+
+ /// Add a yubico entry without validating it, used for user.cfg keys.
+ /// Returns the ID.
+ #[export]
+ fn add_yubico_entry(
+ #[try_from_ref] this: &Tfa,
+ userid: &str,
+ description: String,
+ yubico: String,
+ ) -> String {
+ this.inner
+ .lock()
+ .unwrap()
+ .add_yubico(userid, description, yubico)
+ }
+
+ #[export]
+ fn api_update_tfa_entry(
+ #[try_from_ref] this: &Tfa,
+ userid: &str,
+ id: &str,
+ description: Option<String>,
+ enable: Option<bool>,
+ ) -> Result<(), Error> {
+ match methods::update_tfa_entry(
+ &mut this.inner.lock().unwrap(),
+ userid,
+ id,
+ description,
+ enable,
+ ) {
+ Ok(()) => Ok(()),
+ Err(methods::EntryNotFound) => bail!("no such entry"),
+ }
+ }
+}
+
+/// Attach the path to errors from [`nix::mkir()`].
+pub(crate) fn mkdir<P: AsRef<Path>>(path: P, mode: libc::mode_t) -> Result<(), Error> {
+ let path = path.as_ref();
+ match nix::unistd::mkdir(path, unsafe { Mode::from_bits_unchecked(mode) }) {
+ Ok(()) => Ok(()),
+ Err(nix::Error::Sys(Errno::EEXIST)) => Ok(()),
+ Err(err) => bail!("failed to create directory {:?}: {}", path, err),
+ }
+}
+
+#[cfg(debug_assertions)]
+#[derive(Clone)]
+#[repr(transparent)]
+pub struct UserAccess(perlmod::Value);
+
+#[cfg(debug_assertions)]
+impl UserAccess {
+ #[inline]
+ fn new(value: &perlmod::Value) -> Result<Self, Error> {
+ value
+ .dereference()
+ .ok_or_else(|| format_err!("bad TFA config object"))
+ .map(Self)
+ }
+
+ #[inline]
+ fn is_debug(&self) -> bool {
+ self.0
+ .as_hash()
+ .and_then(|v| v.get("-debug"))
+ .map(|v| v.iv() != 0)
+ .unwrap_or(false)
+ }
+}
+
+#[cfg(not(debug_assertions))]
+#[derive(Clone, Copy)]
+#[repr(transparent)]
+pub struct UserAccess;
+
+#[cfg(not(debug_assertions))]
+impl UserAccess {
+ #[inline]
+ const fn new(_value: &perlmod::Value) -> Result<Self, std::convert::Infallible> {
+ Ok(Self)
+ }
+
+ #[inline]
+ const fn is_debug(&self) -> bool {
+ false
+ }
+}
+
+/// Build the path to the challenge data file for a user.
+fn challenge_data_path(userid: &str, debug: bool) -> PathBuf {
+ if debug {
+ PathBuf::from(format!("./local-tfa-challenges/{}", userid))
+ } else {
+ PathBuf::from(format!("/run/pmg-private/tfa-challenges/{}", userid))
+ }
+}
+
+impl proxmox_tfa::api::OpenUserChallengeData for UserAccess {
+ type Data = UserChallengeData;
+
+ fn open(&self, userid: &str) -> Result<UserChallengeData, Error> {
+ if self.is_debug() {
+ mkdir("./local-tfa-challenges", 0o700)?;
+ } else {
+ mkdir("/run/pmg-private", 0o700)?;
+ mkdir("/run/pmg-private/tfa-challenges", 0o700)?;
+ }
+
+ let path = challenge_data_path(userid, self.is_debug());
+
+ let mut file = std::fs::OpenOptions::new()
+ .create(true)
+ .read(true)
+ .write(true)
+ .truncate(false)
+ .mode(0o600)
+ .open(&path)
+ .map_err(|err| format_err!("failed to create challenge file {:?}: {}", &path, err))?;
+
+ UserChallengeData::lock_file(file.as_raw_fd())?;
+
+ // the file may be empty, so read to a temporary buffer first:
+ let mut data = Vec::with_capacity(4096);
+
+ file.read_to_end(&mut data).map_err(|err| {
+ format_err!("failed to read challenge data for user {}: {}", userid, err)
+ })?;
+
+ let inner = if data.is_empty() {
+ Default::default()
+ } else {
+ match serde_json::from_slice(&data) {
+ Ok(inner) => inner,
+ Err(err) => {
+ eprintln!(
+ "failed to parse challenge data for user {}: {}",
+ userid, err
+ );
+ Default::default()
+ }
+ }
+ };
+
+ Ok(UserChallengeData {
+ inner,
+ path,
+ lock: file,
+ })
+ }
+
+ /// `open` without creating the file if it doesn't exist, to finish WA authentications.
+ fn open_no_create(&self, userid: &str) -> Result<Option<UserChallengeData>, Error> {
+ let path = challenge_data_path(userid, self.is_debug());
+
+ let mut file = match std::fs::OpenOptions::new()
+ .read(true)
+ .write(true)
+ .truncate(false)
+ .mode(0o600)
+ .open(&path)
+ {
+ Ok(file) => file,
+ Err(err) if err.kind() == io::ErrorKind::NotFound => return Ok(None),
+ Err(err) => return Err(err.into()),
+ };
+
+ UserChallengeData::lock_file(file.as_raw_fd())?;
+
+ let inner = serde_json::from_reader(&mut file).map_err(|err| {
+ format_err!("failed to read challenge data for user {}: {}", userid, err)
+ })?;
+
+ Ok(Some(UserChallengeData {
+ inner,
+ path,
+ lock: file,
+ }))
+ }
+
+ fn remove(&self, userid: &str) -> Result<bool, Error> {
+ let path = challenge_data_path(userid, self.is_debug());
+ match std::fs::remove_file(&path) {
+ Ok(()) => Ok(true),
+ Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(false),
+ Err(err) => Err(err.into()),
+ }
+ }
+}
+
+/// Container of `TfaUserChallenges` with the corresponding file lock guard.
+///
+/// Basically provides the TFA API to the REST server by persisting, updating and verifying active
+/// challenges.
+pub struct UserChallengeData {
+ inner: proxmox_tfa::api::TfaUserChallenges,
+ path: PathBuf,
+ lock: File,
+}
+
+impl proxmox_tfa::api::UserChallengeAccess for UserChallengeData {
+ fn get_mut(&mut self) -> &mut proxmox_tfa::api::TfaUserChallenges {
+ &mut self.inner
+ }
+
+ fn save(self) -> Result<(), Error> {
+ UserChallengeData::save(self)
+ }
+}
+
+impl UserChallengeData {
+ fn lock_file(fd: RawFd) -> Result<(), Error> {
+ let rc = unsafe { libc::flock(fd, libc::LOCK_EX) };
+
+ if rc != 0 {
+ let err = io::Error::last_os_error();
+ bail!("failed to lock tfa user challenge data: {}", err);
+ }
+
+ Ok(())
+ }
+
+ /// Rewind & truncate the file for an update.
+ fn rewind(&mut self) -> Result<(), Error> {
+ use std::io::{Seek, SeekFrom};
+
+ let pos = self.lock.seek(SeekFrom::Start(0))?;
+ if pos != 0 {
+ bail!(
+ "unexpected result trying to rewind file, position is {}",
+ pos
+ );
+ }
+
+ let rc = unsafe { libc::ftruncate(self.lock.as_raw_fd(), 0) };
+ if rc != 0 {
+ let err = io::Error::last_os_error();
+ bail!("failed to truncate challenge data: {}", err);
+ }
+
+ Ok(())
+ }
+
+ /// Save the current data. Note that we do not replace the file here since we lock the file
+ /// itself, as it is in `/run`, and the typical error case for this particular situation
+ /// (machine loses power) simply prevents some login, but that'll probably fail anyway for
+ /// other reasons then...
+ ///
+ /// This currently consumes selfe as we never perform more than 1 insertion/removal, and this
+ /// way also unlocks early.
+ fn save(mut self) -> Result<(), Error> {
+ self.rewind()?;
+
+ serde_json::to_writer(&mut &self.lock, &self.inner).map_err(|err| {
+ format_err!("failed to update challenge file {:?}: {}", self.path, err)
+ })?;
+
+ Ok(())
+ }
+}
--
2.30.2
next prev parent reply other threads:[~2021-11-26 13:55 UTC|newest]
Thread overview: 24+ messages / expand[flat|nested] mbox.gz Atom feed top
2021-11-26 13:55 [pmg-devel] [PATCH multiple 0/7] PMG TFA support Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 1/6] add tfa.json and its lock methods Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 2/6] add PMG::TFAConfig module Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 3/6] add TFA API Wolfgang Bumiller
2021-11-26 17:29 ` Stoiko Ivanov
2021-11-26 13:55 ` [pmg-devel] [PATCH api 4/6] add tfa config api Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 5/6] implement tfa authentication Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH api 6/6] provide qrcode.min.js from libjs-qrcodejs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH gui] add TFA components Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 1/7] pve: bump perlmod to 0.9 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 2/7] pve: update to proxmox-tfa 2.0 Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 3/7] pve: bump d/control Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 4/7] import pmg-rs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 5/7] pmg: bump perlmod to 0.9 Wolfgang Bumiller
2021-11-26 13:55 ` Wolfgang Bumiller [this message]
2021-11-26 13:55 ` [pmg-devel] [PATCH perl-rs 7/7] pmg: bump d/control Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 1/6] tfa: fix typo in docs Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 2/6] tfa: add WebauthnConfig::digest method Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 3/6] tfa: let OriginUrl deref to its inner Url, add FromStr impl Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 4/6] tfa: make configured webauthn origin optional Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 5/6] tfa: clippy fixes Wolfgang Bumiller
2021-11-26 13:55 ` [pmg-devel] [PATCH proxmox 6/6] bump proxmox-tfa to 2.0.0-1 Wolfgang Bumiller
2021-11-26 17:34 ` [pmg-devel] [PATCH multiple 0/7] PMG TFA support Stoiko Ivanov
2021-11-28 21:17 ` [pmg-devel] applied-series: " Thomas Lamprecht
Reply instructions:
You may reply publicly to this message via plain-text email
using any one of the following methods:
* Save the following mbox file, import it into your mail client,
and reply-to-all from there: mbox
Avoid top-posting and favor interleaved quoting:
https://en.wikipedia.org/wiki/Posting_style#Interleaved_style
* Reply using the --to, --cc, and --in-reply-to
switches of git-send-email(1):
git send-email \
--in-reply-to=20211126135524.117846-14-w.bumiller@proxmox.com \
--to=w.bumiller@proxmox.com \
--cc=pmg-devel@lists.proxmox.com \
/path/to/YOUR_REPLY
https://kernel.org/pub/software/scm/git/docs/git-send-email.html
* If your mail client supports setting the In-Reply-To header
via mailto: links, try the mailto: link
Be sure your reply has a Subject: header at the top and a blank line
before the message body.
This is an external index of several public inboxes,
see mirroring instructions on how to clone and mirror
all data and code used by this external index.