Skip to content

Commit

Permalink
feat: Implement Push Notifications sync
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 5, 2023
1 parent 337cbfa commit 59da014
Show file tree
Hide file tree
Showing 31 changed files with 537 additions and 74 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.
@@ -0,0 +1 @@
ALTER TABLE devices RENAME COLUMN uuid TO identifier;
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices ADD uuid TEXT NOT NULL DEFAULT "00000000-0000-0000-0000-000000000000";
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices RENAME COLUMN uuid TO identifier;
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices ADD uuid TEXT NOT NULL DEFAULT "00000000-0000-0000-0000-000000000000";
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices RENAME COLUMN uuid TO identifier;
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices ADD uuid TEXT NOT NULL DEFAULT "00000000-0000-0000-0000-000000000000";
17 changes: 14 additions & 3 deletions src/api/admin.rs
Expand Up @@ -19,6 +19,7 @@ use crate::{
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
error::{Error, MapResult},
mail,
push::{push_logout, unregister_push_device},
util::{
docker_base_image, format_naive_datetime_local, get_display_size, get_reqwest_client, is_running_in_docker,
},
Expand Down Expand Up @@ -382,13 +383,22 @@ async fn delete_user(uuid: String, _token: AdminToken, mut conn: DbConn, ip: Cli
#[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?;
nt.send_logout(&user, None).await;
push_logout(&user, None, &mut conn).await; // Send logout notifications before deleting devices

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();

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

nt.send_logout(&user, None).await;

save_result
}

Expand All @@ -402,6 +412,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
89 changes: 82 additions & 7 deletions src/api/core/accounts.rs
Expand Up @@ -9,7 +9,9 @@ use crate::{
auth::{decode_delete, decode_invite, decode_verify_email, ClientIp, Headers},
crypto,
db::{models::*, DbConn},
mail, CONFIG,
mail,
push::{self, push_logout},
CONFIG,
};

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

Expand Down Expand Up @@ -332,7 +337,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.identifier.clone())).await;
push_logout(&user, Some(headers.device.identifier.clone()), &mut conn).await;

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

save_result
}
Expand Down Expand Up @@ -482,7 +489,8 @@ async fn post_rotatekey(
// 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.identifier.clone())).await;
push_logout(&user, Some(headers.device.identifier.clone()), &mut conn).await;

save_result
}
Expand All @@ -506,6 +514,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 +618,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 @@ -872,12 +882,77 @@ async fn rotate_api_key(data: JsonUpcase<SecretVerificationRequest>, headers: He
_api_key(data, true, headers, conn).await
}

#[get("/devices/knowndevice/<email>/<uuid>")]
async fn get_known_device(email: String, uuid: String, mut conn: DbConn) -> JsonResult {
#[get("/devices/knowndevice/<email>/<identifier>")]
async fn get_known_device(email: String, identifier: String, mut conn: DbConn) -> JsonResult {
// This endpoint doesn't have auth header
let mut result = false;
if let Some(user) = User::find_by_mail(&email, &mut conn).await {
result = Device::find_by_uuid_and_user(&uuid, &user.uuid, &mut conn).await.is_some();
result = Device::find_by_identifier_and_user(&identifier, &user.uuid, &mut conn).await.is_some();
}
Ok(Json(json!(result)))
}

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

#[put("/devices/identifier/<identifier>/token", data = "<data>")]
async fn put_device_token(
identifier: 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_identifier_and_user(&headers.device.identifier, &headers.user.uuid, &mut conn).await {
Some(device) => device,
None => Device::new(identifier, headers.user.uuid.clone(), headers.device.name, headers.device.atype),
};
device.push_token = Some(token);
if device.uuid == *"00000000-0000-0000-0000-000000000000".to_string() {
device.uuid = 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) = push::register_push_device(headers.user.uuid, device).await {
error!("An error occured while proceeding registration of a device: {}", e);
};
}

Ok(())
}

