Skip to content

Commit

Permalink
feat: Push Notifications
Browse files Browse the repository at this point in the history
Co-authored-by: samb-devel <125741162+samb-devel@users.noreply.github.com>
  • Loading branch information
GeekCornerGH and samb-devel committed Mar 16, 2023
1 parent 729b563 commit 5a6bfe3
Show file tree
Hide file tree
Showing 25 changed files with 529 additions and 88 deletions.
6 changes: 6 additions & 0 deletions .env.template
Expand Up @@ -72,6 +72,12 @@
# WEBSOCKET_ADDRESS=0.0.0.0
# WEBSOCKET_PORT=3012

## Enables push notifications, get key and id from https://bitwarden.com/host
# PUSH_ENABLED=true
# PUSH_RELAY_URI=https://push.bitwarden.com
# PUSH_INSTALLATION_ID=CHANGEME
# PUSH_INSTALLATION_KEY=CHANGEME

## Controls whether users are allowed to create Bitwarden Sends.
## This setting applies globally to all users.
## To control this on a per-org basis instead, use the "Disable Send" org policy.
Expand Down
Empty file.
1 change: 1 addition & 0 deletions migrations/mysql/2023-02-18-125735_push_uuid_table/up.sql
@@ -0,0 +1 @@
ALTER TABLE devices CREATE COLUMN push_uuid TEXT;
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices CREATE COLUMN push_uuid TEXT;
Empty file.
1 change: 1 addition & 0 deletions migrations/sqlite/2023-02-18-125735_push_uuid_table/up.sql
@@ -0,0 +1 @@
ALTER TABLE devices CREATE COLUMN push_uuid TEXT;
25 changes: 18 additions & 7 deletions src/api/admin.rs
Expand Up @@ -13,7 +13,10 @@ use rocket::{
};

use crate::{
api::{core::log_event, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString},
api::{
core::log_event, push_logout, unregister_push_device, ApiResult, EmptyResult, JsonResult, Notify,
NumberOrString,
},
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
config::ConfigBuilder,
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
Expand Down Expand Up @@ -395,14 +398,21 @@ async fn delete_user(uuid: String, token: AdminToken, mut conn: DbConn) -> Empty
#[post("/users/<uuid>/deauth")]
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();

let save_result = user.save(&mut conn).await;

nt.send_logout(&user, None).await;
push_logout(&user, None, &mut conn).await; // Send logout notifications before deleting devices

save_result
match Device::find_push_device_by_user(&user.uuid, &mut conn).await {
Some(d) => {
match unregister_push_device(d.uuid).await {
Ok(r) => r,
Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e),
};
}
None => debug!("No device to unregister for {}", &user.email),
};
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
user.reset_security_stamp();
user.save(&mut conn).await
}

#[post("/users/<uuid>/disable")]
Expand All @@ -415,6 +425,7 @@ async fn disable_user(uuid: String, _token: AdminToken, mut conn: DbConn, nt: No
let save_result = user.save(&mut conn).await;

nt.send_logout(&user, None).await;
push_logout(&user, None, &mut conn).await;

save_result
}
Expand Down
80 changes: 75 additions & 5 deletions src/api/core/accounts.rs
Expand Up @@ -4,12 +4,15 @@ use serde_json::Value;

use crate::{
api::{
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
core::log_user_event, push_logout, register_push_device, unregister_push_device, EmptyResult, JsonResult,
JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
},
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
crypto,
db::{models::*, DbConn},
mail, CONFIG,
mail,
//push::{self, push_logout},
CONFIG,
};

use rocket::{
Expand Down Expand Up @@ -46,6 +49,9 @@ pub fn routes() -> Vec<rocket::Route> {
get_known_device,
get_known_device_from_path,
put_avatar,
put_device_token,
clear_device_token,
clear_device_token_post,
]
}

Expand Down Expand Up @@ -338,7 +344,8 @@ 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)).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
push_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;

save_result
}
Expand Down Expand Up @@ -395,7 +402,8 @@ 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)).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
push_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;

save_result
}
Expand Down Expand Up @@ -482,7 +490,8 @@ 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)).await;
nt.send_logout(&user, Some(headers.device.uuid.clone())).await;
push_logout(&user, Some(headers.device.uuid.clone()), &mut conn).await;

