Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Implement Push Notifications sync #3304

Merged
merged 1 commit into from
Jun 12, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .env.template
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,13 @@
# 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
## Don't change this unless you know what you're doing.
# PUSH_RELAY_BASE_URI=https://push.bitwarden.com

GeekCornerGH marked this conversation as resolved.
Show resolved Hide resolved
## 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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
Empty file.
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
22 changes: 15 additions & 7 deletions src/api/admin.rs
Original file line number Diff line number Diff line change
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;
GeekCornerGH marked this conversation as resolved.
Show resolved Hide resolved

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
78 changes: 72 additions & 6 deletions src/api/core/accounts.rs
Original file line number Diff line number Diff line change
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,
GeekCornerGH marked this conversation as resolved.
Show resolved Hide resolved
put_clear_device_token,
post_clear_device_token,
]
}

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), &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), &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,64 @@ 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: &str, 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: &str, 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(())
GeekCornerGH marked this conversation as resolved.
Show resolved Hide resolved
}

#[put("/devices/identifier/<uuid>/clear-token")]
async fn put_clear_device_token(uuid: &str, 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. Implementing the POST method in case it would be useful somewhere
#[post("/devices/identifier/<uuid>/clear-token")]
async fn post_clear_device_token(uuid: &str, conn: DbConn) -> EmptyResult {
put_clear_device_token(uuid, conn).await
}
26 changes: 21 additions & 5 deletions src/api/core/ciphers.rs
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Loading