use async_trait::async_trait;
use cosmian_logger::{debug, error};
use serde::{Deserialize, Serialize};
use version_compare::{Cmp, compare};
use crate::{DbError, error::DbResult};
pub(crate) const KMS_VERSION_BEFORE_MIGRATION_SUPPORT: &str = "4.12.0";
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub(crate) enum DbState {
Ready,
Upgrading,
}
fn lower(version: &str, target: &str) -> DbResult<bool> {
let cmp = compare(version, target).map_err(|()| {
DbError::DatabaseError(format!(
"Error comparing versions. The current DB version: {version}, cannot be parsed."
))
})?;
Ok(matches!(cmp, Cmp::Lt))
}
pub(super) trait HasDatabase {
type Database: sqlx::Database;
}
#[async_trait(?Send)]
pub(crate) trait Migrate {
async fn get_db_state(&self) -> DbResult<Option<DbState>>;
async fn set_db_state(&self, state: DbState) -> DbResult<()>;
async fn get_current_db_version(&self) -> DbResult<Option<String>>;
async fn set_current_db_version(&self, version: &str) -> DbResult<()>;
}
#[async_trait(?Send)]
pub(crate) trait SqlMigrate<DB>: Migrate {
async fn migrate(&self) -> DbResult<()> {
let db_state = self.get_db_state().await?.unwrap_or(DbState::Ready);
if db_state != DbState::Ready {
let error_string = "Database is not in a ready state; it is either upgrading or a \
previous upgrading failed. Bailing out. Please wait for the \
migration to complete or restore a previous version of the \
database.";
error!("{error_string}");
return Err(DbError::DatabaseError(error_string.to_owned()));
}
let current_db_version = self
.get_current_db_version()
.await?
.unwrap_or_else(|| KMS_VERSION_BEFORE_MIGRATION_SUPPORT.to_owned());
let kms_version = env!("CARGO_PKG_VERSION");
debug!("Database version: {current_db_version}, Current KMS version: {kms_version}");
if lower(¤t_db_version, "5.0.0")? {
let msg = format!(
"Database version {current_db_version} cannot be upgraded to version \
5.0.0.\nPlease export all keys using standard formats such as PKCS#8 or Raw and \
reimport them in this KMS version."
);
error!("{}", msg);
return Err(DbError::DatabaseError(msg));
}
debug!(" ==> database is up to date.");
Ok(())
}
#[expect(dead_code)]
async fn migrate_from_4_12_0_to_4_13_0(&self) -> DbResult<()>;
#[expect(dead_code)]
async fn migrate_to_4_22_2(&self) -> DbResult<()>;
}
#[cfg(feature = "non-fips")]
mod redis_migrate {
use cloudproof_findex::Label;
use cosmian_kms_crypto::reexport::cosmian_crypto_core::Secret;
use super::{DbError, DbResult, DbState, Migrate, debug, error, lower};
pub(crate) const LOWEST_DB_VERSION_WITH_REDIS_SUPPORT: &str = "5.0.0";
#[derive(Debug, Clone)]
pub(crate) struct MigrateTo5_12_0Parameters<'a> {
pub redis_url: String,
pub master_key: &'a Secret<32>, pub label: Label,
}
#[derive(Debug, Default)]
pub(crate) struct MigrationParams<'a> {
pub(crate) migrate_to_5_12_0_parameters: Option<MigrateTo5_12_0Parameters<'a>>,
}
pub(crate) trait RedisMigrate: Migrate {
async fn migrate(&self, parameters: MigrationParams<'_>) -> DbResult<()> {
let db_state = self.get_db_state().await?;
let current_db_version = self.get_current_db_version().await?;
if db_state.is_none() || current_db_version.is_none() {
let msg = "Database state (and/or version) not set - which usually means that it was constructed with
a KMS version below 5.0.0. If that's the case,
please export all keys using standard formats such as PKCS#8 or Raw and
reimport them in this KMS version.".to_owned();
error!("{}", msg);
return Err(DbError::DatabaseError(msg));
}
#[allow(clippy::unwrap_used)] let current_db_version = current_db_version.unwrap();
if db_state != Some(DbState::Ready) {
let error_string = "Database is not in a ready state; it is either upgrading or a \
previous update failed. Bailing out. Please wait for the \
migration to complete or restore a previous version of the \
database.";
error!("{}", error_string,);
return Err(DbError::DatabaseError(error_string.to_owned()));
}
let kms_version = env!("CARGO_PKG_VERSION");
if kms_version == current_db_version {
debug!(" ==> database is up to date.");
return Ok(());
}
if lower(¤t_db_version, LOWEST_DB_VERSION_WITH_REDIS_SUPPORT)? {
let msg = format!(
"Databases before version {LOWEST_DB_VERSION_WITH_REDIS_SUPPORT} do not support \
Findex with Redis's database. Aborting. Please export all keys - if any - (using \
standard formats such as PKCS#8 or Raw) and reimport them using the latest KMS \
version."
);
error!("{}", msg);
return Err(DbError::DatabaseError(msg));
}
self.set_db_state(DbState::Upgrading).await?;
debug!(
"Database version before migration: {current_db_version}, Current KMS version: \
{kms_version}, starting migration process..."
);
if lower(¤t_db_version, "5.12.0")? {
debug!(" ==> migrating to version 5.12.0");
let migration_params = parameters.migrate_to_5_12_0_parameters.ok_or_else(|| {
let msg = "Missing parameters for migration to version 5.12.0. Aborting. Please \
provide the Redis URL, the master key and the label used by the \
previous DB instance.";
error!("{}", msg);
DbError::DatabaseError(msg.to_owned())
})?;
self.migrate_to_5_12_0(migration_params).await?;
self.set_current_db_version("5.12.0").await?;
}
if kms_version != current_db_version {
self.set_current_db_version(kms_version).await?;
self.set_db_state(DbState::Ready).await?;
debug!("Redis database version was migrated to the latest version: {kms_version}");
}
debug!(" ==> database is up to date.");
Ok(())
}
async fn migrate_to_5_12_0(&self, parameters: MigrateTo5_12_0Parameters) -> DbResult<()>;
}
}
#[cfg(feature = "non-fips")]
pub(crate) use redis_migrate::*;