From 8b8507f8cc4626dedac7272fdd89ddd18138cf63 Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 15:03:30 +0100 Subject: [PATCH] introduce cipher_id newtype --- src/api/core/accounts.rs | 6 +- src/api/core/ciphers.rs | 172 ++++++++++++++++++---------------- src/api/core/events.rs | 18 ++-- src/api/core/organizations.rs | 6 +- src/api/notifications.rs | 2 +- src/api/web.rs | 7 +- src/auth.rs | 6 +- src/db/models/attachment.rs | 16 +++- src/db/models/cipher.rs | 74 +++++++++++++-- src/db/models/collection.rs | 12 ++- src/db/models/event.rs | 12 +-- src/db/models/favorite.rs | 19 ++-- src/db/models/folder.rs | 20 ++-- src/db/models/mod.rs | 2 +- src/db/models/organization.rs | 18 +++- 15 files changed, 240 insertions(+), 150 deletions(-) diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index 38dc657b..03d339a1 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -488,13 +488,13 @@ fn validate_keydata( existing_sends: &[Send], ) -> EmptyResult { // Check that we're correctly rotating all the user's ciphers - let existing_cipher_ids = existing_ciphers.iter().map(|c| c.uuid.as_str()).collect::>(); + let existing_cipher_ids = existing_ciphers.iter().map(|c| &c.uuid).collect::>(); let provided_cipher_ids = data .ciphers .iter() .filter(|c| c.organization_id.is_none()) - .filter_map(|c| c.id.as_deref()) - .collect::>(); + .filter_map(|c| c.id.as_ref()) + .collect::>(); if !provided_cipher_ids.is_superset(&existing_cipher_ids) { err!("All existing ciphers must be included in the rotation") } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 3fbddc45..a391abdb 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -192,8 +192,8 @@ async fn get_ciphers(headers: Headers, mut conn: DbConn) -> Json { } #[get("/ciphers/")] -async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { +async fn get_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -205,13 +205,13 @@ async fn get_cipher(uuid: &str, headers: Headers, mut conn: DbConn) -> JsonResul } #[get("/ciphers//admin")] -async fn get_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_cipher_admin(uuid: CipherId, headers: Headers, conn: DbConn) -> JsonResult { // TODO: Implement this correctly get_cipher(uuid, headers, conn).await } #[get("/ciphers//details")] -async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult { +async fn get_cipher_details(uuid: CipherId, headers: Headers, conn: DbConn) -> JsonResult { get_cipher(uuid, headers, conn).await } @@ -219,7 +219,7 @@ async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonR #[serde(rename_all = "camelCase")] pub struct CipherData { // Id is optional as it is included only in bulk share - pub id: Option, + pub id: Option, // Folder id is not included in import pub folder_id: Option, // TODO: Some of these might appear all the time, no need for Option @@ -256,7 +256,7 @@ pub struct CipherData { // 'Attachments' is unused, contains map of {id: filename} #[allow(dead_code)] attachments: Option, - attachments2: Option>, + attachments2: Option>, // The revision datetime (in ISO 8601 format) of the client's local copy // of the cipher. This is used to prevent a client from updating a cipher @@ -620,7 +620,7 @@ async fn post_ciphers_import( /// Called when an org admin modifies an existing org cipher. #[put("/ciphers//admin", data = "")] async fn put_cipher_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -631,7 +631,7 @@ async fn put_cipher_admin( #[post("/ciphers//admin", data = "")] async fn post_cipher_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -641,13 +641,19 @@ async fn post_cipher_admin( } #[post("/ciphers/", data = "")] -async fn post_cipher(uuid: &str, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { +async fn post_cipher( + uuid: CipherId, + data: Json, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { put_cipher(uuid, data, headers, conn, nt).await } #[put("/ciphers/", data = "")] async fn put_cipher( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -655,7 +661,7 @@ async fn put_cipher( ) -> JsonResult { let data: CipherData = data.into_inner(); - let Some(mut cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(mut cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -674,21 +680,26 @@ async fn put_cipher( } #[post("/ciphers//partial", data = "")] -async fn post_cipher_partial(uuid: &str, data: Json, headers: Headers, conn: DbConn) -> JsonResult { +async fn post_cipher_partial( + uuid: CipherId, + data: Json, + headers: Headers, + conn: DbConn, +) -> JsonResult { put_cipher_partial(uuid, data, headers, conn).await } // Only update the folder and favorite for the user, since this cipher is read-only #[put("/ciphers//partial", data = "")] async fn put_cipher_partial( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, ) -> JsonResult { let data: PartialCipherData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -715,7 +726,7 @@ struct CollectionsAdminData { #[put("/ciphers//collections_v2", data = "")] async fn put_collections2_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -726,7 +737,7 @@ async fn put_collections2_update( #[post("/ciphers//collections_v2", data = "")] async fn post_collections2_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -742,7 +753,7 @@ async fn post_collections2_update( #[put("/ciphers//collections", data = "")] async fn put_collections_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -753,7 +764,7 @@ async fn put_collections_update( #[post("/ciphers//collections", data = "")] async fn post_collections_update( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -761,7 +772,7 @@ async fn post_collections_update( ) -> JsonResult { let data: CollectionsAdminData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -771,7 +782,7 @@ async fn post_collections_update( let posted_collections = HashSet::::from_iter(data.collection_ids); let current_collections = - HashSet::::from_iter(cipher.get_collections(headers.user.uuid.to_string(), &mut conn).await); + HashSet::::from_iter(cipher.get_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -819,7 +830,7 @@ async fn post_collections_update( #[put("/ciphers//collections-admin", data = "")] async fn put_collections_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, conn: DbConn, @@ -830,7 +841,7 @@ async fn put_collections_admin( #[post("/ciphers//collections-admin", data = "")] async fn post_collections_admin( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -838,7 +849,7 @@ async fn post_collections_admin( ) -> EmptyResult { let data: CollectionsAdminData = data.into_inner(); - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -847,9 +858,8 @@ async fn post_collections_admin( } let posted_collections = HashSet::::from_iter(data.collection_ids); - let current_collections = HashSet::::from_iter( - cipher.get_admin_collections(headers.user.uuid.to_string(), &mut conn).await, - ); + let current_collections = + HashSet::::from_iter(cipher.get_admin_collections(headers.user.uuid.clone(), &mut conn).await); for collection in posted_collections.symmetric_difference(¤t_collections) { match Collection::find_by_uuid_and_org(collection, cipher.organization_uuid.as_ref().unwrap(), &mut conn).await @@ -906,7 +916,7 @@ struct ShareCipherData { #[post("/ciphers//share", data = "")] async fn post_cipher_share( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -914,12 +924,12 @@ async fn post_cipher_share( ) -> JsonResult { let data: ShareCipherData = data.into_inner(); - share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await } #[put("/ciphers//share", data = "")] async fn put_cipher_share( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, @@ -927,7 +937,7 @@ async fn put_cipher_share( ) -> JsonResult { let data: ShareCipherData = data.into_inner(); - share_cipher_by_uuid(uuid, data, &headers, &mut conn, &nt).await + share_cipher_by_uuid(&uuid, data, &headers, &mut conn, &nt).await } #[derive(Deserialize)] @@ -976,7 +986,7 @@ async fn put_cipher_share_selected( } async fn share_cipher_by_uuid( - uuid: &str, + uuid: &CipherId, data: ShareCipherData, headers: &Headers, conn: &mut DbConn, @@ -1030,8 +1040,8 @@ async fn share_cipher_by_uuid( /// their object storage service. For self-hosted instances, it basically just /// redirects to the same location as before the v2 API. #[get("/ciphers//attachment/")] -async fn get_attachment(uuid: &str, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { +async fn get_attachment(uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1066,12 +1076,12 @@ enum FileUploadType { /// For self-hosted instances, it's another API on the local instance. #[post("/ciphers//attachment/v2", data = "")] async fn post_attachment_v2( - uuid: &str, + uuid: CipherId, data: Json, headers: Headers, mut conn: DbConn, ) -> JsonResult { - let Some(cipher) = Cipher::find_by_uuid(uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1121,7 +1131,7 @@ struct UploadData<'f> { /// database record, which is passed in as `attachment`. async fn save_attachment( mut attachment: Option, - cipher_uuid: &str, + cipher_uuid: CipherId, data: Form>, headers: &Headers, mut conn: DbConn, @@ -1136,7 +1146,7 @@ async fn save_attachment( err!("Attachment size can't be negative") } - let Some(cipher) = Cipher::find_by_uuid(cipher_uuid, &mut conn).await else { + let Some(cipher) = Cipher::find_by_uuid(&cipher_uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1250,11 +1260,11 @@ async fn save_attachment( err!("No attachment key provided") } let attachment = - Attachment::new(file_id.clone(), String::from(cipher_uuid), encrypted_filename.unwrap(), size, data.key); + Attachment::new(file_id.clone(), cipher_uuid.clone(), encrypted_filename.unwrap(), size, data.key); attachment.save(&mut conn).await.expect("Error saving attachment"); } - let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid); + let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid.as_ref()); let file_path = folder_path.join(&file_id); tokio::fs::create_dir_all(&folder_path).await?; @@ -1294,7 +1304,7 @@ async fn save_attachment( /// with this one. #[post("/ciphers//attachment/", format = "multipart/form-data", data = "", rank = 1)] async fn post_attachment_v2_data( - uuid: &str, + uuid: CipherId, attachment_id: &str, data: Form>, headers: Headers, @@ -1315,7 +1325,7 @@ async fn post_attachment_v2_data( /// Legacy API for creating an attachment associated with a cipher. #[post("/ciphers//attachment", format = "multipart/form-data", data = "")] async fn post_attachment( - uuid: &str, + uuid: CipherId, data: Form>, headers: Headers, conn: DbConn, @@ -1332,7 +1342,7 @@ async fn post_attachment( #[post("/ciphers//attachment-admin", format = "multipart/form-data", data = "")] async fn post_attachment_admin( - uuid: &str, + uuid: CipherId, data: Form>, headers: Headers, conn: DbConn, @@ -1343,20 +1353,20 @@ async fn post_attachment_admin( #[post("/ciphers//attachment//share", format = "multipart/form-data", data = "")] async fn post_attachment_share( - uuid: &str, + uuid: CipherId, attachment_id: &str, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await?; + _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await?; post_attachment(uuid, data, headers, conn, nt).await } #[post("/ciphers//attachment//delete-admin")] async fn delete_attachment_post_admin( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, conn: DbConn, @@ -1367,7 +1377,7 @@ async fn delete_attachment_post_admin( #[post("/ciphers//attachment//delete")] async fn delete_attachment_post( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, conn: DbConn, @@ -1378,58 +1388,58 @@ async fn delete_attachment_post( #[delete("/ciphers//attachment/")] async fn delete_attachment( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await } #[delete("/ciphers//attachment//admin")] async fn delete_attachment_admin( - uuid: &str, + uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await } #[post("/ciphers//delete")] -async fn delete_cipher_post(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_post(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[post("/ciphers//delete-admin")] -async fn delete_cipher_post_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_post_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[put("/ciphers//delete")] -async fn delete_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await +async fn delete_cipher_put(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await // soft delete } #[put("/ciphers//delete-admin")] -async fn delete_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, true, &nt).await +async fn delete_cipher_put_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, true, &nt).await } #[delete("/ciphers/")] -async fn delete_cipher(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } #[delete("/ciphers//admin")] -async fn delete_cipher_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { - _delete_cipher_by_uuid(uuid, &headers, &mut conn, false, &nt).await +async fn delete_cipher_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { + _delete_cipher_by_uuid(&uuid, &headers, &mut conn, false, &nt).await // permanent delete } @@ -1494,13 +1504,13 @@ async fn delete_cipher_selected_put_admin( } #[put("/ciphers//restore")] -async fn restore_cipher_put(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await +async fn restore_cipher_put(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await } #[put("/ciphers//restore-admin")] -async fn restore_cipher_put_admin(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { - _restore_cipher_by_uuid(uuid, &headers, &mut conn, &nt).await +async fn restore_cipher_put_admin(uuid: CipherId, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> JsonResult { + _restore_cipher_by_uuid(&uuid, &headers, &mut conn, &nt).await } #[put("/ciphers/restore", data = "")] @@ -1517,7 +1527,7 @@ async fn restore_cipher_selected( #[serde(rename_all = "camelCase")] struct MoveCipherData { folder_id: Option, - ids: Vec, + ids: Vec, } #[post("/ciphers/move", data = "")] @@ -1640,7 +1650,7 @@ async fn delete_all( } async fn _delete_cipher_by_uuid( - uuid: &str, + uuid: &CipherId, headers: &Headers, conn: &mut DbConn, soft_delete: bool, @@ -1695,7 +1705,7 @@ async fn _delete_cipher_by_uuid( #[derive(Deserialize)] #[serde(rename_all = "camelCase")] struct CipherIdsData { - ids: Vec, + ids: Vec, } async fn _delete_multiple_ciphers( @@ -1716,7 +1726,7 @@ async fn _delete_multiple_ciphers( Ok(()) } -async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { +async fn _restore_cipher_by_uuid(uuid: &CipherId, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>) -> JsonResult { let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else { err!("Cipher doesn't exist") }; @@ -1778,7 +1788,7 @@ async fn _restore_multiple_ciphers( } async fn _delete_cipher_attachment_by_id( - uuid: &str, + uuid: &CipherId, attachment_id: &str, headers: &Headers, conn: &mut DbConn, @@ -1788,7 +1798,7 @@ async fn _delete_cipher_attachment_by_id( err!("Attachment doesn't exist") }; - if attachment.cipher_uuid != uuid { + if &attachment.cipher_uuid != uuid { err!("Attachment from other cipher") } @@ -1832,10 +1842,10 @@ async fn _delete_cipher_attachment_by_id( /// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed. /// 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_favorites: HashSet, - pub cipher_collections: HashMap>, + pub cipher_attachments: HashMap>, + pub cipher_folders: HashMap, + pub cipher_favorites: HashSet, + pub cipher_collections: HashMap>, pub members: HashMap, pub user_collections: HashMap, pub user_collections_groups: HashMap, @@ -1850,8 +1860,8 @@ 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_favorites: HashSet; + let cipher_folders: HashMap; + let cipher_favorites: HashSet; match sync_type { // User Sync supports Folders and Favorites CipherSyncType::User => { @@ -1872,14 +1882,14 @@ impl CipherSyncData { // Generate a list of Cipher UUID's containing a Vec with one or more Attachment records let orgs = Membership::get_orgs_by_user(user_uuid, conn).await; let attachments = Attachment::find_all_by_user_and_orgs(user_uuid, &orgs, conn).await; - let mut cipher_attachments: HashMap> = HashMap::with_capacity(attachments.len()); + let mut cipher_attachments: HashMap> = HashMap::with_capacity(attachments.len()); for attachment in attachments { cipher_attachments.entry(attachment.cipher_uuid.clone()).or_default().push(attachment); } // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's - let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_uuid.to_string(), conn).await; - let mut cipher_collections: HashMap> = + let user_cipher_collections = Cipher::get_collections_with_cipher_by_user(user_uuid.clone(), conn).await; + let mut cipher_collections: HashMap> = HashMap::with_capacity(user_cipher_collections.len()); for (cipher, collection) in user_cipher_collections { cipher_collections.entry(cipher).or_default().push(collection); diff --git a/src/api/core/events.rs b/src/api/core/events.rs index a49dc785..ac53744c 100644 --- a/src/api/core/events.rs +++ b/src/api/core/events.rs @@ -8,7 +8,7 @@ use crate::{ api::{EmptyResult, JsonResult}, auth::{AdminHeaders, Headers}, db::{ - models::{Cipher, Event, Membership, MembershipId, OrganizationId, UserId}, + models::{Cipher, CipherId, Event, Membership, MembershipId, OrganizationId, UserId}, DbConn, DbPool, }, util::parse_date, @@ -59,14 +59,14 @@ async fn get_org_events(org_id: &str, data: EventRange, _headers: AdminHeaders, } #[get("/ciphers//events?")] -async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_cipher_events(cipher_id: CipherId, data: EventRange, headers: Headers, mut conn: DbConn) -> JsonResult { // Return an empty vec when we org events are disabled. // This prevents client errors let events_json: Vec = if !CONFIG.org_events_enabled() { Vec::with_capacity(0) } else { let mut events_json = Vec::with_capacity(0); - if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, cipher_id, &mut conn).await { + if Membership::user_has_ge_admin_access_to_cipher(&headers.user.uuid, &cipher_id, &mut conn).await { let start_date = parse_date(&data.start); let end_date = if let Some(before_date) = &data.continuation_token { parse_date(before_date) @@ -74,7 +74,7 @@ async fn get_cipher_events(cipher_id: &str, data: EventRange, headers: Headers, parse_date(&data.end) }; - events_json = Event::find_by_cipher_uuid(cipher_id, &start_date, &end_date, &mut conn) + events_json = Event::find_by_cipher_uuid(&cipher_id, &start_date, &end_date, &mut conn) .await .iter() .map(|e| e.to_json()) @@ -152,7 +152,7 @@ struct EventCollection { date: String, // Optional - cipher_id: Option, + cipher_id: Option, organization_id: Option, } @@ -290,19 +290,19 @@ async fn _log_event( // 1000..=1099 Are user events, they need to be logged via log_user_event() // Cipher Events 1100..=1199 => { - event.cipher_uuid = Some(String::from(source_uuid)); + event.cipher_uuid = Some(source_uuid.to_string().into()); } // Collection Events 1300..=1399 => { - event.collection_uuid = Some(String::from(source_uuid)); + event.collection_uuid = Some(source_uuid.to_string().into()); } // Group Events 1400..=1499 => { - event.group_uuid = Some(String::from(source_uuid)); + event.group_uuid = Some(source_uuid.to_string().into()); } // Org User Events 1500..=1599 => { - event.org_user_uuid = Some(String::from(source_uuid)); + event.org_user_uuid = Some(source_uuid.to_string().into()); } // 1600..=1699 Are organizational events, and they do not need the source_uuid // Policy Events diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 6b5048bc..15168d94 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1645,7 +1645,7 @@ async fn post_org_import( let headers: Headers = headers.into(); - let mut ciphers: Vec = Vec::with_capacity(data.ciphers.len()); + let mut ciphers: Vec = Vec::with_capacity(data.ciphers.len()); for mut cipher_data in data.ciphers { // Always clear folder_id's via an organization import cipher_data.folder_id = None; @@ -1670,7 +1670,7 @@ async fn post_org_import( #[allow(dead_code)] struct BulkCollectionsData { organization_id: OrganizationId, - cipher_ids: Vec, + cipher_ids: Vec, collection_ids: HashSet, remove_collections: bool, } @@ -2659,7 +2659,7 @@ async fn get_group_users( err!("Group support is disabled"); } - if Group::find_by_uuid_and_org(&&group_id, &org_id, &mut conn).await.is_none() { + if Group::find_by_uuid_and_org(&group_id, &org_id, &mut conn).await.is_none() { err!("Group could not be found!", "Group uuid is invalid or does not belong to the organization") }; diff --git a/src/api/notifications.rs b/src/api/notifications.rs index 832e5668..d9d3a60f 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -437,7 +437,7 @@ impl WebSocketUsers { let data = create_update( vec![ - ("Id".into(), cipher.uuid.clone().into()), + ("Id".into(), cipher.uuid.to_string().into()), ("UserId".into(), user_uuid), ("OrganizationId".into(), org_uuid), ("CollectionIds".into(), collection_uuids), diff --git a/src/api/web.rs b/src/api/web.rs index a96d7e2a..1439fb1e 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -13,6 +13,7 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, + db::models::CipherId, error::Error, util::{get_web_vault_version, Cached, SafeString}, CONFIG, @@ -196,15 +197,15 @@ async fn web_files(p: PathBuf) -> Cached> { } #[get("/attachments//?")] -async fn attachments(uuid: SafeString, file_id: SafeString, token: String) -> Option { +async fn attachments(uuid: CipherId, file_id: SafeString, token: String) -> Option { let Ok(claims) = decode_file_download(&token) else { return None; }; - if claims.sub != *uuid || claims.file_id != *file_id { + if claims.sub != uuid || claims.file_id != *file_id { return None; } - NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file_id)).await.ok() + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id)).await.ok() } // We use DbConn here to let the alive healthcheck also verify the database connection. diff --git a/src/auth.rs b/src/auth.rs index 2a7c33d6..565c3630 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{CollectionId, MembershipId, OrganizationId, UserId}; +use crate::db::models::{CipherId, CollectionId, MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -293,12 +293,12 @@ pub struct FileDownloadClaims { // Issuer pub iss: String, // Subject - pub sub: String, + pub sub: CipherId, pub file_id: String, } -pub fn generate_file_download_claims(uuid: String, file_id: String) -> FileDownloadClaims { +pub fn generate_file_download_claims(uuid: CipherId, file_id: String) -> FileDownloadClaims { let time_now = Utc::now(); FileDownloadClaims { nbf: time_now.timestamp(), diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 443704ac..156cdadb 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -3,7 +3,7 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; use serde_json::Value; -use super::{OrganizationId, UserId}; +use super::{CipherId, OrganizationId, UserId}; use crate::CONFIG; db_object! { @@ -13,7 +13,7 @@ db_object! { #[diesel(primary_key(id))] pub struct Attachment { pub id: String, - pub cipher_uuid: String, + pub cipher_uuid: CipherId, pub file_name: String, // encrypted pub file_size: i64, pub akey: Option, @@ -22,7 +22,13 @@ db_object! { /// Local methods impl Attachment { - pub const fn new(id: String, cipher_uuid: String, file_name: String, file_size: i64, akey: Option) -> Self { + pub const fn new( + id: String, + cipher_uuid: CipherId, + file_name: String, + file_size: i64, + akey: Option, + ) -> Self { Self { id, cipher_uuid, @@ -118,7 +124,7 @@ impl Attachment { }} } - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { for attachment in Attachment::find_by_cipher(cipher_uuid, conn).await { attachment.delete(conn).await?; } @@ -135,7 +141,7 @@ impl Attachment { }} } - pub async fn find_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> Vec { + pub async fn find_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> Vec { db_run! { conn: { attachments::table .filter(attachments::cipher_uuid.eq(cipher_uuid)) diff --git a/src/db/models/cipher.rs b/src/db/models/cipher.rs index 56e272bf..29d5bbeb 100644 --- a/src/db/models/cipher.rs +++ b/src/db/models/cipher.rs @@ -1,7 +1,13 @@ use crate::util::LowerCase; use crate::CONFIG; use chrono::{NaiveDateTime, TimeDelta, Utc}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{ Attachment, CollectionCipher, CollectionId, Favorite, FolderCipher, Group, Membership, MembershipStatus, @@ -18,7 +24,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(uuid))] pub struct Cipher { - pub uuid: String, + pub uuid: CipherId, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, @@ -58,7 +64,7 @@ impl Cipher { let now = Utc::now().naive_utc(); Self { - uuid: crate::util::get_uuid(), + uuid: CipherId(crate::util::get_uuid()), created_at: now, updated_at: now, @@ -279,7 +285,7 @@ impl Cipher { Cow::from(Vec::with_capacity(0)) } } else { - Cow::from(self.get_admin_collections(user_uuid.to_string(), conn).await) + Cow::from(self.get_admin_collections(user_uuid.clone(), conn).await) }; // There are three types of cipher response models in upstream @@ -683,7 +689,7 @@ impl Cipher { }} } - pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid(uuid: &CipherId, conn: &mut DbConn) -> Option { db_run! {conn: { ciphers::table .filter(ciphers::uuid.eq(uuid)) @@ -693,7 +699,7 @@ impl Cipher { }} } - pub async fn find_by_uuid_and_org(cipher_uuid: &str, org_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_uuid_and_org(cipher_uuid: &CipherId, org_uuid: &str, conn: &mut DbConn) -> Option { db_run! {conn: { ciphers::table .filter(ciphers::uuid.eq(cipher_uuid)) @@ -862,7 +868,7 @@ impl Cipher { }} } - pub async fn get_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_collections(&self, user_id: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -921,7 +927,7 @@ impl Cipher { } } - pub async fn get_admin_collections(&self, user_id: String, conn: &mut DbConn) -> Vec { + pub async fn get_admin_collections(&self, user_id: UserId, conn: &mut DbConn) -> Vec { if CONFIG.org_groups_enabled() { db_run! {conn: { ciphers_collections::table @@ -985,9 +991,9 @@ impl Cipher { /// 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: String, + user_id: UserId, conn: &mut DbConn, - ) -> Vec<(String, CollectionId)> { + ) -> Vec<(CipherId, CollectionId)> { db_run! {conn: { ciphers_collections::table .inner_join(collections::table.on( @@ -1021,7 +1027,55 @@ impl Cipher { .or_filter(collections_groups::collections_uuid.is_not_null()) //Access via group .select(ciphers_collections::all_columns) .distinct() - .load::<(String, CollectionId)>(conn).unwrap_or_default() + .load::<(CipherId, CollectionId)>(conn).unwrap_or_default() }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct CipherId(String); + +impl AsRef for CipherId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for CipherId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for CipherId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for CipherId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for CipherId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for CipherId { + 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/collection.rs b/src/db/models/collection.rs index ed3b762b..1b8c11bf 100644 --- a/src/db/models/collection.rs +++ b/src/db/models/collection.rs @@ -6,7 +6,9 @@ use std::{ ops::Deref, }; -use super::{CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId}; +use super::{ + CipherId, CollectionGroup, GroupUser, Membership, MembershipStatus, MembershipType, OrganizationId, User, UserId, +}; use crate::CONFIG; db_object! { @@ -34,7 +36,7 @@ db_object! { #[diesel(table_name = ciphers_collections)] #[diesel(primary_key(cipher_uuid, collection_uuid))] pub struct CollectionCipher { - pub cipher_uuid: String, + pub cipher_uuid: CipherId, pub collection_uuid: CollectionId, } } @@ -710,7 +712,7 @@ impl CollectionUser { /// Database methods impl CollectionCipher { - pub async fn save(cipher_uuid: &str, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { + pub async fn save(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: @@ -740,7 +742,7 @@ impl CollectionCipher { } } - pub async fn delete(cipher_uuid: &str, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { + pub async fn delete(cipher_uuid: &CipherId, collection_uuid: &CollectionId, conn: &mut DbConn) -> EmptyResult { Self::update_users_revision(collection_uuid, conn).await; db_run! { conn: { @@ -754,7 +756,7 @@ impl CollectionCipher { }} } - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(ciphers_collections::table.filter(ciphers_collections::cipher_uuid.eq(cipher_uuid))) .execute(conn) diff --git a/src/db/models/event.rs b/src/db/models/event.rs index f52190ec..c840cd47 100644 --- a/src/db/models/event.rs +++ b/src/db/models/event.rs @@ -1,7 +1,7 @@ use crate::db::DbConn; use serde_json::Value; -use super::{OrganizationId, UserId}; +use super::{CipherId, CollectionId, GroupId, MembershipId, OrganizationId, UserId}; use crate::{api::EmptyResult, error::MapResult, CONFIG}; use chrono::{NaiveDateTime, TimeDelta, Utc}; @@ -20,10 +20,10 @@ db_object! { pub event_type: i32, // EventType pub user_uuid: Option, pub org_uuid: Option, - pub cipher_uuid: Option, - pub collection_uuid: Option, - pub group_uuid: Option, - pub org_user_uuid: Option, + pub cipher_uuid: Option, + pub collection_uuid: Option, + pub group_uuid: Option, + pub org_user_uuid: Option, pub act_user_uuid: Option, // Upstream enum: https://github.com/bitwarden/server/blob/8a22c0479e987e756ce7412c48a732f9002f0a2d/src/Core/Enums/DeviceType.cs pub device_type: Option, @@ -298,7 +298,7 @@ impl Event { } pub async fn find_by_cipher_uuid( - cipher_uuid: &str, + cipher_uuid: &CipherId, start: &NaiveDateTime, end: &NaiveDateTime, conn: &mut DbConn, diff --git a/src/db/models/favorite.rs b/src/db/models/favorite.rs index 14f89eaf..de2e0feb 100644 --- a/src/db/models/favorite.rs +++ b/src/db/models/favorite.rs @@ -1,4 +1,4 @@ -use super::{User, UserId}; +use super::{CipherId, User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable)] @@ -6,7 +6,7 @@ db_object! { #[diesel(primary_key(user_uuid, cipher_uuid))] pub struct Favorite { pub user_uuid: UserId, - pub cipher_uuid: String, + pub cipher_uuid: CipherId, } } @@ -17,7 +17,7 @@ use crate::error::MapResult; impl Favorite { // Returns whether the specified cipher is a favorite of the specified user. - pub async fn is_favorite(cipher_uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> bool { + pub async fn is_favorite(cipher_uuid: &CipherId, user_uuid: &UserId, conn: &mut DbConn) -> bool { db_run! { conn: { let query = favorites::table .filter(favorites::cipher_uuid.eq(cipher_uuid)) @@ -29,7 +29,12 @@ impl Favorite { } // Sets whether the specified cipher is a favorite of the specified user. - pub async fn set_favorite(favorite: bool, cipher_uuid: &str, user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult { + pub async fn set_favorite( + favorite: bool, + cipher_uuid: &CipherId, + user_uuid: &UserId, + conn: &mut DbConn, + ) -> EmptyResult { let (old, new) = (Self::is_favorite(cipher_uuid, user_uuid, conn).await, favorite); match (old, new) { (false, true) => { @@ -62,7 +67,7 @@ impl Favorite { } // Delete all favorite entries associated with the specified cipher. - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(favorites::table.filter(favorites::cipher_uuid.eq(cipher_uuid))) .execute(conn) @@ -81,12 +86,12 @@ impl Favorite { /// 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: &UserId, conn: &mut DbConn) -> Vec { + pub async fn get_all_cipher_uuid_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec { db_run! { conn: { favorites::table .filter(favorites::user_uuid.eq(user_uuid)) .select(favorites::cipher_uuid) - .load::(conn) + .load::(conn) .unwrap_or_default() }} } diff --git a/src/db/models/folder.rs b/src/db/models/folder.rs index 9666e159..3e2f3e98 100644 --- a/src/db/models/folder.rs +++ b/src/db/models/folder.rs @@ -1,7 +1,7 @@ use chrono::{NaiveDateTime, Utc}; use serde_json::Value; -use super::{User, UserId}; +use super::{CipherId, User, UserId}; db_object! { #[derive(Identifiable, Queryable, Insertable, AsChangeset)] @@ -19,7 +19,7 @@ db_object! { #[diesel(table_name = folders_ciphers)] #[diesel(primary_key(cipher_uuid, folder_uuid))] pub struct FolderCipher { - pub cipher_uuid: String, + pub cipher_uuid: CipherId, pub folder_uuid: String, } } @@ -52,10 +52,10 @@ impl Folder { } impl FolderCipher { - pub fn new(folder_uuid: &str, cipher_uuid: &str) -> Self { + pub fn new(folder_uuid: &str, cipher_uuid: &CipherId) -> Self { Self { folder_uuid: folder_uuid.to_string(), - cipher_uuid: cipher_uuid.to_string(), + cipher_uuid: cipher_uuid.clone(), } } } @@ -177,7 +177,7 @@ impl FolderCipher { }} } - pub async fn delete_all_by_cipher(cipher_uuid: &str, conn: &mut DbConn) -> EmptyResult { + pub async fn delete_all_by_cipher(cipher_uuid: &CipherId, conn: &mut DbConn) -> EmptyResult { db_run! { conn: { diesel::delete(folders_ciphers::table.filter(folders_ciphers::cipher_uuid.eq(cipher_uuid))) .execute(conn) @@ -193,7 +193,11 @@ impl FolderCipher { }} } - pub async fn find_by_folder_and_cipher(folder_uuid: &str, cipher_uuid: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_folder_and_cipher( + folder_uuid: &str, + cipher_uuid: &CipherId, + conn: &mut DbConn, + ) -> Option { db_run! { conn: { folders_ciphers::table .filter(folders_ciphers::folder_uuid.eq(folder_uuid)) @@ -216,13 +220,13 @@ 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<(String, String)> { + pub async fn find_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<(CipherId, 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) + .load::<(CipherId, String)>(conn) .unwrap_or_default() }} } diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index e10951e5..4a9ba70a 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -18,7 +18,7 @@ mod user; pub use self::attachment::Attachment; pub use self::auth_request::AuthRequest; -pub use self::cipher::{Cipher, RepromptType}; +pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; pub use self::device::{Device, DeviceType}; pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 016bd3da..15bd25af 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -11,8 +11,8 @@ use std::{ }; use super::{ - Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, OrgPolicyType, - TwoFactor, User, UserId, + CipherId, Collection, CollectionGroup, CollectionId, CollectionUser, Group, GroupId, GroupUser, OrgPolicy, + OrgPolicyType, TwoFactor, User, UserId, }; use crate::CONFIG; @@ -897,7 +897,11 @@ impl Membership { }} } - pub async fn find_by_cipher_and_org(cipher_uuid: &str, org_uuid: &OrganizationId, conn: &mut DbConn) -> Vec { + pub async fn find_by_cipher_and_org( + cipher_uuid: &CipherId, + org_uuid: &OrganizationId, + conn: &mut DbConn, + ) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::org_uuid.eq(org_uuid)) @@ -921,7 +925,7 @@ impl Membership { } pub async fn find_by_cipher_and_org_with_group( - cipher_uuid: &str, + cipher_uuid: &CipherId, org_uuid: &OrganizationId, conn: &mut DbConn, ) -> Vec { @@ -950,7 +954,11 @@ impl Membership { }} } - pub async fn user_has_ge_admin_access_to_cipher(user_uuid: &UserId, cipher_uuid: &str, conn: &mut DbConn) -> bool { + pub async fn user_has_ge_admin_access_to_cipher( + user_uuid: &UserId, + cipher_uuid: &CipherId, + conn: &mut DbConn, + ) -> bool { db_run! { conn: { users_organizations::table .inner_join(ciphers::table.on(ciphers::uuid.eq(cipher_uuid).and(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))))