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

ciphers.rs (47930B)


      1 use super::folders::FolderData;
      2 use crate::{
      3     api::{self, EmptyResult, JsonResult, PasswordOrOtpData},
      4     auth::Headers,
      5     db::{
      6         models::{
      7             Cipher, Collection, CollectionCipher, CollectionUser, Favorite, Folder, FolderCipher,
      8             OrgPolicy, OrgPolicyType, UserOrgType, UserOrganization,
      9         },
     10         DbConn,
     11     },
     12     error::Error,
     13     util::NumberOrString,
     14 };
     15 use chrono::{NaiveDateTime, Utc};
     16 use rocket::{
     17     form::{Form, FromForm},
     18     fs::TempFile,
     19     serde::json::Json,
     20     Route,
     21 };
     22 use serde_json::Value;
     23 use std::collections::{HashMap, HashSet};
     24 pub fn routes() -> Vec<Route> {
     25     // Note that many routes have an `admin` variant; this seems to be
     26     // because the stored procedure that upstream Bitwarden uses to determine
     27     // whether the user can edit a cipher doesn't take into account whether
     28     // the user is an org owner/admin. The `admin` variant first checks
     29     // whether the user is an owner/admin of the relevant org, and if so,
     30     // allows the operation unconditionally.
     31     //
     32     // vaultwarden factors in the org owner/admin status as part of
     33     // determining the write accessibility of a cipher, so most
     34     // admin/non-admin implementations can be shared.
     35     routes![
     36         delete_all,
     37         delete_attachment,
     38         delete_attachment_admin,
     39         delete_attachment_post,
     40         delete_attachment_post_admin,
     41         delete_cipher,
     42         delete_cipher_admin,
     43         delete_cipher_post,
     44         delete_cipher_post_admin,
     45         delete_cipher_put,
     46         delete_cipher_put_admin,
     47         delete_cipher_selected,
     48         delete_cipher_selected_admin,
     49         delete_cipher_selected_post,
     50         delete_cipher_selected_post_admin,
     51         delete_cipher_selected_put,
     52         delete_cipher_selected_put_admin,
     53         get_attachment,
     54         get_cipher,
     55         get_cipher_admin,
     56         get_cipher_details,
     57         get_ciphers,
     58         move_cipher_selected,
     59         move_cipher_selected_put,
     60         post_attachment,
     61         post_attachment_admin,
     62         post_attachment_share,
     63         post_attachment_v2,
     64         post_attachment_v2_data,
     65         post_cipher,
     66         post_cipher_admin,
     67         post_cipher_partial,
     68         post_cipher_share,
     69         post_ciphers,
     70         post_ciphers_admin,
     71         post_ciphers_create,
     72         post_ciphers_import,
     73         post_collections2_update,
     74         post_collections_admin,
     75         post_collections_update,
     76         put_cipher,
     77         put_cipher_admin,
     78         put_cipher_partial,
     79         put_cipher_share,
     80         put_cipher_share_selected,
     81         put_collections2_update,
     82         put_collections_admin,
     83         put_collections_update,
     84         restore_cipher_put,
     85         restore_cipher_put_admin,
     86         restore_cipher_selected,
     87         sync,
     88     ]
     89 }
     90 
     91 #[derive(FromForm, Default)]
     92 struct SyncData {
     93     #[field(name = "excludeDomains")]
     94     exclude_domains: bool, // Default: 'false'
     95 }
     96 
     97 #[get("/sync?<data..>")]
     98 async fn sync(data: SyncData, headers: Headers, conn: DbConn) -> Json<Value> {
     99     let user_json = headers.user.to_json(&conn).await;
    100     // Get all ciphers which are visible by the user
    101     let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;
    102     let cipher_sync_data =
    103         CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;
    104 
    105     // Lets generate the ciphers_json using all the gathered info
    106     let mut ciphers_json = Vec::with_capacity(ciphers.len());
    107     for c in ciphers {
    108         ciphers_json.push(
    109             c.to_json(
    110                 &headers.user.uuid,
    111                 Some(&cipher_sync_data),
    112                 CipherSyncType::User,
    113                 &conn,
    114             )
    115             .await,
    116         );
    117     }
    118     let collections = Collection::find_by_user_uuid(headers.user.uuid.clone(), &conn).await;
    119     let mut collections_json = Vec::with_capacity(collections.len());
    120     for c in collections {
    121         collections_json.push(
    122             c.to_json_details(&headers.user.uuid, Some(&cipher_sync_data), &conn)
    123                 .await,
    124         );
    125     }
    126     let folders_json: Vec<Value> = Folder::find_by_user(&headers.user.uuid, &conn)
    127         .await
    128         .iter()
    129         .map(Folder::to_json)
    130         .collect();
    131     let policies_json: Vec<Value> = OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn)
    132         .await
    133         .iter()
    134         .map(OrgPolicy::to_json)
    135         .collect();
    136     let domains_json = if data.exclude_domains {
    137         Value::Null
    138     } else {
    139         api::core::_get_eq_domains(headers, true).into_inner()
    140     };
    141     Json(json!({
    142         "profile": user_json,
    143         "folders": folders_json,
    144         "collections": collections_json,
    145         "policies": policies_json,
    146         "ciphers": ciphers_json,
    147         "domains": domains_json,
    148         "sends": Vec::<Value>::new(),
    149         "unofficialServer": true,
    150         "object": "sync"
    151     }))
    152 }
    153 
    154 #[get("/ciphers")]
    155 async fn get_ciphers(headers: Headers, conn: DbConn) -> Json<Value> {
    156     let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn).await;
    157     let cipher_sync_data =
    158         CipherSyncData::new(&headers.user.uuid, CipherSyncType::User, &conn).await;
    159     let mut ciphers_json = Vec::with_capacity(ciphers.len());
    160     for c in ciphers {
    161         ciphers_json.push(
    162             c.to_json(
    163                 &headers.user.uuid,
    164                 Some(&cipher_sync_data),
    165                 CipherSyncType::User,
    166                 &conn,
    167             )
    168             .await,
    169         );
    170     }
    171     Json(json!({
    172       "data": ciphers_json,
    173       "object": "list",
    174       "continuationToken": null
    175     }))
    176 }
    177 
    178 #[get("/ciphers/<uuid>")]
    179 async fn get_cipher(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
    180     let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else {
    181         err!("Cipher doesn't exist")
    182     };
    183     if !cipher
    184         .is_accessible_to_user(&headers.user.uuid, &conn)
    185         .await
    186     {
    187         err!("Cipher is not owned by user")
    188     }
    189     Ok(Json(
    190         cipher
    191             .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn)
    192             .await,
    193     ))
    194 }
    195 
    196 #[get("/ciphers/<uuid>/admin")]
    197 async fn get_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
    198     // TODO: Implement this correctly
    199     get_cipher(uuid, headers, conn).await
    200 }
    201 
    202 #[get("/ciphers/<uuid>/details")]
    203 async fn get_cipher_details(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
    204     get_cipher(uuid, headers, conn).await
    205 }
    206 
    207 #[derive(Deserialize)]
    208 #[serde(rename_all = "camelCase")]
    209 pub struct CipherData {
    210     // id is optional as it is included only in bulk share
    211     pub id: Option<String>,
    212     // folder id is not included in import
    213     folder_id: Option<String>,
    214     // TODO: Some of these might appear all the time, no need for Option
    215     #[serde(alias = "organizationID")]
    216     pub organization_id: Option<String>,
    217     key: Option<String>,
    218     pub r#type: i32,
    219     pub name: String,
    220     pub notes: Option<String>,
    221     fields: Option<Value>,
    222     // Only one of these should exist, depending on type
    223     login: Option<Value>,
    224     secure_note: Option<Value>,
    225     card: Option<Value>,
    226     identity: Option<Value>,
    227     favorite: Option<bool>,
    228     reprompt: Option<i32>,
    229     pub password_history: Option<Value>,
    230     // These are used during key rotation
    231     // 'Attachments' is unused, contains map of {id: filename}
    232     #[allow(dead_code)]
    233     attachments: Option<Value>,
    234     #[allow(dead_code)]
    235     attachments2: Option<HashMap<String, Attachments2Data>>,
    236     // The revision datetime (in ISO 8601 format) of the client's local copy
    237     // of the cipher. This is used to prevent a client from updating a cipher
    238     // when it doesn't have the latest version, as that can result in data
    239     // loss. It's not an error when no value is provided; this can happen
    240     // when using older client versions, or if the operation doesn't involve
    241     // updating an existing cipher.
    242     last_known_revision_date: Option<String>,
    243 }
    244 
    245 #[derive(Deserialize)]
    246 #[serde(rename_all = "camelCase")]
    247 struct PartialCipherData {
    248     folder_id: Option<String>,
    249     favorite: bool,
    250 }
    251 
    252 #[derive(Deserialize)]
    253 #[serde(rename_all = "camelCase")]
    254 struct Attachments2Data {
    255     #[allow(dead_code)]
    256     file_name: String,
    257     #[allow(dead_code)]
    258     key: String,
    259 }
    260 
    261 /// Called when an org admin clones an org cipher.
    262 #[post("/ciphers/admin", data = "<data>")]
    263 async fn post_ciphers_admin(
    264     data: Json<ShareCipherData>,
    265     headers: Headers,
    266     conn: DbConn,
    267 ) -> JsonResult {
    268     post_ciphers_create(data, headers, conn).await
    269 }
    270 
    271 /// Called when creating a new org-owned cipher, or cloning a cipher (whether
    272 /// user- or org-owned). When cloning a cipher to a user-owned cipher,
    273 /// `organizationId` is null.
    274 #[post("/ciphers/create", data = "<data>")]
    275 async fn post_ciphers_create(
    276     data: Json<ShareCipherData>,
    277     headers: Headers,
    278     conn: DbConn,
    279 ) -> JsonResult {
    280     let mut data: ShareCipherData = data.into_inner();
    281     // Check if there are one more more collections selected when this cipher is part of an organization.
    282     // err if this is not the case before creating an empty cipher.
    283     if data.cipher.organization_id.is_some() && data.collection_ids.is_empty() {
    284         err!("You must select at least one collection.");
    285     }
    286     // This check is usually only needed in update_cipher_from_data(), but we
    287     // need it here as well to avoid creating an empty cipher in the call to
    288     // cipher.save() below.
    289     enforce_personal_ownership_policy(Some(&data.cipher), &headers, &conn).await?;
    290     let mut cipher = Cipher::new(data.cipher.r#type, data.cipher.name.clone());
    291     cipher.user_uuid = Some(headers.user.uuid.clone());
    292     cipher.save(&conn).await?;
    293     // When cloning a cipher, the Bitwarden clients seem to set this field
    294     // based on the cipher being cloned (when creating a new cipher, it's set
    295     // to null as expected). However, `cipher.created_at` is initialized to
    296     // the current time, so the stale data check will end up failing down the
    297     // line. Since this function only creates new ciphers (whether by cloning
    298     // or otherwise), we can just ignore this field entirely.
    299     data.cipher.last_known_revision_date = None;
    300     share_cipher_by_uuid(&cipher.uuid, data, &headers, &conn).await
    301 }
    302 
    303 /// Called when creating a new user-owned cipher.
    304 #[post("/ciphers", data = "<data>")]
    305 async fn post_ciphers(data: Json<CipherData>, headers: Headers, conn: DbConn) -> JsonResult {
    306     let mut data: CipherData = data.into_inner();
    307     // The web/browser clients set this field to null as expected, but the
    308     // mobile clients seem to set the invalid value `0001-01-01T00:00:00`,
    309     // which results in a warning message being logged. This field isn't
    310     // needed when creating a new cipher, so just ignore it unconditionally.
    311     data.last_known_revision_date = None;
    312     let mut cipher = Cipher::new(data.r#type, data.name.clone());
    313     update_cipher_from_data(&mut cipher, data, &headers, None, &conn, false).await?;
    314     Ok(Json(
    315         cipher
    316             .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn)
    317             .await,
    318     ))
    319 }
    320 
    321 /// Enforces the personal ownership policy on user-owned ciphers, if applicable.
    322 /// A non-owner/admin user belonging to an org with the personal ownership policy
    323 /// enabled isn't allowed to create new user-owned ciphers or modify existing ones
    324 /// (that were created before the policy was applicable to the user). The user is
    325 /// allowed to delete or share such ciphers to an org, however.
    326 ///
    327 /// Ref: https://bitwarden.com/help/article/policies/#personal-ownership
    328 async fn enforce_personal_ownership_policy(
    329     data: Option<&CipherData>,
    330     headers: &Headers,
    331     conn: &DbConn,
    332 ) -> EmptyResult {
    333     if data.is_none() || data.unwrap().organization_id.is_none() {
    334         let user_uuid = &headers.user.uuid;
    335         let policy_type = OrgPolicyType::PersonalOwnership;
    336         if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await {
    337             err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.")
    338         }
    339     }
    340     Ok(())
    341 }
    342 
    343 pub async fn update_cipher_from_data(
    344     cipher: &mut Cipher,
    345     data: CipherData,
    346     headers: &Headers,
    347     shared_to_collections: Option<Vec<String>>,
    348     conn: &DbConn,
    349     import_cipher: bool,
    350 ) -> EmptyResult {
    351     enforce_personal_ownership_policy(Some(&data), headers, conn).await?;
    352     // Check that the client isn't updating an existing cipher with stale data.
    353     // And only perform this check when not importing ciphers, else the date/time check will fail.
    354     if !import_cipher {
    355         if let Some(dt) = data.last_known_revision_date {
    356             match NaiveDateTime::parse_from_str(&dt, "%+") {
    357                 // ISO 8601 format
    358                 Err(err) => warn!("Error parsing LastKnownRevisionDate '{}': {}", dt, err),
    359                 Ok(dt) if cipher.updated_at.signed_duration_since(dt).num_seconds() > 1 => {
    360                     err!("The client copy of this cipher is out of date. Resync the client and try again.")
    361                 }
    362                 Ok(_) => (),
    363             }
    364         }
    365     }
    366     if cipher.organization_uuid.is_some() && cipher.organization_uuid != data.organization_id {
    367         err!("Organization mismatch. Please resync the client before updating the cipher")
    368     }
    369     if let Some(ref note) = data.notes {
    370         if note.len() > 10_000 {
    371             err!("The field Notes exceeds the maximum encrypted value length of 10000 characters.")
    372         }
    373     }
    374     if let Some(org_id) = data.organization_id {
    375         match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, conn).await {
    376             None => err!("You don't have permission to add item to organization"),
    377             Some(org_user) => {
    378                 if shared_to_collections.is_some()
    379                     || org_user.has_full_access()
    380                     || cipher
    381                         .is_write_accessible_to_user(&headers.user.uuid, conn)
    382                         .await
    383                 {
    384                     cipher.organization_uuid = Some(org_id);
    385                     // After some discussion in PR #1329 re-added the user_uuid = None again.
    386                     // TODO: Audit/Check the whole save/update cipher chain.
    387                     // Upstream uses the user_uuid to allow a cipher added by a user to an org to still allow the user to view/edit the cipher
    388                     // even when the user has hide-passwords configured as there policy.
    389                     // Removing the line below would fix that, but we have to check which effect this would have on the rest of the code.
    390                     cipher.user_uuid = None;
    391                 } else {
    392                     err!("You don't have permission to add cipher directly to organization")
    393                 }
    394             }
    395         }
    396     } else {
    397         cipher.user_uuid = Some(headers.user.uuid.clone());
    398     }
    399     if let Some(ref folder_id) = data.folder_id {
    400         match Folder::find_by_uuid(folder_id, conn).await {
    401             Some(folder) => {
    402                 if folder.user_uuid != headers.user.uuid {
    403                     err!("Folder is not owned by user")
    404                 }
    405             }
    406             None => err!("Folder doesn't exist"),
    407         }
    408     }
    409     // Cleanup cipher data, like removing the 'Response' key.
    410     // This key is somewhere generated during Javascript so no way for us this fix this.
    411     // Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.
    412     // We do not mind which data is in it, the keep our model more flexible when there are upstream changes.
    413     // But, we at least know we do not need to store and return this specific key.
    414     fn _clean_cipher_data(mut json_data: Value) -> Value {
    415         if json_data.is_array() {
    416             json_data
    417                 .as_array_mut()
    418                 .unwrap()
    419                 .iter_mut()
    420                 .for_each(|ref mut f| {
    421                     f.as_object_mut().unwrap().remove("Response");
    422                 });
    423         };
    424         json_data
    425     }
    426     let type_data_opt = match data.r#type {
    427         1i32 => data.login,
    428         2i32 => data.secure_note,
    429         3i32 => data.card,
    430         4i32 => data.identity,
    431         _ => err!("Invalid type"),
    432     };
    433     let type_data = match type_data_opt {
    434         Some(mut data_in_type) => {
    435             // Remove the 'Response' key from the base object.
    436             data_in_type.as_object_mut().unwrap().remove("Response");
    437             // Remove the 'Response' key from every Uri.
    438             if data_in_type["Uris"].is_array() {
    439                 data_in_type["Uris"] = _clean_cipher_data(data_in_type["Uris"].clone());
    440             }
    441             data_in_type
    442         }
    443         None => err!("Data missing"),
    444     };
    445     cipher.key = data.key;
    446     cipher.name = data.name;
    447     cipher.notes = data.notes;
    448     cipher.fields = data.fields.map(|f| _clean_cipher_data(f).to_string());
    449     cipher.data = type_data.to_string();
    450     cipher.password_history = data.password_history.map(|f| f.to_string());
    451     cipher.reprompt = data.reprompt;
    452     cipher.save(conn).await?;
    453     cipher
    454         .move_to_folder(data.folder_id, &headers.user.uuid, conn)
    455         .await?;
    456     cipher
    457         .set_favorite(data.favorite, &headers.user.uuid, conn)
    458         .await?;
    459     Ok(())
    460 }
    461 
    462 #[derive(Deserialize)]
    463 #[serde(rename_all = "camelCase")]
    464 struct ImportData {
    465     ciphers: Vec<CipherData>,
    466     folders: Vec<FolderData>,
    467     folder_relationships: Vec<RelationsData>,
    468 }
    469 
    470 #[derive(Deserialize)]
    471 #[serde(rename_all = "camelCase")]
    472 struct RelationsData {
    473     // Cipher id
    474     key: usize,
    475     // Folder id
    476     value: usize,
    477 }
    478 
    479 #[post("/ciphers/import", data = "<data>")]
    480 async fn post_ciphers_import(
    481     data: Json<ImportData>,
    482     headers: Headers,
    483     conn: DbConn,
    484 ) -> EmptyResult {
    485     enforce_personal_ownership_policy(None, &headers, &conn).await?;
    486     let data: ImportData = data.into_inner();
    487     // Validate the import before continuing
    488     // Bitwarden does not process the import if there is one item invalid.
    489     // Since we check for the size of the encrypted note length, we need to do that here to pre-validate it.
    490     // TODO: See if we can optimize the whole cipher adding/importing and prevent duplicate code and checks.
    491     Cipher::validate_cipher_data(&data.ciphers)?;
    492     let existing_folders: Vec<String> = Folder::find_by_user(&headers.user.uuid, &conn)
    493         .await
    494         .into_iter()
    495         .map(|f| f.uuid)
    496         .collect();
    497     let mut folders: Vec<String> = Vec::with_capacity(data.folders.len());
    498     for folder in data.folders {
    499         let folder_uuid =
    500             if folder.id.is_some() && existing_folders.contains(folder.id.as_ref().unwrap()) {
    501                 folder.id.unwrap()
    502             } else {
    503                 let mut new_folder = Folder::new(headers.user.uuid.clone(), folder.name);
    504                 new_folder.save(&conn).await?;
    505                 new_folder.uuid
    506             };
    507         folders.push(folder_uuid);
    508     }
    509     // Read the relations between folders and ciphers
    510     let mut relations_map = HashMap::with_capacity(data.folder_relationships.len());
    511     for relation in data.folder_relationships {
    512         relations_map.insert(relation.key, relation.value);
    513     }
    514     // Read and create the ciphers
    515     for (index, mut cipher_data) in data.ciphers.into_iter().enumerate() {
    516         let folder_uuid = relations_map.get(&index).map(|i| folders[*i].clone());
    517         cipher_data.folder_id = folder_uuid;
    518         let mut cipher = Cipher::new(cipher_data.r#type, cipher_data.name.clone());
    519         update_cipher_from_data(&mut cipher, cipher_data, &headers, None, &conn, true).await?;
    520     }
    521     let mut user = headers.user;
    522     user.update_revision(&conn).await?;
    523     Ok(())
    524 }
    525 
    526 /// Called when an org admin modifies an existing org cipher.
    527 #[put("/ciphers/<uuid>/admin", data = "<data>")]
    528 async fn put_cipher_admin(
    529     uuid: &str,
    530     data: Json<CipherData>,
    531     headers: Headers,
    532     conn: DbConn,
    533 ) -> JsonResult {
    534     put_cipher(uuid, data, headers, conn).await
    535 }
    536 
    537 #[post("/ciphers/<uuid>/admin", data = "<data>")]
    538 async fn post_cipher_admin(
    539     uuid: &str,
    540     data: Json<CipherData>,
    541     headers: Headers,
    542     conn: DbConn,
    543 ) -> JsonResult {
    544     post_cipher(uuid, data, headers, conn).await
    545 }
    546 
    547 #[post("/ciphers/<uuid>", data = "<data>")]
    548 async fn post_cipher(
    549     uuid: &str,
    550     data: Json<CipherData>,
    551     headers: Headers,
    552     conn: DbConn,
    553 ) -> JsonResult {
    554     put_cipher(uuid, data, headers, conn).await
    555 }
    556 
    557 #[put("/ciphers/<uuid>", data = "<data>")]
    558 async fn put_cipher(
    559     uuid: &str,
    560     data: Json<CipherData>,
    561     headers: Headers,
    562     conn: DbConn,
    563 ) -> JsonResult {
    564     let data: CipherData = data.into_inner();
    565     let Some(mut cipher) = Cipher::find_by_uuid(uuid, &conn).await else {
    566         err!("Cipher doesn't exist")
    567     };
    568     // TODO: Check if only the folder ID or favorite status is being changed.
    569     // These are per-user properties that technically aren't part of the
    570     // cipher itself, so the user shouldn't need write access to change these.
    571     // Interestingly, upstream Bitwarden doesn't properly handle this either.
    572     if !cipher
    573         .is_write_accessible_to_user(&headers.user.uuid, &conn)
    574         .await
    575     {
    576         err!("Cipher is not write accessible")
    577     }
    578     update_cipher_from_data(&mut cipher, data, &headers, None, &conn, false).await?;
    579     Ok(Json(
    580         cipher
    581             .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn)
    582             .await,
    583     ))
    584 }
    585 
    586 #[post("/ciphers/<uuid>/partial", data = "<data>")]
    587 async fn post_cipher_partial(
    588     uuid: &str,
    589     data: Json<PartialCipherData>,
    590     headers: Headers,
    591     conn: DbConn,
    592 ) -> JsonResult {
    593     put_cipher_partial(uuid, data, headers, conn).await
    594 }
    595 
    596 // Only update the folder and favorite for the user, since this cipher is read-only
    597 #[put("/ciphers/<uuid>/partial", data = "<data>")]
    598 async fn put_cipher_partial(
    599     uuid: &str,
    600     data: Json<PartialCipherData>,
    601     headers: Headers,
    602     conn: DbConn,
    603 ) -> JsonResult {
    604     let data: PartialCipherData = data.into_inner();
    605     let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else {
    606         err!("Cipher doesn't exist")
    607     };
    608     if let Some(ref folder_id) = data.folder_id {
    609         match Folder::find_by_uuid(folder_id, &conn).await {
    610             Some(folder) => {
    611                 if folder.user_uuid != headers.user.uuid {
    612                     err!("Folder is not owned by user")
    613                 }
    614             }
    615             None => err!("Folder doesn't exist"),
    616         }
    617     }
    618     // Move cipher
    619     cipher
    620         .move_to_folder(data.folder_id.clone(), &headers.user.uuid, &conn)
    621         .await?;
    622     // Update favorite
    623     cipher
    624         .set_favorite(Some(data.favorite), &headers.user.uuid, &conn)
    625         .await?;
    626     Ok(Json(
    627         cipher
    628             .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn)
    629             .await,
    630     ))
    631 }
    632 
    633 #[derive(Deserialize)]
    634 #[serde(rename_all = "camelCase")]
    635 struct CollectionsAdminData {
    636     #[serde(alias = "CollectionIds")]
    637     collection_ids: Vec<String>,
    638 }
    639 
    640 #[put("/ciphers/<uuid>/collections_v2", data = "<data>")]
    641 async fn put_collections2_update(
    642     uuid: &str,
    643     data: Json<CollectionsAdminData>,
    644     headers: Headers,
    645     conn: DbConn,
    646 ) -> JsonResult {
    647     post_collections2_update(uuid, data, headers, conn).await
    648 }
    649 
    650 #[post("/ciphers/<uuid>/collections_v2", data = "<data>")]
    651 async fn post_collections2_update(
    652     uuid: &str,
    653     data: Json<CollectionsAdminData>,
    654     headers: Headers,
    655     conn: DbConn,
    656 ) -> JsonResult {
    657     let cipher_details = post_collections_update(uuid, data, headers, conn).await?;
    658     Ok(Json(json!({
    659         "object": "optionalCipherDetails",
    660         "unavailable": false,
    661         "cipher": *cipher_details
    662     })))
    663 }
    664 
    665 #[put("/ciphers/<uuid>/collections", data = "<data>")]
    666 async fn put_collections_update(
    667     uuid: &str,
    668     data: Json<CollectionsAdminData>,
    669     headers: Headers,
    670     conn: DbConn,
    671 ) -> JsonResult {
    672     post_collections_update(uuid, data, headers, conn).await
    673 }
    674 
    675 #[post("/ciphers/<uuid>/collections", data = "<data>")]
    676 async fn post_collections_update(
    677     uuid: &str,
    678     data: Json<CollectionsAdminData>,
    679     headers: Headers,
    680     conn: DbConn,
    681 ) -> JsonResult {
    682     let data: CollectionsAdminData = data.into_inner();
    683     let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else {
    684         err!("Cipher doesn't exist")
    685     };
    686     if !cipher
    687         .is_write_accessible_to_user(&headers.user.uuid, &conn)
    688         .await
    689     {
    690         err!("Cipher is not write accessible")
    691     }
    692 
    693     let posted_collections = HashSet::<String>::from_iter(data.collection_ids);
    694     let current_collections = HashSet::<String>::from_iter(
    695         cipher
    696             .get_collections(headers.user.uuid.clone(), &conn)
    697             .await,
    698     );
    699 
    700     for collection in posted_collections.symmetric_difference(&current_collections) {
    701         match Collection::find_by_uuid(collection, &conn).await {
    702             None => err!("Invalid collection ID provided"),
    703             Some(collection) => {
    704                 if collection
    705                     .is_writable_by_user(&headers.user.uuid, &conn)
    706                     .await
    707                 {
    708                     if posted_collections.contains(&collection.uuid) {
    709                         // Add to collection
    710                         CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?;
    711                     } else {
    712                         // Remove from collection
    713                         CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?;
    714                     }
    715                 } else {
    716                     err!("No rights to modify the collection")
    717                 }
    718             }
    719         }
    720     }
    721     Ok(Json(
    722         cipher
    723             .to_json(&headers.user.uuid, None, CipherSyncType::User, &conn)
    724             .await,
    725     ))
    726 }
    727 
    728 #[put("/ciphers/<uuid>/collections-admin", data = "<data>")]
    729 async fn put_collections_admin(
    730     uuid: &str,
    731     data: Json<CollectionsAdminData>,
    732     headers: Headers,
    733     conn: DbConn,
    734 ) -> EmptyResult {
    735     post_collections_admin(uuid, data, headers, conn).await
    736 }
    737 
    738 #[post("/ciphers/<uuid>/collections-admin", data = "<data>")]
    739 async fn post_collections_admin(
    740     uuid: &str,
    741     data: Json<CollectionsAdminData>,
    742     headers: Headers,
    743     conn: DbConn,
    744 ) -> EmptyResult {
    745     let data: CollectionsAdminData = data.into_inner();
    746     let Some(cipher) = Cipher::find_by_uuid(uuid, &conn).await else {
    747         err!("Cipher doesn't exist")
    748     };
    749     if !cipher
    750         .is_write_accessible_to_user(&headers.user.uuid, &conn)
    751         .await
    752     {
    753         err!("Cipher is not write accessible")
    754     }
    755     let posted_collections = HashSet::<String>::from_iter(data.collection_ids);
    756     let current_collections = HashSet::<String>::from_iter(
    757         cipher
    758             .get_admin_collections(headers.user.uuid.clone(), &conn)
    759             .await,
    760     );
    761     for collection in posted_collections.symmetric_difference(&current_collections) {
    762         match Collection::find_by_uuid(collection, &conn).await {
    763             None => err!("Invalid collection ID provided"),
    764             Some(collection) => {
    765                 if collection
    766                     .is_writable_by_user(&headers.user.uuid, &conn)
    767                     .await
    768                 {
    769                     if posted_collections.contains(&collection.uuid) {
    770                         // Add to collection
    771                         CollectionCipher::save(&cipher.uuid, &collection.uuid, &conn).await?;
    772                     } else {
    773                         // Remove from collection
    774                         CollectionCipher::delete(&cipher.uuid, &collection.uuid, &conn).await?;
    775                     }
    776                 } else {
    777                     err!("No rights to modify the collection")
    778                 }
    779             }
    780         }
    781     }
    782     Ok(())
    783 }
    784 
    785 #[derive(Deserialize)]
    786 #[serde(rename_all = "camelCase")]
    787 struct ShareCipherData {
    788     #[serde(alias = "Cipher")]
    789     cipher: CipherData,
    790     #[serde(alias = "CollectionIds")]
    791     collection_ids: Vec<String>,
    792 }
    793 
    794 #[post("/ciphers/<uuid>/share", data = "<data>")]
    795 async fn post_cipher_share(
    796     uuid: &str,
    797     data: Json<ShareCipherData>,
    798     headers: Headers,
    799     conn: DbConn,
    800 ) -> JsonResult {
    801     let data: ShareCipherData = data.into_inner();
    802     share_cipher_by_uuid(uuid, data, &headers, &conn).await
    803 }
    804 
    805 #[put("/ciphers/<uuid>/share", data = "<data>")]
    806 async fn put_cipher_share(
    807     uuid: &str,
    808     data: Json<ShareCipherData>,
    809     headers: Headers,
    810     conn: DbConn,
    811 ) -> JsonResult {
    812     let data: ShareCipherData = data.into_inner();
    813     share_cipher_by_uuid(uuid, data, &headers, &conn).await
    814 }
    815 
    816 #[derive(Deserialize)]
    817 #[serde(rename_all = "camelCase")]
    818 struct ShareSelectedCipherData {
    819     ciphers: Vec<CipherData>,
    820     collection_ids: Vec<String>,
    821 }
    822 
    823 #[put("/ciphers/share", data = "<data>")]
    824 async fn put_cipher_share_selected(
    825     data: Json<ShareSelectedCipherData>,
    826     headers: Headers,
    827     conn: DbConn,
    828 ) -> EmptyResult {
    829     let mut data: ShareSelectedCipherData = data.into_inner();
    830     if data.ciphers.is_empty() {
    831         err!("You must select at least one cipher.")
    832     }
    833     if data.collection_ids.is_empty() {
    834         err!("You must select at least one collection.")
    835     }
    836     for cipher in &data.ciphers {
    837         if cipher.id.is_none() {
    838             err!("Request missing ids field");
    839         }
    840     }
    841     while let Some(cipher) = data.ciphers.pop() {
    842         let mut shared_cipher_data = ShareCipherData {
    843             cipher,
    844             collection_ids: data.collection_ids.clone(),
    845         };
    846         match shared_cipher_data.cipher.id.take() {
    847             Some(id) => share_cipher_by_uuid(&id, shared_cipher_data, &headers, &conn).await?,
    848             None => err!("Request missing ids field"),
    849         };
    850     }
    851     Ok(())
    852 }
    853 
    854 async fn share_cipher_by_uuid(
    855     uuid: &str,
    856     data: ShareCipherData,
    857     headers: &Headers,
    858     conn: &DbConn,
    859 ) -> JsonResult {
    860     let mut cipher = match Cipher::find_by_uuid(uuid, conn).await {
    861         Some(cipher) => {
    862             if cipher
    863                 .is_write_accessible_to_user(&headers.user.uuid, conn)
    864                 .await
    865             {
    866                 cipher
    867             } else {
    868                 err!("Cipher is not write accessible")
    869             }
    870         }
    871         None => err!("Cipher doesn't exist"),
    872     };
    873     let mut shared_to_collections = Vec::new();
    874     if let Some(ref organization_uuid) = data.cipher.organization_id {
    875         for col_uuid in &data.collection_ids {
    876             match Collection::find_by_uuid_and_org(col_uuid, organization_uuid, conn).await {
    877                 None => err!("Invalid collection ID provided"),
    878                 Some(collection) => {
    879                     if collection
    880                         .is_writable_by_user(&headers.user.uuid, conn)
    881                         .await
    882                     {
    883                         CollectionCipher::save(&cipher.uuid, &collection.uuid, conn).await?;
    884                         shared_to_collections.push(collection.uuid);
    885                     } else {
    886                         err!("No rights to modify the collection")
    887                     }
    888                 }
    889             }
    890         }
    891     };
    892     update_cipher_from_data(
    893         &mut cipher,
    894         data.cipher,
    895         headers,
    896         Some(shared_to_collections),
    897         conn,
    898         false,
    899     )
    900     .await?;
    901     Ok(Json(
    902         cipher
    903             .to_json(&headers.user.uuid, None, CipherSyncType::User, conn)
    904             .await,
    905     ))
    906 }
    907 
    908 const ATTACHMENTS_DISABLED_MSG: &str = "Attachments are disabled.";
    909 /// v2 API for downloading an attachment. This just redirects the client to
    910 /// the actual location of an attachment.
    911 ///
    912 /// Upstream added this v2 API to support direct download of attachments from
    913 /// their object storage service. For self-hosted instances, it basically just
    914 /// redirects to the same location as before the v2 API.
    915 #[allow(unused_variables, clippy::needless_pass_by_value)]
    916 #[get("/ciphers/<uuid>/attachment/<attachment_id>")]
    917 fn get_attachment(uuid: &str, attachment_id: &str, _headers: Headers) -> Error {
    918     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
    919 }
    920 
    921 #[derive(Deserialize)]
    922 #[serde(rename_all = "camelCase")]
    923 struct AttachmentRequestData {
    924     #[allow(dead_code)]
    925     key: String,
    926     #[allow(dead_code)]
    927     file_name: String,
    928     #[allow(dead_code)]
    929     file_size: NumberOrString,
    930     #[allow(dead_code)]
    931     admin_request: Option<bool>, // true when attaching from an org vault view
    932 }
    933 
    934 /// v2 API for creating an attachment associated with a cipher.
    935 /// This redirects the client to the API it should use to upload the attachment.
    936 /// For upstream's cloud-hosted service, it's an Azure object storage API.
    937 /// For self-hosted instances, it's another API on the local instance.
    938 #[allow(unused_variables, clippy::needless_pass_by_value)]
    939 #[post("/ciphers/<uuid>/attachment/v2", data = "<data>")]
    940 fn post_attachment_v2(uuid: &str, data: Json<AttachmentRequestData>, _headers: Headers) -> Error {
    941     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
    942 }
    943 
    944 #[allow(dead_code)]
    945 #[derive(FromForm)]
    946 struct UploadData<'f> {
    947     key: Option<String>,
    948     data: TempFile<'f>,
    949 }
    950 
    951 /// v2 API for uploading the actual data content of an attachment.
    952 /// This route needs a rank specified so that Rocket prioritizes the
    953 /// /ciphers/<uuid>/attachment/v2 route, which would otherwise conflict
    954 /// with this one.
    955 #[allow(unused_variables, clippy::needless_pass_by_value)]
    956 #[post(
    957     "/ciphers/<uuid>/attachment/<attachment_id>",
    958     format = "multipart/form-data",
    959     data = "<data>",
    960     rank = 1
    961 )]
    962 fn post_attachment_v2_data(
    963     uuid: &str,
    964     attachment_id: &str,
    965     data: Form<UploadData<'_>>,
    966     _headers: Headers,
    967 ) -> Error {
    968     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
    969 }
    970 
    971 /// Legacy API for creating an attachment associated with a cipher.
    972 #[allow(unused_variables, clippy::needless_pass_by_value)]
    973 #[post(
    974     "/ciphers/<uuid>/attachment",
    975     format = "multipart/form-data",
    976     data = "<data>"
    977 )]
    978 fn post_attachment(uuid: &str, data: Form<UploadData<'_>>, _headers: Headers) -> Error {
    979     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
    980 }
    981 
    982 #[allow(unused_variables, clippy::needless_pass_by_value)]
    983 #[post(
    984     "/ciphers/<uuid>/attachment-admin",
    985     format = "multipart/form-data",
    986     data = "<data>"
    987 )]
    988 fn post_attachment_admin(uuid: &str, data: Form<UploadData<'_>>, _headers: Headers) -> Error {
    989     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
    990 }
    991 
    992 #[allow(unused_variables, clippy::needless_pass_by_value)]
    993 #[post(
    994     "/ciphers/<uuid>/attachment/<attachment_id>/share",
    995     format = "multipart/form-data",
    996     data = "<data>"
    997 )]
    998 fn post_attachment_share(
    999     uuid: &str,
   1000     attachment_id: &str,
   1001     data: Form<UploadData<'_>>,
   1002     _headers: Headers,
   1003 ) -> Error {
   1004     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
   1005 }
   1006 
   1007 #[allow(unused_variables, clippy::needless_pass_by_value)]
   1008 #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
   1009 fn delete_attachment_post_admin(uuid: &str, attachment_id: &str, _headers: Headers) -> Error {
   1010     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
   1011 }
   1012 
   1013 #[allow(unused_variables, clippy::needless_pass_by_value)]
   1014 #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
   1015 fn delete_attachment_post(uuid: &str, attachment_id: &str, _headers: Headers) -> Error {
   1016     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
   1017 }
   1018 
   1019 #[allow(unused_variables, clippy::needless_pass_by_value)]
   1020 #[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
   1021 fn delete_attachment(uuid: &str, attachment_id: &str, _headers: Headers) -> Error {
   1022     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
   1023 }
   1024 
   1025 #[allow(unused_variables, clippy::needless_pass_by_value)]
   1026 #[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
   1027 fn delete_attachment_admin(uuid: &str, attachment_id: &str, _headers: Headers) -> Error {
   1028     Error::new(ATTACHMENTS_DISABLED_MSG, ATTACHMENTS_DISABLED_MSG)
   1029 }
   1030 
   1031 #[post("/ciphers/<uuid>/delete")]
   1032 async fn delete_cipher_post(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult {
   1033     _delete_cipher_by_uuid(uuid, &headers, &conn, false).await
   1034 }
   1035 
   1036 #[post("/ciphers/<uuid>/delete-admin")]
   1037 async fn delete_cipher_post_admin(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult {
   1038     _delete_cipher_by_uuid(uuid, &headers, &conn, false).await
   1039 }
   1040 
   1041 #[put("/ciphers/<uuid>/delete")]
   1042 async fn delete_cipher_put(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult {
   1043     _delete_cipher_by_uuid(uuid, &headers, &conn, true).await
   1044 }
   1045 
   1046 #[put("/ciphers/<uuid>/delete-admin")]
   1047 async fn delete_cipher_put_admin(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult {
   1048     _delete_cipher_by_uuid(uuid, &headers, &conn, true).await
   1049 }
   1050 
   1051 #[delete("/ciphers/<uuid>")]
   1052 async fn delete_cipher(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult {
   1053     _delete_cipher_by_uuid(uuid, &headers, &conn, false).await
   1054 }
   1055 
   1056 #[delete("/ciphers/<uuid>/admin")]
   1057 async fn delete_cipher_admin(uuid: &str, headers: Headers, conn: DbConn) -> EmptyResult {
   1058     _delete_cipher_by_uuid(uuid, &headers, &conn, false).await
   1059 }
   1060 
   1061 #[delete("/ciphers", data = "<data>")]
   1062 async fn delete_cipher_selected(
   1063     data: Json<CipherIdsData>,
   1064     headers: Headers,
   1065     conn: DbConn,
   1066 ) -> EmptyResult {
   1067     _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete
   1068 }
   1069 
   1070 #[post("/ciphers/delete", data = "<data>")]
   1071 async fn delete_cipher_selected_post(
   1072     data: Json<CipherIdsData>,
   1073     headers: Headers,
   1074     conn: DbConn,
   1075 ) -> EmptyResult {
   1076     _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete
   1077 }
   1078 
   1079 #[put("/ciphers/delete", data = "<data>")]
   1080 async fn delete_cipher_selected_put(
   1081     data: Json<CipherIdsData>,
   1082     headers: Headers,
   1083     conn: DbConn,
   1084 ) -> EmptyResult {
   1085     _delete_multiple_ciphers(data, headers, conn, true).await // soft delete
   1086 }
   1087 
   1088 #[delete("/ciphers/admin", data = "<data>")]
   1089 async fn delete_cipher_selected_admin(
   1090     data: Json<CipherIdsData>,
   1091     headers: Headers,
   1092     conn: DbConn,
   1093 ) -> EmptyResult {
   1094     _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete
   1095 }
   1096 
   1097 #[post("/ciphers/delete-admin", data = "<data>")]
   1098 async fn delete_cipher_selected_post_admin(
   1099     data: Json<CipherIdsData>,
   1100     headers: Headers,
   1101     conn: DbConn,
   1102 ) -> EmptyResult {
   1103     _delete_multiple_ciphers(data, headers, conn, false).await // permanent delete
   1104 }
   1105 
   1106 #[put("/ciphers/delete-admin", data = "<data>")]
   1107 async fn delete_cipher_selected_put_admin(
   1108     data: Json<CipherIdsData>,
   1109     headers: Headers,
   1110     conn: DbConn,
   1111 ) -> EmptyResult {
   1112     _delete_multiple_ciphers(data, headers, conn, true).await // soft delete
   1113 }
   1114 
   1115 #[put("/ciphers/<uuid>/restore")]
   1116 async fn restore_cipher_put(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
   1117     _restore_cipher_by_uuid(uuid, &headers, &conn).await
   1118 }
   1119 
   1120 #[put("/ciphers/<uuid>/restore-admin")]
   1121 async fn restore_cipher_put_admin(uuid: &str, headers: Headers, conn: DbConn) -> JsonResult {
   1122     _restore_cipher_by_uuid(uuid, &headers, &conn).await
   1123 }
   1124 
   1125 #[put("/ciphers/restore", data = "<data>")]
   1126 async fn restore_cipher_selected(
   1127     data: Json<CipherIdsData>,
   1128     headers: Headers,
   1129     conn: DbConn,
   1130 ) -> JsonResult {
   1131     _restore_multiple_ciphers(data, &headers, &conn).await
   1132 }
   1133 
   1134 #[derive(Deserialize)]
   1135 #[serde(rename_all = "camelCase")]
   1136 struct MoveCipherData {
   1137     folder_id: Option<String>,
   1138     ids: Vec<String>,
   1139 }
   1140 
   1141 #[post("/ciphers/move", data = "<data>")]
   1142 async fn move_cipher_selected(
   1143     data: Json<MoveCipherData>,
   1144     headers: Headers,
   1145     conn: DbConn,
   1146 ) -> EmptyResult {
   1147     let data = data.into_inner();
   1148     let user_uuid = headers.user.uuid;
   1149     if let Some(ref folder_id) = data.folder_id {
   1150         match Folder::find_by_uuid(folder_id, &conn).await {
   1151             Some(folder) => {
   1152                 if folder.user_uuid != user_uuid {
   1153                     err!("Folder is not owned by user")
   1154                 }
   1155             }
   1156             None => err!("Folder doesn't exist"),
   1157         }
   1158     }
   1159     for uuid in data.ids {
   1160         let Some(cipher) = Cipher::find_by_uuid(&uuid, &conn).await else {
   1161             err!("Cipher doesn't exist")
   1162         };
   1163         if !cipher.is_accessible_to_user(&user_uuid, &conn).await {
   1164             err!("Cipher is not accessible by user")
   1165         }
   1166         // Move cipher
   1167         cipher
   1168             .move_to_folder(data.folder_id.clone(), &user_uuid, &conn)
   1169             .await?;
   1170     }
   1171     Ok(())
   1172 }
   1173 
   1174 #[put("/ciphers/move", data = "<data>")]
   1175 async fn move_cipher_selected_put(
   1176     data: Json<MoveCipherData>,
   1177     headers: Headers,
   1178     conn: DbConn,
   1179 ) -> EmptyResult {
   1180     move_cipher_selected(data, headers, conn).await
   1181 }
   1182 
   1183 #[derive(FromForm)]
   1184 struct OrganizationId {
   1185     #[field(name = "organizationId")]
   1186     org_id: String,
   1187 }
   1188 
   1189 #[post("/ciphers/purge?<organization..>", data = "<data>")]
   1190 async fn delete_all(
   1191     organization: Option<OrganizationId>,
   1192     data: Json<PasswordOrOtpData>,
   1193     headers: Headers,
   1194     conn: DbConn,
   1195 ) -> EmptyResult {
   1196     let data: PasswordOrOtpData = data.into_inner();
   1197     let mut user = headers.user;
   1198     data.validate(&user)?;
   1199     if let Some(org_data) = organization {
   1200         // Organization ID in query params, purging organization vault
   1201         match UserOrganization::find_by_user_and_org(&user.uuid, &org_data.org_id, &conn).await {
   1202             None => err!("You don't have permission to purge the organization vault"),
   1203             Some(user_org) => {
   1204                 if user_org.atype == UserOrgType::Owner {
   1205                     Cipher::delete_all_by_organization(&org_data.org_id, &conn).await?;
   1206                     Ok(())
   1207                 } else {
   1208                     err!("You don't have permission to purge the organization vault");
   1209                 }
   1210             }
   1211         }
   1212     } else {
   1213         // No organization ID in query params, purging user vault
   1214         // Delete ciphers and their attachments
   1215         for cipher in Cipher::find_owned_by_user(&user.uuid, &conn).await {
   1216             cipher.delete(&conn).await?;
   1217         }
   1218         // Delete folders
   1219         for f in Folder::find_by_user(&user.uuid, &conn).await {
   1220             f.delete(&conn).await?;
   1221         }
   1222         user.update_revision(&conn).await?;
   1223         Ok(())
   1224     }
   1225 }
   1226 
   1227 async fn _delete_cipher_by_uuid(
   1228     uuid: &str,
   1229     headers: &Headers,
   1230     conn: &DbConn,
   1231     soft_delete: bool,
   1232 ) -> EmptyResult {
   1233     let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else {
   1234         err!("Cipher doesn't exist")
   1235     };
   1236     if !cipher
   1237         .is_write_accessible_to_user(&headers.user.uuid, conn)
   1238         .await
   1239     {
   1240         err!("Cipher can't be deleted by user")
   1241     }
   1242     if soft_delete {
   1243         cipher.deleted_at = Some(Utc::now().naive_utc());
   1244         cipher.save(conn).await?;
   1245     } else {
   1246         cipher.delete(conn).await?;
   1247     }
   1248     Ok(())
   1249 }
   1250 
   1251 #[derive(Deserialize)]
   1252 #[serde(rename_all = "camelCase")]
   1253 struct CipherIdsData {
   1254     ids: Vec<String>,
   1255 }
   1256 
   1257 async fn _delete_multiple_ciphers(
   1258     data: Json<CipherIdsData>,
   1259     headers: Headers,
   1260     conn: DbConn,
   1261     soft_delete: bool,
   1262 ) -> EmptyResult {
   1263     let data = data.into_inner();
   1264     for uuid in data.ids {
   1265         if let error @ Err(_) = _delete_cipher_by_uuid(&uuid, &headers, &conn, soft_delete).await {
   1266             return error;
   1267         };
   1268     }
   1269     Ok(())
   1270 }
   1271 
   1272 async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &DbConn) -> JsonResult {
   1273     let Some(mut cipher) = Cipher::find_by_uuid(uuid, conn).await else {
   1274         err!("Cipher doesn't exist")
   1275     };
   1276     if !cipher
   1277         .is_write_accessible_to_user(&headers.user.uuid, conn)
   1278         .await
   1279     {
   1280         err!("Cipher can't be restored by user")
   1281     }
   1282     cipher.deleted_at = None;
   1283     cipher.save(conn).await?;
   1284     Ok(Json(
   1285         cipher
   1286             .to_json(&headers.user.uuid, None, CipherSyncType::User, conn)
   1287             .await,
   1288     ))
   1289 }
   1290 
   1291 async fn _restore_multiple_ciphers(
   1292     data: Json<CipherIdsData>,
   1293     headers: &Headers,
   1294     conn: &DbConn,
   1295 ) -> JsonResult {
   1296     let data = data.into_inner();
   1297     let mut ciphers: Vec<Value> = Vec::new();
   1298     for uuid in data.ids {
   1299         match _restore_cipher_by_uuid(&uuid, headers, conn).await {
   1300             Ok(json) => ciphers.push(json.into_inner()),
   1301             err => return err,
   1302         }
   1303     }
   1304     Ok(Json(json!({
   1305       "data": ciphers,
   1306       "object": "list",
   1307       "continuationToken": null
   1308     })))
   1309 }
   1310 /// This will hold all the necessary data to improve a full sync of all the ciphers
   1311 /// It can be used during the `Cipher::to_json()` call.
   1312 /// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.
   1313 /// This will not improve the speed of a single cipher.to_json() call that much, so better not to use it for those calls.
   1314 pub struct CipherSyncData {
   1315     pub cipher_folders: HashMap<String, String>,
   1316     pub cipher_favorites: HashSet<String>,
   1317     pub cipher_collections: HashMap<String, Vec<String>>,
   1318     pub user_organizations: HashMap<String, UserOrganization>,
   1319     pub user_collections: HashMap<String, CollectionUser>,
   1320 }
   1321 
   1322 #[derive(Eq, PartialEq)]
   1323 pub enum CipherSyncType {
   1324     User,
   1325     Organization,
   1326 }
   1327 
   1328 impl CipherSyncData {
   1329     pub async fn new(user_uuid: &str, sync_type: CipherSyncType, conn: &DbConn) -> Self {
   1330         let cipher_folders: HashMap<String, String>;
   1331         let cipher_favorites: HashSet<String>;
   1332         match sync_type {
   1333             // User Sync supports Folders and Favorites
   1334             CipherSyncType::User => {
   1335                 // Generate a HashMap with the Cipher UUID as key and the Folder UUID as value
   1336                 cipher_folders = FolderCipher::find_by_user(user_uuid, conn)
   1337                     .await
   1338                     .into_iter()
   1339                     .collect();
   1340                 // Generate a HashSet of all the Cipher UUID's which are marked as favorite
   1341                 cipher_favorites = Favorite::get_all_cipher_uuid_by_user(user_uuid, conn)
   1342                     .await
   1343                     .into_iter()
   1344                     .collect();
   1345             }
   1346             // Organization Sync does not support Folders and Favorites.
   1347             // If these are set, it will cause issues in the web-vault.
   1348             CipherSyncType::Organization => {
   1349                 cipher_folders = HashMap::with_capacity(0);
   1350                 cipher_favorites = HashSet::with_capacity(0);
   1351             }
   1352         }
   1353         // Generate a HashMap with the Cipher UUID as key and one or more Collection UUID's
   1354         let user_cipher_collections =
   1355             Cipher::get_collections_with_cipher_by_user(user_uuid.to_owned(), conn).await;
   1356         let mut cipher_collections: HashMap<String, Vec<String>> =
   1357             HashMap::with_capacity(user_cipher_collections.len());
   1358         for (cipher, collection) in user_cipher_collections {
   1359             cipher_collections
   1360                 .entry(cipher)
   1361                 .or_default()
   1362                 .push(collection);
   1363         }
   1364         // Generate a HashMap with the Organization UUID as key and the UserOrganization record
   1365         let user_organizations: HashMap<String, UserOrganization> =
   1366             UserOrganization::find_by_user(user_uuid, conn)
   1367                 .await
   1368                 .into_iter()
   1369                 .map(|uo| (uo.org_uuid.clone(), uo))
   1370                 .collect();
   1371         // Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
   1372         let user_collections: HashMap<String, CollectionUser> =
   1373             CollectionUser::find_by_user(user_uuid, conn)
   1374                 .await
   1375                 .into_iter()
   1376                 .map(|uc| (uc.collection_uuid.clone(), uc))
   1377                 .collect();
   1378         Self {
   1379             cipher_folders,
   1380             cipher_favorites,
   1381             cipher_collections,
   1382             user_organizations,
   1383             user_collections,
   1384         }
   1385     }
   1386 }