mirror of
https://github.com/dani-garcia/vaultwarden
synced 2024-11-21 19:33:11 +00:00
feat: Push Notifications
Co-authored-by: samb-devel <125741162+samb-devel@users.noreply.github.com> Co-authored-by: Zoruk <Zoruk@users.noreply.github.com>
This commit is contained in:
parent
adf67a8ee8
commit
2d66292350
22 changed files with 526 additions and 70 deletions
|
@ -72,6 +72,13 @@
|
||||||
# WEBSOCKET_ADDRESS=0.0.0.0
|
# WEBSOCKET_ADDRESS=0.0.0.0
|
||||||
# WEBSOCKET_PORT=3012
|
# 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
|
||||||
|
|
||||||
## Controls whether users are allowed to create Bitwarden Sends.
|
## Controls whether users are allowed to create Bitwarden Sends.
|
||||||
## This setting applies globally to all users.
|
## This setting applies globally to all users.
|
||||||
## To control this on a per-org basis instead, use the "Disable Send" org policy.
|
## To control this on a per-org basis instead, use the "Disable Send" org policy.
|
||||||
|
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
|
|
@ -0,0 +1 @@
|
||||||
|
ALTER TABLE devices ADD COLUMN push_uuid TEXT;
|
|
@ -13,7 +13,7 @@ use rocket::{
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
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},
|
auth::{decode_admin, encode_jwt, generate_admin_claims, ClientIp},
|
||||||
config::ConfigBuilder,
|
config::ConfigBuilder,
|
||||||
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
db::{backup_database, get_sql_server_version, models::*, DbConn, DbConnType},
|
||||||
|
@ -402,14 +402,22 @@ async fn delete_user(uuid: &str, token: AdminToken, mut conn: DbConn) -> EmptyRe
|
||||||
#[post("/users/<uuid>/deauth")]
|
#[post("/users/<uuid>/deauth")]
|
||||||
async fn deauth_user(uuid: &str, _token: AdminToken, mut conn: DbConn, nt: Notify<'_>) -> EmptyResult {
|
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?;
|
let mut user = get_user_or_404(uuid, &mut conn).await?;
|
||||||
|
|
||||||
|
nt.send_logout(&user, None, &mut conn).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),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
Device::delete_all_by_user(&user.uuid, &mut conn).await?;
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
|
|
||||||
let save_result = user.save(&mut conn).await;
|
user.save(&mut conn).await
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
|
||||||
|
|
||||||
save_result
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[post("/users/<uuid>/disable")]
|
#[post("/users/<uuid>/disable")]
|
||||||
|
@ -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;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,8 @@ use serde_json::Value;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
api::{
|
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},
|
auth::{decode_delete, decode_invite, decode_verify_email, Headers},
|
||||||
crypto,
|
crypto,
|
||||||
|
@ -35,6 +36,7 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
post_verify_email_token,
|
post_verify_email_token,
|
||||||
post_delete_recover,
|
post_delete_recover,
|
||||||
post_delete_recover_token,
|
post_delete_recover_token,
|
||||||
|
post_device_token,
|
||||||
delete_account,
|
delete_account,
|
||||||
post_delete_account,
|
post_delete_account,
|
||||||
revision_date,
|
revision_date,
|
||||||
|
@ -46,6 +48,9 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||||
get_known_device,
|
get_known_device,
|
||||||
get_known_device_from_path,
|
get_known_device_from_path,
|
||||||
put_avatar,
|
put_avatar,
|
||||||
|
put_device_token,
|
||||||
|
put_clear_device_token,
|
||||||
|
post_clear_device_token,
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -338,7 +343,7 @@ async fn post_password(
|
||||||
// Prevent loging out the client where the user requested this endpoint from.
|
// 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.
|
// If you do logout the user it will causes issues at the client side.
|
||||||
// Adding the device uuid will prevent this.
|
// 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
|
save_result
|
||||||
}
|
}
|
||||||
|
@ -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);
|
user.set_password(&data.NewMasterPasswordHash, Some(data.Key), true, None);
|
||||||
let save_result = user.save(&mut conn).await;
|
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
|
save_result
|
||||||
}
|
}
|
||||||
|
@ -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.
|
// 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.
|
// If you do logout the user it will causes issues at the client side.
|
||||||
// Adding the device uuid will prevent this.
|
// 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
|
save_result
|
||||||
}
|
}
|
||||||
|
@ -508,7 +513,7 @@ async fn post_sstamp(
|
||||||
user.reset_security_stamp();
|
user.reset_security_stamp();
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
@ -611,7 +616,7 @@ async fn post_email(
|
||||||
|
|
||||||
let save_result = user.save(&mut conn).await;
|
let save_result = user.save(&mut conn).await;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
save_result
|
save_result
|
||||||
}
|
}
|
||||||
|
@ -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(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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
|
||||||
|
}
|
||||||
|
|
|
@ -511,10 +511,9 @@ pub async fn update_cipher_from_data(
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid, None, conn)
|
||||||
nt.send_cipher_update(ut, cipher, &cipher.update_users_revision(conn).await, &headers.device.uuid, None).await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -580,6 +579,7 @@ async fn post_ciphers_import(
|
||||||
let mut user = headers.user;
|
let mut user = headers.user;
|
||||||
user.update_revision(&mut conn).await?;
|
user.update_revision(&mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -777,6 +777,7 @@ async fn post_collections_admin(
|
||||||
&cipher.update_users_revision(&mut conn).await,
|
&cipher.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
Some(Vec::from_iter(posted_collections)),
|
Some(Vec::from_iter(posted_collections)),
|
||||||
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -1122,6 +1123,7 @@ async fn save_attachment(
|
||||||
&cipher.update_users_revision(&mut conn).await,
|
&cipher.update_users_revision(&mut conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
None,
|
None,
|
||||||
|
&mut conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
|
@ -1407,8 +1409,15 @@ async fn move_cipher_selected(
|
||||||
// Move cipher
|
// Move cipher
|
||||||
cipher.move_to_folder(data.FolderId.clone(), &user_uuid, &mut conn).await?;
|
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)
|
nt.send_cipher_update(
|
||||||
.await;
|
UpdateType::SyncCipherUpdate,
|
||||||
|
&cipher,
|
||||||
|
&[user_uuid.clone()],
|
||||||
|
&headers.device.uuid,
|
||||||
|
None,
|
||||||
|
&mut conn,
|
||||||
|
)
|
||||||
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
|
@ -1489,6 +1498,7 @@ async fn delete_all(
|
||||||
|
|
||||||
user.update_revision(&mut conn).await?;
|
user.update_revision(&mut conn).await?;
|
||||||
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
nt.send_user_update(UpdateType::SyncVault, &user).await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1519,6 +1529,7 @@ async fn _delete_cipher_by_uuid(
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
None,
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
} else {
|
} else {
|
||||||
|
@ -1529,6 +1540,7 @@ async fn _delete_cipher_by_uuid(
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
None,
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
@ -1599,8 +1611,10 @@ async fn _restore_cipher_by_uuid(uuid: &str, headers: &Headers, conn: &mut DbCon
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
None,
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(org_uuid) = &cipher.organization_uuid {
|
if let Some(org_uuid) = &cipher.organization_uuid {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::CipherRestored as i32,
|
EventType::CipherRestored as i32,
|
||||||
|
@ -1681,8 +1695,10 @@ async fn _delete_cipher_attachment_by_id(
|
||||||
&cipher.update_users_revision(conn).await,
|
&cipher.update_users_revision(conn).await,
|
||||||
&headers.device.uuid,
|
&headers.device.uuid,
|
||||||
None,
|
None,
|
||||||
|
conn,
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
if let Some(org_uuid) = cipher.organization_uuid {
|
if let Some(org_uuid) = cipher.organization_uuid {
|
||||||
log_event(
|
log_event(
|
||||||
EventType::CipherAttachmentDeleted as i32,
|
EventType::CipherAttachmentDeleted as i32,
|
||||||
|
|
|
@ -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);
|
let mut folder = Folder::new(headers.user.uuid, data.Name);
|
||||||
|
|
||||||
folder.save(&mut conn).await?;
|
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()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
@ -88,7 +88,7 @@ async fn put_folder(
|
||||||
folder.name = data.Name;
|
folder.name = data.Name;
|
||||||
|
|
||||||
folder.save(&mut conn).await?;
|
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()))
|
Ok(Json(folder.to_json()))
|
||||||
}
|
}
|
||||||
|
@ -112,6 +112,6 @@ async fn delete_folder(uuid: &str, headers: Headers, mut conn: DbConn, nt: Notif
|
||||||
// Delete the actual folder entry
|
// Delete the actual folder entry
|
||||||
folder.delete(&mut conn).await?;
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,6 @@ pub use sends::purge_sends;
|
||||||
pub use two_factor::send_incomplete_2fa_notifications;
|
pub use two_factor::send_incomplete_2fa_notifications;
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
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 eq_domains_routes = routes![get_eq_domains, post_eq_domains, put_eq_domains];
|
||||||
let mut hibp_routes = routes![hibp_breach];
|
let mut hibp_routes = routes![hibp_breach];
|
||||||
let mut meta_routes = routes![alive, now, version, config];
|
let mut meta_routes = routes![alive, now, version, config];
|
||||||
|
@ -28,7 +27,6 @@ pub fn routes() -> Vec<Route> {
|
||||||
routes.append(&mut organizations::routes());
|
routes.append(&mut organizations::routes());
|
||||||
routes.append(&mut two_factor::routes());
|
routes.append(&mut two_factor::routes());
|
||||||
routes.append(&mut sends::routes());
|
routes.append(&mut sends::routes());
|
||||||
routes.append(&mut device_token_routes);
|
|
||||||
routes.append(&mut eq_domains_routes);
|
routes.append(&mut eq_domains_routes);
|
||||||
routes.append(&mut hibp_routes);
|
routes.append(&mut hibp_routes);
|
||||||
routes.append(&mut meta_routes);
|
routes.append(&mut meta_routes);
|
||||||
|
@ -57,37 +55,6 @@ use crate::{
|
||||||
util::get_reqwest_client,
|
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)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
#[allow(non_snake_case)]
|
#[allow(non_snake_case)]
|
||||||
struct GlobalDomain {
|
struct GlobalDomain {
|
||||||
|
|
|
@ -2716,7 +2716,7 @@ async fn put_reset_password(
|
||||||
user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None);
|
user.set_password(reset_request.NewMasterPasswordHash.as_str(), Some(reset_request.Key), true, None);
|
||||||
user.save(&mut conn).await?;
|
user.save(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_logout(&user, None).await;
|
nt.send_logout(&user, None, &mut conn).await;
|
||||||
|
|
||||||
log_event(
|
log_event(
|
||||||
EventType::OrganizationUserAdminResetPassword as i32,
|
EventType::OrganizationUserAdminResetPassword as i32,
|
||||||
|
|
|
@ -180,7 +180,8 @@ async fn post_send(data: JsonUpcase<SendData>, headers: Headers, mut conn: DbCon
|
||||||
|
|
||||||
let mut send = create_send(data, headers.user.uuid)?;
|
let mut send = create_send(data, headers.user.uuid)?;
|
||||||
send.save(&mut conn).await?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
@ -252,7 +253,8 @@ async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, mut conn:
|
||||||
|
|
||||||
// Save the changes in the database
|
// Save the changes in the database
|
||||||
send.save(&mut conn).await?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
@ -335,7 +337,8 @@ async fn post_send_file_v2_data(
|
||||||
data.data.move_copy_to(file_path).await?
|
data.data.move_copy_to(file_path).await?
|
||||||
}
|
}
|
||||||
|
|
||||||
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendCreate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
} else {
|
} else {
|
||||||
err!("Send not found. Unable to save the file.");
|
err!("Send not found. Unable to save the file.");
|
||||||
}
|
}
|
||||||
|
@ -397,7 +400,8 @@ async fn post_access(
|
||||||
|
|
||||||
send.save(&mut conn).await?;
|
send.save(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(send.to_json_access(&mut conn).await))
|
Ok(Json(send.to_json_access(&mut conn).await))
|
||||||
}
|
}
|
||||||
|
@ -448,7 +452,8 @@ async fn post_access_file(
|
||||||
|
|
||||||
send.save(&mut conn).await?;
|
send.save(&mut conn).await?;
|
||||||
|
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
let token_claims = crate::auth::generate_send_claims(send_id, file_id);
|
||||||
let token = crate::auth::encode_jwt(&token_claims);
|
let token = crate::auth::encode_jwt(&token_claims);
|
||||||
|
@ -530,7 +535,8 @@ async fn put_send(
|
||||||
}
|
}
|
||||||
|
|
||||||
send.save(&mut conn).await?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
@ -547,7 +553,8 @@ async fn delete_send(id: &str, headers: Headers, mut conn: DbConn, nt: Notify<'_
|
||||||
}
|
}
|
||||||
|
|
||||||
send.delete(&mut conn).await?;
|
send.delete(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendDelete, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
@ -567,7 +574,8 @@ async fn put_remove_password(id: &str, headers: Headers, mut conn: DbConn, nt: N
|
||||||
|
|
||||||
send.set_password(None);
|
send.set_password(None);
|
||||||
send.save(&mut conn).await?;
|
send.save(&mut conn).await?;
|
||||||
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await).await;
|
nt.send_send_update(UpdateType::SyncSendUpdate, &send, &send.update_users_revision(&mut conn).await, &mut conn)
|
||||||
|
.await;
|
||||||
|
|
||||||
Ok(Json(send.to_json()))
|
Ok(Json(send.to_json()))
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,6 +3,7 @@ pub mod core;
|
||||||
mod icons;
|
mod icons;
|
||||||
mod identity;
|
mod identity;
|
||||||
mod notifications;
|
mod notifications;
|
||||||
|
mod push;
|
||||||
mod web;
|
mod web;
|
||||||
|
|
||||||
use rocket::serde::json::Json;
|
use rocket::serde::json::Json;
|
||||||
|
@ -22,6 +23,10 @@ pub use crate::api::{
|
||||||
identity::routes as identity_routes,
|
identity::routes as identity_routes,
|
||||||
notifications::routes as notifications_routes,
|
notifications::routes as notifications_routes,
|
||||||
notifications::{start_notification_server, Notify, UpdateType},
|
notifications::{start_notification_server, Notify, UpdateType},
|
||||||
|
push::{
|
||||||
|
push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update, register_push_device,
|
||||||
|
unregister_push_device,
|
||||||
|
},
|
||||||
web::catchers as web_catchers,
|
web::catchers as web_catchers,
|
||||||
web::routes as web_routes,
|
web::routes as web_routes,
|
||||||
web::static_files,
|
web::static_files,
|
||||||
|
|
|
@ -21,7 +21,10 @@ use tokio_tungstenite::{
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
auth::ClientIp,
|
auth::ClientIp,
|
||||||
db::models::{Cipher, Folder, Send as DbSend, User},
|
db::{
|
||||||
|
models::{Cipher, Folder, Send as DbSend, User},
|
||||||
|
DbConn,
|
||||||
|
},
|
||||||
Error, CONFIG,
|
Error, CONFIG,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -33,6 +36,8 @@ static WS_USERS: Lazy<Arc<WebSocketUsers>> = Lazy::new(|| {
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
|
||||||
|
use super::{push_cipher_update, push_folder_update, push_logout, push_send_update, push_user_update};
|
||||||
|
|
||||||
pub fn routes() -> Vec<Route> {
|
pub fn routes() -> Vec<Route> {
|
||||||
routes![websockets_hub]
|
routes![websockets_hub]
|
||||||
}
|
}
|
||||||
|
@ -233,19 +238,33 @@ impl WebSocketUsers {
|
||||||
);
|
);
|
||||||
|
|
||||||
self.send_update(&user.uuid, &data).await;
|
self.send_update(&user.uuid, &data).await;
|
||||||
|
|
||||||
|
if CONFIG.push_enabled() {
|
||||||
|
push_user_update(ut, user).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>) {
|
pub async fn send_logout(&self, user: &User, acting_device_uuid: Option<String>, conn: &mut DbConn) {
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
vec![("UserId".into(), user.uuid.clone().into()), ("Date".into(), serialize_date(user.updated_at))],
|
||||||
UpdateType::LogOut,
|
UpdateType::LogOut,
|
||||||
acting_device_uuid,
|
acting_device_uuid.clone(),
|
||||||
);
|
);
|
||||||
|
|
||||||
self.send_update(&user.uuid, &data).await;
|
self.send_update(&user.uuid, &data).await;
|
||||||
|
|
||||||
|
if CONFIG.push_enabled() {
|
||||||
|
push_logout(user, acting_device_uuid, conn).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_folder_update(&self, ut: UpdateType, folder: &Folder, acting_device_uuid: &String) {
|
pub async fn send_folder_update(
|
||||||
|
&self,
|
||||||
|
ut: UpdateType,
|
||||||
|
folder: &Folder,
|
||||||
|
acting_device_uuid: &String,
|
||||||
|
conn: &mut DbConn,
|
||||||
|
) {
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
vec![
|
vec![
|
||||||
("Id".into(), folder.uuid.clone().into()),
|
("Id".into(), folder.uuid.clone().into()),
|
||||||
|
@ -257,6 +276,10 @@ impl WebSocketUsers {
|
||||||
);
|
);
|
||||||
|
|
||||||
self.send_update(&folder.user_uuid, &data).await;
|
self.send_update(&folder.user_uuid, &data).await;
|
||||||
|
|
||||||
|
if CONFIG.push_enabled() {
|
||||||
|
push_folder_update(ut, folder, acting_device_uuid, conn).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_cipher_update(
|
pub async fn send_cipher_update(
|
||||||
|
@ -266,6 +289,7 @@ impl WebSocketUsers {
|
||||||
user_uuids: &[String],
|
user_uuids: &[String],
|
||||||
acting_device_uuid: &String,
|
acting_device_uuid: &String,
|
||||||
collection_uuids: Option<Vec<String>>,
|
collection_uuids: Option<Vec<String>>,
|
||||||
|
conn: &mut DbConn,
|
||||||
) {
|
) {
|
||||||
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
let org_uuid = convert_option(cipher.organization_uuid.clone());
|
||||||
// Depending if there are collections provided or not, we need to have different values for the following variables.
|
// Depending if there are collections provided or not, we need to have different values for the following variables.
|
||||||
|
@ -295,9 +319,13 @@ impl WebSocketUsers {
|
||||||
for uuid in user_uuids {
|
for uuid in user_uuids {
|
||||||
self.send_update(uuid, &data).await;
|
self.send_update(uuid, &data).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||||
|
push_cipher_update(ut, cipher, acting_device_uuid, conn).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub async fn send_send_update(&self, ut: UpdateType, send: &DbSend, user_uuids: &[String]) {
|
pub async fn send_send_update(&self, ut: UpdateType, send: &DbSend, user_uuids: &[String], conn: &mut DbConn) {
|
||||||
let user_uuid = convert_option(send.user_uuid.clone());
|
let user_uuid = convert_option(send.user_uuid.clone());
|
||||||
|
|
||||||
let data = create_update(
|
let data = create_update(
|
||||||
|
@ -313,6 +341,9 @@ impl WebSocketUsers {
|
||||||
for uuid in user_uuids {
|
for uuid in user_uuids {
|
||||||
self.send_update(uuid, &data).await;
|
self.send_update(uuid, &data).await;
|
||||||
}
|
}
|
||||||
|
if CONFIG.push_enabled() && user_uuids.len() == 1 {
|
||||||
|
push_send_update(ut, send, conn).await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -354,7 +385,7 @@ fn create_ping() -> Vec<u8> {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
#[derive(Eq, PartialEq)]
|
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||||
pub enum UpdateType {
|
pub enum UpdateType {
|
||||||
SyncCipherUpdate = 0,
|
SyncCipherUpdate = 0,
|
||||||
SyncCipherCreate = 1,
|
SyncCipherCreate = 1,
|
||||||
|
|
280
src/api/push.rs
Normal file
280
src/api/push.rs
Normal file
|
@ -0,0 +1,280 @@
|
||||||
|
use reqwest::header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE};
|
||||||
|
use serde_json::Value;
|
||||||
|
use tokio::sync::RwLock;
|
||||||
|
|
||||||
|
use crate::{
|
||||||
|
api::{ApiResult, EmptyResult, UpdateType},
|
||||||
|
db::models::{Cipher, Device, Folder, Send, User},
|
||||||
|
util::get_reqwest_client,
|
||||||
|
CONFIG,
|
||||||
|
};
|
||||||
|
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
|
use std::time::{Duration, Instant};
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct AuthPushToken {
|
||||||
|
access_token: String,
|
||||||
|
expires_in: i32,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
struct LocalAuthPushToken {
|
||||||
|
access_token: String,
|
||||||
|
valid_until: Instant,
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_auth_push_token() -> ApiResult<String> {
|
||||||
|
static PUSH_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
|
||||||
|
RwLock::new(LocalAuthPushToken {
|
||||||
|
access_token: String::new(),
|
||||||
|
valid_until: Instant::now(),
|
||||||
|
})
|
||||||
|
});
|
||||||
|
let push_token = PUSH_TOKEN.read().await;
|
||||||
|
|
||||||
|
if push_token.valid_until.saturating_duration_since(Instant::now()).as_secs() > 0 {
|
||||||
|
debug!("Auth Push token still valid, no need for a new one");
|
||||||
|
return Ok(push_token.access_token.clone());
|
||||||
|
}
|
||||||
|
drop(push_token); // Drop the read lock now
|
||||||
|
|
||||||
|
let installation_id = CONFIG.push_installation_id();
|
||||||
|
let client_id = format!("installation.{installation_id}");
|
||||||
|
let client_secret = CONFIG.push_installation_key();
|
||||||
|
|
||||||
|
let params = [
|
||||||
|
("grant_type", "client_credentials"),
|
||||||
|
("scope", "api.push"),
|
||||||
|
("client_id", &client_id),
|
||||||
|
("client_secret", &client_secret),
|
||||||
|
];
|
||||||
|
|
||||||
|
let res = match get_reqwest_client().post("https://identity.bitwarden.com/connect/token").form(¶ms).send().await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("Error getting push token from bitwarden server: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let json_pushtoken = match res.json::<AuthPushToken>().await {
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("Unexpected push token received from bitwarden server: {e}")),
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut push_token = PUSH_TOKEN.write().await;
|
||||||
|
push_token.valid_until = Instant::now()
|
||||||
|
.checked_add(Duration::new((json_pushtoken.expires_in / 2) as u64, 0)) // Token valid for half the specified time
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
push_token.access_token = json_pushtoken.access_token;
|
||||||
|
|
||||||
|
debug!("Token still valid for {}", push_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
|
||||||
|
Ok(push_token.access_token.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn register_push_device(user_uuid: String, device: Device) -> EmptyResult {
|
||||||
|
if !CONFIG.push_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let auth_push_token = get_auth_push_token().await?;
|
||||||
|
|
||||||
|
//Needed to register a device for push to bitwarden :
|
||||||
|
let data = json!({
|
||||||
|
"userId": user_uuid,
|
||||||
|
"deviceId": device.push_uuid,
|
||||||
|
"identifier": device.uuid,
|
||||||
|
"type": device.atype,
|
||||||
|
"pushToken": device.push_token
|
||||||
|
});
|
||||||
|
|
||||||
|
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||||
|
|
||||||
|
get_reqwest_client()
|
||||||
|
.post(CONFIG.push_relay_uri() + "/push/register")
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
|
.header(ACCEPT, "application/json")
|
||||||
|
.header(AUTHORIZATION, auth_header)
|
||||||
|
.json(&data)
|
||||||
|
.send()
|
||||||
|
.await?
|
||||||
|
.error_for_status()?;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn unregister_push_device(uuid: String) -> EmptyResult {
|
||||||
|
if !CONFIG.push_enabled() {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
let auth_push_token = get_auth_push_token().await?;
|
||||||
|
|
||||||
|
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||||
|
|
||||||
|
match get_reqwest_client()
|
||||||
|
.delete(CONFIG.push_relay_uri() + "/push/" + &uuid)
|
||||||
|
.header(AUTHORIZATION, auth_header)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
Ok(r) => r,
|
||||||
|
Err(e) => err!(format!("An error occured during device unregistration: {e}")),
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_cipher_update(
|
||||||
|
ut: UpdateType,
|
||||||
|
cipher: &Cipher,
|
||||||
|
acting_device_uuid: &String,
|
||||||
|
conn: &mut crate::db::DbConn,
|
||||||
|
) {
|
||||||
|
// We shouldn't send a push notification on cipher update if the cipher belongs to an organization, this isn't implemented in the upstream server too.
|
||||||
|
if cipher.organization_uuid.is_some() {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let user_uuid = match &cipher.user_uuid {
|
||||||
|
Some(c) => c,
|
||||||
|
None => {
|
||||||
|
debug!("Cipher has no uuid");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
for device in Device::find_by_user(user_uuid, conn).await {
|
||||||
|
let data = json!({
|
||||||
|
"userId": user_uuid,
|
||||||
|
"organizationId": (),
|
||||||
|
"deviceId": device.push_uuid,
|
||||||
|
"identifier": acting_device_uuid,
|
||||||
|
"type": ut as i32,
|
||||||
|
"payload": {
|
||||||
|
"Id": cipher.uuid,
|
||||||
|
"UserId": cipher.user_uuid,
|
||||||
|
"OrganizationId": (),
|
||||||
|
"RevisionDate": cipher.updated_at
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
send_to_push_relay(data).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!({
|
||||||
|
"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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_user_update(ut: UpdateType, user: &User) {
|
||||||
|
let data = json!({
|
||||||
|
"userId": user.uuid,
|
||||||
|
"organizationId": (),
|
||||||
|
"deviceId": (),
|
||||||
|
"identifier": (),
|
||||||
|
"type": ut as i32,
|
||||||
|
"payload": {
|
||||||
|
"UserId": user.uuid,
|
||||||
|
"Date": user.updated_at
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
send_to_push_relay(data).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn push_folder_update(
|
||||||
|
ut: UpdateType,
|
||||||
|
folder: &Folder,
|
||||||
|
acting_device_uuid: &String,
|
||||||
|
conn: &mut crate::db::DbConn,
|
||||||
|
) {
|
||||||
|
for device in Device::find_by_user(&folder.user_uuid, conn).await {
|
||||||
|
let data = json!({
|
||||||
|
"userId": folder.user_uuid,
|
||||||
|
"organizationId": (),
|
||||||
|
"deviceId": device.push_uuid,
|
||||||
|
"identifier": acting_device_uuid,
|
||||||
|
"type": ut as i32,
|
||||||
|
"payload": {
|
||||||
|
"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) {
|
||||||
|
if let Some(s) = &send.user_uuid {
|
||||||
|
for device in Device::find_by_user(s, conn).await {
|
||||||
|
let data = json!({
|
||||||
|
"userId": send.user_uuid,
|
||||||
|
"organizationId": (),
|
||||||
|
"deviceId": device.push_uuid,
|
||||||
|
"identifier": (),
|
||||||
|
"type": ut as i32,
|
||||||
|
"payload": {
|
||||||
|
"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) {
|
||||||
|
if !CONFIG.push_enabled() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let auth_push_token = match get_auth_push_token().await {
|
||||||
|
Ok(s) => s,
|
||||||
|
Err(e) => {
|
||||||
|
debug!("Could not get the auth push token: {}", e);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let auth_header = format!("Bearer {}", &auth_push_token);
|
||||||
|
|
||||||
|
if let Err(e) = get_reqwest_client()
|
||||||
|
.post(CONFIG.push_relay_uri() + "/push/send")
|
||||||
|
.header(ACCEPT, "application/json")
|
||||||
|
.header(CONTENT_TYPE, "application/json")
|
||||||
|
.header(AUTHORIZATION, auth_header)
|
||||||
|
.json(&data)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!("An error occured while sending a send update to the push relay: {}", e);
|
||||||
|
};
|
||||||
|
}
|
|
@ -377,6 +377,16 @@ make_config! {
|
||||||
/// Websocket port
|
/// Websocket port
|
||||||
websocket_port: u16, false, def, 3012;
|
websocket_port: u16, false, def, 3012;
|
||||||
},
|
},
|
||||||
|
push {
|
||||||
|
/// Enable push notifications
|
||||||
|
push_enabled: bool, false, def, false;
|
||||||
|
/// Push relay base uri
|
||||||
|
push_relay_uri: String, false, def, "https://push.bitwarden.com".to_string();
|
||||||
|
/// Installation id |> The installation id from https://bitwarden.com/host
|
||||||
|
push_installation_id: Pass, false, def, String::new();
|
||||||
|
/// Installation key |> The installation key from https://bitwarden.com/host
|
||||||
|
push_installation_key: Pass, false, def, String::new();
|
||||||
|
},
|
||||||
jobs {
|
jobs {
|
||||||
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
/// Job scheduler poll interval |> How often the job scheduler thread checks for jobs to run.
|
||||||
/// Set to 0 to globally disable scheduled jobs.
|
/// Set to 0 to globally disable scheduled jobs.
|
||||||
|
@ -724,6 +734,17 @@ fn validate_config(cfg: &ConfigItems) -> Result<(), Error> {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if cfg.push_enabled && (cfg.push_installation_id == String::new() || cfg.push_installation_key == String::new()) {
|
||||||
|
err!(
|
||||||
|
"Misconfigured Push Notification service\n\
|
||||||
|
########################################################################################\n\
|
||||||
|
# It looks like you enabled Push Notification feature, but didn't configure it #\n\
|
||||||
|
# properly. Make sure the installation id and key from https://bitwarden.com/host are #\n\
|
||||||
|
# added to your configuration. #\n\
|
||||||
|
########################################################################################\n"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if cfg._enable_duo
|
if cfg._enable_duo
|
||||||
&& (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
|
&& (cfg.duo_host.is_some() || cfg.duo_ikey.is_some() || cfg.duo_skey.is_some())
|
||||||
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
|
&& !(cfg.duo_host.is_some() && cfg.duo_ikey.is_some() && cfg.duo_skey.is_some())
|
||||||
|
|
|
@ -15,7 +15,8 @@ db_object! {
|
||||||
pub user_uuid: String,
|
pub user_uuid: String,
|
||||||
|
|
||||||
pub name: String,
|
pub name: String,
|
||||||
pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
pub atype: i32, // https://github.com/bitwarden/server/blob/master/src/Core/Enums/DeviceType.cs
|
||||||
|
pub push_uuid: Option<String>,
|
||||||
pub push_token: Option<String>,
|
pub push_token: Option<String>,
|
||||||
|
|
||||||
pub refresh_token: String,
|
pub refresh_token: String,
|
||||||
|
@ -38,6 +39,7 @@ impl Device {
|
||||||
name,
|
name,
|
||||||
atype,
|
atype,
|
||||||
|
|
||||||
|
push_uuid: None,
|
||||||
push_token: None,
|
push_token: None,
|
||||||
refresh_token: String::new(),
|
refresh_token: String::new(),
|
||||||
twofactor_remember: None,
|
twofactor_remember: None,
|
||||||
|
@ -155,6 +157,35 @@ impl Device {
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
devices::table
|
||||||
|
.filter(devices::user_uuid.eq(user_uuid))
|
||||||
|
.load::<DeviceDb>(conn)
|
||||||
|
.expect("Error loading devices")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn find_by_uuid(uuid: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
devices::table
|
||||||
|
.filter(devices::uuid.eq(uuid))
|
||||||
|
.first::<DeviceDb>(conn)
|
||||||
|
.ok()
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub async fn clear_push_token_by_uuid(uuid: &str, conn: &mut DbConn) -> EmptyResult {
|
||||||
|
db_run! { conn: {
|
||||||
|
diesel::update(devices::table)
|
||||||
|
.filter(devices::uuid.eq(uuid))
|
||||||
|
.set(devices::push_token.eq::<Option<String>>(None))
|
||||||
|
.execute(conn)
|
||||||
|
.map_res("Error removing push token")
|
||||||
|
}}
|
||||||
|
}
|
||||||
pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option<Self> {
|
pub async fn find_by_refresh_token(refresh_token: &str, conn: &mut DbConn) -> Option<Self> {
|
||||||
db_run! { conn: {
|
db_run! { conn: {
|
||||||
devices::table
|
devices::table
|
||||||
|
@ -175,4 +206,14 @@ impl Device {
|
||||||
.from_db()
|
.from_db()
|
||||||
}}
|
}}
|
||||||
}
|
}
|
||||||
|
pub async fn find_push_device_by_user(user_uuid: &str, conn: &mut DbConn) -> Vec<Self> {
|
||||||
|
db_run! { conn: {
|
||||||
|
devices::table
|
||||||
|
.filter(devices::user_uuid.eq(user_uuid))
|
||||||
|
.filter(devices::push_token.is_not_null())
|
||||||
|
.load::<DeviceDb>(conn)
|
||||||
|
.expect("Error loading push devices")
|
||||||
|
.from_db()
|
||||||
|
}}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -49,6 +49,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
refresh_token -> Text,
|
refresh_token -> Text,
|
||||||
twofactor_remember -> Nullable<Text>,
|
twofactor_remember -> Nullable<Text>,
|
||||||
|
|
|
@ -49,6 +49,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
refresh_token -> Text,
|
refresh_token -> Text,
|
||||||
twofactor_remember -> Nullable<Text>,
|
twofactor_remember -> Nullable<Text>,
|
||||||
|
|
|
@ -49,6 +49,7 @@ table! {
|
||||||
user_uuid -> Text,
|
user_uuid -> Text,
|
||||||
name -> Text,
|
name -> Text,
|
||||||
atype -> Integer,
|
atype -> Integer,
|
||||||
|
push_uuid -> Nullable<Text>,
|
||||||
push_token -> Nullable<Text>,
|
push_token -> Nullable<Text>,
|
||||||
refresh_token -> Text,
|
refresh_token -> Text,
|
||||||
twofactor_remember -> Nullable<Text>,
|
twofactor_remember -> Nullable<Text>,
|
||||||
|
|
Loading…
Reference in a new issue