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>
Co-authored-by: Zoruk <Zoruk@users.noreply.github.com>
  • Loading branch information
3 people committed Jun 10, 2023
1 parent adf67a8 commit 05b0eca
Show file tree
Hide file tree
Showing 22 changed files with 529 additions and 70 deletions.
5 changes: 5 additions & 0 deletions .env.template
Expand Up @@ -72,6 +72,11 @@
# WEBSOCKET_ADDRESS=0.0.0.0
# WEBSOCKET_PORT=3012

## Enables push notifications (requires key and id from https://bitwarden.com/host)
# PUSH_ENABLED=true
# 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 ADD COLUMN push_uuid TEXT;
Empty file.
@@ -0,0 +1 @@
ALTER TABLE devices ADD 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 ADD COLUMN push_uuid TEXT;
22 changes: 15 additions & 7 deletions src/api/admin.rs
Expand Up @@ -13,7 +13,7 @@ use rocket::{
};

use crate::{
api::{core::log_event, ApiResult, EmptyResult, JsonResult, Notify, NumberOrString},
api::{core::log_event, 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 @@ -402,14 +402,22 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe
#[post("/users/<uuid>/deauth")]
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?;
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, &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 {
match unregister_push_device(device.uuid).await {
Ok(r) => r,
Err(e) => error!("Unable to unregister devices from Bitwarden server: {}", e),
};
}
}

save_result
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 @@ -421,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).await;
nt.send_logout(&user, None, &mut conn).await;

save_result
}
Expand Down
83 changes: 77 additions & 6 deletions src/api/core/accounts.rs
Expand Up @@ -4,7 +4,8 @@ use serde_json::Value;

use crate::{
api::{
core::log_user_event, EmptyResult, JsonResult, JsonUpcase, Notify, NumberOrString, PasswordData, UpdateType,
core::log_user_event, register_push_device, unregister_push_device, EmptyResult, JsonResult, JsonUpcase,
Notify, NumberOrString, PasswordData, UpdateType,
},
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
crypto,
Expand Down Expand Up @@ -35,6 +36,7 @@ pub fn routes() -> Vec<rocket::Route> {
post_verify_email_token,
post_delete_recover,
post_delete_recover_token,
post_device_token,
delete_account,
post_delete_account,
revision_date,
Expand All @@ -46,6 +48,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 +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)).await;
nt.send_logout(&user, Some(headers.device.uuid), &mut conn).await;

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

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

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

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

save_result
}
Expand Down Expand Up @@ -611,7 +616,7 @@ async fn post_email(

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

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

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

#[derive(Deserialize)]
#[allow(non_snake_case)]
struct PushToken {
PushToken: String,
}

#[post("/devices/identifier/<uuid>/token", data = "<data>")]
async fn post_device_token(uuid: String, data: JsonUpcase<PushToken>, headers: Headers, conn: DbConn) -> EmptyResult {
put_device_token(uuid, data, headers, conn).await
}

#[put("/devices/identifier/<uuid>/token", data = "<data>")]
async fn put_device_token(
uuid: String,
data: JsonUpcase<PushToken>,
headers: Headers,
mut conn: DbConn,
) -> EmptyResult {
if !CONFIG.push_enabled() {
return Ok(());
}

let data = data.into_inner().data;
let token = data.PushToken;
let mut device = match Device::find_by_uuid_and_user(&headers.device.uuid, &headers.user.uuid, &mut conn).await {
Some(device) => device,
None => err!(format!("Error: device {uuid} should be present before a token can be assigned")),
};
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 {
err!(format!("An error occured while trying to save the device push token: {e}"));
}
if let Err(e) = register_push_device(headers.user.uuid, device).await {
err!(format!("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) -> EmptyResult {
// 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
if !CONFIG.push_enabled() {
return Ok(());
}

if let Some(device) = Device::find_by_uuid(&uuid, &mut conn).await {
Device::clear_push_token_by_uuid(&uuid, &mut conn).await?;
unregister_push_device(device.uuid).await?;
}

Ok(())
}

// On upstream server, both PUT and POST are declared.
#[post("/devices/identifier/<uuid>/clear-token")]
async fn clear_device_token_post(uuid: String, conn: DbConn) -> EmptyResult {
clear_device_token(uuid, conn).await
}
26 changes: 21 additions & 5 deletions src/api/core/ciphers.rs
Expand Up @@ -511,10 +511,9 @@ pub async fn update_cipher_from_data(
)
.await;
}

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

Ok(())
}

Expand Down Expand Up @@ -580,6 +579,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;

Ok(())
}

Expand Down Expand Up @@ -777,6 +777,7 @@ async fn post_collections_admin(
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
Some(Vec::from_iter(posted_collections)),
&mut conn,
)
.await;

Expand Down Expand Up @@ -1122,6 +1123,7 @@ async fn save_attachment(
&cipher.update_users_revision(&mut conn).await,
&headers.device.uuid,
None,
&mut conn,
)
.await;

Expand Down Expand Up @@ -1407,8 +1409,15 @@ 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, None)
.await;
nt.send_cipher_update(
UpdateType::SyncCipherUpdate,
&cipher,
&[user_uuid.clone()],
&headers.device.uuid,
None,
&mut conn,
)
.await;
}

