From c1235a71866bdaed3125a77af5f852d9eac46120 Mon Sep 17 00:00:00 2001 From: Firstyear Date: Thu, 23 May 2024 11:48:37 +1000 Subject: [PATCH] Check for same version with backup/restore (#2789) --- README.md | 16 ++++---- book/src/backup_and_restore.md | 3 ++ libs/profiles/src/lib.rs | 8 ++++ proto/src/internal/error.rs | 4 ++ server/lib/src/be/dbentry.rs | 27 +++++++++---- server/lib/src/be/mod.rs | 69 ++++++++++++++++++++-------------- 6 files changed, 83 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 18dd10781..d908b578b 100644 --- a/README.md +++ b/README.md @@ -146,8 +146,8 @@ elements in a simpler and correct way out of the box in comparison.
Rauthy -Rauthy is a minimal OIDC provider. It supports WebAuthn just like Kanidm - they actually use our library -for it! +Rauthy is a minimal OIDC provider. It supports WebAuthn just like Kanidm - they actually use our +library for it! Rauthy only provides support for OIDC and so is unable to support other use cases like RADIUS and unix authentication. @@ -160,15 +160,15 @@ then Kanidm will support those.
Authentik / Authelia / Zitadel -Authentik is an IDM provider written in Python and, Authelia and Zitadel are written in Go. -all similar to Kanidm in the features it offers but notably all have weaker support for -unix authentication and do not support the same level of authentication policy as Kanidm. Notably, -all are missing WebAuthn Attestation. +Authentik is an IDM provider written in Python and, Authelia and Zitadel are written in Go. all +similar to Kanidm in the features it offers but notably all have weaker support for unix +authentication and do not support the same level of authentication policy as Kanidm. Notably, all +are missing WebAuthn Attestation. All three use an external SQL server such as PostgreSQL. This can create a potential single source of failure and performance limitation compared to Kanidm which opted to write our own high -performance database and replication system instead based on our experience with enterprise -LDAP servers. +performance database and replication system instead based on our experience with enterprise LDAP +servers.
diff --git a/book/src/backup_and_restore.md b/book/src/backup_and_restore.md index a1aa7d000..a28dcfbe3 100644 --- a/book/src/backup_and_restore.md +++ b/book/src/backup_and_restore.md @@ -4,6 +4,9 @@ With any Identity Management (IDM) software, it's important you have the capabil case of a disaster - be that physical damage or a mistake. Kanidm supports backup and restore of the database with three methods. +It is important that you only attempt to restore data with the same version of the server that the +backup originated from. + ## Method 1 - Automatic Backup Automatic backups can be generated online by a `kanidmd server` instance by including the diff --git a/libs/profiles/src/lib.rs b/libs/profiles/src/lib.rs index b720e220f..a4dc9acab 100644 --- a/libs/profiles/src/lib.rs +++ b/libs/profiles/src/lib.rs @@ -97,6 +97,14 @@ pub fn apply_profile() { println!("cargo:rustc-env=KANIDM_PRE_RELEASE=1"); } + // For some checks we only want the series (i.e. exclude the patch version). + let version_major = env!("CARGO_PKG_VERSION_MAJOR"); + let version_minor = env!("CARGO_PKG_VERSION_MINOR"); + println!( + "cargo:rustc-env=KANIDM_PKG_SERIES={}.{}", + version_major, version_minor + ); + match profile_cfg.cpu_flags { CpuOptLevel::apple_m1 => println!("cargo:rustc-env=RUSTFLAGS=-Ctarget-cpu=apple_m1"), CpuOptLevel::none => {} diff --git a/proto/src/internal/error.rs b/proto/src/internal/error.rs index 6dd7870e2..d8bd44c0d 100644 --- a/proto/src/internal/error.rs +++ b/proto/src/internal/error.rs @@ -132,6 +132,10 @@ pub enum OperationError { // Value Errors VL0001ValueSshPublicKeyString, + // DB low level errors. + DB0001MismatchedRestoreVersion, + DB0002MismatchedRestoreVersion, + // SCIM SC0001IncomingSshPublicKey, // Migration diff --git a/server/lib/src/be/dbentry.rs b/server/lib/src/be/dbentry.rs index 60d4db314..2000d24e4 100644 --- a/server/lib/src/be/dbentry.rs +++ b/server/lib/src/be/dbentry.rs @@ -56,18 +56,15 @@ pub struct DbEntry { #[derive(Serialize, Deserialize)] #[serde(untagged)] pub enum DbBackup { - V1(Vec), - V2 { - db_s_uuid: Uuid, - db_d_uuid: Uuid, - db_ts_max: Duration, - entries: Vec, - }, - V3 { + // Because of untagged, this has to be in order of newest + // to oldest as untagged does a first-match when deserialising. + V5 { + version: String, db_s_uuid: Uuid, db_d_uuid: Uuid, db_ts_max: Duration, keyhandles: BTreeMap, + repl_meta: DbReplMeta, entries: Vec, }, V4 { @@ -78,6 +75,20 @@ pub enum DbBackup { repl_meta: DbReplMeta, entries: Vec, }, + V3 { + db_s_uuid: Uuid, + db_d_uuid: Uuid, + db_ts_max: Duration, + keyhandles: BTreeMap, + entries: Vec, + }, + V2 { + db_s_uuid: Uuid, + db_d_uuid: Uuid, + db_ts_max: Duration, + entries: Vec, + }, + V1(Vec), } fn from_vec_dbval1(attr_val: NonEmpty) -> Result { diff --git a/server/lib/src/be/mod.rs b/server/lib/src/be/mod.rs index 509ad7e2e..97a068338 100644 --- a/server/lib/src/be/mod.rs +++ b/server/lib/src/be/mod.rs @@ -901,7 +901,9 @@ pub trait BackendTransaction { let keyhandles = idlayer.get_key_handles()?; - let bak = DbBackup::V4 { + let bak = DbBackup::V5 { + // remember env is evaled at compile time. + version: env!("KANIDM_PKG_SERIES").to_string(), db_s_uuid, db_d_uuid, db_ts_max, @@ -1756,8 +1758,8 @@ impl<'a> BackendWriteTransaction<'a> { OperationError::SerdeJsonError })?; - let (dbentries, repl_meta) = match dbbak { - DbBackup::V1(dbentries) => (dbentries, None), + let (dbentries, repl_meta, maybe_version) = match dbbak { + DbBackup::V1(dbentries) => (dbentries, None, None), DbBackup::V2 { db_s_uuid, db_d_uuid, @@ -1768,7 +1770,7 @@ impl<'a> BackendWriteTransaction<'a> { idlayer.write_db_s_uuid(db_s_uuid)?; idlayer.write_db_d_uuid(db_d_uuid)?; idlayer.set_db_ts_max(db_ts_max)?; - (entries, None) + (entries, None, None) } DbBackup::V3 { db_s_uuid, @@ -1782,7 +1784,7 @@ impl<'a> BackendWriteTransaction<'a> { idlayer.write_db_d_uuid(db_d_uuid)?; idlayer.set_db_ts_max(db_ts_max)?; idlayer.set_key_handles(keyhandles)?; - (entries, None) + (entries, None, None) } DbBackup::V4 { db_s_uuid, @@ -1797,8 +1799,34 @@ impl<'a> BackendWriteTransaction<'a> { idlayer.write_db_d_uuid(db_d_uuid)?; idlayer.set_db_ts_max(db_ts_max)?; idlayer.set_key_handles(keyhandles)?; - (entries, Some(repl_meta)) + (entries, Some(repl_meta), None) } + DbBackup::V5 { + version, + db_s_uuid, + db_d_uuid, + db_ts_max, + keyhandles, + repl_meta, + entries, + } => { + // Do stuff. + idlayer.write_db_s_uuid(db_s_uuid)?; + idlayer.write_db_d_uuid(db_d_uuid)?; + idlayer.set_db_ts_max(db_ts_max)?; + idlayer.set_key_handles(keyhandles)?; + (entries, Some(repl_meta), Some(version)) + } + }; + + if let Some(version) = maybe_version { + if version != env!("KANIDM_PKG_SERIES") { + error!("The provided backup data is from server version {} and is unable to be restored on this instance ({})", version, env!("KANIDM_PKG_SERIES")); + return Err(OperationError::DB0001MismatchedRestoreVersion); + } + } else { + error!("The provided backup data is from an older server version and is unable to be restored."); + return Err(OperationError::DB0002MismatchedRestoreVersion); }; // Rebuild the RUV from the backup. @@ -2587,31 +2615,12 @@ mod tests { // Now here, we need to tamper with the file. let serialized_string = fs::read_to_string(&db_backup_file_name).unwrap(); + trace!(?serialized_string); let mut dbbak: DbBackup = serde_json::from_str(&serialized_string).unwrap(); match &mut dbbak { - DbBackup::V1(_) => { - // We no longer use these format versions! - unreachable!() - } - DbBackup::V2 { - db_s_uuid: _, - db_d_uuid: _, - db_ts_max: _, - entries, - } => { - let _ = entries.pop(); - } - DbBackup::V3 { - db_s_uuid: _, - db_d_uuid: _, - db_ts_max: _, - keyhandles: _, - entries, - } => { - let _ = entries.pop(); - } - DbBackup::V4 { + DbBackup::V5 { + version: _, db_s_uuid: _, db_d_uuid: _, db_ts_max: _, @@ -2621,6 +2630,10 @@ mod tests { } => { let _ = entries.pop(); } + _ => { + // We no longer use these format versions! + unreachable!() + } }; let serialized_entries_str = serde_json::to_string_pretty(&dbbak).unwrap();