mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2025-09-10 10:45:57 +03:00
* WIP Sync with Upstream WIP on syncing API Responses with upstream. This to prevent issues with new clients, and find possible current issues like members, collections, groups etc.. Signed-off-by: BlackDex <black.dex@gmail.com> * More API Response fixes - Some 2fa checks - Some org checks - Reconfigured the experimental flags and noted which are deprecated Also removed some hard-coded defaults. - Updated crates Signed-off-by: BlackDex <black.dex@gmail.com> * Add avatar color to emergency access api Signed-off-by: BlackDex <black.dex@gmail.com> * Fix spelling and some crate updates Signed-off-by: BlackDex <black.dex@gmail.com> * Use PushId and always generate the PushId Signed-off-by: BlackDex <black.dex@gmail.com> * Fix clippy lints Signed-off-by: BlackDex <black.dex@gmail.com> * Fix several Push issues and API's Signed-off-by: BlackDex <black.dex@gmail.com> * Check if push_uuid is empty and generate when needed Signed-off-by: BlackDex <black.dex@gmail.com> * Updated some comments and removed old export format Signed-off-by: BlackDex <black.dex@gmail.com> * cargo update Signed-off-by: BlackDex <black.dex@gmail.com> * Fix bulk edit Fixes #5737 Signed-off-by: BlackDex <black.dex@gmail.com> * Send an email when an account exists already When you want to change your email address into an account which already exists, upstream sends an email to the existing account. Lets do the same. Kinda fixes #5630 Signed-off-by: BlackDex <black.dex@gmail.com> * Update 2fa removal/revoke email Signed-off-by: BlackDex <black.dex@gmail.com> * Allow col managers to import This commit adds functionality to allow users with manage access to a collection, or managers with all access to import into an organization. Fixes #5592 Signed-off-by: BlackDex <black.dex@gmail.com> * Filter deprected flags and only return active flags Signed-off-by: BlackDex <black.dex@gmail.com> * Fix grammer Signed-off-by: BlackDex <black.dex@gmail.com> * Rename Small to Compact Signed-off-by: BlackDex <black.dex@gmail.com> * Rebase with upstream and fix conflicts Signed-off-by: BlackDex <black.dex@gmail.com> --------- Signed-off-by: BlackDex <black.dex@gmail.com>
339 lines
12 KiB
Rust
339 lines
12 KiB
Rust
use reqwest::{
|
|
header::{ACCEPT, AUTHORIZATION, CONTENT_TYPE},
|
|
Method,
|
|
};
|
|
use serde_json::Value;
|
|
use tokio::sync::RwLock;
|
|
|
|
use crate::{
|
|
api::{ApiResult, EmptyResult, UpdateType},
|
|
db::models::{AuthRequestId, Cipher, Device, DeviceId, Folder, PushId, Send, User, UserId},
|
|
http_client::make_http_request,
|
|
util::{format_date, get_uuid},
|
|
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_api_token() -> ApiResult<String> {
|
|
static API_TOKEN: Lazy<RwLock<LocalAuthPushToken>> = Lazy::new(|| {
|
|
RwLock::new(LocalAuthPushToken {
|
|
access_token: String::new(),
|
|
valid_until: Instant::now(),
|
|
})
|
|
});
|
|
let api_token = API_TOKEN.read().await;
|
|
|
|
if api_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(api_token.access_token.clone());
|
|
}
|
|
drop(api_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 make_http_request(Method::POST, &format!("{}/connect/token", CONFIG.push_identity_uri()))?
|
|
.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 api_token = API_TOKEN.write().await;
|
|
api_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();
|
|
|
|
api_token.access_token = json_pushtoken.access_token;
|
|
|
|
debug!("Token still valid for {}", api_token.valid_until.saturating_duration_since(Instant::now()).as_secs());
|
|
Ok(api_token.access_token.clone())
|
|
}
|
|
|
|
pub async fn register_push_device(device: &mut Device, conn: &mut crate::db::DbConn) -> EmptyResult {
|
|
if !CONFIG.push_enabled() || !device.is_push_device() {
|
|
return Ok(());
|
|
}
|
|
|
|
if device.push_token.is_none() {
|
|
warn!("Skipping the registration of the device {:?} because the push_token field is empty.", device.uuid);
|
|
warn!("To get rid of this message you need to logout, clear the app data and login again on the device.");
|
|
return Ok(());
|
|
}
|
|
|
|
debug!("Registering Device {:?}", device.push_uuid);
|
|
|
|
// Generate a random push_uuid so if it doesn't already have one
|
|
if device.push_uuid.is_none() {
|
|
device.push_uuid = Some(PushId(get_uuid()));
|
|
}
|
|
|
|
//Needed to register a device for push to bitwarden :
|
|
let data = json!({
|
|
"deviceId": device.push_uuid, // Unique UUID per user/device
|
|
"pushToken": device.push_token,
|
|
"userId": device.user_uuid,
|
|
"type": device.atype,
|
|
"identifier": device.uuid, // Unique UUID of the device/app, determined by the device/app it self currently registering
|
|
// "organizationIds:" [] // TODO: This is not yet implemented by Vaultwarden!
|
|
"installationId": CONFIG.push_installation_id(),
|
|
});
|
|
|
|
let auth_api_token = get_auth_api_token().await?;
|
|
let auth_header = format!("Bearer {auth_api_token}");
|
|
|
|
if let Err(e) = make_http_request(Method::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()
|
|
{
|
|
err!(format!("An error occurred while proceeding registration of a device: {e}"));
|
|
}
|
|
|
|
if let Err(e) = device.save(conn).await {
|
|
err!(format!("An error occurred while trying to save the (registered) device push uuid: {e}"));
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn unregister_push_device(push_id: &Option<PushId>) -> EmptyResult {
|
|
if !CONFIG.push_enabled() || push_id.is_none() {
|
|
return Ok(());
|
|
}
|
|
let auth_api_token = get_auth_api_token().await?;
|
|
|
|
let auth_header = format!("Bearer {auth_api_token}");
|
|
|
|
match make_http_request(
|
|
Method::POST,
|
|
&format!("{}/push/delete/{}", CONFIG.push_relay_uri(), push_id.as_ref().unwrap()),
|
|
)?
|
|
.header(AUTHORIZATION, auth_header)
|
|
.send()
|
|
.await
|
|
{
|
|
Ok(r) => r,
|
|
Err(e) => err!(format!("An error occurred during device unregistration: {e}")),
|
|
};
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn push_cipher_update(ut: UpdateType, cipher: &Cipher, device: &Device, 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 Some(user_id) = &cipher.user_uuid else {
|
|
debug!("Cipher has no uuid");
|
|
return;
|
|
};
|
|
|
|
if Device::check_user_has_push_device(user_id, conn).await {
|
|
send_to_push_relay(json!({
|
|
"userId": user_id,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"id": cipher.uuid,
|
|
"userId": cipher.user_uuid,
|
|
"organizationId": null,
|
|
"collectionIds": null,
|
|
"revisionDate": format_date(&cipher.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
}))
|
|
.await;
|
|
}
|
|
}
|
|
|
|
pub async fn push_logout(user: &User, acting_device_id: Option<DeviceId>, conn: &mut crate::db::DbConn) {
|
|
let acting_device_id: Value = acting_device_id.map(|v| v.to_string().into()).unwrap_or_else(|| Value::Null);
|
|
|
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user.uuid,
|
|
"organizationId": (),
|
|
"deviceId": acting_device_id,
|
|
"identifier": acting_device_id,
|
|
"type": UpdateType::LogOut as i32,
|
|
"payload": {
|
|
"userId": user.uuid,
|
|
"date": format_date(&user.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_user_update(ut: UpdateType, user: &User, push_uuid: &Option<PushId>, conn: &mut crate::db::DbConn) {
|
|
if Device::check_user_has_push_device(&user.uuid, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user.uuid,
|
|
"organizationId": null,
|
|
"deviceId": push_uuid,
|
|
"identifier": null,
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"userId": user.uuid,
|
|
"date": format_date(&user.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_folder_update(ut: UpdateType, folder: &Folder, device: &Device, conn: &mut crate::db::DbConn) {
|
|
if Device::check_user_has_push_device(&folder.user_uuid, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": folder.user_uuid,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"id": folder.uuid,
|
|
"userId": folder.user_uuid,
|
|
"revisionDate": format_date(&folder.updated_at)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn: &mut crate::db::DbConn) {
|
|
if let Some(s) = &send.user_uuid {
|
|
if Device::check_user_has_push_device(s, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": send.user_uuid,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": ut as i32,
|
|
"payload": {
|
|
"id": send.uuid,
|
|
"userId": send.user_uuid,
|
|
"revisionDate": format_date(&send.revision_date)
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
}
|
|
|
|
async fn send_to_push_relay(notification_data: Value) {
|
|
if !CONFIG.push_enabled() {
|
|
return;
|
|
}
|
|
|
|
let auth_api_token = match get_auth_api_token().await {
|
|
Ok(s) => s,
|
|
Err(e) => {
|
|
debug!("Could not get the auth push token: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
let auth_header = format!("Bearer {auth_api_token}");
|
|
|
|
let req = match make_http_request(Method::POST, &(CONFIG.push_relay_uri() + "/push/send")) {
|
|
Ok(r) => r,
|
|
Err(e) => {
|
|
error!("An error occurred while sending a send update to the push relay: {e}");
|
|
return;
|
|
}
|
|
};
|
|
|
|
if let Err(e) = req
|
|
.header(ACCEPT, "application/json")
|
|
.header(CONTENT_TYPE, "application/json")
|
|
.header(AUTHORIZATION, &auth_header)
|
|
.json(¬ification_data)
|
|
.send()
|
|
.await
|
|
{
|
|
error!("An error occurred while sending a send update to the push relay: {e}");
|
|
};
|
|
}
|
|
|
|
pub async fn push_auth_request(user_id: &UserId, auth_request_id: &str, device: &Device, conn: &mut crate::db::DbConn) {
|
|
if Device::check_user_has_push_device(user_id, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user_id,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": UpdateType::AuthRequest as i32,
|
|
"payload": {
|
|
"userId": user_id,
|
|
"id": auth_request_id,
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|
|
|
|
pub async fn push_auth_response(
|
|
user_id: &UserId,
|
|
auth_request_id: &AuthRequestId,
|
|
device: &Device,
|
|
conn: &mut crate::db::DbConn,
|
|
) {
|
|
if Device::check_user_has_push_device(user_id, conn).await {
|
|
tokio::task::spawn(send_to_push_relay(json!({
|
|
"userId": user_id,
|
|
"organizationId": null,
|
|
"deviceId": device.push_uuid, // Should be the records unique uuid of the acting device (unique uuid per user/device)
|
|
"identifier": device.uuid, // Should be the acting device id (aka uuid per device/app)
|
|
"type": UpdateType::AuthRequestResponse as i32,
|
|
"payload": {
|
|
"userId": user_id,
|
|
"id": auth_request_id,
|
|
},
|
|
"clientType": null,
|
|
"installationId": null
|
|
})));
|
|
}
|
|
}
|