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