From 2972904eb83e7148b22f1228dfaa15a27f002ed8 Mon Sep 17 00:00:00 2001 From: BlackDex Date: Fri, 30 Dec 2022 21:23:55 +0100 Subject: [PATCH] Update WebSocket Notifications Previously the websocket notifications were using `app_id` as the `ContextId`. This was incorrect and should have been the device_uuid from the client device executing the request. The clients will ignore the websocket request if the uuid matches. This also fixes some issues with the Desktop client which is able to modify attachments within the same screen and causes an issue when saving the attachment afterwards. Also changed the way to handle removed attachments, since that causes an error saving the vault cipher afterwards, complaining about a missing attachment. Bitwarden ignores this, and continues with the remaining attachments (if any). This also fixes #2591 . Further some more websocket notifications have been added to some other functions which enhance the user experience. - Logout users when deauthed, changed password, rotated keys - Trigger OrgSyncKeys on user confirm and removal - Added some extra to the send feature Also renamed UpdateTypes to match Bitwarden naming. --- src/api/admin.rs | 18 ++++++--- src/api/core/accounts.rs | 47 ++++++++++++++++++---- src/api/core/ciphers.rs | 73 +++++++++++++++++++++++++++-------- src/api/core/folders.rs | 6 +-- src/api/core/mod.rs | 27 ++++++++----- src/api/core/organizations.rs | 30 +++++++++++--- src/api/core/sends.rs | 6 +++ src/api/notifications.rs | 43 ++++++++++++++------- 8 files changed, 187 insertions(+), 63 deletions(-) diff --git a/src/api/admin.rs b/src/api/admin.rs index fd2293d6..6e0c2acf 100644 --- a/src/api/admin.rs +++ b/src/api/admin.rs @@ -13,7 +13,7 @@ use rocket::{ }; use crate::{ - api::{core::log_event, ApiResult, EmptyResult, JsonResult, NumberOrString}, + api::{core::log_event, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString, UpdateType}, auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp}, config::ConfigBuilder, db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType}, @@ -365,22 +365,30 @@ async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn, ip: Cli } #[post("/users//deauth")] -async fn deauth_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult { +async fn deauth_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&uuid, &mut conn).await?; Device::delete_all_by_user(&user.uuid, &mut conn).await?; user.reset_security_stamp(); - user.save(&mut conn).await + let save_result = user.save(&mut conn).await; + + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[post("/users//disable")] -async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn) -> EmptyResult { +async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let mut user = get_user_or_404(&uuid, &mut conn).await?; Device::delete_all_by_user(&user.uuid, &mut conn).await?; user.reset_security_stamp(); user.enabled = false; - user.save(&mut conn).await + let save_result = user.save(&mut conn).await; + + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[post("/users//enable")] diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs index cb6dc4fc..27bdb12a 100644 --- a/src/api/core/accounts.rs +++ b/src/api/core/accounts.rs @@ -275,6 +275,7 @@ async fn post_password( headers: Headers, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> EmptyResult { let data: ChangePassData = data.into_inner().data; let mut user = headers.user; @@ -293,7 +294,11 @@ async fn post_password( Some(vec![String::from("post_rotatekey"), String::from("get_contacts"), String::from("get_public_keys")]), ); user.akey = data.Key; - user.save(&mut conn).await + let save_result = user.save(&mut conn).await; + + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[derive(Deserialize)] @@ -308,7 +313,7 @@ struct ChangeKdfData { } #[post("/accounts/kdf", data = "")] -async fn post_kdf(data: JsonUpcase, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn post_kdf(data: JsonUpcase, headers: Headers, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult { let data: ChangeKdfData = data.into_inner().data; let mut user = headers.user; @@ -320,7 +325,11 @@ async fn post_kdf(data: JsonUpcase, headers: Headers, mut conn: D user.client_kdf_type = data.Kdf; user.set_password(&data.NewMasterPasswordHash, None); user.akey = data.Key; - user.save(&mut conn).await + let save_result = user.save(&mut conn).await; + + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[derive(Deserialize)] @@ -388,6 +397,7 @@ async fn post_rotatekey( // Prevent triggering cipher updates via WebSockets by settings UpdateType::None // The user sessions are invalidated because all the ciphers were re-encrypted and thus triggering an update could cause issues. + // We force the users to logout after the user has been saved to try and prevent these issues. update_cipher_from_data(&mut saved_cipher, cipher_data, &headers, false, &mut conn, &ip, &nt, UpdateType::None) .await? } @@ -399,11 +409,20 @@ async fn post_rotatekey( user.private_key = Some(data.PrivateKey); user.reset_security_stamp(); - user.save(&mut conn).await + let save_result = user.save(&mut conn).await; + + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[post("/accounts/security-stamp", data = "")] -async fn post_sstamp(data: JsonUpcase, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn post_sstamp( + data: JsonUpcase, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { let data: PasswordData = data.into_inner().data; let mut user = headers.user; @@ -413,7 +432,11 @@ async fn post_sstamp(data: JsonUpcase, headers: Headers, mut conn: Device::delete_all_by_user(&user.uuid, &mut conn).await?; user.reset_security_stamp(); - user.save(&mut conn).await + let save_result = user.save(&mut conn).await; + + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[derive(Deserialize)] @@ -465,7 +488,12 @@ struct ChangeEmailData { } #[post("/accounts/email", data = "")] -async fn post_email(data: JsonUpcase, headers: Headers, mut conn: DbConn) -> EmptyResult { +async fn post_email( + data: JsonUpcase, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> EmptyResult { let data: ChangeEmailData = data.into_inner().data; let mut user = headers.user; @@ -507,8 +535,11 @@ async fn post_email(data: JsonUpcase, headers: Headers, mut con user.set_password(&data.NewMasterPasswordHash, None); user.akey = data.Key; + let save_result = user.save(&mut conn).await; - user.save(&mut conn).await + nt.send_user_update(UpdateType::LogOut, &user).await; + + save_result } #[post("/accounts/verify-email")] diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 7077f905..9750e4b6 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -310,7 +310,8 @@ async fn post_ciphers( data.LastKnownRevisionDate = None; let mut cipher = Cipher::new(data.Type, data.Name.clone()); - update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::CipherCreate).await?; + update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherCreate) + .await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) } @@ -415,7 +416,14 @@ pub async fn update_cipher_from_data( for (id, attachment) in attachments { let mut saved_att = match Attachment::find_by_id(&id, conn).await { Some(att) => att, - None => err!("Attachment doesn't exist"), + None => { + // Warn and continue here. + // A missing attachment means it was removed via an other client. + // Also the Desktop Client supports removing attachments and save an update afterwards. + // Bitwarden it self ignores these mismatches server side. + warn!("Attachment {id} doesn't exist"); + continue; + } }; if saved_att.cipher_uuid != cipher.uuid { @@ -482,8 +490,8 @@ pub async fn update_cipher_from_data( // Only log events for organizational ciphers if let Some(org_uuid) = &cipher.organization_uuid { let event_type = match (&ut, transfer_cipher) { - (UpdateType::CipherCreate, true) => EventType::CipherCreated, - (UpdateType::CipherUpdate, true) => EventType::CipherShared, + (UpdateType::SyncCipherCreate, true) => EventType::CipherCreated, + (UpdateType::SyncCipherUpdate, true) => EventType::CipherShared, (_, _) => EventType::CipherUpdated, }; @@ -499,7 +507,7 @@ pub async fn update_cipher_from_data( .await; } - nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await).await; + nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid).await; } Ok(()) @@ -562,7 +570,7 @@ async fn post_ciphers_import( let mut user = headers.user; user.update_revision(&mut conn).await?; - nt.send_user_update(UpdateType::Vault, &user).await; + nt.send_user_update(UpdateType::SyncVault, &user).await; Ok(()) } @@ -628,7 +636,8 @@ async fn put_cipher( err!("Cipher is not write accessible") } - update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::CipherUpdate).await?; + update_cipher_from_data(&mut cipher, data, &headers, false, &mut conn, &ip, &nt, UpdateType::SyncCipherUpdate) + .await?; Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, None, &mut conn).await)) } @@ -850,9 +859,9 @@ async fn share_cipher_by_uuid( // When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate. let ut = if data.Cipher.LastKnownRevisionDate.is_some() { - UpdateType::CipherUpdate + UpdateType::SyncCipherUpdate } else { - UpdateType::CipherCreate + UpdateType::SyncCipherCreate }; update_cipher_from_data(&mut cipher, data.Cipher, headers, shared_to_collection, conn, ip, nt, ut).await?; @@ -1054,7 +1063,13 @@ async fn save_attachment( data.data.move_copy_to(file_path).await? } - nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&mut conn).await).await; + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(&mut conn).await, + &headers.device.uuid, + ) + .await; if let Some(org_uuid) = &cipher.organization_uuid { log_event( @@ -1390,7 +1405,7 @@ async fn move_cipher_selected( // Move cipher cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?; - nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &[user_uuid.clone()]).await; + nt.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &[user_uuid.clone()], &headers.device.uuid).await; } Ok(()) @@ -1438,7 +1453,7 @@ async fn delete_all( Some(user_org) => { if user_org.atype == UserOrgType::Owner { Cipher::delete_all_by_organization(&org_data.org_id, &mut conn).await?; - nt.send_user_update(UpdateType::Vault, &user).await; + nt.send_user_update(UpdateType::SyncVault, &user).await; log_event( EventType::OrganizationPurgedVault as i32, @@ -1471,7 +1486,7 @@ async fn delete_all( } user.update_revision(&mut conn).await?; - nt.send_user_update(UpdateType::Vault, &user).await; + nt.send_user_update(UpdateType::SyncVault, &user).await; Ok(()) } } @@ -1497,10 +1512,22 @@ async fn _delete_cipher_by_uuid( if soft_delete { cipher.deleted_at = Some(Utc::now().naive_utc()); cipher.save(conn).await?; - nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await; + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(conn).await, + &headers.device.uuid, + ) + .await; } else { cipher.delete(conn).await?; - nt.send_cipher_update(UpdateType::CipherDelete, &cipher, &cipher.update_users_revision(conn).await).await; + nt.send_cipher_update( + UpdateType::SyncCipherDelete, + &cipher, + &cipher.update_users_revision(conn).await, + &headers.device.uuid, + ) + .await; } if let Some(org_uuid) = cipher.organization_uuid { @@ -1562,7 +1589,13 @@ async fn _restore_cipher_by_uuid( cipher.deleted_at = None; cipher.save(conn).await?; - nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await; + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(conn).await, + &headers.device.uuid, + ) + .await; if let Some(org_uuid) = &cipher.organization_uuid { log_event( EventType::CipherRestored as i32, @@ -1639,7 +1672,13 @@ async fn _delete_cipher_attachment_by_id( // Delete attachment attachment.delete(conn).await?; - nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(conn).await).await; + nt.send_cipher_update( + UpdateType::SyncCipherUpdate, + &cipher, + &cipher.update_users_revision(conn).await, + &headers.device.uuid, + ) + .await; if let Some(org_uuid) = cipher.organization_uuid { log_event( EventType::CipherAttachmentDeleted as i32, diff --git a/src/api/core/folders.rs b/src/api/core/folders.rs index 95155803..c27d5455 100644 --- a/src/api/core/folders.rs +++ b/src/api/core/folders.rs @@ -50,7 +50,7 @@ async fn post_folders(data: JsonUpcase, headers: Headers, mut conn: let mut folder = Folder::new(headers.user.uuid, data.Name); folder.save(&mut conn).await?; - nt.send_folder_update(UpdateType::FolderCreate, &folder).await; + nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid).await; Ok(Json(folder.to_json())) } @@ -88,7 +88,7 @@ async fn put_folder( folder.name = data.Name; folder.save(&mut conn).await?; - nt.send_folder_update(UpdateType::FolderUpdate, &folder).await; + nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid).await; Ok(Json(folder.to_json())) } @@ -112,6 +112,6 @@ async fn delete_folder(uuid: String, headers: Headers, mut conn: DbConn, nt: Not // Delete the actual folder entry folder.delete(&mut conn).await?; - nt.send_folder_update(UpdateType::FolderDelete, &folder).await; + nt.send_folder_update(UpdateType::SyncFolderDelete, &folder, &headers.device.uuid).await; Ok(()) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs index 885fae81..3393a4d0 100644 --- a/src/api/core/mod.rs +++ b/src/api/core/mod.rs @@ -7,8 +7,7 @@ mod organizations; mod sends; pub mod two_factor; -pub use ciphers::purge_trashed_ciphers; -pub use ciphers::{CipherSyncData, CipherSyncType}; +pub use ciphers::{purge_trashed_ciphers, CipherSyncData, CipherSyncType}; pub use emergency_access::{emergency_notification_reminder_job, emergency_request_timeout_job}; pub use events::{event_cleanup_job, log_event, log_user_event}; pub use sends::purge_sends; @@ -47,13 +46,11 @@ pub fn events_routes() -> Vec { // // Move this somewhere else // -use rocket::serde::json::Json; -use rocket::Catcher; -use rocket::Route; +use rocket::{serde::json::Json, Catcher, Route}; use serde_json::Value; use crate::{ - api::{JsonResult, JsonUpcase}, + api::{JsonResult, JsonUpcase, Notify, UpdateType}, auth::Headers, db::DbConn, error::Error, @@ -138,7 +135,12 @@ struct EquivDomainData { } #[post("/settings/domains", data = "")] -async fn post_eq_domains(data: JsonUpcase, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn post_eq_domains( + data: JsonUpcase, + headers: Headers, + mut conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { let data: EquivDomainData = data.into_inner().data; let excluded_globals = data.ExcludedGlobalEquivalentDomains.unwrap_or_default(); @@ -152,12 +154,19 @@ async fn post_eq_domains(data: JsonUpcase, headers: Headers, mu user.save(&mut conn).await?; + nt.send_user_update(UpdateType::SyncSettings, &user).await; + Ok(Json(json!({}))) } #[put("/settings/domains", data = "")] -async fn put_eq_domains(data: JsonUpcase, headers: Headers, conn: DbConn) -> JsonResult { - post_eq_domains(data, headers, conn).await +async fn put_eq_domains( + data: JsonUpcase, + headers: Headers, + conn: DbConn, + nt: Notify<'_>, +) -> JsonResult { + post_eq_domains(data, headers, conn, nt).await } #[get("/hibp/breach?")] diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index d50a1f12..60d6f714 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -957,6 +957,7 @@ async fn bulk_confirm_invite( headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> Json { let data = data.into_inner().data; @@ -966,7 +967,8 @@ async fn bulk_confirm_invite( for invite in keys { let org_user_id = invite["Id"].as_str().unwrap_or_default(); let user_key = invite["Key"].as_str().unwrap_or_default(); - let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn, &ip).await { + let err_msg = match _confirm_invite(&org_id, org_user_id, user_key, &headers, &mut conn, &ip, &nt).await + { Ok(_) => String::new(), Err(e) => format!("{:?}", e), }; @@ -998,10 +1000,11 @@ async fn confirm_invite( headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> EmptyResult { let data = data.into_inner().data; let user_key = data["Key"].as_str().unwrap_or_default(); - _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn, &ip).await + _confirm_invite(&org_id, &org_user_id, user_key, &headers, &mut conn, &ip, &nt).await } async fn _confirm_invite( @@ -1011,6 +1014,7 @@ async fn _confirm_invite( headers: &AdminHeaders, conn: &mut DbConn, ip: &ClientIp, + nt: &Notify<'_>, ) -> EmptyResult { if key.is_empty() || org_user_id.is_empty() { err!("Key or UserId is not set, unable to process request"); @@ -1069,7 +1073,13 @@ async fn _confirm_invite( mail::send_invite_confirmed(&address, &org_name).await?; } - user_to_confirm.save(conn).await + let save_result = user_to_confirm.save(conn).await; + + if let Some(user) = User::find_by_uuid(&user_to_confirm.user_uuid, conn).await { + nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; + } + + save_result } #[get("/organizations//users/")] @@ -1206,12 +1216,13 @@ async fn bulk_delete_user( headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> Json { let data: OrgBulkIds = data.into_inner().data; let mut bulk_response = Vec::new(); for org_user_id in data.Ids { - let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await { + let err_msg = match _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip, &nt).await { Ok(_) => String::new(), Err(e) => format!("{:?}", e), }; @@ -1239,8 +1250,9 @@ async fn delete_user( headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> EmptyResult { - _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await + _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip, &nt).await } #[post("/organizations//users//delete")] @@ -1250,8 +1262,9 @@ async fn post_delete_user( headers: AdminHeaders, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> EmptyResult { - _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip).await + _delete_user(&org_id, &org_user_id, &headers, &mut conn, &ip, &nt).await } async fn _delete_user( @@ -1260,6 +1273,7 @@ async fn _delete_user( headers: &AdminHeaders, conn: &mut DbConn, ip: &ClientIp, + nt: &Notify<'_>, ) -> EmptyResult { let user_to_delete = match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { Some(user) => user, @@ -1288,6 +1302,10 @@ async fn _delete_user( ) .await; + if let Some(user) = User::find_by_uuid(&user_to_delete.user_uuid, conn).await { + nt.send_user_update(UpdateType::SyncOrgKeys, &user).await; + } + user_to_delete.delete(conn).await } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index 90fd560c..7d021d08 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -355,6 +355,7 @@ async fn post_access( data: JsonUpcase, mut conn: DbConn, ip: ClientIp, + nt: Notify<'_>, ) -> JsonResult { let mut send = match Send::find_by_access_id(&access_id, &mut conn).await { Some(s) => s, @@ -396,6 +397,8 @@ async fn post_access( send.save(&mut conn).await?; + nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await; + Ok(Json(send.to_json_access(&mut conn).await)) } @@ -406,6 +409,7 @@ async fn post_access_file( data: JsonUpcase, host: Host, mut conn: DbConn, + nt: Notify<'_>, ) -> JsonResult { let mut send = match Send::find_by_uuid(&send_id, &mut conn).await { Some(s) => s, @@ -444,6 +448,8 @@ async fn post_access_file( send.save(&mut conn).await?; + nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await; + let token_claims = crate::auth::generate_send_claims(&send_id, &file_id); let token = crate::auth::encode_jwt(&token_claims); Ok(Json(json!({ diff --git a/src/api/notifications.rs b/src/api/notifications.rs index cd53c96d..b51e1380 100644 --- a/src/api/notifications.rs +++ b/src/api/notifications.rs @@ -164,12 +164,13 @@ impl WebSocketUsers { let data = create_update( vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))], ut, + None, ); self.send_update(&user.uuid, &data).await; } - pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder) { + pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, acting_device_uuid: &String) { let data = create_update( vec![ ("Id".into(), folder.uuid.clone().into()), @@ -177,12 +178,19 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(folder.updated_at)), ], ut, + Some(acting_device_uuid.into()), ); self.send_update(&folder.user_uuid, &data).await; } - pub async fn send_cipher_update(&self, ut: UpdateType, cipher: &Cipher, user_uuids: &[String]) { + pub async fn send_cipher_update( + &self, + ut: UpdateType, + cipher: &Cipher, + user_uuids: &[String], + acting_device_uuid: &String, + ) { let user_uuid = convert_option(cipher.user_uuid.clone()); let org_uuid = convert_option(cipher.organization_uuid.clone()); @@ -195,6 +203,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(cipher.updated_at)), ], ut, + Some(acting_device_uuid.into()), ); for uuid in user_uuids { @@ -212,6 +221,7 @@ impl WebSocketUsers { ("RevisionDate".into(), serialize_date(send.revision_date)), ], ut, + None, ); for uuid in user_uuids { @@ -228,14 +238,14 @@ impl WebSocketUsers { "ReceiveMessage", // Target [ // Arguments { - "ContextId": "app_id", + "ContextId": acting_device_uuid || Nil, "Type": ut as i32, "Payload": {} } ] ] */ -fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType) -> Vec { +fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_uuid: Option) -> Vec { use rmpv::Value as V; let value = V::Array(vec![ @@ -244,7 +254,7 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType) -> Vec { V::Nil, "ReceiveMessage".into(), V::Array(vec![V::Map(vec![ - ("ContextId".into(), "app_id".into()), + ("ContextId".into(), acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| V::Nil)), ("Type".into(), (ut as i32).into()), ("Payload".into(), payload.into()), ])]), @@ -260,17 +270,17 @@ fn create_ping() -> Vec { #[allow(dead_code)] #[derive(Eq, PartialEq)] pub enum UpdateType { - CipherUpdate = 0, - CipherCreate = 1, - LoginDelete = 2, - FolderDelete = 3, - Ciphers = 4, + SyncCipherUpdate = 0, + SyncCipherCreate = 1, + SyncLoginDelete = 2, + SyncFolderDelete = 3, + SyncCiphers = 4, - Vault = 5, - OrgKeys = 6, - FolderCreate = 7, - FolderUpdate = 8, - CipherDelete = 9, + SyncVault = 5, + SyncOrgKeys = 6, + SyncFolderCreate = 7, + SyncFolderUpdate = 8, + SyncCipherDelete = 9, SyncSettings = 10, LogOut = 11, @@ -279,6 +289,9 @@ pub enum UpdateType { SyncSendUpdate = 13, SyncSendDelete = 14, + AuthRequest = 15, + AuthRequestResponse = 16, + None = 100, }