vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 0365b7c6a4d8aa88fd9328fcc14beef300fe33a2
parent 19889187a5d3f48cbe5ad7ec3a0c4d0bcdb7b894
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Tue, 18 Aug 2020 17:15:44 +0200

Add support for multiple simultaneous database features by using macros.
Diesel requires the following changes:
- Separate connection and pool types per connection, the generate_connections! macro generates an enum with a variant per db type
- Separate migrations and schemas, these were always imported as one type depending on db feature, now they are all imported under different module names
- Separate model objects per connection, the db_object! macro generates one object for each connection with the diesel macros, a generic object, and methods to convert between the connection-specific and the generic ones
- Separate connection queries, the db_run! macro allows writing only one that gets compiled for all databases or multiple ones

Diffstat:
MCargo.lock | 33+++++++++++++++++++++++----------
MCargo.toml | 3+++
Mbuild.rs | 15++++++++-------
Msrc/api/admin.rs | 10+++++++---
Msrc/config.rs | 16+++-------------
Msrc/db/mod.rs | 305+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----------
Msrc/db/models/attachment.rs | 176+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/db/models/cipher.rs | 430++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/db/models/collection.rs | 468+++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/db/models/device.rs | 117+++++++++++++++++++++++++++++++++++++++++++-------------------------------------
Msrc/db/models/folder.rs | 199++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/db/models/org_policy.rs | 162++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/db/models/organization.rs | 316+++++++++++++++++++++++++++++++++++++++++++------------------------------------
Msrc/db/models/two_factor.rs | 114++++++++++++++++++++++++++++++++++++++++++++-----------------------------------
Msrc/db/models/user.rs | 254++++++++++++++++++++++++++++++++++++++++---------------------------------------
Msrc/error.rs | 2++
Msrc/main.rs | 82+++++++++++--------------------------------------------------------------------
17 files changed, 1530 insertions(+), 1172 deletions(-)

