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(¤t_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(¤t_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 }