Files
vaultwarden/src/db/models/device.rs
Mathijs van Veluw ef7835d1b0 Sync with Upstream (#5798)
* 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>
2025-05-26 21:00:59 +02:00

415 lines
14 KiB
Rust

use chrono::{NaiveDateTime, Utc};
use derive_more::{Display, From};
use serde_json::Value;
use super::{AuthRequest, UserId};
use crate::{
crypto,
util::{format_date, get_uuid},
CONFIG,
};
use macros::{IdFromParam, UuidFromParam};
db_object! {
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = devices)]
#[diesel(treat_none_as_null = true)]
#[diesel(primary_key(uuid, user_uuid))]
pub struct Device {
pub uuid: DeviceId,
pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime,
pub user_uuid: UserId,
pub name: String,
pub atype: i32, // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/Enums/DeviceType.cs
pub push_uuid: Option<PushId>,
pub push_token: Option<String>,
pub refresh_token: String,
pub twofactor_remember: Option<String>,
}
}
/// Local methods
impl Device {
pub fn new(uuid: DeviceId, user_uuid: UserId, name: String, atype: i32) -> Self {
let now = Utc::now().naive_utc();
Self {
uuid,
created_at: now,
updated_at: now,
user_uuid,
name,
atype,
push_uuid: Some(PushId(get_uuid())),
push_token: None,
refresh_token: String::new(),
twofactor_remember: None,
}
}
pub fn to_json(&self) -> Value {
json!({
"id": self.uuid,
"name": self.name,
"type": self.atype,
"identifier": self.uuid,
"creationDate": format_date(&self.created_at),
"isTrusted": false,
"object":"device"
})
}
pub fn refresh_twofactor_remember(&mut self) -> String {
use data_encoding::BASE64;
let twofactor_remember = crypto::encode_random_bytes::<180>(BASE64);
self.twofactor_remember = Some(twofactor_remember.clone());
twofactor_remember
}
pub fn delete_twofactor_remember(&mut self) {
self.twofactor_remember = None;
}
pub fn refresh_tokens(
&mut self,
user: &super::User,
scope: Vec<String>,
client_id: Option<String>,
) -> (String, i64) {
// If there is no refresh token, we create one
if self.refresh_token.is_empty() {
use data_encoding::BASE64URL;
self.refresh_token = crypto::encode_random_bytes::<64>(BASE64URL);
}
// Update the expiration of the device and the last update date
let time_now = Utc::now();
self.updated_at = time_now.naive_utc();
// Generate a random push_uuid so if it doesn't already have one
if self.push_uuid.is_none() {
self.push_uuid = Some(PushId(get_uuid()));
}
// ---
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
// ---
// fn arg: members: Vec<super::Membership>,
// ---
// let orgowner: Vec<_> = members.iter().filter(|m| m.atype == 0).map(|o| o.org_uuid.clone()).collect();
// let orgadmin: Vec<_> = members.iter().filter(|m| m.atype == 1).map(|o| o.org_uuid.clone()).collect();
// let orguser: Vec<_> = members.iter().filter(|m| m.atype == 2).map(|o| o.org_uuid.clone()).collect();
// let orgmanager: Vec<_> = members.iter().filter(|m| m.atype == 3).map(|o| o.org_uuid.clone()).collect();
// Create the JWT claims struct, to send to the client
use crate::auth::{encode_jwt, LoginJwtClaims, DEFAULT_VALIDITY, JWT_LOGIN_ISSUER};
let claims = LoginJwtClaims {
nbf: time_now.timestamp(),
exp: (time_now + *DEFAULT_VALIDITY).timestamp(),
iss: JWT_LOGIN_ISSUER.to_string(),
sub: user.uuid.clone(),
premium: true,
name: user.name.clone(),
email: user.email.clone(),
email_verified: !CONFIG.mail_enabled() || user.verified_at.is_some(),
// ---
// Disabled these keys to be added to the JWT since they could cause the JWT to get too large
// Also These key/value pairs are not used anywhere by either Vaultwarden or Bitwarden Clients
// Because these might get used in the future, and they are added by the Bitwarden Server, lets keep it, but then commented out
// See: https://github.com/dani-garcia/vaultwarden/issues/4156
// ---
// orgowner,
// orgadmin,
// orguser,
// orgmanager,
sstamp: user.security_stamp.clone(),
device: self.uuid.clone(),
devicetype: DeviceType::from_i32(self.atype).to_string(),
client_id: client_id.unwrap_or("undefined".to_string()),
scope,
amr: vec!["Application".into()],
};
(encode_jwt(&claims), DEFAULT_VALIDITY.num_seconds())
}
pub fn is_push_device(&self) -> bool {
matches!(DeviceType::from_i32(self.atype), DeviceType::Android | DeviceType::Ios)
}
pub fn is_cli(&self) -> bool {
matches!(DeviceType::from_i32(self.atype), DeviceType::WindowsCLI | DeviceType::MacOsCLI | DeviceType::LinuxCLI)
}
}
pub struct DeviceWithAuthRequest {
pub device: Device,
pub pending_auth_request: Option<AuthRequest>,
}
impl DeviceWithAuthRequest {
pub fn to_json(&self) -> Value {
let auth_request = match &self.pending_auth_request {
Some(auth_request) => auth_request.to_json_for_pending_device(),
None => Value::Null,
};
json!({
"id": self.device.uuid,
"name": self.device.name,
"type": self.device.atype,
"identifier": self.device.uuid,
"creationDate": format_date(&self.device.created_at),
"devicePendingAuthRequest": auth_request,
"isTrusted": false,
"encryptedPublicKey": null,
"encryptedUserKey": null,
"object": "device",
})
}
pub fn from(c: Device, a: Option<AuthRequest>) -> Self {
Self {
device: c,
pending_auth_request: a,
}
}
}
use crate::db::DbConn;
use crate::api::EmptyResult;
use crate::error::MapResult;
/// Database methods
impl Device {
pub async fn save(&mut self, conn: &mut DbConn) -> EmptyResult {
self.updated_at = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
crate::util::retry(
|| diesel::replace_into(devices::table).values(DeviceDb::to_db(self)).execute(conn),
10,
).map_res("Error saving device")
}
postgresql {
let value = DeviceDb::to_db(self);
crate::util::retry(
|| diesel::insert_into(devices::table).values(&value).on_conflict((devices::uuid, devices::user_uuid)).do_update().set(&value).execute(conn),
10,
).map_res("Error saving device")
}
}
}
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &mut DbConn) -> EmptyResult {
db_run! { conn: {
diesel::delete(devices::table.filter(devices::user_uuid.eq(user_uuid)))
.execute(conn)
.map_res("Error removing devices for user")
}}
}
pub async fn find_by_uuid_and_user(uuid: &DeviceId, user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::uuid.eq(uuid))
.filter(devices::user_uuid.eq(user_uuid))
.first::<DeviceDb>(conn)
.ok()
.from_db()
}}
}
pub async fn find_with_auth_request_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Vec<DeviceWithAuthRequest> {
let devices = Self::find_by_user(user_uuid, conn).await;
let mut result = Vec::new();
for device in devices {
let auth_request = AuthRequest::find_by_user_and_requested_device(user_uuid, &device.uuid, conn).await;
result.push(DeviceWithAuthRequest::from(device, auth_request));
}
result
}
pub async fn find_by_user(user_uuid: &UserId, 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: &DeviceId, 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: &DeviceId, 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> {
db_run! { conn: {
devices::table
.filter(devices::refresh_token.eq(refresh_token))
.first::<DeviceDb>(conn)
.ok()
.from_db()
}}
}
pub async fn find_latest_active_by_user(user_uuid: &UserId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.order(devices::updated_at.desc())
.first::<DeviceDb>(conn)
.ok()
.from_db()
}}
}
pub async fn find_push_devices_by_user(user_uuid: &UserId, 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()
}}
}
pub async fn check_user_has_push_device(user_uuid: &UserId, conn: &mut DbConn) -> bool {
db_run! { conn: {
devices::table
.filter(devices::user_uuid.eq(user_uuid))
.filter(devices::push_token.is_not_null())
.count()
.first::<i64>(conn)
.ok()
.unwrap_or(0) != 0
}}
}
}
#[derive(Display)]
pub enum DeviceType {
#[display("Android")]
Android = 0,
#[display("iOS")]
Ios = 1,
#[display("Chrome Extension")]
ChromeExtension = 2,
#[display("Firefox Extension")]
FirefoxExtension = 3,
#[display("Opera Extension")]
OperaExtension = 4,
#[display("Edge Extension")]
EdgeExtension = 5,
#[display("Windows")]
WindowsDesktop = 6,
#[display("macOS")]
MacOsDesktop = 7,
#[display("Linux")]
LinuxDesktop = 8,
#[display("Chrome")]
ChromeBrowser = 9,
#[display("Firefox")]
FirefoxBrowser = 10,
#[display("Opera")]
OperaBrowser = 11,
#[display("Edge")]
EdgeBrowser = 12,
#[display("Internet Explorer")]
IEBrowser = 13,
#[display("Unknown Browser")]
UnknownBrowser = 14,
#[display("Android")]
AndroidAmazon = 15,
#[display("UWP")]
Uwp = 16,
#[display("Safari")]
SafariBrowser = 17,
#[display("Vivaldi")]
VivaldiBrowser = 18,
#[display("Vivaldi Extension")]
VivaldiExtension = 19,
#[display("Safari Extension")]
SafariExtension = 20,
#[display("SDK")]
Sdk = 21,
#[display("Server")]
Server = 22,
#[display("Windows CLI")]
WindowsCLI = 23,
#[display("macOS CLI")]
MacOsCLI = 24,
#[display("Linux CLI")]
LinuxCLI = 25,
}
impl DeviceType {
pub fn from_i32(value: i32) -> DeviceType {
match value {
0 => DeviceType::Android,
1 => DeviceType::Ios,
2 => DeviceType::ChromeExtension,
3 => DeviceType::FirefoxExtension,
4 => DeviceType::OperaExtension,
5 => DeviceType::EdgeExtension,
6 => DeviceType::WindowsDesktop,
7 => DeviceType::MacOsDesktop,
8 => DeviceType::LinuxDesktop,
9 => DeviceType::ChromeBrowser,
10 => DeviceType::FirefoxBrowser,
11 => DeviceType::OperaBrowser,
12 => DeviceType::EdgeBrowser,
13 => DeviceType::IEBrowser,
14 => DeviceType::UnknownBrowser,
15 => DeviceType::AndroidAmazon,
16 => DeviceType::Uwp,
17 => DeviceType::SafariBrowser,
18 => DeviceType::VivaldiBrowser,
19 => DeviceType::VivaldiExtension,
20 => DeviceType::SafariExtension,
21 => DeviceType::Sdk,
22 => DeviceType::Server,
23 => DeviceType::WindowsCLI,
24 => DeviceType::MacOsCLI,
25 => DeviceType::LinuxCLI,
_ => DeviceType::UnknownBrowser,
}
}
}
#[derive(
Clone, Debug, DieselNewType, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
)]
pub struct DeviceId(String);
#[derive(Clone, Debug, DieselNewType, Display, From, FromForm, Serialize, Deserialize, UuidFromParam)]
pub struct PushId(pub String);