diff options
author | BlackDex <[email protected]> | 2022-05-04 21:13:05 +0200 |
---|---|---|
committer | BlackDex <[email protected]> | 2022-05-06 17:01:02 +0200 |
commit | 3ca85028ea99ed43c99847b18e222f25ca47374e (patch) | |
tree | 4a0c9369911ee87f9827201af180a63d63ce340a /src/db | |
parent | 3abf173d8954c982383285b29b39d15043e974cb (diff) | |
download | vaultwarden-3ca85028ea99ed43c99847b18e222f25ca47374e.tar.gz vaultwarden-3ca85028ea99ed43c99847b18e222f25ca47374e.zip |
Improve sync speed and updated dep. versions
Improved sync speed by resolving the N+1 query issues.
Solves #1402 and Solves #1453
With this change there is just one query done to retreive all the
important data, and matching is done in-code/memory.
With a very large database the sync time went down about 3 times.
Also updated misc crates and Github Actions versions.
Diffstat (limited to 'src/db')
-rw-r--r-- | src/db/mod.rs | 16 | ||||
-rw-r--r-- | src/db/models/attachment.rs | 15 | ||||
-rw-r--r-- | src/db/models/cipher.rs | 181 | ||||
-rw-r--r-- | src/db/models/collection.rs | 51 | ||||
-rw-r--r-- | src/db/models/device.rs | 4 | ||||
-rw-r--r-- | src/db/models/emergency_access.rs | 3 | ||||
-rw-r--r-- | src/db/models/favorite.rs | 18 | ||||
-rw-r--r-- | src/db/models/folder.rs | 22 | ||||
-rw-r--r-- | src/db/models/org_policy.rs | 5 | ||||
-rw-r--r-- | src/db/models/organization.rs | 9 | ||||
-rw-r--r-- | src/db/models/send.rs | 6 | ||||
-rw-r--r-- | src/db/models/two_factor.rs | 5 | ||||
-rw-r--r-- | src/db/models/two_factor_incomplete.rs | 5 |
13 files changed, 234 insertions, 106 deletions
diff --git a/src/db/mod.rs b/src/db/mod.rs index 6fcb63e5..3223fb65 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -206,16 +206,16 @@ macro_rules! db_run { // Different code for each db ( $conn:ident: $( $($db:ident),+ $body:block )+ ) => {{ #[allow(unused)] use diesel::prelude::*; - #[allow(unused)] use crate::db::FromDb; + #[allow(unused)] use $crate::db::FromDb; let conn = $conn.conn.clone(); let mut conn = conn.lock_owned().await; match conn.as_mut().expect("internal invariant broken: self.connection is Some") { $($( #[cfg($db)] - crate::db::DbConnInner::$db($conn) => { + $crate::db::DbConnInner::$db($conn) => { paste::paste! { - #[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *}; + #[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *}; #[allow(unused)] use [<__ $db _model>]::*; } @@ -227,16 +227,16 @@ macro_rules! db_run { ( @raw $conn:ident: $( $($db:ident),+ $body:block )+ ) => {{ #[allow(unused)] use diesel::prelude::*; - #[allow(unused)] use crate::db::FromDb; + #[allow(unused)] use $crate::db::FromDb; let conn = $conn.conn.clone(); let mut conn = conn.lock_owned().await; match conn.as_mut().expect("internal invariant broken: self.connection is Some") { $($( #[cfg($db)] - crate::db::DbConnInner::$db($conn) => { + $crate::db::DbConnInner::$db($conn) => { paste::paste! { - #[allow(unused)] use crate::db::[<__ $db _schema>]::{self as schema, *}; + #[allow(unused)] use $crate::db::[<__ $db _schema>]::{self as schema, *}; // @ RAW: #[allow(unused)] use [<__ $db _model>]::*; } @@ -297,7 +297,7 @@ macro_rules! db_object { paste::paste! { #[allow(unused)] use super::*; #[allow(unused)] use diesel::prelude::*; - #[allow(unused)] use crate::db::[<__ $db _schema>]::*; + #[allow(unused)] use $crate::db::[<__ $db _schema>]::*; $( #[$attr] )* pub struct [<$name Db>] { $( @@ -309,7 +309,7 @@ macro_rules! db_object { #[inline(always)] pub fn to_db(x: &super::$name) -> Self { Self { $( $field: x.$field.clone(), )+ } } } - impl crate::db::FromDb for [<$name Db>] { + impl $crate::db::FromDb for [<$name Db>] { type Output = super::$name; #[allow(clippy::wrong_self_convention)] #[inline(always)] fn from_db(self) -> Self::Output { super::$name { $( $field: self.$field, )+ } } diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 975687e3..1df4d539 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -2,14 +2,12 @@ use std::io::ErrorKind; use serde_json::Value; -use super::Cipher; use crate::CONFIG; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, 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, @@ -188,4 +186,15 @@ impl Attachment { .unwrap_or(0) }} } + + pub async fn find_all_by_ciphers(cipher_uuids: &Vec<String>, conn: &DbConn) -> Vec<Self> { + db_run! { conn: { + attachments::table + .filter(attachments::cipher_uuid.eq_any(cipher_uuids)) + .select(attachments::all_columns) + .load::<AttachmentDb>(conn) + .expect("Error loading attachments") + .from_db() + }} + } } diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index e6f2050b..d5f78fbe 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,19 +1,17 @@ +use crate::CONFIG; use chrono::{Duration, NaiveDateTime, Utc}; use serde_json::Value; -use crate::CONFIG; +use super::{Attachment, CollectionCipher, Favorite, FolderCipher, User, UserOrgStatus, UserOrgType, UserOrganization}; -use super::{ - Attachment, CollectionCipher, Favorite, FolderCipher, Organization, User, UserOrgStatus, UserOrgType, - UserOrganization, -}; +use crate::api::core::CipherSyncData; + +use std::borrow::Cow; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, 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, @@ -82,22 +80,32 @@ use crate::error::MapResult; /// Database methods impl Cipher { - pub async fn to_json(&self, host: &str, user_uuid: &str, conn: &DbConn) -> Value { + pub async fn to_json( + &self, + host: &str, + user_uuid: &str, + cipher_sync_data: Option<&CipherSyncData>, + conn: &DbConn, + ) -> Value { use crate::util::format_date; - let attachments = Attachment::find_by_cipher(&self.uuid, conn).await; - // When there are no attachments use null instead of an empty array - let attachments_json = if attachments.is_empty() { - Value::Null + let mut attachments_json: Value = Value::Null; + if let Some(cipher_sync_data) = cipher_sync_data { + if let Some(attachments) = cipher_sync_data.cipher_attachments.get(&self.uuid) { + attachments_json = attachments.iter().map(|c| c.to_json(host)).collect(); + } } else { - attachments.iter().map(|c| c.to_json(host)).collect() - }; + let attachments = Attachment::find_by_cipher(&self.uuid, conn).await; + if !attachments.is_empty() { + attachments_json = attachments.iter().map(|c| c.to_json(host)).collect() + } + } let fields_json = self.fields.as_ref().and_then(|s| serde_json::from_str(s).ok()).unwrap_or(Value::Null); 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).await { + let (read_only, hide_passwords) = match self.get_access_restrictions(user_uuid, cipher_sync_data, conn).await { Some((ro, hp)) => (ro, hp), None => { error!("Cipher ownership assertion failure"); @@ -109,7 +117,7 @@ impl Cipher { // If not passing an empty object, mobile clients will crash. let mut type_data_json: Value = serde_json::from_str(&self.data).unwrap_or_else(|_| json!({})); - // NOTE: This was marked as *Backwards Compatibilty Code*, but as of January 2021 this is still being used by upstream + // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // Set the first element of the Uris array as Uri, this is needed several (mobile) clients. if self.atype == 1 { if type_data_json["Uris"].is_array() { @@ -124,13 +132,23 @@ impl Cipher { // Clone the type_data and add some default value. let mut data_json = type_data_json.clone(); - // NOTE: This was marked as *Backwards Compatibilty Code*, but as of January 2021 this is still being used by upstream + // NOTE: This was marked as *Backwards Compatibility Code*, but as of January 2021 this is still being used by upstream // data_json should always contain the following keys with every atype data_json["Fields"] = json!(fields_json); data_json["Name"] = json!(self.name); data_json["Notes"] = json!(self.notes); data_json["PasswordHistory"] = json!(password_history_json); + let collection_ids = if let Some(cipher_sync_data) = cipher_sync_data { + if let Some(cipher_collections) = cipher_sync_data.cipher_collections.get(&self.uuid) { + Cow::from(cipher_collections) + } else { + Cow::from(Vec::with_capacity(0)) + } + } else { + Cow::from(self.get_collections(user_uuid, conn).await) + }; + // There are three types of cipher response models in upstream // Bitwarden: "cipherMini", "cipher", and "cipherDetails" (in order // of increasing level of detail). vaultwarden currently only @@ -144,8 +162,8 @@ 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).await, - "Favorite": self.is_favorite(user_uuid, conn).await, + "FolderId": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string() ) } else { self.get_folder_uuid(user_uuid, conn).await }, + "Favorite": if let Some(cipher_sync_data) = cipher_sync_data { cipher_sync_data.cipher_favorites.contains(&self.uuid) } else { self.is_favorite(user_uuid, conn).await }, "Reprompt": self.reprompt.unwrap_or(RepromptType::None as i32), "OrganizationId": self.organization_uuid, "Attachments": attachments_json, @@ -154,7 +172,7 @@ impl Cipher { "OrganizationUseTotp": true, // This field is specific to the cipherDetails type. - "CollectionIds": self.get_collections(user_uuid, conn).await, + "CollectionIds": collection_ids, "Name": self.name, "Notes": self.notes, @@ -318,13 +336,21 @@ impl Cipher { } /// Returns whether this cipher is owned by an org in which the user has full access. - pub async fn is_in_full_access_org(&self, user_uuid: &str, conn: &DbConn) -> bool { + pub async fn is_in_full_access_org( + &self, + user_uuid: &str, + cipher_sync_data: Option<&CipherSyncData>, + 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).await { + if let Some(cipher_sync_data) = cipher_sync_data { + if let Some(cached_user_org) = cipher_sync_data.user_organizations.get(org_uuid) { + return cached_user_org.has_full_access(); + } + } else if let Some(user_org) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await { return user_org.has_full_access(); } } - false } @@ -333,18 +359,62 @@ impl Cipher { /// not in any collection the user has access to. Otherwise, the user has /// access to this cipher, and Some(read_only, hide_passwords) represents /// the access restrictions. - pub async fn get_access_restrictions(&self, user_uuid: &str, conn: &DbConn) -> Option<(bool, bool)> { + pub async fn get_access_restrictions( + &self, + user_uuid: &str, + cipher_sync_data: Option<&CipherSyncData>, + conn: &DbConn, + ) -> Option<(bool, bool)> { // Check whether this cipher is directly owned by the user, or is in // a collection that the user has full access to. If so, there are no // access restrictions. - if self.is_owned_by_user(user_uuid) || self.is_in_full_access_org(user_uuid, conn).await { + if self.is_owned_by_user(user_uuid) || self.is_in_full_access_org(user_uuid, cipher_sync_data, conn).await { return Some((false, false)); } + let rows = if let Some(cipher_sync_data) = cipher_sync_data { + let mut rows: Vec<(bool, bool)> = Vec::new(); + if let Some(collections) = cipher_sync_data.cipher_collections.get(&self.uuid) { + for collection in collections { + if let Some(uc) = cipher_sync_data.user_collections.get(collection) { + rows.push((uc.read_only, uc.hide_passwords)); + } + } + } + rows + } else { + self.get_collections_access_flags(user_uuid, conn).await + }; + + if rows.is_empty() { + // This cipher isn't in any collections accessible to the user. + return None; + } + + // 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. For a flag to be in effect for a cipher, upstream + // requires all collections the cipher is in to have that flag set. + // Therefore, we do a boolean AND 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 min() or bool_and() function on + // booleans and this behavior isn't portable anyway. + let mut read_only = true; + let mut hide_passwords = true; + for (ro, hp) in rows.iter() { + read_only &= ro; + hide_passwords &= hp; + } + + Some((read_only, hide_passwords)) + } + + pub async fn get_collections_access_flags(&self, user_uuid: &str, conn: &DbConn) -> Vec<(bool, bool)> { 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 rows = ciphers::table + ciphers::table .filter(ciphers::uuid.eq(&self.uuid)) .inner_join(ciphers_collections::table.on( ciphers::uuid.eq(ciphers_collections::cipher_uuid))) @@ -353,42 +423,19 @@ impl Cipher { .and(users_collections::user_uuid.eq(user_uuid)))) .select((users_collections::read_only, users_collections::hide_passwords)) .load::<(bool, bool)>(conn) - .expect("Error getting access restrictions"); - - if rows.is_empty() { - // This cipher isn't in any collections accessible to the user. - return None; - } - - // 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. For a flag to be in effect for a cipher, upstream - // requires all collections the cipher is in to have that flag set. - // Therefore, we do a boolean AND 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 min() or bool_and() function on - // booleans and this behavior isn't portable anyway. - let mut read_only = true; - let mut hide_passwords = true; - for (ro, hp) in rows.iter() { - read_only &= ro; - hide_passwords &= hp; - } - - Some((read_only, hide_passwords)) + .expect("Error getting access restrictions") }} } pub async fn is_write_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { - match self.get_access_restrictions(user_uuid, conn).await { + match self.get_access_restrictions(user_uuid, None, conn).await { Some((read_only, _hide_passwords)) => !read_only, None => false, } } pub async fn is_accessible_to_user(&self, user_uuid: &str, conn: &DbConn) -> bool { - self.get_access_restrictions(user_uuid, conn).await.is_some() + self.get_access_restrictions(user_uuid, None, conn).await.is_some() } // Returns whether this cipher is a favorite of the specified user. @@ -563,4 +610,32 @@ impl Cipher { .load::<String>(conn).unwrap_or_default() }} } + + /// Return a Vec with (cipher_uuid, collection_uuid) + /// This is used during a full sync so we only need one query for all collections accessible. + pub async fn get_collections_with_cipher_by_user(user_id: &str, conn: &DbConn) -> Vec<(String, String)> { + 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(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::all_columns) + .load::<(String, String)>(conn).unwrap_or_default() + }} + } } diff --git a/src/db/models/collection.rs b/src/db/models/collection.rs index b5782f7b..5d9464fd 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -1,11 +1,10 @@ use serde_json::Value; -use super::{Cipher, Organization, User, UserOrgStatus, UserOrgType, UserOrganization}; +use super::{User, UserOrgStatus, UserOrgType, UserOrganization}; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "collections"] - #[belongs_to(Organization, foreign_key = "org_uuid")] #[primary_key(uuid)] pub struct Collection { pub uuid: String, @@ -13,10 +12,8 @@ db_object! { pub name: String, } - #[derive(Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable)] #[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, @@ -25,10 +22,8 @@ db_object! { pub hide_passwords: bool, } - #[derive(Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable)] #[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, @@ -57,11 +52,32 @@ impl Collection { }) } - pub async fn to_json_details(&self, user_uuid: &str, conn: &DbConn) -> Value { + pub async fn to_json_details( + &self, + user_uuid: &str, + cipher_sync_data: Option<&crate::api::core::CipherSyncData>, + conn: &DbConn, + ) -> Value { + let (read_only, hide_passwords) = if let Some(cipher_sync_data) = cipher_sync_data { + match cipher_sync_data.user_organizations.get(&self.org_uuid) { + Some(uo) if uo.has_full_access() => (false, false), + Some(_) => { + if let Some(uc) = cipher_sync_data.user_collections.get(&self.uuid) { + (uc.read_only, uc.hide_passwords) + } else { + (false, false) + } + } + _ => (true, true), + } + } else { + (!self.is_writable_by_user(user_uuid, conn).await, self.hide_passwords_for_user(user_uuid, conn).await) + }; + let mut json_object = self.to_json(); json_object["Object"] = json!("collectionDetails"); - json_object["ReadOnly"] = json!(!self.is_writable_by_user(user_uuid, conn).await); - json_object["HidePasswords"] = json!(self.hide_passwords_for_user(user_uuid, conn).await); + json_object["ReadOnly"] = json!(read_only); + json_object["HidePasswords"] = json!(hide_passwords); json_object } } @@ -374,6 +390,17 @@ impl CollectionUser { }} } + pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { + db_run! { conn: { + users_collections::table + .filter(users_collections::user_uuid.eq(user_uuid)) + .select(users_collections::all_columns) + .load::<CollectionUserDb>(conn) + .expect("Error loading users_collections") + .from_db() + }} + } + pub async fn delete_all_by_collection(collection_uuid: &str, conn: &DbConn) -> EmptyResult { for collection in CollectionUser::find_by_collection(collection_uuid, conn).await.iter() { User::update_uuid_revision(&collection.user_uuid, conn).await; diff --git a/src/db/models/device.rs b/src/db/models/device.rs index e265c31f..afd494c7 100644 --- a/src/db/models/device.rs +++ b/src/db/models/device.rs @@ -1,13 +1,11 @@ use chrono::{NaiveDateTime, Utc}; -use super::User; use crate::CONFIG; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "devices"] #[changeset_options(treat_none_as_null="true")] - #[belongs_to(User, foreign_key = "user_uuid")] #[primary_key(uuid, user_uuid)] pub struct Device { pub uuid: String, diff --git a/src/db/models/emergency_access.rs b/src/db/models/emergency_access.rs index e878507b..1f0b84fd 100644 --- a/src/db/models/emergency_access.rs +++ b/src/db/models/emergency_access.rs @@ -4,10 +4,9 @@ use serde_json::Value; use super::User; db_object! { - #[derive(Debug, Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Debug, Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "emergency_access"] #[changeset_options(treat_none_as_null="true")] - #[belongs_to(User, foreign_key = "grantor_uuid")] #[primary_key(uuid)] pub struct EmergencyAccess { pub uuid: String, diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index 4ff31939..fd67c60c 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,10 +1,8 @@ -use super::{Cipher, User}; +use super::User; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable)] #[table_name = "favorites"] - #[belongs_to(User, foreign_key = "user_uuid")] - #[belongs_to(Cipher, foreign_key = "cipher_uuid")] #[primary_key(user_uuid, cipher_uuid)] pub struct Favorite { pub user_uuid: String, @@ -80,4 +78,16 @@ impl Favorite { .map_res("Error removing favorites by user") }} } + + /// Return a vec with (cipher_uuid) this will only contain favorite flagged ciphers + /// This is used during a full sync so we only need one query for all favorite cipher matches. + pub async fn get_all_cipher_uuid_by_user(user_uuid: &str, conn: &DbConn) -> Vec<String> { + db_run! { conn: { + favorites::table + .filter(favorites::user_uuid.eq(user_uuid)) + .select(favorites::cipher_uuid) + .load::<String>(conn) + .unwrap_or_default() + }} + } } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 33976203..0b76704f 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,12 +1,11 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use super::{Cipher, User}; +use super::User; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "folders"] - #[belongs_to(User, foreign_key = "user_uuid")] #[primary_key(uuid)] pub struct Folder { pub uuid: String, @@ -16,10 +15,8 @@ db_object! { pub name: String, } - #[derive(Identifiable, Queryable, Insertable, Associations)] + #[derive(Identifiable, Queryable, Insertable)] #[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, @@ -215,4 +212,17 @@ impl FolderCipher { .from_db() }} } + + /// Return a vec with (cipher_uuid, folder_uuid) + /// This is used during a full sync so we only need one query for all folder matches. + pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<(String, String)> { + db_run! { conn: { + folders_ciphers::table + .inner_join(folders::table) + .filter(folders::user_uuid.eq(user_uuid)) + .select(folders_ciphers::all_columns) + .load::<(String, String)>(conn) + .unwrap_or_default() + }} + } } diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 04fc6f45..95045f3e 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -6,12 +6,11 @@ use crate::db::DbConn; use crate::error::MapResult; use crate::util::UpCase; -use super::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; +use super::{UserOrgStatus, UserOrgType, UserOrganization}; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "org_policies"] - #[belongs_to(Organization, foreign_key = "org_uuid")] #[primary_key(uuid)] pub struct OrgPolicy { pub uuid: String, diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 56af0c47..3a02867c 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -547,6 +547,15 @@ impl UserOrganization { }} } + pub async fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { + db_run! { conn: { + users_organizations::table + .filter(users_organizations::user_uuid.eq(user_uuid)) + .load::<UserOrganizationDb>(conn) + .expect("Error loading user organizations").from_db() + }} + } + pub async fn find_by_user_and_policy(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> Vec<Self> { db_run! { conn: { users_organizations::table diff --git a/src/db/models/send.rs b/src/db/models/send.rs index cc8fd4fa..8fff5143 100644 --- a/src/db/models/send.rs +++ b/src/db/models/send.rs @@ -1,14 +1,12 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use super::{Organization, User}; +use super::User; db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "sends"] #[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 Send { pub uuid: String, diff --git a/src/db/models/two_factor.rs b/src/db/models/two_factor.rs index 567a90ce..56d7e1e7 100644 --- a/src/db/models/two_factor.rs +++ b/src/db/models/two_factor.rs @@ -2,12 +2,9 @@ use serde_json::Value; use crate::{api::EmptyResult, db::DbConn, error::MapResult}; -use super::User; - db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "twofactor"] - #[belongs_to(User, foreign_key = "user_uuid")] #[primary_key(uuid)] pub struct TwoFactor { pub uuid: String, diff --git a/src/db/models/two_factor_incomplete.rs b/src/db/models/two_factor_incomplete.rs index 1f292a08..7f3021b4 100644 --- a/src/db/models/two_factor_incomplete.rs +++ b/src/db/models/two_factor_incomplete.rs @@ -2,12 +2,9 @@ use chrono::{NaiveDateTime, Utc}; use crate::{api::EmptyResult, auth::ClientIp, db::DbConn, error::MapResult, CONFIG}; -use super::User; - db_object! { - #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[derive(Identifiable, Queryable, Insertable, AsChangeset)] #[table_name = "twofactor_incomplete"] - #[belongs_to(User, foreign_key = "user_uuid")] #[primary_key(user_uuid, device_uuid)] pub struct TwoFactorIncomplete { pub user_uuid: String, |