diff --git a/Cargo.lock b/Cargo.lock index 4ca68ea9..17702c03 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3091,6 +3091,7 @@ dependencies = [ "redhac", "regex", "ring", + "semver", "serde", "serde_json", "sqlx", @@ -3538,6 +3539,9 @@ name = "semver" version = "1.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ad977052201c6de01a8ef2aa3378c4bd23217a056337d1d6da40468d267a4fb0" +dependencies = [ + "serde", +] [[package]] name = "serde" diff --git a/dev_notes.md b/dev_notes.md index 97b2bd14..572e9995 100644 --- a/dev_notes.md +++ b/dev_notes.md @@ -2,26 +2,27 @@ ## CURRENT WORK +## TODO before v0.16.0 + +- add a new table that keeps track about when password expiry / reset emails were sent out to avoid duplicates +- update the book with all the new features + ## Stage 1 - essentials [x] finished ## Stage 2 - features - do before v1.0.0 -- create a nice looking error page for things like expired magic link, bad client values, things like that... -- introduce 'rauthy-db-version' or something like that into the config table to be able to do stricter validation -between feature / major version migrations -- add a new table that keeps track about when password expiry / reset emails were sent out to avoid duplicates - NATS events stream or maybe internal one? -- benchmarks and performance tuning +- add an 'ip blacklist' feature? +- add all default claims for users https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims - double check against https://openid.net/specs/openid-connect-core-1_0.html that everything is implemented correctly one more time +- benchmarks and performance tuning ## Stage 3 - Possible nice to haves -- add an 'ip blacklist' feature? - auto-encrypted backups + backups to remote locations (ssh, nfs, s3, ...) -> postponed - should be applied to sqlite only since postgres has pg_backrest and a lot of well established tooling anyway -- add all default claims for users https://openid.net/specs/openid-connect-core-1_0.html#ScopeClaims - oidc-client (google, github, ...) - 'rauthy-migrate' project to help migrating to rauthy? -- custom event listener template to build own implementation? +- custom event listener template to build own implementation? -> only if NATS will be implemented maybe? diff --git a/rauthy-models/Cargo.toml b/rauthy-models/Cargo.toml index d53ee99f..270a4f6f 100644 --- a/rauthy-models/Cargo.toml +++ b/rauthy-models/Cargo.toml @@ -48,6 +48,7 @@ rauthy-common = { path = "../rauthy-common" } redhac = "0.7" regex = "1" ring = "0.16" +semver = { version = "1.0.19", features = ["serde"] } serde = { version = "1", features = ["derive"] } serde_json = "1" sqlx = { workspace = true } diff --git a/rauthy-models/src/app_state.rs b/rauthy-models/src/app_state.rs index ef58261f..67078750 100644 --- a/rauthy-models/src/app_state.rs +++ b/rauthy-models/src/app_state.rs @@ -1,4 +1,5 @@ use crate::email::EMail; +use crate::entity::db_version::DbVersion; use crate::migration::db_migrate; use crate::migration::db_migrate::migrate_init_prod; use crate::migration::db_migrate_dev::migrate_dev_data; @@ -283,6 +284,13 @@ impl AppState { pool }; + // before we do any db migrations, we need to check the current DB version for + // for compatibility + let db_version = DbVersion::check_app_version(&pool) + .await + .map_err(|err| anyhow::Error::msg(err.message))?; + + // migrate DB data if !*DEV_MODE { migrate_init_prod( &pool, @@ -344,6 +352,11 @@ impl AppState { error!("Error when applying anti-lockout check: {:?}", err); } + // update the DbVersion after successful pool creation and migrations + DbVersion::update(&pool, db_version) + .await + .map_err(|err| anyhow::Error::msg(err.message))?; + Ok(pool) } diff --git a/rauthy-models/src/entity/db_version.rs b/rauthy-models/src/entity/db_version.rs new file mode 100644 index 00000000..a38f2e8b --- /dev/null +++ b/rauthy-models/src/entity/db_version.rs @@ -0,0 +1,135 @@ +use crate::app_state::DbPool; +use rauthy_common::constants::RAUTHY_VERSION; +use rauthy_common::error_response::ErrorResponse; +use semver::Version; +use serde::{Deserialize, Serialize}; +use sqlx::query; +use std::str::FromStr; +use tracing::{debug, warn}; + +#[derive(Debug, Serialize, Deserialize)] +pub struct DbVersion { + pub version: Version, +} + +impl DbVersion { + pub async fn find(db: &DbPool) -> Option { + let res = query!("select data from config where id = 'db_version'") + .fetch_optional(db) + .await + .ok()?; + match res { + Some(res) => { + let data = res + .data + .expect("to get 'data' back from the AppVersion query"); + bincode::deserialize::(&data).ok() + } + None => None, + } + } + + pub async fn update(db: &DbPool, db_version: Option) -> Result<(), ErrorResponse> { + let app_version = Self::app_version(); + if Some(&app_version) != db_version.as_ref() { + let slf = Self { + version: app_version, + }; + let data = bincode::serialize(&slf)?; + + #[cfg(feature = "sqlite")] + let q = query!( + "insert or replace into config (id, data) values ('db_version', $1)", + data, + ); + #[cfg(not(feature = "sqlite"))] + let q = query!( + r#"insert into config (id, data) values ('db_version', $1) + on conflict(id) do update set data = $1"#, + data, + ); + q.execute(db).await?; + } + + Ok(()) + } + + pub async fn check_app_version(db: &DbPool) -> Result, ErrorResponse> { + let app_version = Self::app_version(); + debug!("Current Rauthy Version: {:?}", app_version); + + // check DB version for compatibility + let db_version = match Self::find(db).await { + None => { + debug!(" No Current DB Version found"); + // check if the DB is completely new + let db_exists = query!("select id from config limit 1").fetch_one(db).await; + if db_exists.is_ok() { + Self::is_db_compatible(db, &app_version, None).await?; + } + + None + } + Some(db_version) => { + debug!("Current DB Version: {:?}", db_version); + Self::is_db_compatible(db, &app_version, Some(&db_version.version)).await?; + + Some(db_version.version) + } + }; + + Ok(db_version) + } + + /// Checks if we can use an existing (possibly older) db with this version of rauthy, or if + /// the user may need to take action beforehand. + async fn is_db_compatible( + db: &DbPool, + app_version: &Version, + db_version: Option<&Version>, + ) -> Result<(), ErrorResponse> { + // this check panics on purpose and it is there to never forget to adjust this + // version check before doing any major or minor release + if app_version.major != 0 || app_version.minor != 16 { + panic!( + "\nDbVersion::check_app_version needs adjustment for the new RAUTHY_VERSION: {}", + RAUTHY_VERSION + ); + } + + // warn on prerelease usage + if !app_version.pre.is_empty() { + warn!("!!! Caution: you are using a prerelease version !!!"); + } + + // check for the lowest DB version we can use with this App Version + if let Some(db_version) = db_version { + if db_version.major != 0 || db_version.minor < 15 || db_version.minor > 16 { + panic!( + "\nRauthy {} needs at least a DB version v0.15 and max v0.16", + app_version + ); + } + + return Ok(()); + } + + // check the DB version in another way if we did not find an existing DB version + + // the passkeys table was introduced with v0.15.0 + let passkeys_exist = query!("select user_id from passkeys limit 1") + .fetch_one(db) + .await; + if passkeys_exist.is_err() { + panic!("\nYou need to start at least rauthy v0.15 before you can upgrade"); + } + + Ok(()) + } +} + +impl DbVersion { + pub fn app_version() -> Version { + Version::from_str(RAUTHY_VERSION).expect("bad format for RAUTHY_VERSION") + } +} diff --git a/rauthy-models/src/entity/mod.rs b/rauthy-models/src/entity/mod.rs index 99858e93..39119f47 100644 --- a/rauthy-models/src/entity/mod.rs +++ b/rauthy-models/src/entity/mod.rs @@ -1,6 +1,7 @@ pub mod auth_codes; pub mod clients; pub mod colors; +pub mod db_version; pub mod groups; pub mod jwk; pub mod magic_links;