#[put("/devices/identifier/<identifier>/clear-token")]
async fn clear_device_token(identifier: 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_identifier(&identifier, &mut conn).await {
Err(e) => error!("{}", e),
Ok(_r) => (),
};
let device = match Device::find_by_identifier(&identifier, &mut conn).await {
Some(device) => device,
None => return "",
};
match push::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/<identifier>/clear-token")]
async fn clear_device_token_post(identifier: String, conn: DbConn) -> &'static str {
clear_device_token(identifier, conn).await;
""
}
31 changes: 22 additions & 9 deletions src/api/core/ciphers.rs
Expand Up @@ -9,11 +9,13 @@ use rocket::{
};
use serde_json::Value;

use crate::push::push_user_update;
use crate::{
api::{self, core::log_event, EmptyResult, JsonResult, JsonUpcase, Notify, PasswordData, UpdateType},
auth::{ClientIp, Headers},
crypto,
db::{models::*, DbConn, DbPool},
push::push_cipher_update,
CONFIG,
};

Expand Down Expand Up @@ -522,10 +524,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, cipher, &cipher.update_users_revision(conn).await, &headers.device.identifier).await;
push_cipher_update(ut, cipher, &headers.device.identifier, conn).await;
}

Ok(())
}

Expand Down Expand Up @@ -593,6 +594,7 @@ async fn post_ciphers_import(
let mut user = headers.user;
user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
push_user_update(UpdateType::SyncVault, &user).await;
Ok(())
}

Expand Down Expand Up @@ -1133,9 +1135,10 @@ async fn save_attachment(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
&headers.device.identifier,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, &mut conn).await;

if let Some(org_uuid) = &cipher.organization_uuid {
log_event(
Expand Down Expand Up @@ -1471,7 +1474,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, &cipher, &[user_uuid.clone()], &headers.device.identifier)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, &mut conn).await;
}

Ok(())
Expand Down Expand Up @@ -1520,6 +1525,7 @@ async fn delete_all(
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;
push_user_update(UpdateType::SyncVault, &user).await;

log_event(
EventType::OrganizationPurgedVault as i32,
Expand Down Expand Up @@ -1553,6 +1559,7 @@ async fn delete_all(

user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;
push_user_update(UpdateType::SyncVault, &user).await;
Ok(())
}
}
Expand Down Expand Up @@ -1582,18 +1589,20 @@ async fn _delete_cipher_by_uuid(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device.identifier,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await;
} else {
cipher.delete(conn).await?;
nt.send_cipher_update(
UpdateType::SyncCipherDelete,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device.identifier,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await;
}

if let Some(org_uuid) = cipher.organization_uuid {
Expand Down Expand Up @@ -1659,9 +1668,11 @@ async fn _restore_cipher_by_uuid(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device.identifier,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await;

if let Some(org_uuid) = &cipher.organization_uuid {
log_event(
EventType::CipherRestored as i32,
Expand Down Expand Up @@ -1742,9 +1753,11 @@ async fn _delete_cipher_attachment_by_id(
UpdateType::SyncCipherUpdate,
&cipher,
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
&headers.device.identifier,
)
.await;
push_cipher_update(UpdateType::SyncCipherUpdate, &cipher, &headers.device.identifier, conn).await;

if let Some(org_uuid) = cipher.organization_uuid {
log_event(
EventType::CipherAttachmentDeleted as i32,
Expand Down
10 changes: 7 additions & 3 deletions src/api/core/folders.rs
Expand Up @@ -5,6 +5,7 @@ use crate::{
api::{EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType},
auth::Headers,
db::{models::*, DbConn},
push::push_folder_update,
};

pub fn routes() -> Vec<rocket::Route> {
Expand Down Expand Up @@ -50,7 +51,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, &folder, &headers.device.identifier).await;
push_folder_update(UpdateType::SyncFolderCreate, &folder, &headers.device.identifier, &mut conn).await;

Ok(Json(folder.to_json()))
}
Expand Down Expand Up @@ -88,7 +90,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, &folder, &headers.device.identifier).await;
push_folder_update(UpdateType::SyncFolderUpdate, &folder, &headers.device.identifier, &mut conn).await;

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

0 comments on commit 59da014

Please sign in to comment.