diff --git a/Cargo.lock b/Cargo.lock @@ -152,6 +152,7 @@ dependencies = [ "oath", "once_cell", "openssl", + "paste", "percent-encoding 2.1.0", "rand 0.7.3", "regex", @@ -274,9 +275,9 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.13" +version = "0.4.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c74d84029116787153e02106bf53e66828452a4b325cc8652b788b5967c0a0b6" +checksum = "942f72db697d8767c22d46a598e01f2d3b475501ea43d0db4f16d90259182d0b" dependencies = [ "num-integer", "num-traits", @@ -295,9 +296,9 @@ dependencies = [ [[package]] name = "clap" -version = "2.33.2" +version = "2.33.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "10040cdf04294b565d9e0319955430099ec3813a64c952b86a41200ad714ae48" +checksum = "37e58ac78573c40708d45522f0d80fa2f01cc4f9b4e2bf749807255454312002" dependencies = [ "ansi_term", "atty", @@ -781,14 +782,14 @@ dependencies = [ [[package]] name = "handlebars" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86dbc8a0746b08f363d2e00da48e6c9ceb75c198ac692d2715fcbb5bee74c87d" +checksum = "5deefd4816fb852b1ff3cb48f6c41da67be2d0e1d20b26a7a3b076da11f064b1" dependencies = [ "log 0.4.11", "pest", "pest_derive", - "quick-error", + "quick-error 2.0.0", "serde", "serde_json", "walkdir", @@ -1360,7 +1361,7 @@ dependencies = [ "log 0.4.11", "mime 0.3.16", "mime_guess", - "quick-error", + "quick-error 1.2.3", "rand 0.6.5", "safemem", "tempfile", @@ -1519,9 +1520,9 @@ checksum = "1ab52be62400ca80aa00285d25253d7f7c437b7375c4de678f5405d3afe82ca5" [[package]] name = "once_cell" -version = "1.4.0" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b631f7e854af39a1739f401cf34a8a013dfe09eac4fa4dba91e9768bd28168d" +checksum = "260e51e7efe62b592207e9e13a68e43692a7a279171d6ba57abd208bf23645ad" [[package]] name = "opaque-debug" @@ -1629,6 +1630,12 @@ dependencies = [ ] [[package]] +name = "paste" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ddc8e145de01d9180ac7b78b9676f95a9c2447f6a88b2c2a04702211bc5d71" + +[[package]] name = "pear" version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" @@ -1874,6 +1881,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" [[package]] +name = "quick-error" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ac73b1112776fc109b2e61909bc46c7e1bf0d7f690ffb1676553acce16d5cda" + +[[package]] name = "quote" version = "0.6.13" source = "registry+https://github.com/rust-lang/crates.io-index" diff --git a/Cargo.toml b/Cargo.toml @@ -123,6 +123,9 @@ structopt = "0.3.16" # Logging panics to logfile instead stderr only backtrace = "0.3.50" +# Macro ident concatenation +paste = "1.0" + [patch.crates-io] # Use newest ring rocket = { git = 'https://github.com/SergioBenitez/Rocket', rev = '1010f6a2a88fac899dec0cd2f642156908038a53' } diff --git a/build.rs b/build.rs @@ -1,13 +1,14 @@ use std::process::Command; use std::env; -fn main() { - #[cfg(all(feature = "sqlite", feature = "mysql"))] - compile_error!("Can't enable both sqlite and mysql at the same time"); - #[cfg(all(feature = "sqlite", feature = "postgresql"))] - compile_error!("Can't enable both sqlite and postgresql at the same time"); - #[cfg(all(feature = "mysql", feature = "postgresql"))] - compile_error!("Can't enable both mysql and postgresql at the same time"); +fn main() { + // This allow using #[cfg(sqlite)] instead of #[cfg(feature = "sqlite")], which helps when trying to add them through macros + #[cfg(feature = "sqlite")] + println!("cargo:rustc-cfg=sqlite"); + #[cfg(feature = "mysql")] + println!("cargo:rustc-cfg=mysql"); + #[cfg(feature = "postgresql")] + println!("cargo:rustc-cfg=postgresql"); #[cfg(not(any(feature = "sqlite", feature = "mysql", feature = "postgresql")))] compile_error!("You need to enable one DB backend. To build with previous defaults do: cargo build --features sqlite"); diff --git a/src/api/admin.rs b/src/api/admin.rs @@ -15,7 +15,7 @@ use crate::{ api::{ApiResult, EmptyResult, JsonResult}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, config::ConfigBuilder, - db::{backup_database, models::*, DbConn}, + db::{backup_database, models::*, DbConn, DbConnType}, error::{Error, MapResult}, mail, util::get_display_size, @@ -48,8 +48,12 @@ pub fn routes() -> Vec<Route> { ] } -static CAN_BACKUP: Lazy<bool> = - Lazy::new(|| cfg!(feature = "sqlite") && Command::new("sqlite3").arg("-version").status().is_ok()); +static CAN_BACKUP: Lazy<bool> = Lazy::new(|| { + DbConnType::from_url(&CONFIG.database_url()) + .map(|t| t == DbConnType::sqlite) + .unwrap_or(false) + && Command::new("sqlite3").arg("-version").status().is_ok() +}); #[get("/")] fn admin_disabled() -> &'static str { diff --git a/src/config.rs b/src/config.rs @@ -5,6 +5,7 @@ use once_cell::sync::Lazy; use reqwest::Url; use crate::{ + db::DbConnType, error::Error, util::{get_env, get_env_bool}, }; @@ -421,20 +422,9 @@ make_config! { } fn validate_config(cfg: &ConfigItems) -> Result<(), Error> { - let db_url = cfg.database_url.to_lowercase(); - if cfg!(feature = "sqlite") - && (db_url.starts_with("mysql:") || db_url.starts_with("postgresql:") || db_url.starts_with("postgres:")) - { - err!("`DATABASE_URL` is meant for MySQL or Postgres, while this server is meant for SQLite") - } - if cfg!(feature = "mysql") && !db_url.starts_with("mysql:") { - err!("`DATABASE_URL` should start with mysql: when using the MySQL server") - } - - if cfg!(feature = "postgresql") && !(db_url.starts_with("postgresql:") || db_url.starts_with("postgres:")) { - err!("`DATABASE_URL` should start with postgresql: when using the PostgreSQL server") - } + // Validate connection URL is valid and DB feature is enabled + DbConnType::from_url(&cfg.database_url)?; let dom = cfg.domain.to_lowercase(); if !dom.starts_with("http://") && !dom.starts_with("https://") { diff --git a/src/db/mod.rs b/src/db/mod.rs @@ -1,51 +1,203 @@ use std::process::Command; use chrono::prelude::*; -use diesel::{r2d2, r2d2::ConnectionManager, Connection as DieselConnection, ConnectionError}; +use diesel::r2d2::{ConnectionManager, Pool, PooledConnection}; use rocket::{ http::Status, request::{FromRequest, Outcome}, Request, State, }; -use crate::{error::Error, CONFIG}; - -/// An alias to the database connection used -#[cfg(feature = "sqlite")] -type Connection = diesel::sqlite::SqliteConnection; -#[cfg(feature = "mysql")] -type Connection = diesel::mysql::MysqlConnection; -#[cfg(feature = "postgresql")] -type Connection = diesel::pg::PgConnection; - -/// An alias to the type for a pool of Diesel connections. -type Pool = r2d2::Pool<ConnectionManager<Connection>>; - -/// Connection request guard type: a wrapper around an r2d2 pooled connection. -pub struct DbConn(pub r2d2::PooledConnection<ConnectionManager<Connection>>); +use crate::{ + error::{Error, MapResult}, + CONFIG, +}; -pub mod models; -#[cfg(feature = "sqlite")] +#[cfg(sqlite)] #[path = "schemas/sqlite/schema.rs"] -pub mod schema; -#[cfg(feature = "mysql")] +pub mod __sqlite_schema; + +#[cfg(mysql)] #[path = "schemas/mysql/schema.rs"] -pub mod schema; -#[cfg(feature = "postgresql")] +pub mod __mysql_schema; + +#[cfg(postgresql)] #[path = "schemas/postgresql/schema.rs"] -pub mod schema; +pub mod __postgresql_schema; + + +// This is used to generate the main DbConn and DbPool enums, which contain one variant for each database supported +macro_rules! generate_connections { + ( $( $name:ident: $ty:ty ),+ ) => { + #[allow(non_camel_case_types, dead_code)] + #[derive(Eq, PartialEq)] + pub enum DbConnType { $( $name, )+ } + + #[allow(non_camel_case_types)] + pub enum DbConn { $( #[cfg($name)] $name(PooledConnection<ConnectionManager< $ty >>), )+ } + + #[allow(non_camel_case_types)] + pub enum DbPool { $( #[cfg($name)] $name(Pool<ConnectionManager< $ty >>), )+ } + + impl DbPool { + // For the given database URL, guess it's type, run migrations create pool and return it + pub fn from_config() -> Result<Self, Error> { + let url = CONFIG.database_url(); + let conn_type = DbConnType::from_url(&url)?; + + match conn_type { $( + DbConnType::$name => { + #[cfg($name)] + { + paste::paste!{ [< $name _migrations >]::run_migrations(); } + let manager = ConnectionManager::new(&url); + let pool = Pool::builder().build(manager).map_res("Failed to create pool")?; + return Ok(Self::$name(pool)); + } + #[cfg(not($name))] + #[allow(unreachable_code)] + return unreachable!("Trying to use a DB backend when it's feature is disabled"); + }, + )+ } + } + // Get a connection from the pool + pub fn get(&self) -> Result<DbConn, Error> { + match self { $( + #[cfg($name)] + Self::$name(p) => Ok(DbConn::$name(p.get().map_res("Error retrieving connection from pool")?)), + )+ } + } + } + }; +} -/// Initializes a database pool. -pub fn init_pool() -> Pool { - let manager = ConnectionManager::new(CONFIG.database_url()); +generate_connections! { + sqlite: diesel::sqlite::SqliteConnection, + mysql: diesel::mysql::MysqlConnection, + postgresql: diesel::pg::PgConnection +} + +impl DbConnType { + pub fn from_url(url: &str) -> Result<DbConnType, Error> { + // Mysql + if url.starts_with("mysql:") { + #[cfg(mysql)] + return Ok(DbConnType::mysql); + + #[cfg(not(mysql))] + err!("`DATABASE_URL` is a MySQL URL, but the 'mysql' feature is not enabled") + + // Postgres + } else if url.starts_with("postgresql:") || url.starts_with("postgres:") { + #[cfg(postgresql)] + return Ok(DbConnType::postgresql); - r2d2::Pool::builder().build(manager).expect("Failed to create pool") + #[cfg(not(postgresql))] + err!("`DATABASE_URL` is a PostgreSQL URL, but the 'postgresql' feature is not enabled") + + //Sqlite + } else { + #[cfg(sqlite)] + return Ok(DbConnType::sqlite); + + #[cfg(not(sqlite))] + err!("`DATABASE_URL` looks like a SQLite URL, but 'sqlite' feature is not enabled") + } + } } -pub fn get_connection() -> Result<Connection, ConnectionError> { - Connection::establish(&CONFIG.database_url()) + +#[macro_export] +macro_rules! db_run { + // Same for all dbs + ( $conn:ident: $body:block ) => { + db_run! { $conn: sqlite, mysql, postgresql $body } + }; + + // Different code for each db + ( $conn:ident: $( $($db:ident),+ $body:block )+ ) => { + #[allow(unused)] use diesel::prelude::*; + match $conn { + $($( + #[cfg($db)] + crate::db::DbConn::$db(ref $conn) => { + paste::paste! { + #[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *}; + #[allow(unused)] use [<__ $db _model>]::*; + #[allow(unused)] use crate::db::FromDb; + } + $body + }, + )+)+ + } + }; } + +pub trait FromDb { + type Output; + fn from_db(self) -> Self::Output; +} + +// For each struct eg. Cipher, we create a CipherDb inside a module named __$db_model (where $db is sqlite, mysql or postgresql), +// to implement the Diesel traits. We also provide methods to convert between them and the basic structs. Later, that module will be auto imported when using db_run! +#[macro_export] +macro_rules! db_object { + ( $( + $( #[$attr:meta] )* + pub struct $name:ident { + $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty ),+ + $(,)? + } + )+ ) => { + // Create the normal struct, without attributes + $( pub struct $name { $( /*$( #[$field_attr] )**/ $vis $field : $typ, )+ } )+ + + #[cfg(sqlite)] + pub mod __sqlite_model { $( db_object! { @db sqlite | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ } + #[cfg(mysql)] + pub mod __mysql_model { $( db_object! { @db mysql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ } + #[cfg(postgresql)] + pub mod __postgresql_model { $( db_object! { @db postgresql | $( #[$attr] )* | $name | $( $( #[$field_attr] )* $field : $typ ),+ } )+ } + }; + + ( @db $db:ident | $( #[$attr:meta] )* | $name:ident | $( $( #[$field_attr:meta] )* $vis:vis $field:ident : $typ:ty),+) => { + paste::paste! { + #[allow(unused)] use super::*; + #[allow(unused)] use diesel::prelude::*; + #[allow(unused)] use crate::db::[<__ $db _schema>]::*; + + $( #[$attr] )* + pub struct [<$name Db>] { $( + $( #[$field_attr] )* $vis $field : $typ, + )+ } + + impl [<$name Db>] { + #[inline(always)] pub fn from_db(self) -> super::$name { super::$name { $( $field: self.$field, )+ } } + #[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } } + } + + impl crate::db::FromDb for [<$name Db>] { + type Output = super::$name; + #[inline(always)] fn from_db(self) -> Self::Output { super::$name { $( $field: self.$field, )+ } } + } + + impl crate::db::FromDb for Vec<[<$name Db>]> { + type Output = Vec<super::$name>; + #[inline(always)] fn from_db(self) -> Self::Output { self.into_iter().map(crate::db::FromDb::from_db).collect() } + } + + impl crate::db::FromDb for Option<[<$name Db>]> { + type Output = Option<super::$name>; + #[inline(always)] fn from_db(self) -> Self::Output { self.map(crate::db::FromDb::from_db) } + } + } + }; +} + +// Reexport the models, needs to be after the macros are defined so it can access them +pub mod models; + /// Creates a back-up of the database using sqlite3 pub fn backup_database() -> Result<(), Error> { use std::path::Path; @@ -73,18 +225,99 @@ impl<'a, 'r> FromRequest<'a, 'r> for DbConn { fn from_request(request: &'a Request<'r>) -> Outcome<DbConn, ()> { // https://github.com/SergioBenitez/Rocket/commit/e3c1a4ad3ab9b840482ec6de4200d30df43e357c - let pool = try_outcome!(request.guard::<State<Pool>>()); + let pool = try_outcome!(request.guard::<State<DbPool>>()); match pool.get() { - Ok(conn) => Outcome::Success(DbConn(conn)), + Ok(conn) => Outcome::Success(conn), Err(_) => Outcome::Failure((Status::ServiceUnavailable, ())), } } } -// For the convenience of using an &DbConn as a &Database. -impl std::ops::Deref for DbConn { - type Target = Connection; - fn deref(&self) -> &Self::Target { - &self.0 +// Embed the migrations from the migrations folder into the application +// This way, the program automatically migrates the database to the latest version +// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html +#[cfg(sqlite)] +mod sqlite_migrations { + #[allow(unused_imports)] + embed_migrations!("migrations/sqlite"); + + pub fn run_migrations() { + // Make sure the directory exists + let url = crate::CONFIG.database_url(); + let path = std::path::Path::new(&url); + + if let Some(parent) = path.parent() { + if std::fs::create_dir_all(parent).is_err() { + error!("Error creating database directory"); + std::process::exit(1); + } + } + + use diesel::{Connection, RunQueryDsl}; + // Make sure the database is up to date (create if it doesn't exist, or run the migrations) + let connection = + diesel::sqlite::SqliteConnection::establish(&crate::CONFIG.database_url()).expect("Can't connect to DB"); + // Disable Foreign Key Checks during migration + + // Scoped to a connection. + diesel::sql_query("PRAGMA foreign_keys = OFF") + .execute(&connection) + .expect("Failed to disable Foreign Key Checks during migrations"); + + // Turn on WAL in SQLite + if crate::CONFIG.enable_db_wal() { + diesel::sql_query("PRAGMA journal_mode=wal") + .execute(&connection) + .expect("Failed to turn on WAL"); + } + + embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).expect("Can't run migrations"); + } +} + +#[cfg(mysql)] +mod mysql_migrations { + #[allow(unused_imports)] + embed_migrations!("migrations/mysql"); + + pub fn run_migrations() { + use diesel::{Connection, RunQueryDsl}; + // Make sure the database is up to date (create if it doesn't exist, or run the migrations) + let connection = + diesel::mysql::MysqlConnection::establish(&crate::CONFIG.database_url()).expect("Can't connect to DB"); + // Disable Foreign Key Checks during migration + + // Scoped to a connection/session. + diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0") + .execute(&connection) + .expect("Failed to disable Foreign Key Checks during migrations"); + + embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).expect("Can't run migrations"); + } +} + +#[cfg(postgresql)] +mod postgresql_migrations { + #[allow(unused_imports)] + embed_migrations!("migrations/postgresql"); + + pub fn run_migrations() { + use diesel::{Connection, RunQueryDsl}; + // Make sure the database is up to date (create if it doesn't exist, or run the migrations) + let connection = + diesel::pg::PgConnection::establish(&crate::CONFIG.database_url()).expect("Can't connect to DB"); + // Disable Foreign Key Checks during migration + + // FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html, + // "SET CONSTRAINTS sets the behavior of constraint checking within the + // current transaction", so this setting probably won't take effect for + // any of the migrations since it's being run outside of a transaction. + // Migrations that need to disable foreign key checks should run this + // from within the migration script itself. + diesel::sql_query("SET CONSTRAINTS ALL DEFERRED") + .execute(&connection) + .expect("Failed to disable Foreign Key Checks during migrations"); + + embedded_migrations::run_with_output(&connection, &mut std::io::stdout()).expect("Can't run migrations"); } } diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs @@ -3,17 +3,19 @@ use serde_json::Value; use super::Cipher; use crate::CONFIG; -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "attachments"] -#[changeset_options(treat_none_as_null="true")] -#[belongs_to(Cipher, foreign_key = "cipher_uuid")] -#[primary_key(id)] -pub struct Attachment { - pub id: String, - pub cipher_uuid: String, - pub file_name: String, - pub file_size: i32, - pub akey: Option<String>, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "attachments"] + #[changeset_options(treat_none_as_null="true")] + #[belongs_to(super::Cipher, foreign_key = "cipher_uuid")] + #[primary_key(id)] + pub struct Attachment { + pub id: String, + pub cipher_uuid: String, + pub file_name: String, + pub file_size: i32, + pub akey: Option<String>, + } } /// Local methods @@ -50,43 +52,46 @@ impl Attachment { } } -use crate::db::schema::{attachments, ciphers}; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; /// Database methods impl Attachment { - #[cfg(feature = "postgresql")] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - diesel::insert_into(attachments::table) - .values(self) - .on_conflict(attachments::id) - .do_update() - .set(self) - .execute(&**conn) - .map_res("Error saving attachment") - } - #[cfg(not(feature = "postgresql"))] pub fn save(&self, conn: &DbConn) -> EmptyResult { - diesel::replace_into(attachments::table) - .values(self) - .execute(&**conn) - .map_res("Error saving attachment") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(attachments::table) + .values(AttachmentDb::to_db(self)) + .execute(conn) + .map_res("Error saving attachment") + } + postgresql { + let value = AttachmentDb::to_db(self); + diesel::insert_into(attachments::table) + .values(&value) + .on_conflict(attachments::id) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving attachment") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { - crate::util::retry( - || diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(&**conn), - 10, - ) - .map_res("Error deleting attachment")?; - - crate::util::delete_file(&self.get_file_path())?; - Ok(()) + db_run! { conn: { + crate::util::retry( + || diesel::delete(attachments::table.filter(attachments::id.eq(&self.id))).execute(conn), + 10, + ) + .map_res("Error deleting attachment")?; + + crate::util::delete_file(&self.get_file_path())?; + Ok(()) + }} } pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -97,67 +102,78 @@ impl Attachment { } pub fn find_by_id(id: &str, conn: &DbConn) -> Option<Self> { - let id = id.to_lowercase(); - - attachments::table - .filter(attachments::id.eq(id)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + attachments::table + .filter(attachments::id.eq(id.to_lowercase())) + .first::<AttachmentDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_cipher(cipher_uuid: &str, conn: &DbConn) -> Vec<Self> { - attachments::table - .filter(attachments::cipher_uuid.eq(cipher_uuid)) - .load::<Self>(&**conn) - .expect("Error loading attachments") + db_run! { conn: { + attachments::table + .filter(attachments::cipher_uuid.eq(cipher_uuid)) + .load::<AttachmentDb>(conn) + .expect("Error loading attachments") + .from_db() + }} } pub fn find_by_ciphers(cipher_uuids: Vec<String>, conn: &DbConn) -> Vec<Self> { - attachments::table - .filter(attachments::cipher_uuid.eq_any(cipher_uuids)) - .load::<Self>(&**conn) - .expect("Error loading attachments") + db_run! { conn: { + attachments::table + .filter(attachments::cipher_uuid.eq_any(cipher_uuids)) + .load::<AttachmentDb>(conn) + .expect("Error loading attachments") + .from_db() + }} } pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 { - let result: Option<i64> = attachments::table - .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) - .filter(ciphers::user_uuid.eq(user_uuid)) - .select(diesel::dsl::sum(attachments::file_size)) - .first(&**conn) - .expect("Error loading user attachment total size"); - - result.unwrap_or(0) + db_run! { conn: { + let result: Option<i64> = attachments::table + .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) + .filter(ciphers::user_uuid.eq(user_uuid)) + .select(diesel::dsl::sum(attachments::file_size)) + .first(conn) + .expect("Error loading user attachment total size"); + result.unwrap_or(0) + }} } pub fn count_by_user(user_uuid: &str, conn: &DbConn) -> i64 { - attachments::table - .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) - .filter(ciphers::user_uuid.eq(user_uuid)) - .count() - .first::<i64>(&**conn) - .ok() - .unwrap_or(0) + db_run! { conn: { + attachments::table + .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) + .filter(ciphers::user_uuid.eq(user_uuid)) + .count() + .first(conn) + .unwrap_or(0) + }} } pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 { - let result: Option<i64> = attachments::table - .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) - .filter(ciphers::organization_uuid.eq(org_uuid)) - .select(diesel::dsl::sum(attachments::file_size)) - .first(&**conn) - .expect("Error loading user attachment total size"); - - result.unwrap_or(0) + db_run! { conn: { + let result: Option<i64> = attachments::table + .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) + .filter(ciphers::organization_uuid.eq(org_uuid)) + .select(diesel::dsl::sum(attachments::file_size)) + .first(conn) + .expect("Error loading user attachment total size"); + result.unwrap_or(0) + }} } pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { - attachments::table - .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) - .filter(ciphers::organization_uuid.eq(org_uuid)) - .count() - .first(&**conn) - .ok() - .unwrap_or(0) + db_run! { conn: { + attachments::table + .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) + .filter(ciphers::organization_uuid.eq(org_uuid)) + .count() + .first(conn) + .unwrap_or(0) + }} } } diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs @@ -5,35 +5,37 @@ use super::{ Attachment, CollectionCipher, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization, }; -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "ciphers"] -#[changeset_options(treat_none_as_null="true")] -#[belongs_to(User, foreign_key = "user_uuid")] -#[belongs_to(Organization, foreign_key = "organization_uuid")] -#[primary_key(uuid)] -pub struct Cipher { - pub uuid: String, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - - pub user_uuid: Option<String>, - pub organization_uuid: Option<String>, - - /* - Login = 1, - SecureNote = 2, - Card = 3, - Identity = 4 - */ - pub atype: i32, - pub name: String, - pub notes: Option<String>, - pub fields: Option<String>, - - pub data: String, - - pub password_history: Option<String>, - pub deleted_at: Option<NaiveDateTime>, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "ciphers"] + #[changeset_options(treat_none_as_null="true")] + #[belongs_to(User, foreign_key = "user_uuid")] + #[belongs_to(Organization, foreign_key = "organization_uuid")] + #[primary_key(uuid)] + pub struct Cipher { + pub uuid: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + + pub user_uuid: Option<String>, + pub organization_uuid: Option<String>, + + /* + Login = 1, + SecureNote = 2, + Card = 3, + Identity = 4 + */ + pub atype: i32, + pub name: String, + pub notes: Option<String>, + pub fields: Option<String>, + + pub data: String, + + pub password_history: Option<String>, + pub deleted_at: Option<NaiveDateTime>, + } } /// Local methods @@ -62,9 +64,7 @@ impl Cipher { } } -use crate::db::schema::*; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; @@ -81,7 +81,7 @@ impl Cipher { let password_history_json = self.password_history.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); let (read_only, hide_passwords) = - match self.get_access_restrictions(&user_uuid, &conn) { + match self.get_access_restrictions(&user_uuid, conn) { Some((ro, hp)) => (ro, hp), None => { error!("Cipher ownership assertion failure"); @@ -125,14 +125,14 @@ impl Cipher { "Type": self.atype, "RevisionDate": format_date(&self.updated_at), "DeletedDate": self.deleted_at.map_or(Value::Null, |d| Value::String(format_date(&d))), - "FolderId": self.get_folder_uuid(&user_uuid, &conn), - "Favorite": self.is_favorite(&user_uuid, &conn), + "FolderId": self.get_folder_uuid(&user_uuid, conn), + "Favorite": self.is_favorite(&user_uuid, conn), "OrganizationId": self.organization_uuid, "Attachments": attachments_json, "OrganizationUseTotp": true, // This field is specific to the cipherDetails type. - "CollectionIds": self.get_collections(user_uuid, &conn), + "CollectionIds": self.get_collections(user_uuid, conn), "Name": self.name, "Notes": self.notes, @@ -183,41 +183,42 @@ impl Cipher { user_uuids } - #[cfg(feature = "postgresql")] - pub fn save(&mut self, conn: &DbConn) -> EmptyResult { - self.update_users_revision(conn); - self.updated_at = Utc::now().naive_utc(); - - diesel::insert_into(ciphers::table) - .values(&*self) - .on_conflict(ciphers::uuid) - .do_update() - .set(&*self) - .execute(&**conn) - .map_res("Error saving cipher") - } - - #[cfg(not(feature = "postgresql"))] pub fn save(&mut self, conn: &DbConn) -> EmptyResult { self.update_users_revision(conn); self.updated_at = Utc::now().naive_utc(); - - diesel::replace_into(ciphers::table) - .values(&*self) - .execute(&**conn) - .map_res("Error saving cipher") + + db_run! { conn: + sqlite, mysql { + diesel::replace_into(ciphers::table) + .values(CipherDb::to_db(self)) + .execute(conn) + .map_res("Error saving cipher") + } + postgresql { + let value = CipherDb::to_db(self); + diesel::insert_into(ciphers::table) + .values(&value) + .on_conflict(ciphers::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving cipher") + } + } } pub fn delete(&self, conn: &DbConn) -> EmptyResult { self.update_users_revision(conn); - FolderCipher::delete_all_by_cipher(&self.uuid, &conn)?; - CollectionCipher::delete_all_by_cipher(&self.uuid, &conn)?; - Attachment::delete_all_by_cipher(&self.uuid, &conn)?; + FolderCipher::delete_all_by_cipher(&self.uuid, conn)?; + CollectionCipher::delete_all_by_cipher(&self.uuid, conn)?; + Attachment::delete_all_by_cipher(&self.uuid, conn)?; - diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid))) - .execute(&**conn) - .map_res("Error deleting cipher") + db_run! { conn: { + diesel::delete(ciphers::table.filter(ciphers::uuid.eq(&self.uuid))) + .execute(conn) + .map_res("Error deleting cipher") + }} } pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -235,28 +236,28 @@ impl Cipher { } pub fn move_to_folder(&self, folder_uuid: Option<String>, user_uuid: &str, conn: &DbConn) -> EmptyResult { - User::update_uuid_revision(user_uuid, &conn); + User::update_uuid_revision(user_uuid, conn); - match (self.get_folder_uuid(&user_uuid, &conn), folder_uuid) { + match (self.get_folder_uuid(&user_uuid, conn), folder_uuid) { // No changes (None, None) => Ok(()), (Some(ref old), Some(ref new)) if old == new => Ok(()), // Add to folder - (None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(&conn), + (None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn), // Remove from folder - (Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) { - Some(old) => old.delete(&conn), + (Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn) { + Some(old) => old.delete(conn), None => err!("Couldn't move from previous folder"), }, // Move to another folder (Some(old), Some(new)) => { - if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, &conn) { - old.delete(&conn)?; + if let Some(old) = FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn) { + old.delete(conn)?; } - FolderCipher::new(&new, &self.uuid).save(&conn) + FolderCipher::new(&new, &self.uuid).save(conn) } } } @@ -269,7 +270,7 @@ impl Cipher { /// Returns whether this cipher is owned by an org in which the user has full access. pub fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool { if let Some(ref org_uuid) = self.organization_uuid { - if let Some(user_org) = UserOrganization::find_by_user_and_org(&user_uuid, &org_uuid, &conn) { + if let Some(user_org) = UserOrganization::find_by_user_and_org(&user_uuid, &org_uuid, conn) { return user_org.has_full_access(); } } @@ -290,38 +291,40 @@ impl Cipher { return Some((false, false)); } - // Check whether this cipher is in any collections accessible to the - // user. If so, retrieve the access flags for each collection. - let query = ciphers::table - .filter(ciphers::uuid.eq(&self.uuid)) - .inner_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid))) - .inner_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) - .and(users_collections::user_uuid.eq(user_uuid)))) - .select((users_collections::read_only, users_collections::hide_passwords)); - - // There's an edge case where a cipher can be in multiple collections - // with inconsistent access flags. For example, a cipher could be in - // one collection where the user has read-only access, but also in - // another collection where the user has read/write access. To handle - // this, we do a boolean OR of all values in each of the `read_only` - // and `hide_passwords` columns. This could ideally be done as part - // of the query, but Diesel doesn't support a max() or bool_or() - // function on booleans and this behavior isn't portable anyway. - if let Some(vec) = query.load::<(bool, bool)>(&**conn).ok() { - let mut read_only = false; - let mut hide_passwords = false; - for (ro, hp) in vec.iter() { - read_only |= ro; - hide_passwords |= hp; - } + db_run! {conn: { + // Check whether this cipher is in any collections accessible to the + // user. If so, retrieve the access flags for each collection. + let query = ciphers::table + .filter(ciphers::uuid.eq(&self.uuid)) + .inner_join(ciphers_collections::table.on( + ciphers::uuid.eq(ciphers_collections::cipher_uuid))) + .inner_join(users_collections::table.on( + ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) + .and(users_collections::user_uuid.eq(user_uuid)))) + .select((users_collections::read_only, users_collections::hide_passwords)); + + // There's an edge case where a cipher can be in multiple collections + // with inconsistent access flags. For example, a cipher could be in + // one collection where the user has read-only access, but also in + // another collection where the user has read/write access. To handle + // this, we do a boolean OR of all values in each of the `read_only` + // and `hide_passwords` columns. This could ideally be done as part + // of the query, but Diesel doesn't support a max() or bool_or() + // function on booleans and this behavior isn't portable anyway. + if let Some(vec) = query.load::<(bool, bool)>(conn).ok() { + let mut read_only = false; + let mut hide_passwords = false; + for (ro, hp) in vec.iter() { + read_only |= ro; + hide_passwords |= hp; + } - Some((read_only, hide_passwords)) - } else { - // This cipher isn't in any collections accessible to the user. - None - } + Some((read_only, hide_passwords)) + } else { + // This cipher isn't in any collections accessible to the user. + None + } + }} } pub fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { @@ -337,12 +340,14 @@ impl Cipher { // Returns whether this cipher is a favorite of the specified user. pub fn is_favorite(&self, user_uuid: &str, conn: &DbConn) -> bool { - let query = favorites::table - .filter(favorites::user_uuid.eq(user_uuid)) - .filter(favorites::cipher_uuid.eq(&self.uuid)) - .count(); - - query.first::<i64>(&**conn).ok().unwrap_or(0) != 0 + db_run!{ conn: { + let query = favorites::table + .filter(favorites::user_uuid.eq(user_uuid)) + .filter(favorites::cipher_uuid.eq(&self.uuid)) + .count(); + + query.first::<i64>(conn).ok().unwrap_or(0) != 0 + }} } // Updates whether this cipher is a favorite of the specified user. @@ -356,23 +361,27 @@ impl Cipher { match (old, new) { (false, true) => { User::update_uuid_revision(user_uuid, &conn); - diesel::insert_into(favorites::table) - .values(( - favorites::user_uuid.eq(user_uuid), - favorites::cipher_uuid.eq(&self.uuid), - )) - .execute(&**conn) - .map_res("Error adding favorite") + db_run!{ conn: { + diesel::insert_into(favorites::table) + .values(( + favorites::user_uuid.eq(user_uuid), + favorites::cipher_uuid.eq(&self.uuid), + )) + .execute(conn) + .map_res("Error adding favorite") + }} } (true, false) => { User::update_uuid_revision(user_uuid, &conn); - diesel::delete( - favorites::table - .filter(favorites::user_uuid.eq(user_uuid)) - .filter(favorites::cipher_uuid.eq(&self.uuid)) - ) - .execute(&**conn) - .map_res("Error removing favorite") + db_run!{ conn: { + diesel::delete( + favorites::table + .filter(favorites::user_uuid.eq(user_uuid)) + .filter(favorites::cipher_uuid.eq(&self.uuid)) + ) + .execute(conn) + .map_res("Error removing favorite") + }} } // Otherwise, the favorite status is already what it should be. _ => Ok(()) @@ -380,112 +389,131 @@ impl Cipher { } pub fn get_folder_uuid(&self, user_uuid: &str, conn: &DbConn) -> Option<String> { - folders_ciphers::table - .inner_join(folders::table) - .filter(folders::user_uuid.eq(&user_uuid)) - .filter(folders_ciphers::cipher_uuid.eq(&self.uuid)) - .select(folders_ciphers::folder_uuid) - .first::<String>(&**conn) - .ok() + db_run! {conn: { + folders_ciphers::table + .inner_join(folders::table) + .filter(folders::user_uuid.eq(&user_uuid)) + .filter(folders_ciphers::cipher_uuid.eq(&self.uuid)) + .select(folders_ciphers::folder_uuid) + .first::<String>(conn) + .ok() + }} } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - ciphers::table - .filter(ciphers::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! {conn: { + ciphers::table + .filter(ciphers::uuid.eq(uuid)) + .first::<CipherDb>(conn) + .ok() + .from_db() + }} } // Find all ciphers accessible to user pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - ciphers::table - .left_join(users_organizations::table.on( - ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and( - users_organizations::user_uuid.eq(user_uuid).and( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) - ) - ) - )) - .left_join(ciphers_collections::table.on( - ciphers::uuid.eq(ciphers_collections::cipher_uuid) - )) - .left_join(users_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) - )) - .filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32).or( // Org admin or owner - users_collections::user_uuid.eq(user_uuid).and( // Access to Collection - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + db_run! {conn: { + ciphers::table + .left_join(users_organizations::table.on( + ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()).and( + users_organizations::user_uuid.eq(user_uuid).and( + users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + ) ) - ) - ) - )) - .select(ciphers::all_columns) - .distinct() - .load::<Self>(&**conn).expect("Error loading ciphers") + )) + .left_join(ciphers_collections::table.on( + ciphers::uuid.eq(ciphers_collections::cipher_uuid) + )) + .left_join(users_collections::table.on( + ciphers_collections::collection_uuid.eq(users_collections::collection_uuid) + )) + .filter(ciphers::user_uuid.eq(user_uuid).or( // Cipher owner + users_organizations::access_all.eq(true).or( // access_all in Organization + users_organizations::atype.le(UserOrgType::Admin as i32).or( // Org admin or owner + users_collections::user_uuid.eq(user_uuid).and( // Access to Collection + users_organizations::status.eq(UserOrgStatus::Confirmed as i32) + ) + ) + ) + )) + .select(ciphers::all_columns) + .distinct() + .load::<CipherDb>(conn).expect("Error loading ciphers").from_db() + }} } // Find all ciphers directly owned by user pub fn find_owned_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - ciphers::table - .filter(ciphers::user_uuid.eq(user_uuid)) - .load::<Self>(&**conn).expect("Error loading ciphers") + db_run! {conn: { + ciphers::table + .filter(ciphers::user_uuid.eq(user_uuid)) + .load::<CipherDb>(conn).expect("Error loading ciphers").from_db() + }} } pub fn count_owned_by_user(user_uuid: &str, conn: &DbConn) -> i64 { - ciphers::table - .filter(ciphers::user_uuid.eq(user_uuid)) - .count() - .first::<i64>(&**conn) - .ok() - .unwrap_or(0) + db_run! {conn: { + ciphers::table + .filter(ciphers::user_uuid.eq(user_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} } pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { - ciphers::table - .filter(ciphers::organization_uuid.eq(org_uuid)) - .load::<Self>(&**conn).expect("Error loading ciphers") + db_run! {conn: { + ciphers::table + .filter(ciphers::organization_uuid.eq(org_uuid)) + .load::<CipherDb>(conn).expect("Error loading ciphers").from_db() + }} } pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { - ciphers::table - .filter(ciphers::organization_uuid.eq(org_uuid)) - .count() - .first::<i64>(&**conn) - .ok() - .unwrap_or(0) + db_run! {conn: { + ciphers::table + .filter(ciphers::organization_uuid.eq(org_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} } pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> { - folders_ciphers::table.inner_join(ciphers::table) - .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) - .select(ciphers::all_columns) - .load::<Self>(&**conn).expect("Error loading ciphers") + db_run! {conn: { + folders_ciphers::table.inner_join(ciphers::table) + .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) + .select(ciphers::all_columns) + .load::<CipherDb>(conn).expect("Error loading ciphers").from_db() + }} } pub fn get_collections(&self, user_id: &str, conn: &DbConn) -> Vec<String> { - ciphers_collections::table - .inner_join(collections::table.on( - collections::uuid.eq(ciphers_collections::collection_uuid) - )) - .inner_join(users_organizations::table.on( - users_organizations::org_uuid.eq(collections::org_uuid).and( - users_organizations::user_uuid.eq(user_id) - ) - )) - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( - users_collections::user_uuid.eq(user_id) - ) - )) - .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) - .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection - users_organizations::access_all.eq(true).or( // User has access all - users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner - ) - )) - .select(ciphers_collections::collection_uuid) - .load::<String>(&**conn).unwrap_or_default() + db_run! {conn: { + ciphers_collections::table + .inner_join(collections::table.on( + collections::uuid.eq(ciphers_collections::collection_uuid) + )) + .inner_join(users_organizations::table.on( + users_organizations::org_uuid.eq(collections::org_uuid).and( + users_organizations::user_uuid.eq(user_id) + ) + )) + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(ciphers_collections::collection_uuid).and( + users_collections::user_uuid.eq(user_id) + ) + )) + .filter(ciphers_collections::cipher_uuid.eq(&self.uuid)) + .filter(users_collections::user_uuid.eq(user_id).or( // User has access to collection + users_organizations::access_all.eq(true).or( // User has access all + users_organizations::atype.le(UserOrgType::Admin as i32) // User is admin or owner + ) + )) + .select(ciphers_collections::collection_uuid) + .load::<String>(conn).unwrap_or_default() + }} } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs @@ -1,15 +1,39 @@ use serde_json::Value; -use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; - -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "collections"] -#[belongs_to(Organization, foreign_key = "org_uuid")] -#[primary_key(uuid)] -pub struct Collection { - pub uuid: String, - pub org_uuid: String, - pub name: String, +use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization, User, Cipher}; + +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "collections"] + #[belongs_to(Organization, foreign_key = "org_uuid")] + #[primary_key(uuid)] + pub struct Collection { + pub uuid: String, + pub org_uuid: String, + pub name: String, + } + + #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[table_name = "users_collections"] + #[belongs_to(User, foreign_key = "user_uuid")] + #[belongs_to(Collection, foreign_key = "collection_uuid")] + #[primary_key(user_uuid, collection_uuid)] + pub struct CollectionUser { + pub user_uuid: String, + pub collection_uuid: String, + pub read_only: bool, + pub hide_passwords: bool, + } + + #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[table_name = "ciphers_collections"] + #[belongs_to(Cipher, foreign_key = "cipher_uuid")] + #[belongs_to(Collection, foreign_key = "collection_uuid")] + #[primary_key(cipher_uuid, collection_uuid)] + pub struct CollectionCipher { + pub cipher_uuid: String, + pub collection_uuid: String, + } } /// Local methods @@ -33,36 +57,34 @@ impl Collection { } } -use crate::db::schema::*; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; /// Database methods impl Collection { - #[cfg(feature = "postgresql")] pub fn save(&self, conn: &DbConn) -> EmptyResult { self.update_users_revision(conn); - diesel::insert_into(collections::table) - .values(self) - .on_conflict(collections::uuid) - .do_update() - .set(self) - .execute(&**conn) - .map_res("Error saving collection") - } - - #[cfg(not(feature = "postgresql"))] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - self.update_users_revision(conn); - - diesel::replace_into(collections::table) - .values(self) - .execute(&**conn) - .map_res("Error saving collection") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(collections::table) + .values(CollectionDb::to_db(self)) + .execute(conn) + .map_res("Error saving collection") + } + postgresql { + let value = CollectionDb::to_db(self); + diesel::insert_into(collections::table) + .values(&value) + .on_conflict(collections::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving collection") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { @@ -70,9 +92,11 @@ impl Collection { CollectionCipher::delete_all_by_collection(&self.uuid, &conn)?; CollectionUser::delete_all_by_collection(&self.uuid, &conn)?; - diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error deleting collection") + db_run! { conn: { + diesel::delete(collections::table.filter(collections::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error deleting collection") + }} } pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -91,33 +115,38 @@ impl Collection { } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - collections::table - .filter(collections::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + collections::table + .filter(collections::uuid.eq(uuid)) + .first::<CollectionDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_user_uuid(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid) - ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) - ) - )) - .filter( - users_organizations::status.eq(UserOrgStatus::Confirmed as i32) - ) - .filter( - users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection - users_organizations::access_all.eq(true) // access_all in Organization + db_run! { conn: { + collections::table + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(collections::uuid).and( + users_collections::user_uuid.eq(user_uuid) + ) + )) + .left_join(users_organizations::table.on( + collections::org_uuid.eq(users_organizations::org_uuid).and( + users_organizations::user_uuid.eq(user_uuid) + ) + )) + .filter( + users_organizations::status.eq(UserOrgStatus::Confirmed as i32) ) - ).select(collections::all_columns) - .load::<Self>(&**conn).expect("Error loading collections") + .filter( + users_collections::user_uuid.eq(user_uuid).or( // Directly accessed collection + users_organizations::access_all.eq(true) // access_all in Organization + ) + ).select(collections::all_columns) + .load::<CollectionDb>(conn).expect("Error loading collections").from_db() + }} } pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> { @@ -128,42 +157,51 @@ impl Collection { } pub fn find_by_organization(org_uuid: &str, conn: &DbConn) -> Vec<Self> { - collections::table - .filter(collections::org_uuid.eq(org_uuid)) - .load::<Self>(&**conn) - .expect("Error loading collections") + db_run! { conn: { + collections::table + .filter(collections::org_uuid.eq(org_uuid)) + .load::<CollectionDb>(conn) + .expect("Error loading collections") + .from_db() + }} } pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { - collections::table - .filter(collections::uuid.eq(uuid)) - .filter(collections::org_uuid.eq(org_uuid)) - .select(collections::all_columns) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + collections::table + .filter(collections::uuid.eq(uuid)) + .filter(collections::org_uuid.eq(org_uuid)) + .select(collections::all_columns) + .first::<CollectionDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_uuid_and_user(uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> { - collections::table - .left_join(users_collections::table.on( - users_collections::collection_uuid.eq(collections::uuid).and( - users_collections::user_uuid.eq(user_uuid) - ) - )) - .left_join(users_organizations::table.on( - collections::org_uuid.eq(users_organizations::org_uuid).and( - users_organizations::user_uuid.eq(user_uuid) - ) - )) - .filter(collections::uuid.eq(uuid)) - .filter( - users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection - users_organizations::access_all.eq(true).or( // access_all in Organization - users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + db_run! { conn: { + collections::table + .left_join(users_collections::table.on( + users_collections::collection_uuid.eq(collections::uuid).and( + users_collections::user_uuid.eq(user_uuid) ) - ) - ).select(collections::all_columns) - .first::<Self>(&**conn).ok() + )) + .left_join(users_organizations::table.on( + collections::org_uuid.eq(users_organizations::org_uuid).and( + users_organizations::user_uuid.eq(user_uuid) + ) + )) + .filter(collections::uuid.eq(uuid)) + .filter( + users_collections::collection_uuid.eq(uuid).or( // Directly accessed collection + users_organizations::access_all.eq(true).or( // access_all in Organization + users_organizations::atype.le(UserOrgType::Admin as i32) // Org admin or owner + ) + ) + ).select(collections::all_columns) + .first::<CollectionDb>(conn).ok() + .from_db() + }} } pub fn is_writable_by_user(&self, user_uuid: &str, conn: &DbConn) -> bool { @@ -173,110 +211,108 @@ impl Collection { if user_org.access_all { true } else { - users_collections::table - .inner_join(collections::table) - .filter(users_collections::collection_uuid.eq(&self.uuid)) - .filter(users_collections::user_uuid.eq(&user_uuid)) - .filter(users_collections::read_only.eq(false)) - .select(collections::all_columns) - .first::<Self>(&**conn) - .ok() - .is_some() // Read only or no access to collection + db_run! { conn: { + users_collections::table + .inner_join(collections::table) + .filter(users_collections::collection_uuid.eq(&self.uuid)) + .filter(users_collections::user_uuid.eq(&user_uuid)) + .filter(users_collections::read_only.eq(false)) + .select(collections::all_columns) + .first::<CollectionDb>(conn) + .ok() + .is_some() // Read only or no access to collection + }} } } } } } -use super::User; - -#[derive(Debug, Identifiable, Queryable, Insertable, Associations)] -#[table_name = "users_collections"] -#[belongs_to(User, foreign_key = "user_uuid")] -#[belongs_to(Collection, foreign_key = "collection_uuid")] -#[primary_key(user_uuid, collection_uuid)] -pub struct CollectionUser { - pub user_uuid: String, - pub collection_uuid: String, - pub read_only: bool, - pub hide_passwords: bool, -} - /// Database methods impl CollectionUser { pub fn find_by_organization_and_user_uuid(org_uuid: &str, user_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_collections::table - .filter(users_collections::user_uuid.eq(user_uuid)) - .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) - .filter(collections::org_uuid.eq(org_uuid)) - .select(users_collections::all_columns) - .load::<Self>(&**conn) - .expect("Error loading users_collections") - } - - #[cfg(feature = "postgresql")] - pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult { - User::update_uuid_revision(&user_uuid, conn); - - diesel::insert_into(users_collections::table) - .values(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - )) - .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) - .do_update() - .set(( - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - )) - .execute(&**conn) - .map_res("Error adding user to collection") + db_run! { conn: { + users_collections::table + .filter(users_collections::user_uuid.eq(user_uuid)) + .inner_join(collections::table.on(collections::uuid.eq(users_collections::collection_uuid))) + .filter(collections::org_uuid.eq(org_uuid)) + .select(users_collections::all_columns) + .load::<CollectionUserDb>(conn) + .expect("Error loading users_collections") + .from_db() + }} } - #[cfg(not(feature = "postgresql"))] pub fn save(user_uuid: &str, collection_uuid: &str, read_only: bool, hide_passwords: bool, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&user_uuid, conn); - diesel::replace_into(users_collections::table) - .values(( - users_collections::user_uuid.eq(user_uuid), - users_collections::collection_uuid.eq(collection_uuid), - users_collections::read_only.eq(read_only), - users_collections::hide_passwords.eq(hide_passwords), - )) - .execute(&**conn) - .map_res("Error adding user to collection") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(users_collections::table) + .values(( + users_collections::user_uuid.eq(user_uuid), + users_collections::collection_uuid.eq(collection_uuid), + users_collections::read_only.eq(read_only), + users_collections::hide_passwords.eq(hide_passwords), + )) + .execute(conn) + .map_res("Error adding user to collection") + } + postgresql { + diesel::insert_into(users_collections::table) + .values(( + users_collections::user_uuid.eq(user_uuid), + users_collections::collection_uuid.eq(collection_uuid), + users_collections::read_only.eq(read_only), + users_collections::hide_passwords.eq(hide_passwords), + )) + .on_conflict((users_collections::user_uuid, users_collections::collection_uuid)) + .do_update() + .set(( + users_collections::read_only.eq(read_only), + users_collections::hide_passwords.eq(hide_passwords), + )) + .execute(conn) + .map_res("Error adding user to collection") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&self.user_uuid, conn); - diesel::delete( - users_collections::table - .filter(users_collections::user_uuid.eq(&self.user_uuid)) - .filter(users_collections::collection_uuid.eq(&self.collection_uuid)), - ) - .execute(&**conn) - .map_res("Error removing user from collection") + db_run! { conn: { + diesel::delete( + users_collections::table + .filter(users_collections::user_uuid.eq(&self.user_uuid)) + .filter(users_collections::collection_uuid.eq(&self.collection_uuid)), + ) + .execute(conn) + .map_res("Error removing user from collection") + }} } pub fn find_by_collection(collection_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_collections::table - .filter(users_collections::collection_uuid.eq(collection_uuid)) - .select(users_collections::all_columns) - .load::<Self>(&**conn) - .expect("Error loading users_collections") + db_run! { conn: { + users_collections::table + .filter(users_collections::collection_uuid.eq(collection_uuid)) + .select(users_collections::all_columns) + .load::<CollectionUserDb>(conn) + .expect("Error loading users_collections") + .from_db() + }} } pub fn find_by_collection_and_user(collection_uuid: &str, user_uuid: &str, conn: &DbConn) -> Option<Self> { - users_collections::table - .filter(users_collections::collection_uuid.eq(collection_uuid)) - .filter(users_collections::user_uuid.eq(user_uuid)) - .select(users_collections::all_columns) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + users_collections::table + .filter(users_collections::collection_uuid.eq(collection_uuid)) + .filter(users_collections::user_uuid.eq(user_uuid)) + .select(users_collections::all_columns) + .first::<CollectionUserDb>(conn) + .ok() + .from_db() + }} } pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -286,81 +322,81 @@ impl CollectionUser { User::update_uuid_revision(&collection.user_uuid, conn); }); - diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid))) - .execute(&**conn) - .map_res("Error deleting users from collection") + db_run! { conn: { + diesel::delete(users_collections::table.filter(users_collections::collection_uuid.eq(collection_uuid))) + .execute(conn) + .map_res("Error deleting users from collection") + }} } pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&user_uuid, conn); - diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid))) - .execute(&**conn) - .map_res("Error removing user from collections") + db_run! { conn: { + diesel::delete(users_collections::table.filter(users_collections::user_uuid.eq(user_uuid))) + .execute(conn) + .map_res("Error removing user from collections") + }} } } -use super::Cipher; - -#[derive(Debug, Identifiable, Queryable, Insertable, Associations)] -#[table_name = "ciphers_collections"] -#[belongs_to(Cipher, foreign_key = "cipher_uuid")] -#[belongs_to(Collection, foreign_key = "collection_uuid")] -#[primary_key(cipher_uuid, collection_uuid)] -pub struct CollectionCipher { - pub cipher_uuid: String, - pub collection_uuid: String, -} - /// Database methods impl CollectionCipher { - #[cfg(feature = "postgresql")] pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult { Self::update_users_revision(&collection_uuid, conn); - diesel::insert_into(ciphers_collections::table) - .values(( - ciphers_collections::cipher_uuid.eq(cipher_uuid), - ciphers_collections::collection_uuid.eq(collection_uuid), - )) - .on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid)) - .do_nothing() - .execute(&**conn) - .map_res("Error adding cipher to collection") - } - #[cfg(not(feature = "postgresql"))] - pub fn save(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult { - Self::update_users_revision(&collection_uuid, conn); - diesel::replace_into(ciphers_collections::table) - .values(( - ciphers_collections::cipher_uuid.eq(cipher_uuid), - ciphers_collections::collection_uuid.eq(collection_uuid), - )) - .execute(&**conn) - .map_res("Error adding cipher to collection") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(ciphers_collections::table) + .values(( + ciphers_collections::cipher_uuid.eq(cipher_uuid), + ciphers_collections::collection_uuid.eq(collection_uuid), + )) + .execute(conn) + .map_res("Error adding cipher to collection") + } + postgresql { + diesel::insert_into(ciphers_collections::table) + .values(( + ciphers_collections::cipher_uuid.eq(cipher_uuid), + ciphers_collections::collection_uuid.eq(collection_uuid), + )) + .on_conflict((ciphers_collections::cipher_uuid, ciphers_collections::collection_uuid)) + .do_nothing() + .execute(conn) + .map_res("Error adding cipher to collection") + } + } } pub fn delete(cipher_uuid: &str, collection_uuid: &str, conn: &DbConn) -> EmptyResult { Self::update_users_revision(&collection_uuid, conn); - diesel::delete( - ciphers_collections::table - .filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)) - .filter(ciphers_collections::collection_uuid.eq(collection_uuid)), - ) - .execute(&**conn) - .map_res("Error deleting cipher from collection") + + db_run! { conn: { + diesel::delete( + ciphers_collections::table + .filter(ciphers_collections::cipher_uuid.eq(cipher_uuid)) + .filter(ciphers_collections::collection_uuid.eq(collection_uuid)), + ) + .execute(conn) + .map_res("Error deleting cipher from collection") + }} } pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))) - .execute(&**conn) - .map_res("Error removing cipher from collections") + db_run! { conn: { + diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))) + .execute(conn) + .map_res("Error removing cipher from collections") + }} } pub fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid))) - .execute(&**conn) - .map_res("Error removing ciphers from collection") + db_run! { conn: { + diesel::delete(ciphers_collections::table.filter(ciphers_collections::collection_uuid.eq(collection_uuid))) + .execute(conn) + .map_res("Error removing ciphers from collection") + }} } pub fn update_users_revision(collection_uuid: &str, conn: &DbConn) { diff --git a/src/db/models/device.rs b/src/db/models/device.rs @@ -3,26 +3,28 @@ use chrono::{NaiveDateTime, Utc}; use super::User; use crate::CONFIG; -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "devices"] -#[changeset_options(treat_none_as_null="true")] -#[belongs_to(User, foreign_key = "user_uuid")] -#[primary_key(uuid)] -pub struct Device { - pub uuid: String, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - - pub user_uuid: String, - - pub name: String, - /// https://github.com/bitwarden/core/tree/master/src/Core/Enums - pub atype: i32, - pub push_token: Option<String>, - - pub refresh_token: String, - - pub twofactor_remember: Option<String>, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "devices"] + #[changeset_options(treat_none_as_null="true")] + #[belongs_to(User, foreign_key = "user_uuid")] + #[primary_key(uuid)] + pub struct Device { + pub uuid: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + + pub user_uuid: String, + + pub name: String, + // https://github.com/bitwarden/core/tree/master/src/Core/Enums + pub atype: i32, + pub push_token: Option<String>, + + pub refresh_token: String, + + pub twofactor_remember: Option<String>, + } } /// Local methods @@ -105,41 +107,39 @@ impl Device { } } -use crate::db::schema::devices; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; /// Database methods impl Device { - #[cfg(feature = "postgresql")] pub fn save(&mut self, conn: &DbConn) -> EmptyResult { self.updated_at = Utc::now().naive_utc(); - crate::util::retry( - || diesel::insert_into(devices::table).values(&*self).on_conflict(devices::uuid).do_update().set(&*self).execute(&**conn), - 10, - ) - .map_res("Error saving device") - } - - #[cfg(not(feature = "postgresql"))] - pub fn save(&mut self, conn: &DbConn) -> EmptyResult { - self.updated_at = Utc::now().naive_utc(); - - crate::util::retry( - || diesel::replace_into(devices::table).values(&*self).execute(&**conn), - 10, - ) - .map_res("Error saving device") + db_run! { conn: + sqlite, mysql { + crate::util::retry( + || diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn), + 10, + ).map_res("Error saving device") + } + postgresql { + let value = DeviceDb::to_db(self); + crate::util::retry( + || diesel::insert_into(devices::table).values(&value).on_conflict(devices::uuid).do_update().set(&value).execute(conn), + 10, + ).map_res("Error saving device") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { - diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error removing device") + db_run! { conn: { + diesel::delete(devices::table.filter(devices::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error removing device") + }} } pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -150,23 +150,32 @@ impl Device { } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - devices::table - .filter(devices::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + devices::table + .filter(devices::uuid.eq(uuid)) + .first::<DeviceDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_refresh_token(refresh_token: &str, conn: &DbConn) -> Option<Self> { - devices::table - .filter(devices::refresh_token.eq(refresh_token)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + devices::table + .filter(devices::refresh_token.eq(refresh_token)) + .first::<DeviceDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - devices::table - .filter(devices::user_uuid.eq(user_uuid)) - .load::<Self>(&**conn) - .expect("Error loading devices") + db_run! { conn: { + devices::table + .filter(devices::user_uuid.eq(user_uuid)) + .load::<DeviceDb>(conn) + .expect("Error loading devices") + .from_db() + }} } } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs @@ -3,26 +3,28 @@ use serde_json::Value; use super::{Cipher, User}; -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "folders"] -#[belongs_to(User, foreign_key = "user_uuid")] -#[primary_key(uuid)] -pub struct Folder { - pub uuid: String, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - pub user_uuid: String, - pub name: String, -} - -#[derive(Debug, Identifiable, Queryable, Insertable, Associations)] -#[table_name = "folders_ciphers"] -#[belongs_to(Cipher, foreign_key = "cipher_uuid")] -#[belongs_to(Folder, foreign_key = "folder_uuid")] -#[primary_key(cipher_uuid, folder_uuid)] -pub struct FolderCipher { - pub cipher_uuid: String, - pub folder_uuid: String, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "folders"] + #[belongs_to(User, foreign_key = "user_uuid")] + #[primary_key(uuid)] + pub struct Folder { + pub uuid: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub user_uuid: String, + pub name: String, + } + + #[derive(Debug, Identifiable, Queryable, Insertable, Associations)] + #[table_name = "folders_ciphers"] + #[belongs_to(Cipher, foreign_key = "cipher_uuid")] + #[belongs_to(Folder, foreign_key = "folder_uuid")] + #[primary_key(cipher_uuid, folder_uuid)] + pub struct FolderCipher { + pub cipher_uuid: String, + pub folder_uuid: String, + } } /// Local methods @@ -61,47 +63,47 @@ impl FolderCipher { } } -use crate::db::schema::{folders, folders_ciphers}; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; /// Database methods impl Folder { - #[cfg(feature = "postgresql")] - pub fn save(&mut self, conn: &DbConn) -> EmptyResult { - User::update_uuid_revision(&self.user_uuid, conn); - self.updated_at = Utc::now().naive_utc(); - - diesel::insert_into(folders::table) - .values(&*self) - .on_conflict(folders::uuid) - .do_update() - .set(&*self) - .execute(&**conn) - .map_res("Error saving folder") - } - - #[cfg(not(feature = "postgresql"))] pub fn save(&mut self, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&self.user_uuid, conn); self.updated_at = Utc::now().naive_utc(); - diesel::replace_into(folders::table) - .values(&*self) - .execute(&**conn) - .map_res("Error saving folder") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(folders::table) + .values(FolderDb::to_db(self)) + .execute(conn) + .map_res("Error saving folder") + } + postgresql { + let value = FolderDb::to_db(self); + diesel::insert_into(folders::table) + .values(&value) + .on_conflict(folders::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving folder") + } + } } pub fn delete(&self, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&self.user_uuid, conn); FolderCipher::delete_all_by_folder(&self.uuid, &conn)?; - diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid))) - .execute(&**conn) - .map_res("Error deleting folder") + + db_run! { conn: { + diesel::delete(folders::table.filter(folders::uuid.eq(&self.uuid))) + .execute(conn) + .map_res("Error deleting folder") + }} } pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -112,73 +114,92 @@ impl Folder { } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - folders::table - .filter(folders::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + folders::table + .filter(folders::uuid.eq(uuid)) + .first::<FolderDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - folders::table - .filter(folders::user_uuid.eq(user_uuid)) - .load::<Self>(&**conn) - .expect("Error loading folders") + db_run! { conn: { + folders::table + .filter(folders::user_uuid.eq(user_uuid)) + .load::<FolderDb>(conn) + .expect("Error loading folders") + .from_db() + }} } } impl FolderCipher { - #[cfg(feature = "postgresql")] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - diesel::insert_into(folders_ciphers::table) - .values(&*self) - .on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid)) - .do_nothing() - .execute(&**conn) - .map_res("Error adding cipher to folder") - } - - #[cfg(not(feature = "postgresql"))] pub fn save(&self, conn: &DbConn) -> EmptyResult { - diesel::replace_into(folders_ciphers::table) - .values(&*self) - .execute(&**conn) - .map_res("Error adding cipher to folder") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(folders_ciphers::table) + .values(FolderCipherDb::to_db(self)) + .execute(conn) + .map_res("Error adding cipher to folder") + } + postgresql { + diesel::insert_into(folders_ciphers::table) + .values(FolderCipherDb::to_db(self)) + .on_conflict((folders_ciphers::cipher_uuid, folders_ciphers::folder_uuid)) + .do_nothing() + .execute(conn) + .map_res("Error adding cipher to folder") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { - diesel::delete( - folders_ciphers::table - .filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid)) - .filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)), - ) - .execute(&**conn) - .map_res("Error removing cipher from folder") + db_run! { conn: { + diesel::delete( + folders_ciphers::table + .filter(folders_ciphers::cipher_uuid.eq(self.cipher_uuid)) + .filter(folders_ciphers::folder_uuid.eq(self.folder_uuid)), + ) + .execute(conn) + .map_res("Error removing cipher from folder") + }} } pub fn delete_all_by_cipher(cipher_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))) - .execute(&**conn) - .map_res("Error removing cipher from folders") + db_run! { conn: { + diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))) + .execute(conn) + .map_res("Error removing cipher from folders") + }} } pub fn delete_all_by_folder(folder_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid))) - .execute(&**conn) - .map_res("Error removing ciphers from folder") + db_run! { conn: { + diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid))) + .execute(conn) + .map_res("Error removing ciphers from folder") + }} } pub fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &DbConn) -> Option<Self> { - folders_ciphers::table - .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) - .filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + folders_ciphers::table + .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) + .filter(folders_ciphers::cipher_uuid.eq(cipher_uuid)) + .first::<FolderCipherDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_folder(folder_uuid: &str, conn: &DbConn) -> Vec<Self> { - folders_ciphers::table - .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) - .load::<Self>(&**conn) - .expect("Error loading folders") + db_run! { conn: { + folders_ciphers::table + .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) + .load::<FolderCipherDb>(conn) + .expect("Error loading folders") + .from_db() + }} } } diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs @@ -1,23 +1,23 @@ -use diesel::prelude::*; use serde_json::Value; use crate::api::EmptyResult; -use crate::db::schema::org_policies; use crate::db::DbConn; use crate::error::MapResult; use super::Organization; -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "org_policies"] -#[belongs_to(Organization, foreign_key = "org_uuid")] -#[primary_key(uuid)] -pub struct OrgPolicy { - pub uuid: String, - pub org_uuid: String, - pub atype: i32, - pub enabled: bool, - pub data: String, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "org_policies"] + #[belongs_to(Organization, foreign_key = "org_uuid")] + #[primary_key(uuid)] + pub struct OrgPolicy { + pub uuid: String, + pub org_uuid: String, + pub atype: i32, + pub enabled: bool, + pub data: String, + } } #[allow(dead_code)] @@ -55,87 +55,105 @@ impl OrgPolicy { /// Database methods impl OrgPolicy { - #[cfg(feature = "postgresql")] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - // We need to make sure we're not going to violate the unique constraint on org_uuid and atype. - // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does - // not support multiple constraints on ON CONFLICT clauses. - diesel::delete( - org_policies::table - .filter(org_policies::org_uuid.eq(&self.org_uuid)) - .filter(org_policies::atype.eq(&self.atype)), - ) - .execute(&**conn) - .map_res("Error deleting org_policy for insert")?; - - diesel::insert_into(org_policies::table) - .values(self) - .on_conflict(org_policies::uuid) - .do_update() - .set(self) - .execute(&**conn) - .map_res("Error saving org_policy") - } - - #[cfg(not(feature = "postgresql"))] pub fn save(&self, conn: &DbConn) -> EmptyResult { - diesel::replace_into(org_policies::table) - .values(&*self) - .execute(&**conn) - .map_res("Error saving org_policy") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(org_policies::table) + .values(OrgPolicyDb::to_db(self)) + .execute(conn) + .map_res("Error saving org_policy") + } + postgresql { + let value = OrgPolicyDb::to_db(self); + // We need to make sure we're not going to violate the unique constraint on org_uuid and atype. + // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does + // not support multiple constraints on ON CONFLICT clauses. + diesel::delete( + org_policies::table + .filter(org_policies::org_uuid.eq(&self.org_uuid)) + .filter(org_policies::atype.eq(&self.atype)), + ) + .execute(conn) + .map_res("Error deleting org_policy for insert")?; + + diesel::insert_into(org_policies::table) + .values(&value) + .on_conflict(org_policies::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving org_policy") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { - diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error deleting org_policy") + db_run! { conn: { + diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error deleting org_policy") + }} } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - org_policies::table - .filter(org_policies::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + org_policies::table + .filter(org_policies::uuid.eq(uuid)) + .first::<OrgPolicyDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { - org_policies::table - .filter(org_policies::org_uuid.eq(org_uuid)) - .load::<Self>(&**conn) - .expect("Error loading org_policy") + db_run! { conn: { + org_policies::table + .filter(org_policies::org_uuid.eq(org_uuid)) + .load::<OrgPolicyDb>(conn) + .expect("Error loading org_policy") + .from_db() + }} } pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - use crate::db::schema::users_organizations; - - org_policies::table - .left_join( - users_organizations::table.on( - users_organizations::org_uuid.eq(org_policies::org_uuid) - .and(users_organizations::user_uuid.eq(user_uuid))) - ) - .select(org_policies::all_columns) - .load::<Self>(&**conn) - .expect("Error loading org_policy") + db_run! { conn: { + org_policies::table + .left_join( + users_organizations::table.on( + users_organizations::org_uuid.eq(org_policies::org_uuid) + .and(users_organizations::user_uuid.eq(user_uuid))) + ) + .select(org_policies::all_columns) + .load::<OrgPolicyDb>(conn) + .expect("Error loading org_policy") + .from_db() + }} } pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> { - org_policies::table - .filter(org_policies::org_uuid.eq(org_uuid)) - .filter(org_policies::atype.eq(atype)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + org_policies::table + .filter(org_policies::org_uuid.eq(org_uuid)) + .filter(org_policies::atype.eq(atype)) + .first::<OrgPolicyDb>(conn) + .ok() + .from_db() + }} } pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) - .execute(&**conn) - .map_res("Error deleting org_policy") + db_run! { conn: { + diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid))) + .execute(conn) + .map_res("Error deleting org_policy") + }} } /*pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) - .execute(&**conn) - .map_res("Error deleting twofactors") + db_run! { conn: { + diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) + .execute(conn) + .map_res("Error deleting twofactors") + }} }*/ } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -4,27 +4,29 @@ use num_traits::FromPrimitive; use super::{CollectionUser, User, OrgPolicy}; -#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] -#[table_name = "organizations"] -#[primary_key(uuid)] -pub struct Organization { - pub uuid: String, - pub name: String, - pub billing_email: String, -} - -#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] -#[table_name = "users_organizations"] -#[primary_key(uuid)] -pub struct UserOrganization { - pub uuid: String, - pub user_uuid: String, - pub org_uuid: String, - - pub access_all: bool, - pub akey: String, - pub status: i32, - pub atype: i32, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[table_name = "organizations"] + #[primary_key(uuid)] + pub struct Organization { + pub uuid: String, + pub name: String, + pub billing_email: String, + } + + #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[table_name = "users_organizations"] + #[primary_key(uuid)] + pub struct UserOrganization { + pub uuid: String, + pub user_uuid: String, + pub org_uuid: String, + + pub access_all: bool, + pub akey: String, + pub status: i32, + pub atype: i32, + } } pub enum UserOrgStatus { @@ -196,16 +198,13 @@ impl UserOrganization { } } -use crate::db::schema::{ciphers_collections, organizations, users_collections, users_organizations}; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; /// Database methods impl Organization { - #[cfg(feature = "postgresql")] pub fn save(&self, conn: &DbConn) -> EmptyResult { UserOrganization::find_by_org(&self.uuid, conn) .iter() @@ -213,27 +212,24 @@ impl Organization { User::update_uuid_revision(&user_org.user_uuid, conn); }); - diesel::insert_into(organizations::table) - .values(self) - .on_conflict(organizations::uuid) - .do_update() - .set(self) - .execute(&**conn) - .map_res("Error saving organization") - } - - #[cfg(not(feature = "postgresql"))] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - UserOrganization::find_by_org(&self.uuid, conn) - .iter() - .for_each(|user_org| { - User::update_uuid_revision(&user_org.user_uuid, conn); - }); - - diesel::replace_into(organizations::table) - .values(self) - .execute(&**conn) - .map_res("Error saving organization") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(organizations::table) + .values(OrganizationDb::to_db(self)) + .execute(conn) + .map_res("Error saving organization") + } + postgresql { + let value = OrganizationDb::to_db(self); + diesel::insert_into(organizations::table) + .values(&value) + .on_conflict(organizations::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving organization") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { @@ -244,20 +240,27 @@ impl Organization { UserOrganization::delete_all_by_organization(&self.uuid, &conn)?; OrgPolicy::delete_all_by_organization(&self.uuid, &conn)?; - diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error saving organization") + + db_run! { conn: { + diesel::delete(organizations::table.filter(organizations::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error saving organization") + }} } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - organizations::table - .filter(organizations::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + organizations::table + .filter(organizations::uuid.eq(uuid)) + .first::<OrganizationDb>(conn) + .ok().from_db() + }} } pub fn get_all(conn: &DbConn) -> Vec<Self> { - organizations::table.load::<Self>(&**conn).expect("Error loading organizations") + db_run! { conn: { + organizations::table.load::<OrganizationDb>(conn).expect("Error loading organizations").from_db() + }} } } @@ -345,28 +348,27 @@ impl UserOrganization { "Object": "organizationUserDetails", }) } - - #[cfg(feature = "postgresql")] pub fn save(&self, conn: &DbConn) -> EmptyResult { User::update_uuid_revision(&self.user_uuid, conn); - diesel::insert_into(users_organizations::table) - .values(self) - .on_conflict(users_organizations::uuid) - .do_update() - .set(self) - .execute(&**conn) - .map_res("Error adding user to organization") - } - - #[cfg(not(feature = "postgresql"))] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - User::update_uuid_revision(&self.user_uuid, conn); - - diesel::replace_into(users_organizations::table) - .values(self) - .execute(&**conn) - .map_res("Error adding user to organization") + db_run! { conn: + sqlite, mysql { + diesel::replace_into(users_organizations::table) + .values(UserOrganizationDb::to_db(self)) + .execute(conn) + .map_res("Error adding user to organization") + } + postgresql { + let value = UserOrganizationDb::to_db(self); + diesel::insert_into(users_organizations::table) + .values(&value) + .on_conflict(users_organizations::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error adding user to organization") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { @@ -374,9 +376,11 @@ impl UserOrganization { CollectionUser::delete_all_by_user(&self.user_uuid, &conn)?; - diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error removing user from organization") + db_run! { conn: { + diesel::delete(users_organizations::table.filter(users_organizations::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error removing user from organization") + }} } pub fn delete_all_by_organization(org_uuid: &str, conn: &DbConn) -> EmptyResult { @@ -403,107 +407,129 @@ impl UserOrganization { } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - users_organizations::table - .filter(users_organizations::uuid.eq(uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + users_organizations::table + .filter(users_organizations::uuid.eq(uuid)) + .first::<UserOrganizationDb>(conn) + .ok().from_db() + }} } pub fn find_by_uuid_and_org(uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { - users_organizations::table - .filter(users_organizations::uuid.eq(uuid)) - .filter(users_organizations::org_uuid.eq(org_uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + users_organizations::table + .filter(users_organizations::uuid.eq(uuid)) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .first::<UserOrganizationDb>(conn) + .ok().from_db() + }} } pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) - .load::<Self>(&**conn) - .unwrap_or_default() + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) + .load::<UserOrganizationDb>(conn) + .unwrap_or_default().from_db() + }} } pub fn find_invited_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::status.eq(UserOrgStatus::Invited as i32)) - .load::<Self>(&**conn) - .unwrap_or_default() + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::status.eq(UserOrgStatus::Invited as i32)) + .load::<UserOrganizationDb>(conn) + .unwrap_or_default().from_db() + }} } pub fn find_any_state_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::user_uuid.eq(user_uuid)) - .load::<Self>(&**conn) - .unwrap_or_default() + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .load::<UserOrganizationDb>(conn) + .unwrap_or_default().from_db() + }} } pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .load::<Self>(&**conn) - .expect("Error loading user organizations") + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .load::<UserOrganizationDb>(conn) + .expect("Error loading user organizations").from_db() + }} } pub fn count_by_org(org_uuid: &str, conn: &DbConn) -> i64 { - users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .count() - .first::<i64>(&**conn) - .ok() - .unwrap_or(0) + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .count() + .first::<i64>(conn) + .ok() + .unwrap_or(0) + }} } pub fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .filter(users_organizations::atype.eq(atype)) - .load::<Self>(&**conn) - .expect("Error loading user organizations") + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .filter(users_organizations::atype.eq(atype)) + .load::<UserOrganizationDb>(conn) + .expect("Error loading user organizations").from_db() + }} } pub fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { - users_organizations::table - .filter(users_organizations::user_uuid.eq(user_uuid)) - .filter(users_organizations::org_uuid.eq(org_uuid)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .filter(users_organizations::org_uuid.eq(org_uuid)) + .first::<UserOrganizationDb>(conn) + .ok().from_db() + }} } pub fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .left_join(users_collections::table.on( - users_collections::user_uuid.eq(users_organizations::user_uuid) - )) - .left_join(ciphers_collections::table.on( - ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and( - ciphers_collections::cipher_uuid.eq(&cipher_uuid) - ) - )) - .filter( - users_organizations::access_all.eq(true).or( // AccessAll.. - ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .left_join(users_collections::table.on( + users_collections::user_uuid.eq(users_organizations::user_uuid) + )) + .left_join(ciphers_collections::table.on( + ciphers_collections::collection_uuid.eq(users_collections::collection_uuid).and( + ciphers_collections::cipher_uuid.eq(&cipher_uuid) + ) + )) + .filter( + users_organizations::access_all.eq(true).or( // AccessAll.. + ciphers_collections::cipher_uuid.eq(&cipher_uuid) // ..or access to collection with cipher + ) ) - ) - .select(users_organizations::all_columns) - .load::<Self>(&**conn).expect("Error loading user organizations") + .select(users_organizations::all_columns) + .load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db() + }} } pub fn find_by_collection_and_org(collection_uuid: &str, org_uuid: &str, conn: &DbConn) -> Vec<Self> { - users_organizations::table - .filter(users_organizations::org_uuid.eq(org_uuid)) - .left_join(users_collections::table.on( - users_collections::user_uuid.eq(users_organizations::user_uuid) - )) - .filter( - users_organizations::access_all.eq(true).or( // AccessAll.. - users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher + db_run! { conn: { + users_organizations::table + .filter(users_organizations::org_uuid.eq(org_uuid)) + .left_join(users_collections::table.on( + users_collections::user_uuid.eq(users_organizations::user_uuid) + )) + .filter( + users_organizations::access_all.eq(true).or( // AccessAll.. + users_collections::collection_uuid.eq(&collection_uuid) // ..or access to collection with cipher + ) ) - ) - .select(users_organizations::all_columns) - .load::<Self>(&**conn).expect("Error loading user organizations") + .select(users_organizations::all_columns) + .load::<UserOrganizationDb>(conn).expect("Error loading user organizations").from_db() + }} } } diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs @@ -1,24 +1,24 @@ -use diesel::prelude::*; use serde_json::Value; use crate::api::EmptyResult; -use crate::db::schema::twofactor; use crate::db::DbConn; use crate::error::MapResult; use super::User; -#[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] -#[table_name = "twofactor"] -#[belongs_to(User, foreign_key = "user_uuid")] -#[primary_key(uuid)] -pub struct TwoFactor { - pub uuid: String, - pub user_uuid: String, - pub atype: i32, - pub enabled: bool, - pub data: String, - pub last_used: i32, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "twofactor"] + #[belongs_to(User, foreign_key = "user_uuid")] + #[primary_key(uuid)] + pub struct TwoFactor { + pub uuid: String, + pub user_uuid: String, + pub atype: i32, + pub enabled: bool, + pub data: String, + pub last_used: i32, + } } #[allow(dead_code)] @@ -70,57 +70,69 @@ impl TwoFactor { /// Database methods impl TwoFactor { - #[cfg(feature = "postgresql")] pub fn save(&self, conn: &DbConn) -> EmptyResult { - // We need to make sure we're not going to violate the unique constraint on user_uuid and atype. - // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does - // not support multiple constraints on ON CONFLICT clauses. - diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype))) - .execute(&**conn) - .map_res("Error deleting twofactor for insert")?; - - diesel::insert_into(twofactor::table) - .values(self) - .on_conflict(twofactor::uuid) - .do_update() - .set(self) - .execute(&**conn) - .map_res("Error saving twofactor") - } + db_run! { conn: + sqlite, mysql { + diesel::replace_into(twofactor::table) + .values(TwoFactorDb::to_db(self)) + .execute(conn) + .map_res("Error saving twofactor") + } + postgresql { + let value = TwoFactorDb::to_db(self); + // We need to make sure we're not going to violate the unique constraint on user_uuid and atype. + // This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does + // not support multiple constraints on ON CONFLICT clauses. + diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(&self.user_uuid)).filter(twofactor::atype.eq(&self.atype))) + .execute(conn) + .map_res("Error deleting twofactor for insert")?; - #[cfg(not(feature = "postgresql"))] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - diesel::replace_into(twofactor::table) - .values(self) - .execute(&**conn) - .map_res("Error saving twofactor") + diesel::insert_into(twofactor::table) + .values(&value) + .on_conflict(twofactor::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving twofactor") + } + } } pub fn delete(self, conn: &DbConn) -> EmptyResult { - diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error deleting twofactor") + db_run! { conn: { + diesel::delete(twofactor::table.filter(twofactor::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error deleting twofactor") + }} } pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { - twofactor::table - .filter(twofactor::user_uuid.eq(user_uuid)) - .filter(twofactor::atype.lt(1000)) // Filter implementation types - .load::<Self>(&**conn) - .expect("Error loading twofactor") + db_run! { conn: { + twofactor::table + .filter(twofactor::user_uuid.eq(user_uuid)) + .filter(twofactor::atype.lt(1000)) // Filter implementation types + .load::<TwoFactorDb>(conn) + .expect("Error loading twofactor") + .from_db() + }} } pub fn find_by_user_and_type(user_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> { - twofactor::table - .filter(twofactor::user_uuid.eq(user_uuid)) - .filter(twofactor::atype.eq(atype)) - .first::<Self>(&**conn) - .ok() + db_run! { conn: { + twofactor::table + .filter(twofactor::user_uuid.eq(user_uuid)) + .filter(twofactor::atype.eq(atype)) + .first::<TwoFactorDb>(conn) + .ok() + .from_db() + }} } pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { - diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) - .execute(&**conn) - .map_res("Error deleting twofactors") + db_run! { conn: { + diesel::delete(twofactor::table.filter(twofactor::user_uuid.eq(user_uuid))) + .execute(conn) + .map_res("Error deleting twofactors") + }} } } diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -4,43 +4,53 @@ use serde_json::Value; use crate::crypto; use crate::CONFIG; -#[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] -#[table_name = "users"] -#[changeset_options(treat_none_as_null="true")] -#[primary_key(uuid)] -pub struct User { - pub uuid: String, - pub created_at: NaiveDateTime, - pub updated_at: NaiveDateTime, - pub verified_at: Option<NaiveDateTime>, - pub last_verifying_at: Option<NaiveDateTime>, - pub login_verify_count: i32, - - pub email: String, - pub email_new: Option<String>, - pub email_new_token: Option<String>, - pub name: String, - - pub password_hash: Vec<u8>, - pub salt: Vec<u8>, - pub password_iterations: i32, - pub password_hint: Option<String>, - - pub akey: String, - pub private_key: Option<String>, - pub public_key: Option<String>, - - #[column_name = "totp_secret"] - _totp_secret: Option<String>, - pub totp_recover: Option<String>, - - pub security_stamp: String, - - pub equivalent_domains: String, - pub excluded_globals: String, - - pub client_kdf_type: i32, - pub client_kdf_iter: i32, +db_object! { + #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] + #[table_name = "users"] + #[changeset_options(treat_none_as_null="true")] + #[primary_key(uuid)] + pub struct User { + pub uuid: String, + pub created_at: NaiveDateTime, + pub updated_at: NaiveDateTime, + pub verified_at: Option<NaiveDateTime>, + pub last_verifying_at: Option<NaiveDateTime>, + pub login_verify_count: i32, + + pub email: String, + pub email_new: Option<String>, + pub email_new_token: Option<String>, + pub name: String, + + pub password_hash: Vec<u8>, + pub salt: Vec<u8>, + pub password_iterations: i32, + pub password_hint: Option<String>, + + pub akey: String, + pub private_key: Option<String>, + pub public_key: Option<String>, + + #[column_name = "totp_secret"] // Note, this is only added to the UserDb structs, not to User + _totp_secret: Option<String>, + pub totp_recover: Option<String>, + + pub security_stamp: String, + + pub equivalent_domains: String, + pub excluded_globals: String, + + pub client_kdf_type: i32, + pub client_kdf_iter: i32, + } + + + #[derive(Debug, Identifiable, Queryable, Insertable)] + #[table_name = "invitations"] + #[primary_key(email)] + pub struct Invitation { + pub email: String, + } } enum UserStatus { @@ -119,9 +129,7 @@ impl User { } use super::{Cipher, Device, Folder, TwoFactor, UserOrgType, UserOrganization}; -use crate::db::schema::{invitations, users}; use crate::db::DbConn; -use diesel::prelude::*; use crate::api::EmptyResult; use crate::error::MapResult; @@ -158,7 +166,6 @@ impl User { }) } - #[cfg(feature = "postgresql")] pub fn save(&mut self, conn: &DbConn) -> EmptyResult { if self.email.trim().is_empty() { err!("User email can't be empty") @@ -166,49 +173,48 @@ impl User { self.updated_at = Utc::now().naive_utc(); - diesel::insert_into(users::table) // Insert or update - .values(&*self) - .on_conflict(users::uuid) - .do_update() - .set(&*self) - .execute(&**conn) - .map_res("Error saving user") - } - - #[cfg(not(feature = "postgresql"))] - pub fn save(&mut self, conn: &DbConn) -> EmptyResult { - if self.email.trim().is_empty() { - err!("User email can't be empty") + db_run! {conn: + sqlite, mysql { + diesel::replace_into(users::table) // Insert or update + .values(&UserDb::to_db(self)) + .execute(conn) + .map_res("Error saving user") + } + postgresql { + let value = UserDb::to_db(self); + diesel::insert_into(users::table) // Insert or update + .values(&value) + .on_conflict(users::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving user") + } } - - self.updated_at = Utc::now().naive_utc(); - - diesel::replace_into(users::table) // Insert or update - .values(&*self) - .execute(&**conn) - .map_res("Error saving user") } pub fn delete(self, conn: &DbConn) -> EmptyResult { - for user_org in UserOrganization::find_by_user(&self.uuid, &*conn) { + for user_org in UserOrganization::find_by_user(&self.uuid, conn) { if user_org.atype == UserOrgType::Owner { let owner_type = UserOrgType::Owner as i32; - if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, &conn).len() <= 1 { + if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).len() <= 1 { err!("Can't delete last owner") } } } - UserOrganization::delete_all_by_user(&self.uuid, &*conn)?; - Cipher::delete_all_by_user(&self.uuid, &*conn)?; - Folder::delete_all_by_user(&self.uuid, &*conn)?; - Device::delete_all_by_user(&self.uuid, &*conn)?; - TwoFactor::delete_all_by_user(&self.uuid, &*conn)?; - Invitation::take(&self.email, &*conn); // Delete invitation if any - - diesel::delete(users::table.filter(users::uuid.eq(self.uuid))) - .execute(&**conn) - .map_res("Error deleting user") + UserOrganization::delete_all_by_user(&self.uuid, conn)?; + Cipher::delete_all_by_user(&self.uuid, conn)?; + Folder::delete_all_by_user(&self.uuid, conn)?; + Device::delete_all_by_user(&self.uuid, conn)?; + TwoFactor::delete_all_by_user(&self.uuid, conn)?; + Invitation::take(&self.email, conn); // Delete invitation if any + + db_run! {conn: { + diesel::delete(users::table.filter(users::uuid.eq(self.uuid))) + .execute(conn) + .map_res("Error deleting user") + }} } pub fn update_uuid_revision(uuid: &str, conn: &DbConn) { @@ -220,15 +226,14 @@ impl User { pub fn update_all_revisions(conn: &DbConn) -> EmptyResult { let updated_at = Utc::now().naive_utc(); - crate::util::retry( - || { + db_run! {conn: { + crate::util::retry(|| { diesel::update(users::table) .set(users::updated_at.eq(updated_at)) - .execute(&**conn) - }, - 10, - ) - .map_res("Error updating revision date for all users") + .execute(conn) + }, 10) + .map_res("Error updating revision date for all users") + }} } pub fn update_revision(&mut self, conn: &DbConn) -> EmptyResult { @@ -238,84 +243,85 @@ impl User { } fn _update_revision(uuid: &str, date: &NaiveDateTime, conn: &DbConn) -> EmptyResult { - crate::util::retry( - || { + db_run! {conn: { + crate::util::retry(|| { diesel::update(users::table.filter(users::uuid.eq(uuid))) .set(users::updated_at.eq(date)) - .execute(&**conn) - }, - 10, - ) - .map_res("Error updating user revision") + .execute(conn) + }, 10) + .map_res("Error updating user revision") + }} } pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> { let lower_mail = mail.to_lowercase(); - users::table - .filter(users::email.eq(lower_mail)) - .first::<Self>(&**conn) - .ok() + db_run! {conn: { + users::table + .filter(users::email.eq(lower_mail)) + .first::<UserDb>(conn) + .ok() + .from_db() + }} } pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { - users::table.filter(users::uuid.eq(uuid)).first::<Self>(&**conn).ok() + db_run! {conn: { + users::table.filter(users::uuid.eq(uuid)).first::<UserDb>(conn).ok().from_db() + }} } pub fn get_all(conn: &DbConn) -> Vec<Self> { - users::table.load::<Self>(&**conn).expect("Error loading users") + db_run! {conn: { + users::table.load::<UserDb>(conn).expect("Error loading users").from_db() + }} } } -#[derive(Debug, Identifiable, Queryable, Insertable)] -#[table_name = "invitations"] -#[primary_key(email)] -pub struct Invitation { - pub email: String, -} - impl Invitation { pub const fn new(email: String) -> Self { Self { email } } - #[cfg(feature = "postgresql")] pub fn save(&self, conn: &DbConn) -> EmptyResult { if self.email.trim().is_empty() { err!("Invitation email can't be empty") } - diesel::insert_into(invitations::table) - .values(self) - .on_conflict(invitations::email) - .do_nothing() - .execute(&**conn) - .map_res("Error saving invitation") - } - - #[cfg(not(feature = "postgresql"))] - pub fn save(&self, conn: &DbConn) -> EmptyResult { - if self.email.trim().is_empty() { - err!("Invitation email can't be empty") + db_run! {conn: + sqlite, mysql { + diesel::replace_into(invitations::table) + .values(InvitationDb::to_db(self)) + .execute(conn) + .map_res("Error saving invitation") + } + postgresql { + diesel::insert_into(invitations::table) + .values(InvitationDb::to_db(self)) + .on_conflict(invitations::email) + .do_nothing() + .execute(conn) + .map_res("Error saving invitation") + } } - - diesel::replace_into(invitations::table) - .values(self) - .execute(&**conn) - .map_res("Error saving invitation") } pub fn delete(self, conn: &DbConn) -> EmptyResult { - diesel::delete(invitations::table.filter(invitations::email.eq(self.email))) - .execute(&**conn) - .map_res("Error deleting invitation") + db_run! {conn: { + diesel::delete(invitations::table.filter(invitations::email.eq(self.email))) + .execute(conn) + .map_res("Error deleting invitation") + }} } pub fn find_by_mail(mail: &str, conn: &DbConn) -> Option<Self> { let lower_mail = mail.to_lowercase(); - invitations::table - .filter(invitations::email.eq(lower_mail)) - .first::<Self>(&**conn) - .ok() + db_run! {conn: { + invitations::table + .filter(invitations::email.eq(lower_mail)) + .first::<InvitationDb>(conn) + .ok() + .from_db() + }} } pub fn take(mail: &str, conn: &DbConn) -> bool { diff --git a/src/error.rs b/src/error.rs @@ -34,6 +34,7 @@ macro_rules! make_error { } use diesel::result::Error as DieselErr; +use diesel::r2d2::PoolError as R2d2Err; use handlebars::RenderError as HbErr; use jsonwebtoken::errors::Error as JWTErr; use regex::Error as RegexErr; @@ -66,6 +67,7 @@ make_error! { // Used for special return values, like 2FA errors JsonError(Value): _no_source, _serialize, DbError(DieselErr): _has_source, _api_error, + R2d2Error(R2d2Err): _has_source, _api_error, U2fError(U2fErr): _has_source, _api_error, SerdeError(SerdeErr): _has_source, _api_error, JWTError(JWTErr): _has_source, _api_error, diff --git a/src/main.rs b/src/main.rs @@ -33,6 +33,7 @@ mod api; mod auth; mod config; mod crypto; +#[macro_use] mod db; mod mail; mod util; @@ -61,10 +62,8 @@ fn main() { _ => false, }; - check_db(); check_rsa_keys(); check_web_vault(); - migrations::run_migrations(); create_icon_cache_folder(); @@ -200,30 +199,6 @@ fn chain_syslog(logger: fern::Dispatch) -> fern::Dispatch { } } -fn check_db() { - if cfg!(feature = "sqlite") { - let url = CONFIG.database_url(); - let path = Path::new(&url); - - if let Some(parent) = path.parent() { - if create_dir_all(parent).is_err() { - error!("Error creating database directory"); - exit(1); - } - } - - // Turn on WAL in SQLite - if CONFIG.enable_db_wal() { - use diesel::RunQueryDsl; - let connection = db::get_connection().expect("Can't connect to DB"); - diesel::sql_query("PRAGMA journal_mode=wal") - .execute(&connection) - .expect("Failed to turn on WAL"); - } - } - db::get_connection().expect("Can't connect to DB"); -} - fn create_icon_cache_folder() { // Try to create the icon cache folder, and generate an error if it could not. create_dir_all(&CONFIG.icon_cache_folder()).expect("Error creating icon cache directory"); @@ -285,57 +260,22 @@ fn check_web_vault() { let index_path = Path::new(&CONFIG.web_vault_folder()).join("index.html"); if !index_path.exists() { - error!("Web vault is not found. To install it, please follow the steps in: "); + error!("Web vault is not found at '{}'. To install it, please follow the steps in: ", CONFIG.web_vault_folder()); error!("https://github.com/dani-garcia/bitwarden_rs/wiki/Building-binary#install-the-web-vault"); error!("You can also set the environment variable 'WEB_VAULT_ENABLED=false' to disable it"); exit(1); } } -// Embed the migrations from the migrations folder into the application -// This way, the program automatically migrates the database to the latest version -// https://docs.rs/diesel_migrations/*/diesel_migrations/macro.embed_migrations.html -#[allow(unused_imports)] -mod migrations { - - #[cfg(feature = "sqlite")] - embed_migrations!("migrations/sqlite"); - #[cfg(feature = "mysql")] - embed_migrations!("migrations/mysql"); - #[cfg(feature = "postgresql")] - embed_migrations!("migrations/postgresql"); - - pub fn run_migrations() { - // Make sure the database is up to date (create if it doesn't exist, or run the migrations) - let connection = crate::db::get_connection().expect("Can't connect to DB"); - - use std::io::stdout; - - // Disable Foreign Key Checks during migration - use diesel::RunQueryDsl; - - // FIXME: Per https://www.postgresql.org/docs/12/sql-set-constraints.html, - // "SET CONSTRAINTS sets the behavior of constraint checking within the - // current transaction", so this setting probably won't take effect for - // any of the migrations since it's being run outside of a transaction. - // Migrations that need to disable foreign key checks should run this - // from within the migration script itself. - #[cfg(feature = "postgres")] - diesel::sql_query("SET CONSTRAINTS ALL DEFERRED").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations"); - - // Scoped to a connection/session. - #[cfg(feature = "mysql")] - diesel::sql_query("SET FOREIGN_KEY_CHECKS = 0").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations"); - - // Scoped to a connection. - #[cfg(feature = "sqlite")] - diesel::sql_query("PRAGMA foreign_keys = OFF").execute(&connection).expect("Failed to disable Foreign Key Checks during migrations"); - - embedded_migrations::run_with_output(&connection, &mut stdout()).expect("Can't run migrations"); - } -} - fn launch_rocket(extra_debug: bool) { + let pool = match db::DbPool::from_config() { + Ok(p) => p, + Err(e) => { + error!("Error creating database pool: {:?}", e); + exit(1); + } + }; + let basepath = &CONFIG.domain_path(); // If adding more paths here, consider also adding them to @@ -347,7 +287,7 @@ fn launch_rocket(extra_debug: bool) { .mount(&[basepath, "/identity"].concat(), api::identity_routes()) .mount(&[basepath, "/icons"].concat(), api::icons_routes()) .mount(&[basepath, "/notifications"].concat(), api::notifications_routes()) - .manage(db::init_pool()) + .manage(pool) .manage(api::start_notification_server()) .attach(util::AppHeaders()) .attach(util::CORS())