From 0d942e81a37742493fe43ff74ea4f07e78ed2d1e Mon Sep 17 00:00:00 2001 From: Wolfgang Bumiller Date: Thu, 23 Mar 2023 10:57:40 +0100 Subject: [PATCH] tfa: add a 'types' feature to get TfaInfo and TfaType without adding the entire API as well, so API clients can actually use the types used by the api methods without requiring the backend implementation being built in as well... Signed-off-by: Wolfgang Bumiller --- proxmox-tfa/Cargo.toml | 5 +- proxmox-tfa/src/api/methods.rs | 80 ++------------------- proxmox-tfa/src/api/mod.rs | 46 +------------ proxmox-tfa/src/lib.rs | 5 ++ proxmox-tfa/src/types.rs | 122 +++++++++++++++++++++++++++++++++ 5 files changed, 137 insertions(+), 121 deletions(-) create mode 100644 proxmox-tfa/src/types.rs diff --git a/proxmox-tfa/Cargo.toml b/proxmox-tfa/Cargo.toml index c180c16c..b371b0ab 100644 --- a/proxmox-tfa/Cargo.toml +++ b/proxmox-tfa/Cargo.toml @@ -30,6 +30,7 @@ proxmox-uuid = { workspace = true, optional = true } [features] default = [] +types = [ "serde/derive"] u2f = [ "dep:libc", "dep:serde_json", "serde/derive" ] -api = [ "u2f", "dep:webauthn-rs", "dep:proxmox-uuid", "dep:proxmox-time" ] -api-types = [ "dep:proxmox-schema" ] +api = [ "types", "u2f", "dep:webauthn-rs", "dep:proxmox-uuid", "dep:proxmox-time" ] +api-types = [ "types", "dep:proxmox-schema" ] diff --git a/proxmox-tfa/src/api/methods.rs b/proxmox-tfa/src/api/methods.rs index ebb37092..5410ccdc 100644 --- a/proxmox-tfa/src/api/methods.rs +++ b/proxmox-tfa/src/api/methods.rs @@ -12,39 +12,7 @@ use proxmox_schema::api; use super::{OpenUserChallengeData, TfaConfig, TfaInfo, TfaUserData}; use crate::totp::Totp; -#[cfg_attr(feature = "api-types", api)] -/// A TFA entry type. -#[derive(Deserialize, Serialize)] -#[serde(rename_all = "lowercase")] -pub enum TfaType { - /// A TOTP entry type. - Totp, - /// A U2F token entry. - U2f, - /// A Webauthn token entry. - Webauthn, - /// Recovery tokens. - Recovery, - /// Yubico authentication entry. - Yubico, -} - -#[cfg_attr(feature = "api-types", api( - properties: { - type: { type: TfaType }, - info: { type: TfaInfo }, - }, -))] -/// A TFA entry for a user. -#[derive(Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -pub struct TypedTfaInfo { - #[serde(rename = "type")] - pub ty: TfaType, - - #[serde(flatten)] - pub info: TfaInfo, -} +pub use crate::types::{TfaType, TfaUpdateInfo, TypedTfaInfo}; fn to_data(data: &TfaUserData) -> Vec { let mut out = Vec::with_capacity( @@ -258,44 +226,6 @@ pub fn list_tfa( Ok(out) } -#[cfg_attr(feature = "api-types", api( - properties: { - recovery: { - description: "A list of recovery codes as integers.", - type: Array, - items: { - type: Integer, - description: "A one-time usable recovery code entry.", - }, - }, - }, -))] -/// The result returned when adding TFA entries to a user. -#[derive(Default, Serialize)] -pub struct TfaUpdateInfo { - /// The id if a newly added TFA entry. - id: Option, - - /// When adding u2f entries, this contains a challenge the user must respond to in order to - /// finish the registration. - #[serde(skip_serializing_if = "Option::is_none")] - challenge: Option, - - /// When adding recovery codes, this contains the list of codes to be displayed to the user - /// this one time. - #[serde(skip_serializing_if = "Vec::is_empty", default)] - recovery: Vec, -} - -impl TfaUpdateInfo { - fn id(id: String) -> Self { - Self { - id: Some(id), - ..Default::default() - } - } -} - fn need_description(description: Option) -> Result { description.ok_or_else(|| format_err!("'description' is required for new entries")) } @@ -389,7 +319,7 @@ fn add_totp( { bail!("failed to verify TOTP challenge"); } - Ok(TfaUpdateInfo::id(config.add_totp( + Ok(TfaUpdateInfo::with_id(config.add_totp( userid, description, totp, @@ -403,7 +333,7 @@ fn add_yubico( value: Option, ) -> Result { let key = value.ok_or_else(|| format_err!("missing 'value' parameter for 'yubico' entry"))?; - Ok(TfaUpdateInfo::id(config.add_yubico( + Ok(TfaUpdateInfo::with_id(config.add_yubico( userid, description, key, @@ -431,7 +361,7 @@ fn add_u2f( })?; config .u2f_registration_finish(access, userid, &challenge, &value) - .map(TfaUpdateInfo::id) + .map(TfaUpdateInfo::with_id) } } } @@ -458,7 +388,7 @@ fn add_webauthn( })?; config .webauthn_registration_finish(access, userid, &challenge, &value, origin) - .map(TfaUpdateInfo::id) + .map(TfaUpdateInfo::with_id) } } } diff --git a/proxmox-tfa/src/api/mod.rs b/proxmox-tfa/src/api/mod.rs index 73365fb5..3a0373c0 100644 --- a/proxmox-tfa/src/api/mod.rs +++ b/proxmox-tfa/src/api/mod.rs @@ -16,9 +16,6 @@ use webauthn_rs::{proto::UserVerificationPolicy, Webauthn}; use crate::totp::Totp; use proxmox_uuid::Uuid; -#[cfg(feature = "api-types")] -use proxmox_schema::api; - mod serde_tools; mod recovery; @@ -35,6 +32,8 @@ pub use webauthn::{WebauthnConfig, WebauthnCredential}; #[cfg(feature = "api-types")] pub use webauthn::WebauthnConfigUpdater; +pub use crate::types::TfaInfo; + use recovery::Recovery; use u2f::{U2fChallenge, U2fChallengeEntry, U2fRegistrationChallenge}; use webauthn::{WebauthnAuthChallenge, WebauthnRegistrationChallenge}; @@ -825,47 +824,6 @@ impl TfaEntry { } } -#[cfg_attr(feature = "api-types", api)] -/// Over the API we only provide this part when querying a user's second factor list. -#[derive(Clone, Deserialize, Serialize)] -#[serde(deny_unknown_fields)] -pub struct TfaInfo { - /// The id used to reference this entry. - pub id: String, - - /// User chosen description for this entry. - #[serde(skip_serializing_if = "String::is_empty")] - pub description: String, - - /// Creation time of this entry as unix epoch. - pub created: i64, - - /// Whether this TFA entry is currently enabled. - #[serde(skip_serializing_if = "is_default_tfa_enable")] - #[serde(default = "default_tfa_enable")] - pub enable: bool, -} - -impl TfaInfo { - /// For recovery keys we have a fixed entry. - pub fn recovery(created: i64) -> Self { - Self { - id: "recovery".to_string(), - description: String::new(), - enable: true, - created, - } - } -} - -const fn default_tfa_enable() -> bool { - true -} - -const fn is_default_tfa_enable(v: &bool) -> bool { - *v -} - /// When sending a TFA challenge to the user, we include information about what kind of challenge /// the user may perform. If webauthn credentials are available, a webauthn challenge will be /// included. diff --git a/proxmox-tfa/src/lib.rs b/proxmox-tfa/src/lib.rs index 69309661..cda30ce9 100644 --- a/proxmox-tfa/src/lib.rs +++ b/proxmox-tfa/src/lib.rs @@ -5,3 +5,8 @@ pub mod totp; #[cfg(feature = "api")] pub mod api; + +#[cfg(feature = "types")] +mod types; +#[cfg(feature = "types")] +pub use types::{TfaInfo, TfaType, TfaUpdateInfo, TypedTfaInfo}; diff --git a/proxmox-tfa/src/types.rs b/proxmox-tfa/src/types.rs new file mode 100644 index 00000000..f87022e8 --- /dev/null +++ b/proxmox-tfa/src/types.rs @@ -0,0 +1,122 @@ +use serde::{Deserialize, Serialize}; + +#[cfg(feature = "api-types")] +use proxmox_schema::api; + +#[cfg_attr(feature = "api-types", api)] +/// A TFA entry type. +#[derive(Debug, Deserialize, Serialize)] +#[serde(rename_all = "lowercase")] +pub enum TfaType { + /// A TOTP entry type. + Totp, + /// A U2F token entry. + U2f, + /// A Webauthn token entry. + Webauthn, + /// Recovery tokens. + Recovery, + /// Yubico authentication entry. + Yubico, +} +serde_plain::derive_display_from_serialize!(TfaType); +serde_plain::derive_fromstr_from_deserialize!(TfaType); + +#[cfg_attr(feature = "api-types", api)] +/// Over the API we only provide this part when querying a user's second factor list. +#[derive(Clone, Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct TfaInfo { + /// The id used to reference this entry. + pub id: String, + + /// User chosen description for this entry. + #[serde(default, skip_serializing_if = "String::is_empty")] + pub description: String, + + /// Creation time of this entry as unix epoch. + pub created: i64, + + /// Whether this TFA entry is currently enabled. + #[serde(skip_serializing_if = "is_default_tfa_enable")] + #[serde(default = "default_tfa_enable")] + pub enable: bool, +} + +const fn default_tfa_enable() -> bool { + true +} + +const fn is_default_tfa_enable(v: &bool) -> bool { + *v +} + +impl TfaInfo { + /// For recovery keys we have a fixed entry. + pub fn recovery(created: i64) -> Self { + Self { + id: "recovery".to_string(), + description: String::new(), + enable: true, + created, + } + } +} + +#[cfg_attr( + feature = "api-types", + api( + properties: { + type: { type: TfaType }, + info: { type: TfaInfo }, + }, + ) +)] +/// A TFA entry for a user. +#[derive(Deserialize, Serialize)] +#[serde(deny_unknown_fields)] +pub struct TypedTfaInfo { + #[serde(rename = "type")] + pub ty: TfaType, + + #[serde(flatten)] + pub info: TfaInfo, +} + +#[cfg_attr(feature = "api-types", api( + properties: { + recovery: { + description: "A list of recovery codes as integers.", + type: Array, + items: { + type: Integer, + description: "A one-time usable recovery code entry.", + }, + }, + }, +))] +/// The result returned when adding TFA entries to a user. +#[derive(Default, Deserialize, Serialize)] +pub struct TfaUpdateInfo { + /// The id if a newly added TFA entry. + pub id: Option, + + /// When adding u2f entries, this contains a challenge the user must respond to in order to + /// finish the registration. + #[serde(skip_serializing_if = "Option::is_none")] + pub challenge: Option, + + /// When adding recovery codes, this contains the list of codes to be displayed to the user + /// this one time. + #[serde(skip_serializing_if = "Vec::is_empty", default)] + pub recovery: Vec, +} + +impl TfaUpdateInfo { + pub(crate) fn with_id(id: String) -> Self { + Self { + id: Some(id), + ..Default::default() + } + } +}