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 (47896B)


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