diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 03d339a1..c18179e3 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -445,7 +445,7 @@ struct UpdateFolderData { // There is a bug in 2024.3.x which adds a `null` item. // To bypass this we allow a Option here, but skip it during the updates // See: https://github.com/bitwarden/clients/issues/8453 - id: Option, + id: Option, name: String, } @@ -500,8 +500,8 @@ fn validate_keydata( } // Check that we're correctly rotating all the user's folders - let existing_folder_ids = existing_folders.iter().map(|f| f.uuid.as_str()).collect::>(); - let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_deref()).collect::>(); + let existing_folder_ids = existing_folders.iter().map(|f| &f.uuid).collect::>(); + let provided_folder_ids = data.folders.iter().filter_map(|f| f.id.as_ref()).collect::>(); if !provided_folder_ids.is_superset(&existing_folder_ids) { err!("All existing folders must be included in the rotation") } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index a391abdb..758ec3b7 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -221,7 +221,7 @@ pub struct CipherData { // Id is optional as it is included only in bulk share pub id: Option, // Folder id is not included in import - pub folder_id: Option, + pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option #[serde(alias = "organizationID")] pub organization_id: Option, @@ -270,7 +270,7 @@ pub struct CipherData { #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] pub struct PartialCipherData { - folder_id: Option, + folder_id: Option, favorite: bool, } @@ -579,9 +579,9 @@ async fn post_ciphers_import( Cipher::validate_cipher_data(&data.ciphers)?; // Read and create the folders - let existing_folders: HashSet> = + let existing_folders: HashSet> = Folder::find_by_user(&headers.user.uuid, &mut conn).await.into_iter().map(|f| Some(f.uuid)).collect(); - let mut folders: Vec = Vec::with_capacity(data.folders.len()); + let mut folders: Vec = Vec::with_capacity(data.folders.len()); for folder in data.folders.into_iter() { let folder_uuid = if existing_folders.contains(&folder.id) { folder.id.unwrap() @@ -1526,7 +1526,7 @@ async fn restore_cipher_selected( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct MoveCipherData { - folder_id: Option, + folder_id: Option, ids: Vec, } @@ -1843,7 +1843,7 @@ async fn _delete_cipher_attachment_by_id( /// This will not improve the speed of a single cipher.to_json() call that much, so better not to use it for those calls. pub struct CipherSyncData { pub cipher_attachments: HashMap>, - pub cipher_folders: HashMap, + pub cipher_folders: HashMap, pub cipher_favorites: HashSet, pub cipher_collections: HashMap>, pub members: HashMap, @@ -1860,7 +1860,7 @@ pub enum CipherSyncType { impl CipherSyncData { pub async fn new(user_uuid: &UserId, sync_type: CipherSyncType, conn: &mut DbConn) -> Self { - let cipher_folders: HashMap; + let cipher_folders: HashMap; let cipher_favorites: HashSet; match sync_type { // User Sync supports Folders and Favorites diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 3daa2a06..708bedff 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -24,8 +24,8 @@ async fn get_folders(headers: Headers, mut conn: DbConn) -> Json { } #[get("/folders/")] -async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - match Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await { +async fn get_folder(uuid: FolderId, headers: Headers, mut conn: DbConn) -> JsonResult { + match Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await { Some(folder) => Ok(Json(folder.to_json())), _ => err!("Invalid folder", "Folder does not exist or belongs to another user"), } @@ -35,7 +35,7 @@ async fn get_folder(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul #[serde(rename_all = "camelCase")] pub struct FolderData { pub name: String, - pub id: Option, + pub id: Option, } #[post("/folders", data = "")] @@ -51,13 +51,19 @@ async fn post_folders(data: Json, headers: Headers, mut conn: DbConn } #[post("/folders/", data = "")] -async fn post_folder(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn post_folder( + uuid: FolderId, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { put_folder(uuid, data, headers, conn, nt).await } #[put("/folders/", data = "")] async fn put_folder( - uuid: &str, + uuid: FolderId, data: Json, headers: Headers, mut conn: DbConn, @@ -65,7 +71,7 @@ async fn put_folder( ) -> JsonResult { let data: FolderData = data.into_inner(); - let Some(mut folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { + let Some(mut folder) = Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; @@ -78,13 +84,13 @@ async fn put_folder( } #[post("/folders//delete")] -async fn delete_folder_post(uuid: &str, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { +async fn delete_folder_post(uuid: FolderId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> EmptyResult { delete_folder(uuid, headers, conn, nt).await } #[delete("/folders/")] -async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - let Some(folder) = Folder::find_by_uuid_and_user(uuid, &headers.user.uuid, &mut conn).await else { +async fn delete_folder(uuid: FolderId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + let Some(folder) = Folder::find_by_uuid_and_user(&uuid, &headers.user.uuid, &mut conn).await else { err!("Invalid folder", "Folder does not exist or belongs to another user") }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index d9d3a60f..6653799a 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -392,7 +392,7 @@ impl WebSocketUsers { } let data = create_update( vec![ - ("Id".into(), folder.uuid.clone().into()), + ("Id".into(), folder.uuid.to_string().into()), ("UserId".into(), folder.user_uuid.to_string().into()), ("RevisionDate".into(), serialize_date(folder.updated_at)), ], diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 29d5bbeb..933b50ee 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -10,7 +10,7 @@ use std::{ }; use super::{ - Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, Group, Membership, MembershipStatus, + Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, FolderId, Group, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, }; @@ -334,7 +334,7 @@ impl Cipher { // Skip adding these fields in that case if sync_type == CipherSyncType::User { json_object["folderId"] = json!(if let Some(cipher_sync_data) = cipher_sync_data { - cipher_sync_data.cipher_folders.get(&self.uuid).map(|c| c.to_string()) + cipher_sync_data.cipher_folders.get(&self.uuid).cloned() } else { self.get_folder_uuid(user_uuid, conn).await }); @@ -469,7 +469,7 @@ impl Cipher { pub async fn move_to_folder( &self, - folder_uuid: Option, + folder_uuid: Option, user_uuid: &UserId, conn: &mut DbConn, ) -> EmptyResult { @@ -478,23 +478,25 @@ impl Cipher { match (self.get_folder_uuid(user_uuid, conn).await, folder_uuid) { // No changes (None, None) => Ok(()), - (Some(ref old), Some(ref new)) if old == new => Ok(()), + (Some(ref old_folder), Some(ref new_folder)) if old_folder == new_folder => Ok(()), // Add to folder - (None, Some(new)) => FolderCipher::new(&new, &self.uuid).save(conn).await, + (None, Some(new_folder)) => FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await, // Remove from folder - (Some(old), None) => match FolderCipher::find_by_folder_and_cipher(&old, &self.uuid, conn).await { - Some(old) => old.delete(conn).await, - None => err!("Couldn't move from previous folder"), - }, + (Some(old_folder), None) => { + match FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await { + Some(old_folder) => old_folder.delete(conn).await, + 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).await { - old.delete(conn).await?; + (Some(old_folder), Some(new_folder)) => { + if let Some(old_folder) = FolderCipher::find_by_folder_and_cipher(&old_folder, &self.uuid, conn).await { + old_folder.delete(conn).await?; } - FolderCipher::new(&new, &self.uuid).save(conn).await + FolderCipher::new(new_folder, self.uuid.clone()).save(conn).await } } } @@ -677,14 +679,14 @@ impl Cipher { } } - pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn get_folder_uuid(&self, user_uuid: &UserId, conn: &mut DbConn) -> Option { 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::(conn) + .first::(conn) .ok() }} } @@ -850,7 +852,7 @@ impl Cipher { }} } - pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec { db_run! {conn: { folders_ciphers::table.inner_join(ciphers::table) .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 3e2f3e98..6a669ade 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,5 +1,11 @@ use chrono::{NaiveDateTime, Utc}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{CipherId, User, UserId}; @@ -8,7 +14,7 @@ db_object! { #[diesel(table_name = folders)] #[diesel(primary_key(uuid))] pub struct Folder { - pub uuid: String, + pub uuid: FolderId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, pub user_uuid: UserId, @@ -20,7 +26,7 @@ db_object! { #[diesel(primary_key(cipher_uuid, folder_uuid))] pub struct FolderCipher { pub cipher_uuid: CipherId, - pub folder_uuid: String, + pub folder_uuid: FolderId, } } @@ -30,7 +36,7 @@ impl Folder { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: FolderId(crate::util::get_uuid()), created_at: now, updated_at: now, @@ -52,10 +58,10 @@ impl Folder { } impl FolderCipher { - pub fn new(folder_uuid: &str, cipher_uuid: &CipherId) -> Self { + pub fn new(folder_uuid: FolderId, cipher_uuid: CipherId) -> Self { Self { - folder_uuid: folder_uuid.to_string(), - cipher_uuid: cipher_uuid.clone(), + folder_uuid, + cipher_uuid, } } } @@ -120,7 +126,7 @@ impl Folder { Ok(()) } - pub async fn find_by_uuid_and_user(uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_user(uuid: &FolderId, user_uuid: &UserId, conn: &mut DbConn) -> Option { db_run! { conn: { folders::table .filter(folders::uuid.eq(uuid)) @@ -185,7 +191,7 @@ impl FolderCipher { }} } - pub async fn delete_all_by_folder(folder_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(folders_ciphers::table.filter(folders_ciphers::folder_uuid.eq(folder_uuid))) .execute(conn) @@ -194,7 +200,7 @@ impl FolderCipher { } pub async fn find_by_folder_and_cipher( - folder_uuid: &str, + folder_uuid: &FolderId, cipher_uuid: &CipherId, conn: &mut DbConn, ) -> Option { @@ -208,7 +214,7 @@ impl FolderCipher { }} } - pub async fn find_by_folder(folder_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_folder(folder_uuid: &FolderId, conn: &mut DbConn) -> Vec { db_run! { conn: { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) @@ -220,14 +226,62 @@ impl FolderCipher { /// 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: &UserId, conn: &mut DbConn) -> Vec<(CipherId, String)> { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, FolderId)> { db_run! { conn: { folders_ciphers::table .inner_join(folders::table) .filter(folders::user_uuid.eq(user_uuid)) .select(folders_ciphers::all_columns) - .load::<(CipherId, String)>(conn) + .load::<(CipherId, FolderId)>(conn) .unwrap_or_default() }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct FolderId(String); + +impl AsRef for FolderId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for FolderId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for FolderId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for FolderId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for FolderId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for FolderId { + type Error = (); + + #[inline(always)] + fn from_param(param: &'r str) -> Result { + if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { + Ok(Self(param.to_string())) + } else { + Err(()) + } + } +} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index 4a9ba70a..d832c04d 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -24,7 +24,7 @@ pub use self::device::{Device, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; pub use self::event::{Event, EventType}; pub use self::favorite::Favorite; -pub use self::folder::{Folder, FolderCipher}; +pub use self::folder::{Folder, FolderCipher, FolderId}; pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; pub use self::organization::{