mirror of
https://github.com/kanidm/kanidm.git
synced 2025-02-23 12:37:00 +01:00
1121 multiple totp (#1325)
This commit is contained in:
parent
fb265391c8
commit
84fc7d0bac
|
@ -1468,8 +1468,9 @@ impl KanidmClient {
|
|||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
totp_chal: u32,
|
||||
label: &str,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
let scr = CURequest::TotpVerify(totp_chal);
|
||||
let scr = CURequest::TotpVerify(totp_chal, label.to_string());
|
||||
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
|
||||
.await
|
||||
}
|
||||
|
@ -1486,8 +1487,9 @@ impl KanidmClient {
|
|||
pub async fn idm_account_credential_update_remove_totp(
|
||||
&self,
|
||||
session_token: &CUSessionToken,
|
||||
label: &str,
|
||||
) -> Result<CUStatus, ClientError> {
|
||||
let scr = CURequest::TotpRemove;
|
||||
let scr = CURequest::TotpRemove(label.to_string());
|
||||
self.perform_simple_post_request("/v1/credential/_update", &(scr, &session_token))
|
||||
.await
|
||||
}
|
||||
|
|
|
@ -643,7 +643,7 @@ pub enum CredentialDetailType {
|
|||
GeneratedPassword,
|
||||
Passkey(Vec<String>),
|
||||
/// totp, webauthn
|
||||
PasswordMfa(bool, Vec<String>, usize),
|
||||
PasswordMfa(Vec<String>, Vec<String>, usize),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Clone)]
|
||||
|
@ -675,24 +675,30 @@ impl fmt::Display for CredentialDetail {
|
|||
write!(f, "")
|
||||
}
|
||||
}
|
||||
CredentialDetailType::PasswordMfa(totp, labels, backup_code) => {
|
||||
CredentialDetailType::PasswordMfa(totp_labels, wan_labels, backup_code) => {
|
||||
writeln!(f, "password: set")?;
|
||||
if *totp {
|
||||
writeln!(f, "totp: enabled")?;
|
||||
|
||||
if !totp_labels.is_empty() {
|
||||
writeln!(f, "totp:")?;
|
||||
for label in totp_labels {
|
||||
writeln!(f, " * {}", label)?;
|
||||
}
|
||||
} else {
|
||||
writeln!(f, "totp: disabled")?;
|
||||
}
|
||||
|
||||
if *backup_code > 0 {
|
||||
writeln!(f, "backup_code: enabled")?;
|
||||
} else {
|
||||
writeln!(f, "backup_code: disabled")?;
|
||||
}
|
||||
if !labels.is_empty() {
|
||||
|
||||
if !wan_labels.is_empty() {
|
||||
// We no longer show the deprecated security key case by default.
|
||||
writeln!(f, " ⚠️ warning - security keys are deprecated.")?;
|
||||
writeln!(f, " ⚠️ you should re-enroll these to passkeys.")?;
|
||||
writeln!(f, "security keys:")?;
|
||||
for label in labels {
|
||||
for label in wan_labels {
|
||||
writeln!(f, " * {}", label)?;
|
||||
}
|
||||
write!(f, "")
|
||||
|
@ -1123,9 +1129,9 @@ pub enum CURequest {
|
|||
Password(String),
|
||||
CancelMFAReg,
|
||||
TotpGenerate,
|
||||
TotpVerify(u32),
|
||||
TotpVerify(u32, String),
|
||||
TotpAcceptSha1,
|
||||
TotpRemove,
|
||||
TotpRemove(String),
|
||||
BackupCodeGenerate,
|
||||
BackupCodeRemove,
|
||||
PasskeyInit,
|
||||
|
@ -1140,9 +1146,9 @@ impl fmt::Debug for CURequest {
|
|||
CURequest::Password(_) => "CURequest::Password",
|
||||
CURequest::CancelMFAReg => "CURequest::CancelMFAReg",
|
||||
CURequest::TotpGenerate => "CURequest::TotpGenerate",
|
||||
CURequest::TotpVerify(_) => "CURequest::TotpVerify",
|
||||
CURequest::TotpVerify(_, _) => "CURequest::TotpVerify",
|
||||
CURequest::TotpAcceptSha1 => "CURequest::TotpAcceptSha1",
|
||||
CURequest::TotpRemove => "CURequest::TotpRemove",
|
||||
CURequest::TotpRemove(_) => "CURequest::TotpRemove",
|
||||
CURequest::BackupCodeGenerate => "CURequest::BackupCodeGenerate",
|
||||
CURequest::BackupCodeRemove => "CURequest::BackupCodeRemove",
|
||||
CURequest::PasskeyInit => "CURequest::PasskeyInit",
|
||||
|
|
|
@ -8,6 +8,7 @@ use kanidm_client::KanidmClient;
|
|||
use kanidm_proto::messages::{AccountChangeMessage, ConsoleOutputMode, MessageStatus};
|
||||
use kanidm_proto::v1::OperationError::PasswordQuality;
|
||||
use kanidm_proto::v1::{CUIntentToken, CURegState, CUSessionToken, CUStatus, TotpSecret};
|
||||
use kanidm_proto::v1::{CredentialDetail, CredentialDetailType};
|
||||
use qrcode::render::unicode;
|
||||
use qrcode::QrCode;
|
||||
use time::OffsetDateTime;
|
||||
|
@ -577,7 +578,7 @@ impl AccountCredential {
|
|||
println!();
|
||||
println!("This link: {}", url.as_str());
|
||||
println!(
|
||||
"Or run this command: kanidm account credential use_reset_token {}",
|
||||
"Or run this command: kanidm person credential use_reset_token {}",
|
||||
cuintent_token.token
|
||||
);
|
||||
println!();
|
||||
|
@ -672,6 +673,11 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
|
|||
}
|
||||
};
|
||||
|
||||
let label: String = Input::new()
|
||||
.with_prompt("TOTP Label")
|
||||
.interact_text()
|
||||
.expect("Failed to interact with interactive session");
|
||||
|
||||
// gen the qr
|
||||
println!("Scan the following QR code with your OTP app.");
|
||||
|
||||
|
@ -739,7 +745,7 @@ async fn totp_enroll_prompt(session_token: &CUSessionToken, client: &KanidmClien
|
|||
|
||||
// Submit and see what we get.
|
||||
match client
|
||||
.idm_account_credential_update_check_totp(session_token, totp_chal)
|
||||
.idm_account_credential_update_check_totp(session_token, totp_chal, &label)
|
||||
.await
|
||||
{
|
||||
Ok(CUStatus {
|
||||
|
@ -978,13 +984,47 @@ async fn credential_update_exec(
|
|||
}
|
||||
CUAction::Totp => totp_enroll_prompt(&session_token, &client).await,
|
||||
CUAction::TotpRemove => {
|
||||
if Confirm::new()
|
||||
.with_prompt("Do you want to remove your totp?")
|
||||
.interact()
|
||||
.expect("Failed to interact with interactive session")
|
||||
match client
|
||||
.idm_account_credential_update_status(&session_token)
|
||||
.await
|
||||
{
|
||||
Ok(status) => match status.primary {
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
type_: CredentialDetailType::PasswordMfa(totp_labels, ..),
|
||||
}) => {
|
||||
if totp_labels.is_empty() {
|
||||
println!("No totps are configured for this user");
|
||||
return;
|
||||
} else {
|
||||
println!("Current totps:");
|
||||
for totp_label in totp_labels {
|
||||
println!(" {}", totp_label);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
println!("No totps are configured for this user");
|
||||
return;
|
||||
}
|
||||
},
|
||||
Err(e) => {
|
||||
eprintln!(
|
||||
"An error occurred retrieving existing credentials -> {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let label: String = Input::new()
|
||||
.with_prompt("\nEnter the label of the Passkey to remove (blank to stop) # ")
|
||||
.allow_empty(true)
|
||||
.interact_text()
|
||||
.expect("Failed to interact with interactive session");
|
||||
|
||||
if !label.is_empty() {
|
||||
if let Err(e) = client
|
||||
.idm_account_credential_update_remove_totp(&session_token)
|
||||
.idm_account_credential_update_remove_totp(&session_token, &label)
|
||||
.await
|
||||
{
|
||||
eprintln!("An error occurred -> {:?}", e);
|
||||
|
@ -1055,9 +1095,13 @@ async fn credential_update_exec(
|
|||
}
|
||||
}
|
||||
Err(e) => {
|
||||
eprintln!("An error occurred pulling existing credentials -> {:?}", e);
|
||||
eprintln!(
|
||||
"An error occurred retrieving existing credentials -> {:?}",
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let uuid_s: String = Input::new()
|
||||
.with_prompt("\nEnter the UUID of the Passkey to remove (blank to stop) # ")
|
||||
.validate_with(|input: &String| -> Result<(), &str> {
|
||||
|
|
|
@ -1053,8 +1053,8 @@ impl QueryServerReadV1 {
|
|||
);
|
||||
e
|
||||
}),
|
||||
CURequest::TotpVerify(totp_chal) => idms_cred_update
|
||||
.credential_primary_check_totp(&session_token, ct, totp_chal)
|
||||
CURequest::TotpVerify(totp_chal, label) => idms_cred_update
|
||||
.credential_primary_check_totp(&session_token, ct, totp_chal, &label)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
|
@ -1071,8 +1071,8 @@ impl QueryServerReadV1 {
|
|||
);
|
||||
e
|
||||
}),
|
||||
CURequest::TotpRemove => idms_cred_update
|
||||
.credential_primary_remove_totp(&session_token, ct)
|
||||
CURequest::TotpRemove(label) => idms_cred_update
|
||||
.credential_primary_remove_totp(&session_token, ct, &label)
|
||||
.map_err(|e| {
|
||||
admin_error!(
|
||||
err = ?e,
|
||||
|
|
|
@ -176,10 +176,6 @@ pub enum DbCred {
|
|||
uuid: Uuid,
|
||||
},
|
||||
|
||||
#[serde(rename = "V2Pw")]
|
||||
V2Password { password: DbPasswordV1, uuid: Uuid },
|
||||
#[serde(rename = "V2GPw")]
|
||||
V2GenPassword { password: DbPasswordV1, uuid: Uuid },
|
||||
#[serde(rename = "V2PwMfa")]
|
||||
V2PasswordMfa {
|
||||
password: DbPasswordV1,
|
||||
|
@ -188,6 +184,20 @@ pub enum DbCred {
|
|||
webauthn: Vec<(String, SecurityKeyV4)>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
|
||||
// New Formats!
|
||||
#[serde(rename = "V2Pw")]
|
||||
V2Password { password: DbPasswordV1, uuid: Uuid },
|
||||
#[serde(rename = "V2GPw")]
|
||||
V2GenPassword { password: DbPasswordV1, uuid: Uuid },
|
||||
#[serde(rename = "V3PwMfa")]
|
||||
V3PasswordMfa {
|
||||
password: DbPasswordV1,
|
||||
totp: Vec<(String, DbTotpV1)>,
|
||||
backup_code: Option<DbBackupCodeV1>,
|
||||
webauthn: Vec<(String, SecurityKeyV4)>,
|
||||
uuid: Uuid,
|
||||
},
|
||||
}
|
||||
|
||||
impl fmt::Display for DbCred {
|
||||
|
@ -280,6 +290,20 @@ impl fmt::Display for DbCred {
|
|||
backup_code.is_some(),
|
||||
uuid
|
||||
),
|
||||
DbCred::V3PasswordMfa {
|
||||
password: _,
|
||||
totp,
|
||||
backup_code,
|
||||
webauthn,
|
||||
uuid,
|
||||
} => write!(
|
||||
f,
|
||||
"V3PwMfa (p true, w {}, t {}, b {}, u {})",
|
||||
webauthn.len(),
|
||||
totp.len(),
|
||||
backup_code.is_some(),
|
||||
uuid
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -565,7 +565,7 @@ pub const JSON_SYSTEM_INFO_V1: &str = r#"{
|
|||
"class": ["object", "system_info", "system"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000001"],
|
||||
"description": ["System (local) info and metadata object."],
|
||||
"version": ["9"]
|
||||
"version": ["10"]
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
@ -574,8 +574,7 @@ pub const JSON_DOMAIN_INFO_V1: &str = r#"{
|
|||
"class": ["object", "domain_info", "system"],
|
||||
"name": ["domain_local"],
|
||||
"uuid": ["00000000-0000-0000-0000-ffffff000025"],
|
||||
"description": ["This local domain's info and metadata object."],
|
||||
"version": ["1"]
|
||||
"description": ["This local domain's info and metadata object."]
|
||||
}
|
||||
}"#;
|
||||
|
||||
|
|
|
@ -18,6 +18,35 @@ use std::time::Duration;
|
|||
|
||||
// Increment this as we add new schema types and values!!!
|
||||
pub const SYSTEM_INDEX_VERSION: i64 = 27;
|
||||
|
||||
/*
|
||||
* domain functional levels
|
||||
*
|
||||
* The idea here is to allow topology wide upgrades to be performed. We have to
|
||||
* assume that across multiple kanidm instances there may be cases where we have version
|
||||
* N and version N minus 1 as upgrades are rolled out.
|
||||
*
|
||||
* Imagine we set up a new cluster. Machine A and B both have level 1 support.
|
||||
* We upgrade machine A. It has support up to level 2, but machine B does not.
|
||||
* So the overall functional level is level 1. Then we upgrade B, which supports
|
||||
* up to level 2. We still don't do the upgrade! The topology is still level 1
|
||||
* unless an admin at this point *intervenes* and forces the update. OR what
|
||||
* happens we we update machine A again and it now supports up to level 3, with
|
||||
* a target level of 2. So we update machine A now to level 2, and that can
|
||||
* still replicate to machine B since it also supports level 2.
|
||||
*
|
||||
* effectively it means that "some features" may be a "release behind" for users
|
||||
* who don't muck with the levels, but it means that we can do mixed version
|
||||
* upgrades.
|
||||
*/
|
||||
pub const DOMAIN_LEVEL_1: u32 = 1;
|
||||
// The minimum supported domain functional level
|
||||
pub const DOMAIN_MIN_LEVEL: u32 = DOMAIN_LEVEL_1;
|
||||
// The target supported domain functional level
|
||||
pub const DOMAIN_TGT_LEVEL: u32 = DOMAIN_LEVEL_1;
|
||||
// The maximum supported domain functional level
|
||||
pub const DOMAIN_MAX_LEVEL: u32 = DOMAIN_LEVEL_1;
|
||||
|
||||
// On test builds, define to 60 seconds
|
||||
#[cfg(test)]
|
||||
pub const PURGE_FREQUENCY: u64 = 60;
|
||||
|
|
|
@ -114,7 +114,9 @@ pub const JSON_SCHEMA_ATTR_PRIMARY_CREDENTIAL: &str = r#"
|
|||
"description": [
|
||||
"Primary credential material of the account for authentication interactively."
|
||||
],
|
||||
"index": [],
|
||||
"index": [
|
||||
"PRESENCE"
|
||||
],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
|
@ -485,7 +487,9 @@ pub const JSON_SCHEMA_ATTR_UNIX_PASSWORD: &str = r#"{
|
|||
"description": [
|
||||
"A posix users unix login password."
|
||||
],
|
||||
"index": [],
|
||||
"index": [
|
||||
"PRESENCE"
|
||||
],
|
||||
"unique": [
|
||||
"false"
|
||||
],
|
||||
|
|
|
@ -18,6 +18,8 @@ pub mod policy;
|
|||
pub mod softlock;
|
||||
pub mod totp;
|
||||
|
||||
use self::totp::TOTP_DEFAULT_STEP;
|
||||
|
||||
use crate::credential::policy::CryptoPolicy;
|
||||
use crate::credential::softlock::CredSoftLockPolicy;
|
||||
use crate::credential::totp::Totp;
|
||||
|
@ -498,7 +500,7 @@ pub enum CredentialType {
|
|||
GeneratedPassword(Password),
|
||||
PasswordMfa(
|
||||
Password,
|
||||
Option<Totp>,
|
||||
Map<String, Totp>,
|
||||
Map<String, SecurityKey>,
|
||||
Option<BackupCodes>,
|
||||
),
|
||||
|
@ -513,16 +515,18 @@ impl From<&Credential> for CredentialDetail {
|
|||
CredentialType::Password(_) => CredentialDetailType::Password,
|
||||
CredentialType::GeneratedPassword(_) => CredentialDetailType::GeneratedPassword,
|
||||
CredentialType::Webauthn(wan) => {
|
||||
let mut labels: Vec<_> = wan.keys().cloned().collect();
|
||||
labels.sort_unstable();
|
||||
let labels: Vec<_> = wan.keys().cloned().collect();
|
||||
CredentialDetailType::Passkey(labels)
|
||||
}
|
||||
CredentialType::PasswordMfa(_, totp, wan, backup_code) => {
|
||||
let mut labels: Vec<_> = wan.keys().cloned().collect();
|
||||
labels.sort_unstable();
|
||||
// Don't sort - we need these in order to match to what the user
|
||||
// sees so they can remove by index.
|
||||
let wan_labels: Vec<_> = wan.keys().cloned().collect();
|
||||
let totp_labels: Vec<_> = totp.keys().cloned().collect();
|
||||
|
||||
CredentialDetailType::PasswordMfa(
|
||||
totp.is_some(),
|
||||
labels,
|
||||
totp_labels,
|
||||
wan_labels,
|
||||
backup_code.as_ref().map(|c| c.code_set.len()).unwrap_or(0),
|
||||
)
|
||||
}
|
||||
|
@ -588,8 +592,12 @@ impl TryFrom<DbCred> for Credential {
|
|||
let v_password = Password::try_from(db_password)?;
|
||||
|
||||
let v_totp = match totp {
|
||||
Some(dbt) => Some(Totp::try_from(dbt)?),
|
||||
None => None,
|
||||
Some(dbt) => {
|
||||
let l = "totp".to_string();
|
||||
let t = Totp::try_from(dbt)?;
|
||||
Map::from([(l, t)])
|
||||
}
|
||||
None => Map::default(),
|
||||
};
|
||||
|
||||
let v_webauthn = match maybe_db_webauthn {
|
||||
|
@ -679,10 +687,13 @@ impl TryFrom<DbCred> for Credential {
|
|||
} => {
|
||||
let v_password = Password::try_from(db_password)?;
|
||||
|
||||
let v_totp = if let Some(db_totp) = maybe_db_totp {
|
||||
Some(Totp::try_from(db_totp)?)
|
||||
} else {
|
||||
None
|
||||
let v_totp = match maybe_db_totp {
|
||||
Some(dbt) => {
|
||||
let l = "totp".to_string();
|
||||
let t = Totp::try_from(dbt)?;
|
||||
Map::from([(l, t)])
|
||||
}
|
||||
None => Map::default(),
|
||||
};
|
||||
|
||||
let v_backup_code = match backup_code {
|
||||
|
@ -701,6 +712,36 @@ impl TryFrom<DbCred> for Credential {
|
|||
Err(())
|
||||
}
|
||||
}
|
||||
DbCred::V3PasswordMfa {
|
||||
password: db_password,
|
||||
totp: db_totp,
|
||||
backup_code,
|
||||
webauthn: db_webauthn,
|
||||
uuid,
|
||||
} => {
|
||||
let v_password = Password::try_from(db_password)?;
|
||||
|
||||
let v_totp = db_totp
|
||||
.into_iter()
|
||||
.map(|(l, dbt)| Totp::try_from(dbt).map(|t| (l, t)))
|
||||
.collect::<Result<Map<_, _>, _>>()?;
|
||||
|
||||
let v_backup_code = match backup_code {
|
||||
Some(dbb) => Some(BackupCodes::try_from(dbb)?),
|
||||
None => None,
|
||||
};
|
||||
|
||||
let v_webauthn = db_webauthn.into_iter().collect();
|
||||
|
||||
let type_ =
|
||||
CredentialType::PasswordMfa(v_password, v_totp, v_webauthn, v_backup_code);
|
||||
|
||||
if type_.is_valid() {
|
||||
Ok(Credential { type_, uuid })
|
||||
} else {
|
||||
Err(())
|
||||
}
|
||||
}
|
||||
credential => {
|
||||
error!("Database content may be corrupt - invalid credential state");
|
||||
debug!(%credential);
|
||||
|
@ -760,7 +801,7 @@ impl Credential {
|
|||
CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
|
||||
let mut wan = Map::new();
|
||||
wan.insert(label, cred);
|
||||
CredentialType::PasswordMfa(pw.clone(), None, wan, None)
|
||||
CredentialType::PasswordMfa(pw.clone(), Map::default(), wan, None)
|
||||
}
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => {
|
||||
let mut nmap = map.clone();
|
||||
|
@ -802,7 +843,7 @@ impl Credential {
|
|||
)));
|
||||
}
|
||||
if nmap.is_empty() {
|
||||
if totp.is_some() {
|
||||
if !totp.is_empty() {
|
||||
CredentialType::PasswordMfa(
|
||||
pw.clone(),
|
||||
totp.clone(),
|
||||
|
@ -915,9 +956,12 @@ impl Credential {
|
|||
password: pw.to_dbpasswordv1(),
|
||||
uuid,
|
||||
},
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => DbCred::V2PasswordMfa {
|
||||
CredentialType::PasswordMfa(pw, totp, map, backup_code) => DbCred::V3PasswordMfa {
|
||||
password: pw.to_dbpasswordv1(),
|
||||
totp: totp.as_ref().map(|t| t.to_dbtotpv1()),
|
||||
totp: totp
|
||||
.iter()
|
||||
.map(|(l, t)| (l.clone(), t.to_dbtotpv1()))
|
||||
.collect(),
|
||||
backup_code: backup_code.as_ref().map(|b| b.to_dbbackupcodev1()),
|
||||
webauthn: map.iter().map(|(k, v)| (k.clone(), v.clone())).collect(),
|
||||
uuid,
|
||||
|
@ -947,17 +991,23 @@ impl Credential {
|
|||
}
|
||||
|
||||
// We don't make totp accessible from outside the crate for now.
|
||||
pub(crate) fn update_totp(&self, totp: Totp) -> Self {
|
||||
pub(crate) fn append_totp(&self, label: String, totp: Totp) -> Self {
|
||||
let type_ = match &self.type_ {
|
||||
CredentialType::Password(pw) | CredentialType::GeneratedPassword(pw) => {
|
||||
CredentialType::PasswordMfa(pw.clone(), Some(totp), Map::new(), None)
|
||||
CredentialType::PasswordMfa(
|
||||
pw.clone(),
|
||||
Map::from([(label, totp)]),
|
||||
Map::new(),
|
||||
None,
|
||||
)
|
||||
}
|
||||
CredentialType::PasswordMfa(pw, totps, wan, backup_code) => {
|
||||
let mut totps = totps.clone();
|
||||
let replaced = totps.insert(label, totp).is_none();
|
||||
debug_assert!(replaced);
|
||||
|
||||
CredentialType::PasswordMfa(pw.clone(), totps, wan.clone(), backup_code.clone())
|
||||
}
|
||||
CredentialType::PasswordMfa(pw, _, wan, backup_code) => CredentialType::PasswordMfa(
|
||||
pw.clone(),
|
||||
Some(totp),
|
||||
wan.clone(),
|
||||
backup_code.clone(),
|
||||
),
|
||||
CredentialType::Webauthn(wan) => {
|
||||
debug_assert!(false);
|
||||
CredentialType::Webauthn(wan.clone())
|
||||
|
@ -969,14 +1019,18 @@ impl Credential {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn remove_totp(&self) -> Self {
|
||||
pub(crate) fn remove_totp(&self, label: &str) -> Self {
|
||||
let type_ = match &self.type_ {
|
||||
CredentialType::PasswordMfa(pw, Some(_), wan, backup_code) => {
|
||||
if wan.is_empty() {
|
||||
CredentialType::PasswordMfa(pw, totp, wan, backup_code) => {
|
||||
let mut totp = totp.clone();
|
||||
let removed = totp.remove(label).is_some();
|
||||
debug_assert!(removed);
|
||||
|
||||
if wan.is_empty() && totp.is_empty() {
|
||||
// Note: No need to keep backup code if it is no longer MFA
|
||||
CredentialType::Password(pw.clone())
|
||||
} else {
|
||||
CredentialType::PasswordMfa(pw.clone(), None, wan.clone(), backup_code.clone())
|
||||
CredentialType::PasswordMfa(pw.clone(), totp, wan.clone(), backup_code.clone())
|
||||
}
|
||||
}
|
||||
_ => self.type_.clone(),
|
||||
|
@ -1008,8 +1062,14 @@ impl Credential {
|
|||
}
|
||||
CredentialType::PasswordMfa(_pw, totp, wan, _) => {
|
||||
// For backup code, use totp/wan policy (whatever is available)
|
||||
if let Some(r_totp) = totp {
|
||||
CredSoftLockPolicy::Totp(r_totp.step)
|
||||
if !totp.is_empty() {
|
||||
// What's the min step?
|
||||
let min_step = totp
|
||||
.iter()
|
||||
.map(|(_, t)| t.step)
|
||||
.min()
|
||||
.unwrap_or(TOTP_DEFAULT_STEP);
|
||||
CredSoftLockPolicy::Totp(min_step)
|
||||
} else if !wan.is_empty() {
|
||||
CredSoftLockPolicy::Webauthn
|
||||
} else {
|
||||
|
@ -1101,7 +1161,7 @@ impl CredentialType {
|
|||
match self {
|
||||
CredentialType::Password(_) | CredentialType::GeneratedPassword(_) => true,
|
||||
CredentialType::PasswordMfa(_, m_totp, webauthn, _) => {
|
||||
m_totp.is_some() || !webauthn.is_empty() // ignore backup code (it should only be a complement for totp/webauth)
|
||||
!m_totp.is_empty() || !webauthn.is_empty() // ignore backup code (it should only be a complement for totp/webauth)
|
||||
}
|
||||
CredentialType::Webauthn(webauthn) => !webauthn.is_empty(),
|
||||
}
|
||||
|
|
|
@ -1774,20 +1774,14 @@ impl Entry<EntryReduced, EntryCommitted> {
|
|||
.chain(
|
||||
l_attrs
|
||||
.iter()
|
||||
.map(|k| (
|
||||
k.as_str(),
|
||||
ldap_vattr_map(k.as_str()).unwrap_or(k.as_str())
|
||||
))
|
||||
.map(|k| (k.as_str(), ldap_vattr_map(k.as_str()).unwrap_or(k.as_str()))),
|
||||
)
|
||||
.collect()
|
||||
} else {
|
||||
// Just get the requested ones.
|
||||
l_attrs
|
||||
.iter()
|
||||
.map(|k| (
|
||||
k.as_str(),
|
||||
ldap_vattr_map(k.as_str()).unwrap_or(k.as_str())
|
||||
))
|
||||
.map(|k| (k.as_str(), ldap_vattr_map(k.as_str()).unwrap_or(k.as_str())))
|
||||
.collect()
|
||||
};
|
||||
|
||||
|
|
|
@ -65,7 +65,7 @@ enum CredVerifyState {
|
|||
struct CredMfa {
|
||||
pw: Password,
|
||||
pw_state: CredVerifyState,
|
||||
totp: Option<Totp>,
|
||||
totp: BTreeMap<String, Totp>,
|
||||
wan: Option<(RequestChallengeResponse, SecurityKeyAuthentication)>,
|
||||
backup_code: Option<BackupCodes>,
|
||||
mfa_state: CredVerifyState,
|
||||
|
@ -120,14 +120,17 @@ impl TryFrom<(&Credential, &Webauthn)> for CredHandler {
|
|||
let cmfa = Box::new(CredMfa {
|
||||
pw: pw.clone(),
|
||||
pw_state: CredVerifyState::Init,
|
||||
totp: maybe_totp.clone(),
|
||||
totp: maybe_totp
|
||||
.iter()
|
||||
.map(|(l, t)| (l.clone(), t.clone()))
|
||||
.collect(),
|
||||
wan,
|
||||
backup_code: maybe_backup_code.clone(),
|
||||
mfa_state: CredVerifyState::Init,
|
||||
});
|
||||
|
||||
// Paranoia. Should NEVER occur.
|
||||
if cmfa.totp.is_none() && cmfa.wan.is_none() {
|
||||
if cmfa.totp.is_empty() && cmfa.wan.is_none() {
|
||||
security_critical!("Unable to create CredHandler::PasswordMfa - totp and webauthn are both not present. Credentials MAY be corrupt!");
|
||||
return Err(());
|
||||
}
|
||||
|
@ -285,7 +288,7 @@ impl CredHandler {
|
|||
// MFA first
|
||||
match (
|
||||
cred,
|
||||
pw_mfa.totp.as_ref(),
|
||||
!pw_mfa.totp.is_empty(),
|
||||
pw_mfa.wan.as_ref(),
|
||||
pw_mfa.backup_code.as_ref(),
|
||||
) {
|
||||
|
@ -321,11 +324,19 @@ impl CredHandler {
|
|||
}
|
||||
}
|
||||
}
|
||||
(AuthCredential::Totp(totp_chal), Some(totp), _, _) => {
|
||||
if totp.verify(*totp_chal, ts) {
|
||||
(AuthCredential::Totp(totp_chal), true, _, _) => {
|
||||
// So long as one totp matches, success. Log which token was used.
|
||||
// We don't need to worry about the empty case since none will match and we
|
||||
// will get the failure.
|
||||
if let Some(label) = pw_mfa
|
||||
.totp
|
||||
.iter()
|
||||
.find(|(_, t)| t.verify(*totp_chal, ts))
|
||||
.map(|(l, _)| l)
|
||||
{
|
||||
pw_mfa.mfa_state = CredVerifyState::Success;
|
||||
security_info!(
|
||||
"Handler::PasswordMfa -> Result::Continue - TOTP OK, password -"
|
||||
"Handler::PasswordMfa -> Result::Continue - TOTP ({}) OK, password -", label
|
||||
);
|
||||
CredState::Continue(vec![AuthAllowed::Password])
|
||||
} else {
|
||||
|
@ -501,7 +512,11 @@ impl CredHandler {
|
|||
.backup_code
|
||||
.iter()
|
||||
.map(|_| AuthAllowed::BackupCode)
|
||||
.chain(pw_mfa.totp.iter().map(|_| AuthAllowed::Totp))
|
||||
// This looks weird but the idea is that if at least *one*
|
||||
// totp exists, then we only offer TOTP once. If none are
|
||||
// there we offer it none.
|
||||
.chain(pw_mfa.totp.iter().next().map(|_| AuthAllowed::Totp))
|
||||
// This iter is over an option so it's there or not.
|
||||
.chain(
|
||||
pw_mfa
|
||||
.wan
|
||||
|
@ -1180,7 +1195,7 @@ mod tests {
|
|||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_good)
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
.append_totp("totp".to_string(), totp);
|
||||
// add totp also
|
||||
account.primary = Some(cred);
|
||||
|
||||
|
@ -1335,7 +1350,7 @@ mod tests {
|
|||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_badlist)
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
.append_totp("totp".to_string(), totp);
|
||||
// add totp also
|
||||
account.primary = Some(cred);
|
||||
|
||||
|
@ -1820,7 +1835,7 @@ mod tests {
|
|||
.unwrap()
|
||||
.append_securitykey("soft".to_string(), wan_cred)
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
.append_totp("totp".to_string(), totp);
|
||||
|
||||
account.primary = Some(cred);
|
||||
|
||||
|
@ -2073,7 +2088,7 @@ mod tests {
|
|||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_good)
|
||||
.unwrap()
|
||||
.update_totp(totp)
|
||||
.append_totp("totp".to_string(), totp)
|
||||
.update_backup_code(backup_codes)
|
||||
.unwrap();
|
||||
|
||||
|
@ -2234,4 +2249,115 @@ mod tests {
|
|||
drop(async_tx);
|
||||
assert!(async_rx.blocking_recv().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_idm_authsession_multiple_totp_password_mech() {
|
||||
// Slightly different to the other TOTP test, this
|
||||
// checks handling when multiple TOTP's are registered.
|
||||
let _ = sketching::test_init();
|
||||
let webauthn = create_webauthn();
|
||||
let jws_signer = create_jwt_signer();
|
||||
// create the ent
|
||||
let mut account = entry_to_account!(E_ADMIN_V1);
|
||||
|
||||
// Setup a fake time stamp for consistency.
|
||||
let ts = Duration::from_secs(12345);
|
||||
|
||||
// manually load in a cred
|
||||
let totp_a = Totp::generate_secure(TOTP_DEFAULT_STEP);
|
||||
let totp_b = Totp::generate_secure(TOTP_DEFAULT_STEP);
|
||||
|
||||
let totp_good_a = totp_a
|
||||
.do_totp_duration_from_epoch(&ts)
|
||||
.expect("failed to perform totp.");
|
||||
|
||||
let totp_good_b = totp_b
|
||||
.do_totp_duration_from_epoch(&ts)
|
||||
.expect("failed to perform totp.");
|
||||
|
||||
assert!(totp_good_a != totp_good_b);
|
||||
|
||||
let pw_good = "test_password";
|
||||
|
||||
let p = CryptoPolicy::minimum();
|
||||
let cred = Credential::new_password_only(&p, pw_good)
|
||||
.unwrap()
|
||||
.append_totp("totp_a".to_string(), totp_a)
|
||||
.append_totp("totp_b".to_string(), totp_b);
|
||||
// add totp also
|
||||
account.primary = Some(cred);
|
||||
|
||||
let (async_tx, mut async_rx) = unbounded();
|
||||
|
||||
// Test totp_a
|
||||
{
|
||||
let (mut session, _, pw_badlist_cache) =
|
||||
start_password_mfa_session!(account, &webauthn);
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Totp(totp_good_a),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
Some(&pw_badlist_cache),
|
||||
&jws_signer,
|
||||
) {
|
||||
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
|
||||
_ => panic!(),
|
||||
};
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Password(pw_good.to_string()),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
Some(&pw_badlist_cache),
|
||||
&jws_signer,
|
||||
) {
|
||||
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
// Test totp_b
|
||||
{
|
||||
let (mut session, _, pw_badlist_cache) =
|
||||
start_password_mfa_session!(account, &webauthn);
|
||||
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Totp(totp_good_b),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
Some(&pw_badlist_cache),
|
||||
&jws_signer,
|
||||
) {
|
||||
Ok(AuthState::Continue(cont)) => assert!(cont == vec![AuthAllowed::Password]),
|
||||
_ => panic!(),
|
||||
};
|
||||
match session.validate_creds(
|
||||
&AuthCredential::Password(pw_good.to_string()),
|
||||
&ts,
|
||||
&async_tx,
|
||||
&webauthn,
|
||||
Some(&pw_badlist_cache),
|
||||
&jws_signer,
|
||||
) {
|
||||
Ok(AuthState::Success(_, AuthIssueSession::Token)) => {}
|
||||
_ => panic!(),
|
||||
};
|
||||
|
||||
match async_rx.blocking_recv() {
|
||||
Some(DelayedAction::AuthSessionRecord(_)) => {}
|
||||
_ => assert!(false),
|
||||
}
|
||||
}
|
||||
|
||||
drop(async_tx);
|
||||
assert!(async_rx.blocking_recv().is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -57,7 +57,7 @@ enum MfaRegState {
|
|||
None,
|
||||
TotpInit(Totp),
|
||||
TotpTryAgain(Totp),
|
||||
TotpInvalidSha1(Totp, Totp),
|
||||
TotpInvalidSha1(Totp, Totp, String),
|
||||
Passkey(Box<CreationChallengeResponse>, PasskeyRegistration),
|
||||
}
|
||||
|
||||
|
@ -67,7 +67,7 @@ impl fmt::Debug for MfaRegState {
|
|||
MfaRegState::None => "MfaRegState::None",
|
||||
MfaRegState::TotpInit(_) => "MfaRegState::TotpInit",
|
||||
MfaRegState::TotpTryAgain(_) => "MfaRegState::TotpTryAgain",
|
||||
MfaRegState::TotpInvalidSha1(_, _) => "MfaRegState::TotpInvalidSha1",
|
||||
MfaRegState::TotpInvalidSha1(_, _, _) => "MfaRegState::TotpInvalidSha1",
|
||||
MfaRegState::Passkey(_, _) => "MfaRegState::Passkey",
|
||||
};
|
||||
write!(f, "{}", t)
|
||||
|
@ -116,6 +116,37 @@ impl fmt::Debug for CredentialUpdateSession {
|
|||
}
|
||||
}
|
||||
|
||||
impl CredentialUpdateSession {
|
||||
// In future this should be a Vec of the issues with the current session so that UI's can highlight
|
||||
// properly how to proceed.
|
||||
fn can_commit(&self) -> bool {
|
||||
// Should be it's own PR and use account policy
|
||||
|
||||
/*
|
||||
// We'll check policy here in future.
|
||||
let is_primary_valid = match self.primary.as_ref() {
|
||||
Some(Credential {
|
||||
uuid: _,
|
||||
type_: CredentialType::Password(_),
|
||||
}) => {
|
||||
// We refuse password-only auth now.
|
||||
info!("Password only authentication.");
|
||||
false
|
||||
}
|
||||
// So far valid.
|
||||
_ => true,
|
||||
};
|
||||
|
||||
info!("can_commit -> {}", is_primary_valid);
|
||||
|
||||
// For logic later.
|
||||
is_primary_valid
|
||||
*/
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
enum MfaRegStateStatus {
|
||||
// Nothing in progress.
|
||||
None,
|
||||
|
@ -183,8 +214,7 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
|||
CredentialUpdateSessionStatus {
|
||||
spn: session.account.spn.clone(),
|
||||
displayname: session.account.displayname.clone(),
|
||||
|
||||
can_commit: true,
|
||||
can_commit: session.can_commit(),
|
||||
primary: session.primary.as_ref().map(|c| c.into()),
|
||||
passkeys: session
|
||||
.passkeys
|
||||
|
@ -200,7 +230,7 @@ impl From<&CredentialUpdateSession> for CredentialUpdateSessionStatus {
|
|||
token.to_proto(session.account.name.as_str(), session.issuer.as_str()),
|
||||
),
|
||||
MfaRegState::TotpTryAgain(_) => MfaRegStateStatus::TotpTryAgain,
|
||||
MfaRegState::TotpInvalidSha1(_, _) => MfaRegStateStatus::TotpInvalidSha1,
|
||||
MfaRegState::TotpInvalidSha1(_, _, _) => MfaRegStateStatus::TotpInvalidSha1,
|
||||
MfaRegState::Passkey(r, _) => MfaRegStateStatus::Passkey(r.as_ref().clone()),
|
||||
},
|
||||
}
|
||||
|
@ -728,6 +758,12 @@ impl<'a> IdmServerProxyWriteTransaction<'a> {
|
|||
let (mut modlist, session, session_token) =
|
||||
self.credential_update_commit_common(cust, ct)?;
|
||||
|
||||
// Can we actually proceed?
|
||||
if !session.can_commit() {
|
||||
admin_error!("Session is unable to commit due to a constraint violation.");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
|
||||
// Setup mods for the various bits. We always assert an *exact* state.
|
||||
|
||||
// IF an intent was used on this session, AND that intent is not in our
|
||||
|
@ -1150,6 +1186,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
totp_chal: u32,
|
||||
label: &str,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
|
@ -1162,13 +1199,13 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
match &session.mfaregstate {
|
||||
MfaRegState::TotpInit(totp_token)
|
||||
| MfaRegState::TotpTryAgain(totp_token)
|
||||
| MfaRegState::TotpInvalidSha1(totp_token, _) => {
|
||||
| MfaRegState::TotpInvalidSha1(totp_token, _, _) => {
|
||||
if totp_token.verify(totp_chal, &ct) {
|
||||
// It was valid. Update the credential.
|
||||
let ncred = session
|
||||
.primary
|
||||
.as_ref()
|
||||
.map(|cred| cred.update_totp(totp_token.clone()))
|
||||
.map(|cred| cred.append_totp(label.to_string(), totp_token.clone()))
|
||||
.ok_or_else(|| {
|
||||
admin_error!("A TOTP was added, but no primary credential stub exists");
|
||||
OperationError::InvalidState
|
||||
|
@ -1188,8 +1225,11 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
if token_sha1.verify(totp_chal, &ct) {
|
||||
// Greeeaaaaaatttt. It's a broken app. Let's check the user
|
||||
// knows this is broken, before we proceed.
|
||||
session.mfaregstate =
|
||||
MfaRegState::TotpInvalidSha1(totp_token.clone(), token_sha1);
|
||||
session.mfaregstate = MfaRegState::TotpInvalidSha1(
|
||||
totp_token.clone(),
|
||||
token_sha1,
|
||||
label.to_string(),
|
||||
);
|
||||
Ok(session.deref().into())
|
||||
} else {
|
||||
// Let them check again, it's a typo.
|
||||
|
@ -1216,12 +1256,12 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
|
||||
// Are we in a totp reg state?
|
||||
match &session.mfaregstate {
|
||||
MfaRegState::TotpInvalidSha1(_, token_sha1) => {
|
||||
MfaRegState::TotpInvalidSha1(_, token_sha1, label) => {
|
||||
// They have accepted it as sha1
|
||||
let ncred = session
|
||||
.primary
|
||||
.as_ref()
|
||||
.map(|cred| cred.update_totp(token_sha1.clone()))
|
||||
.map(|cred| cred.append_totp(label.to_string(), token_sha1.clone()))
|
||||
.ok_or_else(|| {
|
||||
admin_error!("A TOTP was added, but no primary credential stub exists");
|
||||
OperationError::InvalidState
|
||||
|
@ -1243,6 +1283,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
&self,
|
||||
cust: &CredentialUpdateSessionToken,
|
||||
ct: Duration,
|
||||
label: &str,
|
||||
) -> Result<CredentialUpdateSessionStatus, OperationError> {
|
||||
let session_handle = self.get_current_session(cust, ct)?;
|
||||
let mut session = session_handle.try_lock().map_err(|_| {
|
||||
|
@ -1259,7 +1300,7 @@ impl<'a> IdmServerCredUpdateTransaction<'a> {
|
|||
let ncred = session
|
||||
.primary
|
||||
.as_ref()
|
||||
.map(|cred| cred.remove_totp())
|
||||
.map(|cred| cred.remove_totp(label))
|
||||
.ok_or_else(|| {
|
||||
admin_error!("Try to remove TOTP, but no primary credential stub exists");
|
||||
OperationError::InvalidState
|
||||
|
@ -2086,7 +2127,7 @@ mod tests {
|
|||
|
||||
// Intentionally get it wrong.
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal + 1)
|
||||
.credential_primary_check_totp(&cust, ct, chal + 1, "totp")
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(
|
||||
|
@ -2095,14 +2136,14 @@ mod tests {
|
|||
));
|
||||
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal)
|
||||
.credential_primary_check_totp(&cust, ct, chal, "totp")
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 0))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// Should be okay now!
|
||||
|
||||
|
@ -2122,7 +2163,7 @@ mod tests {
|
|||
let cutxn = idms.cred_update_transaction();
|
||||
|
||||
let c_status = cutxn
|
||||
.credential_primary_remove_totp(&cust, ct)
|
||||
.credential_primary_remove_totp(&cust, ct, "totp")
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
@ -2182,7 +2223,7 @@ mod tests {
|
|||
|
||||
// Should getn the warn that it's sha1
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal)
|
||||
.credential_primary_check_totp(&cust, ct, chal, "totp")
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(
|
||||
|
@ -2196,10 +2237,10 @@ mod tests {
|
|||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 0))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// Should be okay now!
|
||||
|
||||
|
@ -2243,7 +2284,6 @@ mod tests {
|
|||
|
||||
let totp_token: Totp = match c_status.mfaregstate {
|
||||
MfaRegStateStatus::TotpCheck(secret) => Some(secret.into()),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
.expect("Unable to retrieve totp token, invalid state.");
|
||||
|
@ -2254,14 +2294,14 @@ mod tests {
|
|||
.expect("Failed to perform totp step");
|
||||
|
||||
let c_status = cutxn
|
||||
.credential_primary_check_totp(&cust, ct, chal)
|
||||
.expect("Failed to update the primary cred password");
|
||||
.credential_primary_check_totp(&cust, ct, chal, "totp")
|
||||
.expect("Failed to update the primary cred totp");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 0))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// Now good to go, we need to now add our backup codes.
|
||||
// What's the right way to get these back?
|
||||
|
@ -2277,10 +2317,10 @@ mod tests {
|
|||
|
||||
// Should error because the number is not 0
|
||||
debug!("{:?}", c_status.primary.as_ref().map(|c| &c.type_));
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 8))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// Should be okay now!
|
||||
drop(cutxn);
|
||||
|
@ -2308,10 +2348,10 @@ mod tests {
|
|||
.credential_update_status(&cust, ct)
|
||||
.expect("Failed to get the current session status.");
|
||||
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 7))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 7)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// If we remove codes, it leaves totp.
|
||||
let c_status = cutxn
|
||||
|
@ -2319,10 +2359,10 @@ mod tests {
|
|||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 0))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 0)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// Re-add the codes.
|
||||
let c_status = cutxn
|
||||
|
@ -2333,14 +2373,14 @@ mod tests {
|
|||
c_status.mfaregstate,
|
||||
MfaRegStateStatus::BackupCodes(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
c_status.primary.as_ref().map(|c| &c.type_),
|
||||
Some(CredentialDetailType::PasswordMfa(true, _, 8))
|
||||
));
|
||||
assert!(match c_status.primary.as_ref().map(|c| &c.type_) {
|
||||
Some(CredentialDetailType::PasswordMfa(totp, _, 8)) => !totp.is_empty(),
|
||||
_ => false,
|
||||
});
|
||||
|
||||
// If we remove totp, it removes codes.
|
||||
let c_status = cutxn
|
||||
.credential_primary_remove_totp(&cust, ct)
|
||||
.credential_primary_remove_totp(&cust, ct, "totp")
|
||||
.expect("Failed to update the primary cred password");
|
||||
|
||||
assert!(matches!(c_status.mfaregstate, MfaRegStateStatus::None));
|
||||
|
|
|
@ -265,10 +265,7 @@ impl LdapServer {
|
|||
if a == "entrydn" || a == "dn" {
|
||||
None
|
||||
} else {
|
||||
Some(AttrString::from(
|
||||
ldap_vattr_map(a)
|
||||
.unwrap_or(a)
|
||||
))
|
||||
Some(AttrString::from(ldap_vattr_map(a).unwrap_or(a)))
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
@ -1149,7 +1146,7 @@ mod tests {
|
|||
// Already being returned
|
||||
"name".to_string(),
|
||||
// This is a virtual attribute
|
||||
"entryuuid".to_string()
|
||||
"entryuuid".to_string(),
|
||||
],
|
||||
};
|
||||
let r1 = task::block_on(ldaps.do_search(idms, &sr, &anon_t)).unwrap();
|
||||
|
@ -1169,8 +1166,6 @@ mod tests {
|
|||
}
|
||||
_ => assert!(false),
|
||||
};
|
||||
|
||||
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
@ -72,6 +72,29 @@ macro_rules! entry_str_to_account {
|
|||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! entry_to_account {
|
||||
($entry:expr) => {{
|
||||
use std::iter::once;
|
||||
|
||||
use crate::entry::{Entry, EntryInvalid, EntryNew};
|
||||
use crate::idm::account::Account;
|
||||
use crate::value::Value;
|
||||
|
||||
let mut e: Entry<EntryInvalid, EntryNew> = unsafe { $entry.clone().into_invalid_new() };
|
||||
// Add spn, because normally this is generated but in tests we can't.
|
||||
let spn = e
|
||||
.get_ava_single_iname("name")
|
||||
.map(|s| Value::new_spn_str(s, "example.com"))
|
||||
.expect("Failed to munge spn from name!");
|
||||
e.set_ava("spn", once(spn));
|
||||
|
||||
let e = unsafe { e.into_sealed_committed() };
|
||||
|
||||
Account::try_from_entry_no_groups(&e).expect("Account conversion failure")
|
||||
}};
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
macro_rules! run_idm_test_inner {
|
||||
($test_fn:expr) => {{
|
||||
|
|
|
@ -69,6 +69,14 @@ impl Domain {
|
|||
e.set_ava("domain_name", once(n));
|
||||
trace!("plugin_domain: Applying domain_name transform");
|
||||
}
|
||||
|
||||
// Setup the minimum functional level if one is not set already.
|
||||
if !e.attribute_pres("version") {
|
||||
let n = Value::Uint32(DOMAIN_MIN_LEVEL);
|
||||
e.set_ava("version", once(n));
|
||||
trace!("plugin_domain: Applying domain version transform");
|
||||
}
|
||||
|
||||
// create the domain_display_name if it's missing
|
||||
if !e.attribute_pres("domain_display_name") {
|
||||
let domain_display_name = Value::new_utf8(format!("Kanidm {}", qs.get_domain_name()));
|
||||
|
|
|
@ -261,7 +261,7 @@ mod tests {
|
|||
let p = CryptoPolicy::minimum();
|
||||
let c = Credential::new_password_only(&p, "password")
|
||||
.unwrap()
|
||||
.update_totp(totp);
|
||||
.append_totp("totp".to_string(), totp);
|
||||
ea.add_ava("primary_credential", Value::new_credential("primary", c));
|
||||
|
||||
let preload = vec![ea];
|
||||
|
@ -285,7 +285,7 @@ mod tests {
|
|||
.expect("failed to get primary cred.");
|
||||
match &c.type_ {
|
||||
CredentialType::PasswordMfa(_pw, totp, webauthn, backup_code) => {
|
||||
assert!(totp.is_some());
|
||||
assert!(!totp.is_empty());
|
||||
assert!(webauthn.is_empty());
|
||||
assert!(backup_code.is_none());
|
||||
}
|
||||
|
|
|
@ -78,32 +78,20 @@ impl QueryServer {
|
|||
}?;
|
||||
admin_debug!(?system_info_version);
|
||||
|
||||
if system_info_version < 3 {
|
||||
migrate_txn.migrate_2_to_3()?;
|
||||
}
|
||||
if system_info_version > 0 {
|
||||
if system_info_version <= 6 {
|
||||
error!("Your instance of Kanidm is version 1.1.0-alpha.9 or lower, and you are trying to perform a skip upgrade. This will not work.");
|
||||
error!("You need to upgrade one version at a time to ensure upgrade migrations are performed in the correct order.");
|
||||
return Err(OperationError::InvalidState);
|
||||
}
|
||||
|
||||
if system_info_version < 4 {
|
||||
migrate_txn.migrate_3_to_4()?;
|
||||
}
|
||||
if system_info_version < 9 {
|
||||
migrate_txn.migrate_8_to_9()?;
|
||||
}
|
||||
|
||||
if system_info_version < 5 {
|
||||
migrate_txn.migrate_4_to_5()?;
|
||||
}
|
||||
|
||||
if system_info_version < 6 {
|
||||
migrate_txn.migrate_5_to_6()?;
|
||||
}
|
||||
|
||||
if system_info_version < 7 {
|
||||
migrate_txn.migrate_6_to_7()?;
|
||||
}
|
||||
|
||||
if system_info_version < 8 {
|
||||
migrate_txn.migrate_7_to_8()?;
|
||||
}
|
||||
|
||||
if system_info_version < 9 {
|
||||
migrate_txn.migrate_8_to_9()?;
|
||||
if system_info_version < 10 {
|
||||
migrate_txn.migrate_9_to_10()?;
|
||||
}
|
||||
}
|
||||
|
||||
migrate_txn.commit()?;
|
||||
|
@ -114,7 +102,7 @@ impl QueryServer {
|
|||
ts_write_3.set_phase(ServerPhase::Running);
|
||||
ts_write_3.commit()
|
||||
})?;
|
||||
// TODO: work out if we've actually done any migrations before printing this
|
||||
|
||||
admin_debug!("Database version check and migrations success! ☀️ ");
|
||||
Ok(())
|
||||
}
|
||||
|
@ -183,146 +171,6 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Migrate 2 to 3 changes the name, domain_name types from iutf8 to iname.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_2_to_3(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 2 to 3 migration. THIS MAY TAKE A LONG TIME!");
|
||||
// Get all entries where pres name or domain_name. INCLUDE TS + RECYCLE.
|
||||
|
||||
let filt = filter_all!(f_or!([f_pres("name"), f_pres("domain_name"),]));
|
||||
|
||||
let pre_candidates = self.internal_search(filt).map_err(|e| {
|
||||
admin_error!(err = ?e, "migrate_2_to_3 internal search failure");
|
||||
e
|
||||
})?;
|
||||
|
||||
// If there is nothing, we don't need to do anything.
|
||||
if pre_candidates.is_empty() {
|
||||
admin_info!("migrate_2_to_3 no entries to migrate, complete");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Change the value type.
|
||||
let mut candidates: Vec<Entry<EntryInvalid, EntryCommitted>> = pre_candidates
|
||||
.iter()
|
||||
.map(|er| er.as_ref().clone().invalidate(self.cid.clone()))
|
||||
.collect();
|
||||
|
||||
candidates.iter_mut().try_for_each(|er| {
|
||||
let nvs = if let Some(vs) = er.get_ava_set("name") {
|
||||
vs.migrate_iutf8_iname()?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(nvs) = nvs {
|
||||
er.set_ava_set("name", nvs)
|
||||
}
|
||||
|
||||
let nvs = if let Some(vs) = er.get_ava_set("domain_name") {
|
||||
vs.migrate_iutf8_iname()?
|
||||
} else {
|
||||
None
|
||||
};
|
||||
if let Some(nvs) = nvs {
|
||||
er.set_ava_set("domain_name", nvs)
|
||||
}
|
||||
|
||||
Ok(())
|
||||
})?;
|
||||
|
||||
// Schema check all.
|
||||
let res: Result<Vec<Entry<EntrySealed, EntryCommitted>>, SchemaError> = candidates
|
||||
.into_iter()
|
||||
.map(|e| e.validate(&self.schema).map(|e| e.seal(&self.schema)))
|
||||
.collect();
|
||||
|
||||
let norm_cand: Vec<Entry<_, _>> = match res {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
admin_error!("migrate_2_to_3 schema error -> {:?}", e);
|
||||
return Err(OperationError::SchemaViolation(e));
|
||||
}
|
||||
};
|
||||
|
||||
// Write them back.
|
||||
self.be_txn
|
||||
.modify(&self.cid, &pre_candidates, &norm_cand)
|
||||
.map_err(|e| {
|
||||
admin_error!("migrate_2_to_3 modification failure -> {:?}", e);
|
||||
e
|
||||
})
|
||||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 3 to 4 - this triggers a regen of the domains security token
|
||||
/// as we previously did not have it in the entry.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_3_to_4(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 3 to 4 migration.");
|
||||
let filter = filter!(f_eq("uuid", (*PVUUID_DOMAIN_INFO).clone()));
|
||||
let modlist = ModifyList::new_purge("domain_token_key");
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 4 to 5 - this triggers a regen of all oauth2 RS es256 der keys
|
||||
/// as we previously did not generate them on entry creation.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_4_to_5(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 4 to 5 migration.");
|
||||
let filter = filter!(f_and!([
|
||||
f_eq("class", (*PVCLASS_OAUTH2_RS).clone()),
|
||||
f_andnot(f_pres("es256_private_key_der")),
|
||||
]));
|
||||
let modlist = ModifyList::new_purge("es256_private_key_der");
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 5 to 6 - This updates the domain info item to reset the token
|
||||
/// keys based on the new encryption types.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_5_to_6(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 5 to 6 migration.");
|
||||
let filter = filter!(f_eq("uuid", (*PVUUID_DOMAIN_INFO).clone()));
|
||||
let mut modlist = ModifyList::new_purge("domain_token_key");
|
||||
// We need to also push the version here so that we pass schema.
|
||||
modlist.push_mod(Modify::Present(
|
||||
AttrString::from("version"),
|
||||
Value::Uint32(0),
|
||||
));
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 6 to 7
|
||||
///
|
||||
/// Modify accounts that are not persons, to be service accounts so that the extension
|
||||
/// rules remain valid.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_6_to_7(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 6 to 7 migration.");
|
||||
let filter = filter!(f_and!([
|
||||
f_eq("class", (*PVCLASS_ACCOUNT).clone()),
|
||||
f_andnot(f_eq("class", (*PVCLASS_PERSON).clone())),
|
||||
]));
|
||||
let modlist = ModifyList::new_append("class", Value::new_class("service_account"));
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 7 to 8
|
||||
///
|
||||
/// Touch all service accounts to trigger a regen of their es256 jws keys for api tokens
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_7_to_8(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 7 to 8 migration.");
|
||||
let filter = filter!(f_eq("class", (*PVCLASS_SERVICE_ACCOUNT).clone()));
|
||||
let modlist = ModifyList::new_append("class", Value::new_class("service_account"));
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 8 to 9
|
||||
///
|
||||
/// This migration updates properties of oauth2 relying server properties. First, it changes
|
||||
|
@ -406,6 +254,27 @@ impl<'a> QueryServerWriteTransaction<'a> {
|
|||
// Complete
|
||||
}
|
||||
|
||||
/// Migrate 9 to 10
|
||||
///
|
||||
/// This forces a load and rewrite of all credentials stored on all accounts so that they are
|
||||
/// updated to new on-disk formats. This will allow us to purge some older on disk formats in
|
||||
/// a future version.
|
||||
///
|
||||
/// An extended feature of this is the ability to store multiple TOTP's per entry.
|
||||
#[instrument(level = "debug", skip_all)]
|
||||
pub fn migrate_9_to_10(&mut self) -> Result<(), OperationError> {
|
||||
admin_warn!("starting 9 to 10 migration.");
|
||||
let filter = filter!(f_or!([
|
||||
f_pres("primary_credential"),
|
||||
f_pres("unix_password"),
|
||||
]));
|
||||
// This "does nothing" since everything has object anyway, but it forces the entry to be
|
||||
// loaded and rewritten.
|
||||
let modlist = ModifyList::new_append("class", Value::new_class("object"));
|
||||
self.internal_modify(&filter, &modlist)
|
||||
// Complete
|
||||
}
|
||||
|
||||
#[instrument(level = "info", skip_all)]
|
||||
pub fn initialise_schema_core(&mut self) -> Result<(), OperationError> {
|
||||
admin_debug!("initialise_schema_core -> start ...");
|
||||
|
@ -698,6 +567,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
/*
|
||||
#[qs_test_no_init]
|
||||
async fn test_qs_upgrade_entry_attrs(server: &QueryServer) {
|
||||
let mut server_txn = server.write(duration_from_epoch_now()).await;
|
||||
|
@ -813,4 +683,5 @@ mod tests {
|
|||
);
|
||||
assert!(server_txn.commit().is_ok());
|
||||
}
|
||||
*/
|
||||
}
|
||||
|
|
|
@ -1027,7 +1027,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
|
|||
.expect("Failed to do totp?");
|
||||
|
||||
let _status = rsclient
|
||||
.idm_account_credential_update_check_totp(&session_token, totp_chal)
|
||||
.idm_account_credential_update_check_totp(&session_token, totp_chal, "totp")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
@ -1061,7 +1061,7 @@ async fn test_server_credential_update_session_totp_pw(rsclient: KanidmClient) {
|
|||
.unwrap();
|
||||
|
||||
let _status = rsclient
|
||||
.idm_account_credential_update_remove_totp(&session_token)
|
||||
.idm_account_credential_update_remove_totp(&session_token, "totp")
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
|
|
@ -233,19 +233,19 @@ function addBorrowedObject(obj) {
|
|||
}
|
||||
function __wbg_adapter_50(arg0, arg1, arg2) {
|
||||
try {
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h3fce4a672296c312(arg0, arg1, addBorrowedObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h0984b1b8ad634c42(arg0, arg1, addBorrowedObject(arg2));
|
||||
} finally {
|
||||
heap[stack_pointer++] = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
function __wbg_adapter_53(arg0, arg1, arg2) {
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h90c46f524e922c83(arg0, arg1, addHeapObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut__A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h721315d2d8351be6(arg0, arg1, addHeapObject(arg2));
|
||||
}
|
||||
|
||||
function __wbg_adapter_56(arg0, arg1, arg2) {
|
||||
try {
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h785bf27b1a1ece21(arg0, arg1, addBorrowedObject(arg2));
|
||||
wasm._dyn_core__ops__function__FnMut___A____Output___R_as_wasm_bindgen__closure__WasmClosure___describe__invoke__h6abde9f4b8744c87(arg0, arg1, addBorrowedObject(arg2));
|
||||
} finally {
|
||||
heap[stack_pointer++] = undefined;
|
||||
}
|
||||
|
@ -557,6 +557,13 @@ function getImports() {
|
|||
const ret = getObject(arg0).querySelector(getStringFromWasm0(arg1, arg2));
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_state_4896ba54c2e3301e = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).state;
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_pushState_38917fb88b4add30 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
|
||||
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_parentNode_e397bbbe28be7b28 = function(arg0) {
|
||||
const ret = getObject(arg0).parentNode;
|
||||
return isLikeNone(ret) ? 0 : addHeapObject(ret);
|
||||
|
@ -621,13 +628,6 @@ function getImports() {
|
|||
const ret = getObject(arg0).text();
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_state_4896ba54c2e3301e = function() { return handleError(function (arg0) {
|
||||
const ret = getObject(arg0).state;
|
||||
return addHeapObject(ret);
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_pushState_38917fb88b4add30 = function() { return handleError(function (arg0, arg1, arg2, arg3, arg4, arg5) {
|
||||
getObject(arg0).pushState(getObject(arg1), getStringFromWasm0(arg2, arg3), arg4 === 0 ? undefined : getStringFromWasm0(arg4, arg5));
|
||||
}, arguments) };
|
||||
imports.wbg.__wbg_href_bbb11e0e61ea410e = function() { return handleError(function (arg0, arg1) {
|
||||
const ret = getObject(arg1).href;
|
||||
const ptr0 = passStringToWasm0(ret, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc);
|
||||
|
@ -1159,16 +1159,16 @@ function getImports() {
|
|||
const ret = wasm.memory;
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper4699 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1064, __wbg_adapter_50);
|
||||
imports.wbg.__wbindgen_closure_wrapper4757 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1089, __wbg_adapter_50);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper5635 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1412, __wbg_adapter_53);
|
||||
imports.wbg.__wbindgen_closure_wrapper5693 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1437, __wbg_adapter_53);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
imports.wbg.__wbindgen_closure_wrapper5705 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1442, __wbg_adapter_56);
|
||||
imports.wbg.__wbindgen_closure_wrapper5763 = function(arg0, arg1, arg2) {
|
||||
const ret = makeMutClosure(arg0, arg1, 1467, __wbg_adapter_56);
|
||||
return addHeapObject(ret);
|
||||
};
|
||||
|
||||
|
|
Binary file not shown.
|
@ -5,3 +5,4 @@ mod passkey;
|
|||
mod passkeyremove;
|
||||
mod pwmodal;
|
||||
mod totpmodal;
|
||||
mod totpremove;
|
||||
|
|
|
@ -136,8 +136,9 @@ impl Component for PwModalApp {
|
|||
match msg {
|
||||
Msg::PasswordCheck => {
|
||||
// default is empty string
|
||||
let pw = utils::get_value_from_element_id("password").unwrap_or_default();
|
||||
let check = utils::get_value_from_element_id("password-check").unwrap_or_default();
|
||||
let pw = utils::get_value_from_element_id("new-password").unwrap_or_default();
|
||||
let check =
|
||||
utils::get_value_from_element_id("new-password-check").unwrap_or_default();
|
||||
|
||||
if pw == check {
|
||||
self.pw_check = PwCheck::Valid
|
||||
|
@ -155,7 +156,7 @@ impl Component for PwModalApp {
|
|||
self.state = PwState::Waiting;
|
||||
|
||||
// default is empty string
|
||||
let pw = utils::get_value_from_element_id("password").unwrap_or_default();
|
||||
let pw = utils::get_value_from_element_id("new-password").unwrap_or_default();
|
||||
let token_c = ctx.props().token.clone();
|
||||
|
||||
ctx.link().send_future(async {
|
||||
|
@ -263,12 +264,13 @@ impl Component for PwModalApp {
|
|||
}
|
||||
} ) }
|
||||
>
|
||||
<label for="password" class="form-label">{ "Enter New Password" }</label>
|
||||
<input hidden=true type="text" autocomplete="username" />
|
||||
<label for="new-password" class="form-label">{ "Enter New Password" }</label>
|
||||
<input
|
||||
aria-describedby="password-validation-feedback"
|
||||
autocomplete="new-password"
|
||||
class={ pw_class }
|
||||
id="password"
|
||||
id="new-password"
|
||||
oninput={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
|
@ -281,12 +283,12 @@ impl Component for PwModalApp {
|
|||
value={ pw_val }
|
||||
/>
|
||||
{ pw_feedback }
|
||||
<label for="password-check" class="form-label">{ "Repeat Password" }</label>
|
||||
<label for="new-password-check" class="form-label">{ "Repeat Password" }</label>
|
||||
<input
|
||||
aria-describedby="password-check-feedback"
|
||||
aria-describedby="new-password-check-feedback"
|
||||
autocomplete="new-password"
|
||||
class={ pw_check_class }
|
||||
id="password-check"
|
||||
id="new-password-check"
|
||||
oninput={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
|
|
|
@ -14,6 +14,7 @@ use super::passkey::PasskeyModalApp;
|
|||
use super::passkeyremove::PasskeyRemoveModalApp;
|
||||
use super::pwmodal::PwModalApp;
|
||||
use super::totpmodal::TotpModalApp;
|
||||
use super::totpremove::TotpRemoveComp;
|
||||
use crate::error::*;
|
||||
use crate::{models, utils};
|
||||
|
||||
|
@ -32,6 +33,13 @@ pub struct ModalProps {
|
|||
pub cb: Callback<EventBusMsg>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
pub struct TotpRemoveProps {
|
||||
pub token: CUSessionToken,
|
||||
pub label: String,
|
||||
pub cb: Callback<EventBusMsg>,
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Properties)]
|
||||
pub struct PasskeyRemoveModalProps {
|
||||
pub token: CUSessionToken,
|
||||
|
@ -354,15 +362,68 @@ impl CredentialResetApp {
|
|||
html! {
|
||||
<>
|
||||
<p>{ "✅ Password Set" }</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
||||
{ "Change Password" }
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p>{ "❌ MFA Disabled" }</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTotpCreate">
|
||||
{ "Add TOTP" }
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTotpCreate">
|
||||
{ "Add TOTP" }
|
||||
</button>
|
||||
<p>
|
||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||
{ "Delete this Insecure Password" }
|
||||
</button>
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
}
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
type_:
|
||||
CredentialDetailType::PasswordMfa(
|
||||
// Used for what TOTP the user has.
|
||||
totp_set,
|
||||
// Being deprecated.
|
||||
_security_key_labels,
|
||||
// Need to wire in backup codes.
|
||||
_backup_codes_remaining,
|
||||
),
|
||||
}) => {
|
||||
html! {
|
||||
<>
|
||||
<p>{ "✅ Password Set" }</p>
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
||||
{ "Change Password" }
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p>{ "✅ MFA Enabled" }</p>
|
||||
|
||||
<>
|
||||
{ for totp_set.iter()
|
||||
.map(|detail| html! { <TotpRemoveComp token={ token.clone() } label={ detail.clone() } cb={ cb.clone() } /> })
|
||||
}
|
||||
</>
|
||||
|
||||
<p>
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTotpCreate">
|
||||
{ "Add TOTP" }
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||
{ "Delete this Legacy MFA Credential" }
|
||||
</button>
|
||||
</p>
|
||||
|
||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||
{ "Delete this Password" }
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
}
|
||||
|
@ -392,32 +453,6 @@ impl CredentialResetApp {
|
|||
</>
|
||||
}
|
||||
}
|
||||
Some(CredentialDetail {
|
||||
uuid: _,
|
||||
type_:
|
||||
// TODO: review this and find out why we aren't using these variables
|
||||
// Because I'm lazy 🙃
|
||||
CredentialDetailType::PasswordMfa(
|
||||
_totp_set,
|
||||
_security_key_labels,
|
||||
_backup_codes_remaining,
|
||||
),
|
||||
}) => {
|
||||
html! {
|
||||
<>
|
||||
<p>{ "✅ Password Set" }</p>
|
||||
<p>{ "✅ MFA Enabled" }</p>
|
||||
|
||||
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#staticTotpCreate">
|
||||
{ "Reset TOTP" }
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-danger" data-bs-toggle="modal" data-bs-target="#staticDeletePrimaryCred">
|
||||
{ "Delete this MFA Credential" }
|
||||
</button>
|
||||
</>
|
||||
}
|
||||
}
|
||||
None => {
|
||||
html! {
|
||||
<button type="button" class="btn btn-warning" data-bs-toggle="modal" data-bs-target="#staticPassword">
|
||||
|
@ -479,7 +514,7 @@ impl CredentialResetApp {
|
|||
|
||||
<h4>{"Password / TOTP"}</h4>
|
||||
<p>{ "Legacy password paired with other authentication factors." }</p>
|
||||
<p>{ "It is recommended you avoid setting these if possible." }</p>
|
||||
<p>{ "It is recommended you avoid setting these if possible, as these can be phished or exploited." }</p>
|
||||
{ pw_html }
|
||||
|
||||
<hr class="my-4" />
|
||||
|
|
|
@ -164,12 +164,17 @@ impl Component for TotpModalApp {
|
|||
// Send off the submit, lock the form.
|
||||
// default is empty str
|
||||
let totp = utils::get_value_from_element_id("totp").unwrap_or_default();
|
||||
let totp_label = utils::get_value_from_element_id("totp-label").unwrap_or_default();
|
||||
|
||||
match totp.trim().parse::<u32>() {
|
||||
Ok(totp) => {
|
||||
ctx.link().send_future(async move {
|
||||
match Self::submit_totp_update(token_c, CURequest::TotpVerify(totp), cb)
|
||||
.await
|
||||
match Self::submit_totp_update(
|
||||
token_c,
|
||||
CURequest::TotpVerify(totp, totp_label),
|
||||
cb,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
|
@ -398,6 +403,22 @@ impl Component for TotpModalApp {
|
|||
value={secret.to_uri()}
|
||||
/>
|
||||
|
||||
<label for="totp-label" class="form-label">{ "Enter a name for your TOTP" }</label>
|
||||
<input
|
||||
class="form-control"
|
||||
id="totp-label"
|
||||
oninput={
|
||||
ctx.link()
|
||||
.callback(move |_| {
|
||||
Msg::TotpClearInvalid
|
||||
})
|
||||
}
|
||||
placeholder=""
|
||||
required=true
|
||||
type="text"
|
||||
/>
|
||||
|
||||
|
||||
<label for="totp" class="form-label">{ "Enter a TOTP code to confirm it's working" }</label>
|
||||
<input
|
||||
aria-describedby="totp-validation-feedback"
|
||||
|
|
147
kanidmd_web_ui/src/credential/totpremove.rs
Normal file
147
kanidmd_web_ui/src/credential/totpremove.rs
Normal file
|
@ -0,0 +1,147 @@
|
|||
use super::reset::{EventBusMsg, TotpRemoveProps};
|
||||
#[cfg(debug_assertions)]
|
||||
use gloo::console;
|
||||
use kanidm_proto::v1::{CURequest, CUSessionToken, CUStatus};
|
||||
use wasm_bindgen::{JsCast, JsValue, UnwrapThrowExt};
|
||||
use wasm_bindgen_futures::JsFuture;
|
||||
use web_sys::{Request, RequestInit, RequestMode, Response};
|
||||
|
||||
use crate::error::*;
|
||||
use crate::utils;
|
||||
use yew::prelude::*;
|
||||
|
||||
pub enum Msg {
|
||||
Submit,
|
||||
Success,
|
||||
Error { emsg: String, kopid: Option<String> },
|
||||
}
|
||||
|
||||
impl From<FetchError> for Msg {
|
||||
fn from(fe: FetchError) -> Self {
|
||||
Msg::Error {
|
||||
emsg: fe.as_string(),
|
||||
kopid: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct TotpRemoveComp {
|
||||
enabled: bool,
|
||||
}
|
||||
|
||||
impl Component for TotpRemoveComp {
|
||||
type Message = Msg;
|
||||
type Properties = TotpRemoveProps;
|
||||
|
||||
fn create(_ctx: &Context<Self>) -> Self {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("totp remove::create");
|
||||
|
||||
TotpRemoveComp { enabled: true }
|
||||
}
|
||||
|
||||
fn changed(&mut self, _ctx: &Context<Self>, _old_props: &Self::Properties) -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("totp remove::change");
|
||||
false
|
||||
}
|
||||
|
||||
fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("totp remove::update");
|
||||
let cb = ctx.props().cb.clone();
|
||||
|
||||
match msg {
|
||||
Msg::Submit => {
|
||||
let token_c = ctx.props().token.clone();
|
||||
let label = ctx.props().label.clone();
|
||||
ctx.link().send_future(async {
|
||||
match Self::submit_totp_update(token_c, CURequest::TotpRemove(label), cb).await
|
||||
{
|
||||
Ok(v) => v,
|
||||
Err(v) => v.into(),
|
||||
}
|
||||
});
|
||||
self.enabled = false;
|
||||
}
|
||||
Msg::Success => {
|
||||
// Do nothing, very well.
|
||||
}
|
||||
Msg::Error { emsg, kopid } => {
|
||||
// Submit the error to the parent.
|
||||
cb.emit(EventBusMsg::Error { emsg, kopid });
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn view(&self, ctx: &Context<Self>) -> Html {
|
||||
let label = ctx.props().label.clone();
|
||||
let submit_enabled = self.enabled;
|
||||
|
||||
html! {
|
||||
<div class="row mb-3">
|
||||
<div class="col">{ label }</div>
|
||||
<div class="col">
|
||||
<button type="button" class="btn btn-dark btn-sml"
|
||||
disabled={ !submit_enabled }
|
||||
onclick={
|
||||
ctx.link()
|
||||
.callback(move |_| Msg::Submit)
|
||||
}
|
||||
>
|
||||
{ "Remove TOTP" }
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TotpRemoveComp {
|
||||
async fn submit_totp_update(
|
||||
token: CUSessionToken,
|
||||
req: CURequest,
|
||||
cb: Callback<EventBusMsg>,
|
||||
) -> Result<Msg, FetchError> {
|
||||
let req_jsvalue = serde_json::to_string(&(req, token))
|
||||
.map(|s| JsValue::from(&s))
|
||||
.expect_throw("Failed to serialise pw curequest");
|
||||
|
||||
let mut opts = RequestInit::new();
|
||||
opts.method("POST");
|
||||
opts.mode(RequestMode::SameOrigin);
|
||||
|
||||
opts.body(Some(&req_jsvalue));
|
||||
|
||||
let request = Request::new_with_str_and_init("/v1/credential/_update", &opts)?;
|
||||
request
|
||||
.headers()
|
||||
.set("content-type", "application/json")
|
||||
.expect_throw("failed to set header");
|
||||
|
||||
let window = utils::window();
|
||||
let resp_value = JsFuture::from(window.fetch_with_request(&request)).await?;
|
||||
let resp: Response = resp_value.dyn_into().expect_throw("Invalid response type");
|
||||
let status = resp.status();
|
||||
let headers = resp.headers();
|
||||
|
||||
let kopid = headers.get("x-kanidm-opid").ok().flatten();
|
||||
|
||||
if status == 200 {
|
||||
let jsval = JsFuture::from(resp.json()?).await?;
|
||||
let status: CUStatus =
|
||||
serde_wasm_bindgen::from_value(jsval).expect_throw("Invalid response type");
|
||||
|
||||
cb.emit(EventBusMsg::UpdateStatus {
|
||||
status: status.clone(),
|
||||
});
|
||||
|
||||
Ok(Msg::Success)
|
||||
} else {
|
||||
let text = JsFuture::from(resp.text()?).await?;
|
||||
let emsg = text.as_string().unwrap_or_default();
|
||||
Ok(Msg::Error { emsg, kopid })
|
||||
}
|
||||
}
|
||||
}
|
|
@ -268,6 +268,7 @@ impl LoginApp {
|
|||
name="username"
|
||||
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e))) }
|
||||
type="text"
|
||||
autocomplete="username"
|
||||
value={ inputvalue }
|
||||
/>
|
||||
</div>
|
||||
|
@ -333,6 +334,9 @@ impl LoginApp {
|
|||
LoginAppMsg::PasswordSubmit
|
||||
} ) }
|
||||
>
|
||||
<div>
|
||||
<input hidden=true type="text" autocomplete="username" />
|
||||
</div>
|
||||
<div class={CLASS_DIV_LOGIN_FIELD}>
|
||||
<input
|
||||
autofocus=true
|
||||
|
@ -342,6 +346,7 @@ impl LoginApp {
|
|||
name="password"
|
||||
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e))) }
|
||||
type="password"
|
||||
autocomplete="current-password"
|
||||
value={ inputvalue }
|
||||
/>
|
||||
</div>
|
||||
|
@ -377,6 +382,7 @@ impl LoginApp {
|
|||
name="backup_code"
|
||||
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e))) }
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value={ inputvalue }
|
||||
/>
|
||||
</div>
|
||||
|
@ -405,10 +411,11 @@ impl LoginApp {
|
|||
autofocus=true
|
||||
class="autofocus form-control"
|
||||
disabled={ state==&TotpState::Disabled }
|
||||
id="totp"
|
||||
name="totp"
|
||||
id="otp"
|
||||
name="otp"
|
||||
oninput={ ctx.link().callback(|e: InputEvent| LoginAppMsg::Input(utils::get_value_from_input_event(e)))}
|
||||
type="text"
|
||||
autocomplete="off"
|
||||
value={ inputvalue }
|
||||
/>
|
||||
</div>
|
||||
|
@ -977,5 +984,10 @@ impl Component for LoginApp {
|
|||
fn rendered(&mut self, _ctx: &Context<Self>, _first_render: bool) {
|
||||
#[cfg(debug_assertions)]
|
||||
console::debug!("login::rendered".to_string());
|
||||
// Force autofocus on elements that need it if present.
|
||||
crate::utils::autofocus("username");
|
||||
crate::utils::autofocus("password");
|
||||
crate::utils::autofocus("backup_code");
|
||||
crate::utils::autofocus("otp");
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue