diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs index 63934e85e..fd2c2bac1 100644 --- a/proto/src/internal/error.rs +++ b/proto/src/internal/error.rs @@ -151,6 +151,7 @@ pub enum OperationError { MG0003ServerPhaseInvalidForMigration, MG0004DomainLevelInDevelopment, MG0005GidConstraintsNotMet, + MG0006SKConstraintsNotMet, // KP0001KeyProviderNotLoaded, KP0002KeyProviderInvalidClass, @@ -302,6 +303,7 @@ impl OperationError { Self::DB0002MismatchedRestoreVersion => None, Self::MG0004DomainLevelInDevelopment => None, Self::MG0005GidConstraintsNotMet => None, + Self::MG0006SKConstraintsNotMet => Some("Migration Constraints Not Met - Security Keys should not be present."), Self::KP0001KeyProviderNotLoaded => None, Self::KP0002KeyProviderInvalidClass => None, Self::KP0003KeyProviderInvalidType => None, diff --git a/proto/src/internal/mod.rs b/proto/src/internal/mod.rs index 74ad4d2df..947808dab 100644 --- a/proto/src/internal/mod.rs +++ b/proto/src/internal/mod.rs @@ -242,6 +242,9 @@ pub struct DomainUpgradeCheckReport { pub enum DomainUpgradeCheckStatus { Pass6To7Gidnumber, Fail6To7Gidnumber, + + Pass7To8SecurityKeys, + Fail7To8SecurityKeys, } #[derive(Debug, Serialize, Deserialize, Clone)] diff --git a/server/daemon/src/main.rs b/server/daemon/src/main.rs index e28c174a5..482f7623c 100644 --- a/server/daemon/src/main.rs +++ b/server/daemon/src/main.rs @@ -221,6 +221,23 @@ async fn submit_admin_req(path: &str, req: AdminTaskRequest, output_mode: Consol info!("affected_entry : {}", entry_id); } } + ProtoDomainUpgradeCheckStatus::Pass7To8SecurityKeys => { + info!("upgrade_item : security key usage"); + debug!("from_level : {}", item.from_level); + debug!("to_level : {}", item.to_level); + info!("status : PASS"); + } + ProtoDomainUpgradeCheckStatus::Fail7To8SecurityKeys => { + info!("upgrade_item : security key usage"); + debug!("from_level : {}", item.from_level); + debug!("to_level : {}", item.to_level); + info!("status : FAIL"); + info!("description : Security keys no longer function as a second factor due to the introduction of CTAP2 and greater forcing PIN interactions."); + info!("action : Modify the accounts in question to remove their security key and add it as a passkey or enable TOTP"); + for entry_id in item.affected_entries { + info!("affected_entry : {}", entry_id); + } + } } } } diff --git a/server/lib/src/credential/mod.rs b/server/lib/src/credential/mod.rs index 84960d28b..7c30597e2 100644 --- a/server/lib/src/credential/mod.rs +++ b/server/lib/src/credential/mod.rs @@ -645,6 +645,13 @@ impl Credential { })) } + pub(crate) fn has_securitykey(&self) -> bool { + match &self.type_ { + CredentialType::PasswordMfa(_, _, map, _) => !map.is_empty(), + _ => false, + } + } + /// Get a reference to the contained webuthn credentials, if any. pub fn securitykey_ref(&self) -> Result<&Map, OperationError> { match &self.type_ { diff --git a/server/lib/src/server/migrations.rs b/server/lib/src/server/migrations.rs index 2110fc042..5392ae25e 100644 --- a/server/lib/src/server/migrations.rs +++ b/server/lib/src/server/migrations.rs @@ -797,6 +797,39 @@ impl<'a> QueryServerWriteTransaction<'a> { return Err(OperationError::MG0004DomainLevelInDevelopment); } + // ============== Apply constraints =============== + let filter = filter!(f_and!([ + f_eq(Attribute::Class, EntryClass::Account.into()), + f_pres(Attribute::PrimaryCredential), + ])); + + let results = self.internal_search(filter)?; + + let affected_entries = results + .into_iter() + .filter_map(|entry| { + if entry + .get_ava_single_credential(Attribute::PrimaryCredential) + .map(|cred| cred.has_securitykey()) + .unwrap_or_default() + { + Some(entry.get_display_id()) + } else { + None + } + }) + .collect::>(); + + if !affected_entries.is_empty() { + error!("Unable to proceed. Not all entries meet gid/uid constraints."); + for sk_present in affected_entries { + error!(%sk_present); + } + return Err(OperationError::MG0006SKConstraintsNotMet); + } + + // =========== Apply changes ============== + Ok(()) } @@ -1095,6 +1128,19 @@ impl<'a> QueryServerReadTransaction<'a> { report_items.push(item); } + if current_level <= DOMAIN_LEVEL_7 && upgrade_level >= DOMAIN_LEVEL_8 { + let item = self + .domain_upgrade_check_7_to_8_security_keys() + .map_err(|err| { + error!( + ?err, + "Failed to perform domain upgrade check 7 to 8 - security-keys" + ); + err + })?; + report_items.push(item); + } + Ok(ProtoDomainUpgradeCheckReport { name, uuid, @@ -1191,6 +1237,45 @@ impl<'a> QueryServerReadTransaction<'a> { affected_entries, }) } + + pub(crate) fn domain_upgrade_check_7_to_8_security_keys( + &mut self, + ) -> Result { + let filter = filter!(f_and!([ + f_eq(Attribute::Class, EntryClass::Account.into()), + f_pres(Attribute::PrimaryCredential), + ])); + + let results = self.internal_search(filter)?; + + let affected_entries = results + .into_iter() + .filter_map(|entry| { + if entry + .get_ava_single_credential(Attribute::PrimaryCredential) + .map(|cred| cred.has_securitykey()) + .unwrap_or_default() + { + Some(entry.get_display_id()) + } else { + None + } + }) + .collect::>(); + + let status = if affected_entries.is_empty() { + ProtoDomainUpgradeCheckStatus::Pass7To8SecurityKeys + } else { + ProtoDomainUpgradeCheckStatus::Fail7To8SecurityKeys + }; + + Ok(ProtoDomainUpgradeCheckItem { + status, + from_level: DOMAIN_LEVEL_7, + to_level: DOMAIN_LEVEL_8, + affected_entries, + }) + } } #[cfg(test)]