save_result
}
Expand All @@ -506,6 +515,7 @@ async fn post_sstamp(
let save_result = user.save(&mut conn).await;

nt.send_logout(&user, None).await;
push_logout(&user, None, &mut conn).await;

save_result
}
Expand Down Expand Up @@ -609,6 +619,7 @@ async fn post_email(
let save_result = user.save(&mut conn).await;

nt.send_logout(&user, None).await;
push_logout(&user, None, &mut conn).await;

save_result
}
Expand Down Expand Up @@ -930,3 +941,62 @@ impl<'r> FromRequest<'r> for KnownDevice {
})
}
}

#[derive(Deserialize, Debug)]
#[allow(non_snake_case)]
struct PushTokenData {
pushToken: String,
}

#[put("/devices/identifier/<uuid>/token", data = "<data>")]
async fn put_device_token(uuid: String, data: Json<PushTokenData>, headers: Headers, mut conn: DbConn) -> EmptyResult {
let data: PushTokenData = data.into_inner();
let token = data.pushToken.clone();
let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await {
Some(device) => device,
None => Device::new(uuid, headers.user.uuid.clone(), headers.device.name, headers.device.atype),
};
device.push_token = Some(token);
if device.push_uuid.is_none() {
device.push_uuid = Some(uuid::Uuid::new_v4().to_string());
}
if let Err(e) = device.save(&mut conn).await {
error!("An error occured while trying to save the device push token: {}", e);
return Err(e);
}
if CONFIG.push_enabled() {
if let Err(e) = register_push_device(headers.user.uuid, device).await {
error!("An error occured while proceeding registration of a device: {}", e);
};
}

Ok(())
}

#[put("/devices/identifier/<uuid>/clear-token")]
async fn clear_device_token(uuid: String, mut conn: DbConn) -> &'static str {
// This only clears push token
// https://github.com/bitwarden/core/blob/master/src/Api/Controllers/DevicesController.cs#L109
// https://github.com/bitwarden/core/blob/master/src/Core/Services/Implementations/DeviceService.cs#L37
// This is somehow not implemented in any app, added it in case it is required
match Device::delete_token_by_uuid(&uuid, &mut conn).await {
Err(e) => error!("{}", e),
Ok(_r) => (),
};
let device = match Device::find_by_uuid(&uuid, &mut conn).await {
Some(device) => device,
None => return "",
};
match unregister_push_device(device.uuid).await {
Err(e) => error!("{}", e),
Ok(_r) => (),
};
""
}

// On upstream server, both PUT and POST are declared. Sadly Rocket doesn't allows to put multiple methods on the same function, so we call the function manually
#[post("/devices/identifier/<uuid>/clear-token")]
async fn clear_device_token_post(uuid: String, conn: DbConn) -> &'static str {
clear_device_token(uuid, conn).await;
""
}
40 changes: 27 additions & 13 deletions src/api/core/ciphers.rs
Expand Up @@ -10,7 +10,10 @@ use rocket::{
use serde_json::Value;

use crate::{
api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
api::{
self, core::log_event, push_cipher_update, push_user_update, EmptyResult, JsonResult, JsonUpcase, Notify,
PasswordData, UpdateType,
},
auth::Headers,
crypto,
db::{models::*, DbConn, DbPool},
Expand Down Expand Up @@ -511,10 +514,9 @@ pub async fn update_cipher_from_data(
)
.await;
}

nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid).await;
nt.send_cipher_update(ut as i32, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid).await;
push_cipher_update(ut as i32, cipher, &headers.device.uuid, conn).await;
}

Ok(())
}

Expand Down Expand Up @@ -579,7 +581,8 @@ async fn post_ciphers_import(

let mut user = headers.user;
user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
nt.send_user_update(UpdateType::SyncVault as i32, &user).await;
push_user_update(UpdateType::SyncVault as i32, &user).await;
Ok(())
}

Expand Down Expand Up @@ -1103,12 +1106,13 @@ async fn save_attachment(
}

nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
UpdateType::SyncCipherUpdate as i32,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &headers.device.uuid, &mut conn).await;

