From e4606431d1f79133fe7d708736b4083e9adb8a01 Mon Sep 17 00:00:00 2001
From: Bernd Schoolmann <mail@quexten.com>
Date: Fri, 16 Jun 2023 23:34:16 +0200
Subject: [PATCH] Fix mobile push blocking requests and spamming push server

---
 src/api/admin.rs              |   6 +-
 src/api/core/accounts.rs      |  10 +--
 src/api/core/organizations.rs |   2 +-
 src/api/core/sends.rs         |  82 +++++++++++++++++++-----
 src/api/notifications.rs      |  17 +++--
 src/api/push.rs               | 115 ++++++++++++++--------------------
 src/db/models/device.rs       |  14 ++++-
 7 files changed, 146 insertions(+), 100 deletions(-)

diff --git a/src/api/admin.rs b/src/api/admin.rs
index 005856de..9eb965cc 100644
--- a/src/api/admin.rs
+++ b/src/api/admin.rs
@@ -403,10 +403,10 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe
 async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
     let mut user = get_user_or_404(uuid, &mut conn).await?;
 
-    nt.send_logout(&user, None, &mut conn).await;
+    nt.send_logout(&user, None).await;
 
     if CONFIG.push_enabled() {
-        for device in Device::find_push_device_by_user(&user.uuid, &mut conn).await {
+        for device in Device::find_push_devices_by_user(&user.uuid, &mut conn).await {
             match unregister_push_device(device.uuid).await {
                 Ok(r) => r,
                 Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e),
@@ -429,7 +429,7 @@ async fn disable_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Noti
 
     let save_result = user.save(&mut conn).await;
 
-    nt.send_logout(&user, None, &mut conn).await;
+    nt.send_logout(&user, None).await;
 
     save_result
 }
diff --git a/src/api/core/accounts.rs b/src/api/core/accounts.rs
index 351a1d5c..6c7d8ed5 100644
--- a/src/api/core/accounts.rs
+++ b/src/api/core/accounts.rs
@@ -343,7 +343,7 @@ async fn post_password(
     // Prevent loging out the client where the user requested this endpoint from.
     // If you do logout the user it will causes issues at the client side.
     // Adding the device uuid will prevent this.
-    nt.send_logout(&user, Some(headers.device.uuid), &mut conn).await;
+    nt.send_logout(&user, Some(headers.device.uuid)).await;
 
     save_result
 }
@@ -403,7 +403,7 @@ async fn post_kdf(data: JsonUpcase<ChangeKdfData>, headers: Headers, mut conn: D
     user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
     let save_result = user.save(&mut conn).await;
 
-    nt.send_logout(&user, Some(headers.device.uuid), &mut conn).await;
+    nt.send_logout(&user, Some(headers.device.uuid)).await;
 
     save_result
 }
@@ -490,7 +490,7 @@ async fn post_rotatekey(data: JsonUpcase<KeyData>, headers: Headers, mut conn: D
     // Prevent loging out the client where the user requested this endpoint from.
     // If you do logout the user it will causes issues at the client side.
     // Adding the device uuid will prevent this.
-    nt.send_logout(&user, Some(headers.device.uuid), &mut conn).await;
+    nt.send_logout(&user, Some(headers.device.uuid)).await;
 
     save_result
 }
@@ -513,7 +513,7 @@ async fn post_sstamp(
     user.reset_security_stamp();
     let save_result = user.save(&mut conn).await;
 
-    nt.send_logout(&user, None, &mut conn).await;
+    nt.send_logout(&user, None).await;
 
     save_result
 }
@@ -616,7 +616,7 @@ async fn post_email(
 
     let save_result = user.save(&mut conn).await;
 
-    nt.send_logout(&user, None, &mut conn).await;
+    nt.send_logout(&user, None).await;
 
     save_result
 }
diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs
index c404fbf0..c3e98973 100644
--- a/src/api/core/organizations.rs
+++ b/src/api/core/organizations.rs
@@ -2718,7 +2718,7 @@ async fn put_reset_password(
     user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None);
     user.save(&mut conn).await?;
 
-    nt.send_logout(&user, None, &mut conn).await;
+    nt.send_logout(&user, None).await;
 
     log_event(
         EventType::OrganizationUserAdminResetPassword as i32,
diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs
index 4ae9e8f3..56613a2f 100644
--- a/src/api/core/sends.rs
+++ b/src/api/core/sends.rs
@@ -180,8 +180,14 @@ async fn post_send(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbCon
 
     let mut send = create_send(data, headers.user.uuid)?;
     send.save(&mut conn).await?;
-    nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendCreate,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     Ok(Json(send.to_json()))
 }
@@ -253,8 +259,14 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
 
     // Save the changes in the database
     send.save(&mut conn).await?;
-    nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendCreate,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     Ok(Json(send.to_json()))
 }
@@ -337,8 +349,14 @@ async fn post_send_file_v2_data(
             data.data.move_copy_to(file_path).await?
         }
 
-        nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-            .await;
+        nt.send_send_update(
+            UpdateType::SyncSendCreate,
+            &send,
+            &send.update_users_revision(&mut conn).await,
+            &headers.device.uuid,
+            &mut conn,
+        )
+        .await;
     } else {
         err!("Send not found. Unable to save the file.");
     }
@@ -356,6 +374,7 @@ pub struct SendAccessData {
 async fn post_access(
     access_id: &str,
     data: JsonUpcase<SendAccessData>,
+    headers: Headers,
     mut conn: DbConn,
     ip: ClientIp,
     nt: Notify<'_>,
@@ -400,8 +419,14 @@ async fn post_access(
 
     send.save(&mut conn).await?;
 
-    nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendUpdate,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     Ok(Json(send.to_json_access(&mut conn).await))
 }
@@ -412,6 +437,7 @@ async fn post_access_file(
     file_id: &str,
     data: JsonUpcase<SendAccessData>,
     host: Host,
+    headers: Headers,
     mut conn: DbConn,
     nt: Notify<'_>,
 ) -> JsonResult {
@@ -452,8 +478,14 @@ async fn post_access_file(
 
     send.save(&mut conn).await?;
 
-    nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendUpdate,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     let token_claims = crate::auth::generate_send_claims(send_id, file_id);
     let token = crate::auth::encode_jwt(&token_claims);
@@ -535,8 +567,14 @@ async fn put_send(
     }
 
     send.save(&mut conn).await?;
-    nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendUpdate,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     Ok(Json(send.to_json()))
 }
@@ -553,8 +591,14 @@ async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_
     }
 
     send.delete(&mut conn).await?;
-    nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendDelete,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     Ok(())
 }
@@ -574,8 +618,14 @@ async fn put_remove_password(id: &str, headers: Headers, mut conn: DbConn, nt: N
 
     send.set_password(None);
     send.save(&mut conn).await?;
-    nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
-        .await;
+    nt.send_send_update(
+        UpdateType::SyncSendUpdate,
+        &send,
+        &send.update_users_revision(&mut conn).await,
+        &headers.device.uuid,
+        &mut conn,
+    )
+    .await;
 
     Ok(Json(send.to_json()))
 }
diff --git a/src/api/notifications.rs b/src/api/notifications.rs
index d67e7804..896caa41 100644
--- a/src/api/notifications.rs
+++ b/src/api/notifications.rs
@@ -240,11 +240,11 @@ impl WebSocketUsers {
         self.send_update(&user.uuid, &data).await;
 
         if CONFIG.push_enabled() {
-            push_user_update(ut, user).await;
+            push_user_update(ut, user);
         }
     }
 
-    pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>, conn: &mut DbConn) {
+    pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
         let data = create_update(
             vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
             UpdateType::LogOut,
@@ -254,7 +254,7 @@ impl WebSocketUsers {
         self.send_update(&user.uuid, &data).await;
 
         if CONFIG.push_enabled() {
-            push_logout(user, acting_device_uuid, conn).await;
+            push_logout(user, acting_device_uuid);
         }
     }
 
@@ -325,7 +325,14 @@ impl WebSocketUsers {
         }
     }
 
-    pub async fn send_send_update(&self, ut: UpdateType, send: &DbSend, user_uuids: &[String], conn: &mut DbConn) {
+    pub async fn send_send_update(
+        &self,
+        ut: UpdateType,
+        send: &DbSend,
+        user_uuids: &[String],
+        acting_device_uuid: &String,
+        conn: &mut DbConn,
+    ) {
         let user_uuid = convert_option(send.user_uuid.clone());
 
         let data = create_update(
@@ -342,7 +349,7 @@ impl WebSocketUsers {
             self.send_update(uuid, &data).await;
         }
         if CONFIG.push_enabled() && user_uuids.len() == 1 {
-            push_send_update(ut, send, conn).await;
+            push_send_update(ut, send, acting_device_uuid, conn).await;
         }
     }
 }
diff --git a/src/api/push.rs b/src/api/push.rs
index 3c39c0ae..da9255a6 100644
--- a/src/api/push.rs
+++ b/src/api/push.rs
@@ -139,71 +139,52 @@ pub async fn push_cipher_update(
         }
     };
 
-    for device in Device::find_by_user(user_uuid, conn).await {
-        let data = json!({
+    if Device::check_user_has_push_device(user_uuid, conn).await {
+        send_to_push_relay(json!({
             "userId": user_uuid,
             "organizationId": (),
-            "deviceId": device.push_uuid,
+            "deviceId": acting_device_uuid,
             "identifier": acting_device_uuid,
             "type": ut as i32,
             "payload": {
-                "Id": cipher.uuid,
-                "UserId": cipher.user_uuid,
-                "OrganizationId": (),
-                "RevisionDate": cipher.updated_at
+                "id": cipher.uuid,
+                "userId": cipher.user_uuid,
+                "organizationId": (),
+                "revisionDate": cipher.updated_at
             }
-        });
-
-        send_to_push_relay(data).await;
+        }))
+        .await;
     }
 }
 
-pub async fn push_logout(user: &User, acting_device_uuid: Option<String>, conn: &mut crate::db::DbConn) {
-    if let Some(d) = acting_device_uuid {
-        for device in Device::find_by_user(&user.uuid, conn).await {
-            let data = json!({
-                "userId": user.uuid,
-                "organizationId": (),
-                "deviceId": device.push_uuid,
-                "identifier": d,
-                "type": UpdateType::LogOut as i32,
-                "payload": {
-                    "UserId": user.uuid,
-                    "Date": user.updated_at
-                }
-            });
-            send_to_push_relay(data).await;
-        }
-    } else {
-        let data = json!({
+pub fn push_logout(user: &User, acting_device_uuid: Option<String>) {
+    let acting_device_uuid: Value = acting_device_uuid.map(|v| v.into()).unwrap_or_else(|| Value::Null);
+
+    tokio::task::spawn(send_to_push_relay(json!({
+        "userId": user.uuid,
+        "organizationId": (),
+        "deviceId": acting_device_uuid,
+        "identifier": acting_device_uuid,
+        "type": UpdateType::LogOut as i32,
+        "payload": {
             "userId": user.uuid,
-            "organizationId": (),
-            "deviceId": (),
-            "identifier": (),
-            "type": UpdateType::LogOut as i32,
-            "payload": {
-                "UserId": user.uuid,
-                "Date": user.updated_at
-            }
-        });
-        send_to_push_relay(data).await;
-    }
+            "date": user.updated_at
+        }
+    })));
 }
 
-pub async fn push_user_update(ut: UpdateType, user: &User) {
-    let data = json!({
+pub fn push_user_update(ut: UpdateType, user: &User) {
+    tokio::task::spawn(send_to_push_relay(json!({
         "userId": user.uuid,
         "organizationId": (),
         "deviceId": (),
         "identifier": (),
         "type": ut as i32,
         "payload": {
-            "UserId": user.uuid,
-            "Date": user.updated_at
+            "userId": user.uuid,
+            "date": user.updated_at
         }
-    });
-
-    send_to_push_relay(data).await;
+    })));
 }
 
 pub async fn push_folder_update(
@@ -212,46 +193,42 @@ pub async fn push_folder_update(
     acting_device_uuid: &String,
     conn: &mut crate::db::DbConn,
 ) {
-    for device in Device::find_by_user(&folder.user_uuid, conn).await {
-        let data = json!({
+    if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
+        tokio::task::spawn(send_to_push_relay(json!({
             "userId": folder.user_uuid,
             "organizationId": (),
-            "deviceId": device.push_uuid,
+            "deviceId": acting_device_uuid,
             "identifier": acting_device_uuid,
             "type": ut as i32,
             "payload": {
-                "Id": folder.uuid,
-                "UserId": folder.user_uuid,
-                "RevisionDate": folder.updated_at
+                "id": folder.uuid,
+                "userId": folder.user_uuid,
+                "revisionDate": folder.updated_at
             }
-        });
-
-        send_to_push_relay(data).await;
+        })));
     }
 }
 
-pub async fn push_send_update(ut: UpdateType, send: &Send, conn: &mut crate::db::DbConn) {
+pub async fn push_send_update(ut: UpdateType, send: &Send, acting_device_uuid: &String, conn: &mut crate::db::DbConn) {
     if let Some(s) = &send.user_uuid {
-        for device in Device::find_by_user(s, conn).await {
-            let data = json!({
+        if Device::check_user_has_push_device(s, conn).await {
+            tokio::task::spawn(send_to_push_relay(json!({
                 "userId": send.user_uuid,
                 "organizationId": (),
-                "deviceId": device.push_uuid,
-                "identifier": (),
+                "deviceId": acting_device_uuid,
+                "identifier": acting_device_uuid,
                 "type": ut as i32,
                 "payload": {
-                    "Id": send.uuid,
-                    "UserId": send.user_uuid,
-                    "RevisionDate": send.revision_date
+                    "id": send.uuid,
+                    "userId": send.user_uuid,
+                    "revisionDate": send.revision_date
                 }
-            });
-
-            send_to_push_relay(data).await;
+            })));
         }
     }
 }
 
-async fn send_to_push_relay(data: Value) {
+async fn send_to_push_relay(notification_data: Value) {
     if !CONFIG.push_enabled() {
         return;
     }
@@ -270,8 +247,8 @@ async fn send_to_push_relay(data: Value) {
         .post(CONFIG.push_relay_uri() + "/push/send")
         .header(ACCEPT, "application/json")
         .header(CONTENT_TYPE, "application/json")
-        .header(AUTHORIZATION, auth_header)
-        .json(&data)
+        .header(AUTHORIZATION, &auth_header)
+        .json(&notification_data)
         .send()
         .await
     {
diff --git a/src/db/models/device.rs b/src/db/models/device.rs
index 81f12e18..78519737 100644
--- a/src/db/models/device.rs
+++ b/src/db/models/device.rs
@@ -202,7 +202,7 @@ impl Device {
                 .from_db()
         }}
     }
-    pub async fn find_push_device_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
+    pub async fn find_push_devices_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
         db_run! { conn: {
             devices::table
                 .filter(devices::user_uuid.eq(user_uuid))
@@ -212,4 +212,16 @@ impl Device {
                 .from_db()
         }}
     }
+
+    pub async fn check_user_has_push_device(user_uuid: &str, conn: &mut DbConn) -> bool {
+        db_run! { conn: {
+            devices::table
+            .filter(devices::user_uuid.eq(user_uuid))
+            .filter(devices::push_token.is_not_null())
+            .count()
+            .first::<i64>(conn)
+            .ok()
+            .unwrap_or(0) != 0
+        }}
+    }
 }