Ok(())
Expand Down Expand Up @@ -1489,6 +1498,7 @@ async fn delete_all(

user.update_revision(&mut conn).await?;
nt.send_user_update(UpdateType::SyncVault, &user).await;

Ok(())
}
}
Expand Down Expand Up @@ -1519,6 +1529,7 @@ async fn _delete_cipher_by_uuid(
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
None,
conn,
)
.await;
} else {
Expand All @@ -1529,6 +1540,7 @@ async fn _delete_cipher_by_uuid(
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
None,
conn,
)
.await;
}
Expand Down Expand Up @@ -1599,8 +1611,10 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
None,
conn,
)
.await;

if let Some(org_uuid) = &cipher.organization_uuid {
log_event(
EventType::CipherRestored as i32,
Expand Down Expand Up @@ -1681,8 +1695,10 @@ async fn _delete_cipher_attachment_by_id(
&cipher.update_users_revision(conn).await,
&headers.device.uuid,
None,
conn,
)
.await;

if let Some(org_uuid) = cipher.organization_uuid {
log_event(
EventType::CipherAttachmentDeleted as i32,
Expand Down
6 changes: 3 additions & 3 deletions src/api/core/folders.rs
Expand Up @@ -50,7 +50,7 @@ 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.uuid, &mut conn).await;

Ok(Json(folder.to_json()))
}
Expand Down Expand Up @@ -88,7 +88,7 @@ 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.uuid, &mut conn).await;

Ok(Json(folder.to_json()))
}
Expand All @@ -112,6 +112,6 @@ async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notif
// 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.uuid, &mut conn).await;
Ok(())
}
33 changes: 0 additions & 33 deletions src/api/core/mod.rs
Expand Up @@ -14,7 +14,6 @@ pub use sends::purge_sends;
pub use two_factor::send_incomplete_2fa_notifications;

pub fn routes() -> Vec<Route> {
let mut device_token_routes = routes![clear_device_token, put_device_token];
let mut eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
let mut hibp_routes = routes![hibp_breach];
let mut meta_routes = routes![alive, now, version, config];
Expand All @@ -28,7 +27,6 @@ pub fn routes() -> Vec<Route> {
routes.append(&mut organizations::routes());
routes.append(&mut two_factor::routes());
routes.append(&mut sends::routes());
routes.append(&mut device_token_routes);
routes.append(&mut eq_domains_routes);
routes.append(&mut hibp_routes);
routes.append(&mut meta_routes);
Expand Down Expand Up @@ -57,37 +55,6 @@ use crate::{
util::get_reqwest_client,
};

#[put("/devices/identifier/<uuid>/clear-token")]
fn clear_device_token(uuid: &str) -> &'static str {
// This endpoint doesn't have auth header

let _ = uuid;
// uuid is not related to deviceId

// 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
""
}

#[put("/devices/identifier/<uuid>/token", data = "<data>")]
fn put_device_token(uuid: &str, data: JsonUpcase<Value>, headers: Headers) -> Json<Value> {
let _data: Value = data.into_inner().data;
// Data has a single string value "PushToken"
let _ = uuid;
// uuid is not related to deviceId

// TODO: This should save the push token, but we don't have push functionality

Json(json!({
"Id": headers.device.uuid,
"Name": headers.device.name,
"Type": headers.device.atype,
"Identifier": headers.device.uuid,
"CreationDate": crate::util::format_date(&headers.device.created_at),
}))
}

#[derive(Serialize, Deserialize, Debug)]
#[allow(non_snake_case)]
struct GlobalDomain {
Expand Down
2 changes: 1 addition & 1 deletion src/api/core/organizations.rs
Expand Up @@ -2716,7 +2716,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).await;
nt.send_logout(&user, None, &mut conn).await;

log_event(
EventType::OrganizationUserAdminResetPassword as i32,
Expand Down

0 comments on commit 05b0eca

Please sign in to comment.