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() + } + } +}