if let Some(org_uuid) = &cipher.organization_uuid {
log_event(
Expand Down Expand Up @@ -1392,7 +1396,9 @@ async fn move_cipher_selected(
// Move cipher
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?;

nt.send_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &[user_uuid.clone()], &headers.device.uuid).await;
nt.send_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &[user_uuid.clone()], &headers.device.uuid)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &headers.device.uuid, &mut conn).await;
}

Ok(())
Expand Down Expand Up @@ -1439,7 +1445,8 @@ 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::SyncVault, &user).await;
nt.send_user_update(UpdateType::SyncVault as i32, &user).await;
push_user_update(UpdateType::SyncVault as i32, &user).await;

log_event(
EventType::OrganizationPurgedVault as i32,
Expand Down Expand Up @@ -1472,7 +1479,8 @@ async fn delete_all(
}

user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
nt.send_user_update(UpdateType::SyncVault as i32, &user).await;
push_user_update(UpdateType::SyncVault as i32, &user).await;
Ok(())
}
}
Expand All @@ -1498,21 +1506,23 @@ async fn _delete_cipher_by_uuid(
cipher.deleted_at = Some(Utc::now().naive_utc());
cipher.save(conn).await?;
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
UpdateType::SyncCipherUpdate as i32,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &headers.device.uuid, conn).await;
} else {
cipher.delete(conn).await?;
nt.send_cipher_update(
UpdateType::SyncCipherDelete,
UpdateType::SyncCipherDelete as i32,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &headers.device.uuid, conn).await;
}

if let Some(org_uuid) = cipher.organization_uuid {
Expand Down Expand Up @@ -1576,12 +1586,14 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
cipher.save(conn).await?;

nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
UpdateType::SyncCipherUpdate as i32,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &headers.device.uuid, conn).await;

if let Some(org_uuid) = &cipher.organization_uuid {
log_event(
EventType::CipherRestored as i32,
Expand Down Expand Up @@ -1657,12 +1669,14 @@ async fn _delete_cipher_attachment_by_id(
// Delete attachment
attachment.delete(conn).await?;
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
UpdateType::SyncCipherUpdate as i32,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate as i32, &cipher, &headers.device.uuid, conn).await;

if let Some(org_uuid) = cipher.organization_uuid {
log_event(
EventType::CipherAttachmentDeleted as i32,
Expand Down
11 changes: 7 additions & 4 deletions src/api/core/folders.rs
Expand Up @@ -2,7 +2,7 @@ use rocket::serde::json::Json;
use serde_json::Value;

use crate::{
api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
api::{push_folder_update, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
auth::Headers,
db::{models::*, DbConn},
};
Expand Down Expand Up @@ -50,7 +50,8 @@ async fn post_folders(data: JsonUpcase<FolderData>, headers: Headers, mut conn:
let mut folder = Folder::new(headers.user.uuid, data.Name);

folder.save(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.uuid).await;
nt.send_folder_update(UpdateType::SyncFolderCreate as i32, &folder, &headers.device.uuid).await;
push_folder_update(UpdateType::SyncFolderCreate as i32, &folder, &headers.device.uuid, &mut conn).await;

Ok(Json(folder.to_json()))
}
Expand Down Expand Up @@ -88,7 +89,8 @@ async fn put_folder(
folder.name = data.Name;

folder.save(&mut conn).await?;
nt.send_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.uuid).await;
nt.send_folder_update(UpdateType::SyncFolderUpdate as i32, &folder, &headers.device.uuid).await;
push_folder_update(UpdateType::SyncFolderUpdate as i32, &folder, &headers.device.uuid, &mut conn).await;

Ok(Json(folder.to_json()))
}
Expand All @@ -112,6 +114,7 @@ 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::SyncFolderDelete, &folder, &headers.device.uuid).await;
nt.send_folder_update(UpdateType::SyncFolderDelete as i32, &folder, &headers.device.uuid).await;
push_folder_update(UpdateType::SyncFolderDelete as i32, &folder, &headers.device.uuid, &mut conn).await;
Ok(())
}

0 comments on commit 5a6bfe3

Please sign in to comment.