From 4e85a4718f7032de0e8ac5bfdff0ea80081b1de7 Mon Sep 17 00:00:00 2001 From: Valentin Tolmer Date: Thu, 13 Apr 2023 10:37:54 +0200 Subject: [PATCH] server: enforce email and uuid unicity --- server/src/domain/sql_backend_handler.rs | 2 +- server/src/domain/sql_migrations.rs | 78 +++++++++++++++++++----- server/src/domain/sql_tables.rs | 73 +++++++++++++++++++--- 3 files changed, 131 insertions(+), 22 deletions(-) diff --git a/server/src/domain/sql_backend_handler.rs b/server/src/domain/sql_backend_handler.rs index 2b0f757..c888a15 100644 --- a/server/src/domain/sql_backend_handler.rs +++ b/server/src/domain/sql_backend_handler.rs @@ -86,7 +86,7 @@ pub mod tests { handler .create_user(CreateUserRequest { user_id: UserId::new(name), - email: "bob@bob.bob".to_string(), + email: format!("{}@bob.bob", name), display_name: Some("display ".to_string() + name), first_name: Some("first ".to_string() + name), last_name: Some("last ".to_string() + name), diff --git a/server/src/domain/sql_migrations.rs b/server/src/domain/sql_migrations.rs index bbbcf17..6b6f749 100644 --- a/server/src/domain/sql_migrations.rs +++ b/server/src/domain/sql_migrations.rs @@ -2,15 +2,14 @@ use crate::domain::{ sql_tables::{DbConnection, SchemaVersion}, types::{GroupId, UserId, Uuid}, }; +use anyhow::Context; use sea_orm::{ - sea_query::{self, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Query, Table, Value}, + sea_query::{self, ColumnDef, Expr, ForeignKey, ForeignKeyAction, Index, Query, Table, Value}, ConnectionTrait, FromQueryResult, Iden, Statement, TransactionTrait, }; use serde::{Deserialize, Serialize}; use tracing::{info, instrument, warn}; -use super::sql_tables::LAST_SCHEMA_VERSION; - #[derive(Iden, PartialEq, Eq, Debug, Serialize, Deserialize, Clone, Copy)] pub enum Users { Table, @@ -460,30 +459,81 @@ async fn migrate_to_v3(pool: &DbConnection) -> anyhow::Result<()> { Ok(()) } +async fn migrate_to_v4(pool: &DbConnection) -> anyhow::Result<()> { + let builder = pool.get_database_backend(); + // Make emails and UUIDs unique. + pool.execute( + builder.build( + Index::create() + .name("unique-user-email") + .table(Users::Table) + .col(Users::Email) + .unique(), + ), + ) + .await + .context("while enforcing unicity on emails (2 users have the same email)")?; + pool.execute( + builder.build( + Index::create() + .name("unique-user-uuid") + .table(Users::Table) + .col(Users::Uuid) + .unique(), + ), + ) + .await + .context("while enforcing unicity on user UUIDs (2 users have the same UUID)")?; + pool.execute( + builder.build( + Index::create() + .name("unique-group-uuid") + .table(Groups::Table) + .col(Groups::Uuid) + .unique(), + ), + ) + .await + .context("while enforcing unicity on group UUIDs (2 groups have the same UUID)")?; + Ok(()) +} + +// This is needed to make an array of async functions. +macro_rules! to_sync { + ($l:ident) => { + |pool| -> std::pin::Pin>>> { + Box::pin($l(pool)) + } + }; +} + pub async fn migrate_from_version( pool: &DbConnection, version: SchemaVersion, + last_version: SchemaVersion, ) -> anyhow::Result<()> { - match version.cmp(&LAST_SCHEMA_VERSION) { - std::cmp::Ordering::Less => info!( - "Upgrading DB schema from {} to {}", - version.0, LAST_SCHEMA_VERSION.0 - ), + match version.cmp(&last_version) { + std::cmp::Ordering::Less => (), std::cmp::Ordering::Equal => return Ok(()), std::cmp::Ordering::Greater => anyhow::bail!("DB version downgrading is not supported"), } - if version < SchemaVersion(2) { - migrate_to_v2(pool).await?; - } - if version < SchemaVersion(3) { - migrate_to_v3(pool).await?; + let migrations = [ + to_sync!(migrate_to_v2), + to_sync!(migrate_to_v3), + to_sync!(migrate_to_v4), + ]; + for migration in 2..=4 { + if version < SchemaVersion(migration) && SchemaVersion(migration) <= last_version { + info!("Upgrading DB schema from {} to {}", version.0, migration); + migrations[(migration - 2) as usize](pool).await?; + } } let builder = pool.get_database_backend(); pool.execute( builder.build( Query::update() .table(Metadata::Table) - .value(Metadata::Version, Value::from(LAST_SCHEMA_VERSION)), + .value(Metadata::Version, Value::from(last_version)), ), ) .await?; diff --git a/server/src/domain/sql_tables.rs b/server/src/domain/sql_tables.rs index fa152f5..2afce3f 100644 --- a/server/src/domain/sql_tables.rs +++ b/server/src/domain/sql_tables.rs @@ -21,7 +21,7 @@ impl From for Value { } } -pub const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(3); +const LAST_SCHEMA_VERSION: SchemaVersion = SchemaVersion(4); pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> { let version = { @@ -32,7 +32,7 @@ pub async fn init_table(pool: &DbConnection) -> anyhow::Result<()> { SchemaVersion(1) } }; - migrate_from_version(pool, version).await?; + migrate_from_version(pool, version, LAST_SCHEMA_VERSION).await?; Ok(()) } @@ -100,21 +100,21 @@ mod tests { let sql_pool = get_in_memory_db().await; sql_pool .execute(raw_statement( - r#"CREATE TABLE users ( user_id TEXT, display_name TEXT, first_name TEXT NOT NULL, last_name TEXT, avatar BLOB, creation_date TEXT);"#, + r#"CREATE TABLE users ( user_id TEXT, display_name TEXT, first_name TEXT NOT NULL, last_name TEXT, avatar BLOB, creation_date TEXT, email TEXT);"#, )) .await .unwrap(); sql_pool .execute(raw_statement( - r#"INSERT INTO users (user_id, display_name, first_name, creation_date) - VALUES ("bôb", "", "", "1970-01-01 00:00:00")"#, + r#"INSERT INTO users (user_id, display_name, first_name, creation_date, email) + VALUES ("bôb", "", "", "1970-01-01 00:00:00", "bob@bob.com")"#, )) .await .unwrap(); sql_pool .execute(raw_statement( - r#"INSERT INTO users (user_id, display_name, first_name, creation_date) - VALUES ("john", "John Doe", "John", "1971-01-01 00:00:00")"#, + r#"INSERT INTO users (user_id, display_name, first_name, creation_date, email) + VALUES ("john", "John Doe", "John", "1971-01-01 00:00:00", "bob2@bob.com")"#, )) .await .unwrap(); @@ -206,6 +206,65 @@ mod tests { ); } + #[tokio::test] + async fn test_migration_to_v4() { + let sql_pool = get_in_memory_db().await; + upgrade_to_v1(&sql_pool).await.unwrap(); + migrate_from_version(&sql_pool, SchemaVersion(1), SchemaVersion(3)) + .await + .unwrap(); + sql_pool + .execute(raw_statement( + r#"INSERT INTO users (user_id, email, display_name, first_name, creation_date, uuid) + VALUES ("bob", "bob@bob.com", "", "", "1970-01-01 00:00:00", "a02eaf13-48a7-30f6-a3d4-040ff7c52b04")"#, + )) + .await + .unwrap(); + sql_pool + .execute(raw_statement( + r#"INSERT INTO users (user_id, email, display_name, first_name, creation_date, uuid) + VALUES ("bob2", "bob@bob.com", "", "", "1970-01-01 00:00:00", "986765a5-3f03-389e-b47b-536b2d6e1bec")"#, + )) + .await + .unwrap(); + migrate_from_version(&sql_pool, SchemaVersion(3), SchemaVersion(4)) + .await + .expect_err("migration should fail"); + assert_eq!( + sql_migrations::JustSchemaVersion::find_by_statement(raw_statement( + r#"SELECT version FROM metadata"# + )) + .one(&sql_pool) + .await + .unwrap() + .unwrap(), + sql_migrations::JustSchemaVersion { + version: SchemaVersion(3) + } + ); + sql_pool + .execute(raw_statement( + r#"UPDATE users SET email = "new@bob.com" WHERE user_id = "bob2""#, + )) + .await + .unwrap(); + migrate_from_version(&sql_pool, SchemaVersion(3), SchemaVersion(4)) + .await + .unwrap(); + assert_eq!( + sql_migrations::JustSchemaVersion::find_by_statement(raw_statement( + r#"SELECT version FROM metadata"# + )) + .one(&sql_pool) + .await + .unwrap() + .unwrap(), + sql_migrations::JustSchemaVersion { + version: SchemaVersion(4) + } + ); + } + #[tokio::test] async fn test_too_high_version() { let sql_pool = get_in_memory_db().await;