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