mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-02-05 00:29:40 +03:00
* Use Diesels MultiConnections Derive With this PR we remove almost all custom macro's to create the multiple database type code. This is now handled by Diesel it self. This removed the need of the following functions/macro's: - `db_object!` - `::to_db` - `.from_db()` It is also possible to just use one schema instead of multiple per type. Also done: - Refactored the SQLite backup function - Some formatting of queries so every call is one a separate line, this looks a bit better - Declare `conn` as mut inside each `db_run!` instead of having to declare it as `mut` in functions or calls - Added an `ACTIVE_DB_TYPE` static which holds the currently active database type - Removed `diesel_logger` crate and use Diesel's `set_default_instrumentation()` If you want debug queries you can now simply change the log level of `vaultwarden::db::query_logger` - Use PostgreSQL v17 in the Alpine images to match the Debian Trixie version - Optimized the Workflows since `diesel_logger` isn't needed anymore And on the extra plus-side, this lowers the compile-time and binary size too. Signed-off-by: BlackDex <black.dex@gmail.com> * Adjust query_logger and some other small items Signed-off-by: BlackDex <black.dex@gmail.com> * Remove macro, replaced with an function Signed-off-by: BlackDex <black.dex@gmail.com> * Implement custom connection manager Signed-off-by: BlackDex <black.dex@gmail.com> * Updated some crates to keep up2date Signed-off-by: BlackDex <black.dex@gmail.com> * Small adjustment Signed-off-by: BlackDex <black.dex@gmail.com> * crate updates Signed-off-by: BlackDex <black.dex@gmail.com> * Update crates Signed-off-by: BlackDex <black.dex@gmail.com> --------- Signed-off-by: BlackDex <black.dex@gmail.com>
398 lines
12 KiB
Rust
398 lines
12 KiB
Rust
use chrono::{NaiveDateTime, Utc};
|
|
use serde_json::Value;
|
|
|
|
use crate::{config::PathType, util::LowerCase, CONFIG};
|
|
|
|
use super::{OrganizationId, User, UserId};
|
|
use crate::db::schema::sends;
|
|
use diesel::prelude::*;
|
|
use id::SendId;
|
|
|
|
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
|
|
#[diesel(table_name = sends)]
|
|
#[diesel(treat_none_as_null = true)]
|
|
#[diesel(primary_key(uuid))]
|
|
pub struct Send {
|
|
pub uuid: SendId,
|
|
|
|
pub user_uuid: Option<UserId>,
|
|
pub organization_uuid: Option<OrganizationId>,
|
|
|
|
pub name: String,
|
|
pub notes: Option<String>,
|
|
|
|
pub atype: i32,
|
|
pub data: String,
|
|
pub akey: String,
|
|
pub password_hash: Option<Vec<u8>>,
|
|
password_salt: Option<Vec<u8>>,
|
|
password_iter: Option<i32>,
|
|
|
|
pub max_access_count: Option<i32>,
|
|
pub access_count: i32,
|
|
|
|
pub creation_date: NaiveDateTime,
|
|
pub revision_date: NaiveDateTime,
|
|
pub expiration_date: Option<NaiveDateTime>,
|
|
pub deletion_date: NaiveDateTime,
|
|
|
|
pub disabled: bool,
|
|
pub hide_email: Option<bool>,
|
|
}
|
|
|
|
#[derive(Copy, Clone, PartialEq, Eq, num_derive::FromPrimitive)]
|
|
pub enum SendType {
|
|
Text = 0,
|
|
File = 1,
|
|
}
|
|
|
|
impl Send {
|
|
pub fn new(atype: i32, name: String, data: String, akey: String, deletion_date: NaiveDateTime) -> Self {
|
|
let now = Utc::now().naive_utc();
|
|
|
|
Self {
|
|
uuid: SendId::from(crate::util::get_uuid()),
|
|
user_uuid: None,
|
|
organization_uuid: None,
|
|
|
|
name,
|
|
notes: None,
|
|
|
|
atype,
|
|
data,
|
|
akey,
|
|
password_hash: None,
|
|
password_salt: None,
|
|
password_iter: None,
|
|
|
|
max_access_count: None,
|
|
access_count: 0,
|
|
|
|
creation_date: now,
|
|
revision_date: now,
|
|
expiration_date: None,
|
|
deletion_date,
|
|
|
|
disabled: false,
|
|
hide_email: None,
|
|
}
|
|
}
|
|
|
|
pub fn set_password(&mut self, password: Option<&str>) {
|
|
const PASSWORD_ITER: i32 = 100_000;
|
|
|
|
if let Some(password) = password {
|
|
self.password_iter = Some(PASSWORD_ITER);
|
|
let salt = crate::crypto::get_random_bytes::<64>().to_vec();
|
|
let hash = crate::crypto::hash_password(password.as_bytes(), &salt, PASSWORD_ITER as u32);
|
|
self.password_salt = Some(salt);
|
|
self.password_hash = Some(hash);
|
|
} else {
|
|
self.password_iter = None;
|
|
self.password_salt = None;
|
|
self.password_hash = None;
|
|
}
|
|
}
|
|
|
|
pub fn check_password(&self, password: &str) -> bool {
|
|
match (&self.password_hash, &self.password_salt, self.password_iter) {
|
|
(Some(hash), Some(salt), Some(iter)) => {
|
|
crate::crypto::verify_password_hash(password.as_bytes(), salt, hash, iter as u32)
|
|
}
|
|
_ => false,
|
|
}
|
|
}
|
|
|
|
pub async fn creator_identifier(&self, conn: &DbConn) -> Option<String> {
|
|
if let Some(hide_email) = self.hide_email {
|
|
if hide_email {
|
|
return None;
|
|
}
|
|
}
|
|
|
|
if let Some(user_uuid) = &self.user_uuid {
|
|
if let Some(user) = User::find_by_uuid(user_uuid, conn).await {
|
|
return Some(user.email);
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
|
|
pub fn to_json(&self) -> Value {
|
|
use crate::util::format_date;
|
|
use data_encoding::BASE64URL_NOPAD;
|
|
use uuid::Uuid;
|
|
|
|
let mut data = serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_default();
|
|
|
|
// Mobile clients expect size to be a string instead of a number
|
|
if let Some(size) = data.get("size").and_then(|v| v.as_i64()) {
|
|
data["size"] = Value::String(size.to_string());
|
|
}
|
|
|
|
json!({
|
|
"id": self.uuid,
|
|
"accessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),
|
|
"type": self.atype,
|
|
|
|
"name": self.name,
|
|
"notes": self.notes,
|
|
"text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
|
"file": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
|
|
|
"key": self.akey,
|
|
"maxAccessCount": self.max_access_count,
|
|
"accessCount": self.access_count,
|
|
"password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)),
|
|
"disabled": self.disabled,
|
|
"hideEmail": self.hide_email,
|
|
|
|
"revisionDate": format_date(&self.revision_date),
|
|
"expirationDate": self.expiration_date.as_ref().map(format_date),
|
|
"deletionDate": format_date(&self.deletion_date),
|
|
"object": "send",
|
|
})
|
|
}
|
|
|
|
pub async fn to_json_access(&self, conn: &DbConn) -> Value {
|
|
use crate::util::format_date;
|
|
|
|
let mut data = serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_default();
|
|
|
|
// Mobile clients expect size to be a string instead of a number
|
|
if let Some(size) = data.get("size").and_then(|v| v.as_i64()) {
|
|
data["size"] = Value::String(size.to_string());
|
|
}
|
|
|
|
json!({
|
|
"id": self.uuid,
|
|
"type": self.atype,
|
|
|
|
"name": self.name,
|
|
"text": if self.atype == SendType::Text as i32 { Some(&data) } else { None },
|
|
"file": if self.atype == SendType::File as i32 { Some(&data) } else { None },
|
|
|
|
"expirationDate": self.expiration_date.as_ref().map(format_date),
|
|
"creatorIdentifier": self.creator_identifier(conn).await,
|
|
"object": "send-access",
|
|
})
|
|
}
|
|
}
|
|
|
|
use crate::db::DbConn;
|
|
|
|
use crate::api::EmptyResult;
|
|
use crate::error::MapResult;
|
|
use crate::util::NumberOrString;
|
|
|
|
impl Send {
|
|
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
|
|
self.update_users_revision(conn).await;
|
|
self.revision_date = Utc::now().naive_utc();
|
|
|
|
db_run! { conn:
|
|
sqlite, mysql {
|
|
match diesel::replace_into(sends::table)
|
|
.values(&*self)
|
|
.execute(conn)
|
|
{
|
|
Ok(_) => Ok(()),
|
|
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
|
|
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
|
|
diesel::update(sends::table)
|
|
.filter(sends::uuid.eq(&self.uuid))
|
|
.set(&*self)
|
|
.execute(conn)
|
|
.map_res("Error saving send")
|
|
}
|
|
Err(e) => Err(e.into()),
|
|
}.map_res("Error saving send")
|
|
}
|
|
postgresql {
|
|
diesel::insert_into(sends::table)
|
|
.values(&*self)
|
|
.on_conflict(sends::uuid)
|
|
.do_update()
|
|
.set(&*self)
|
|
.execute(conn)
|
|
.map_res("Error saving send")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub async fn delete(&self, conn: &DbConn) -> EmptyResult {
|
|
self.update_users_revision(conn).await;
|
|
|
|
if self.atype == SendType::File as i32 {
|
|
let operator = CONFIG.opendal_operator_for_path_type(PathType::Sends)?;
|
|
operator.remove_all(&self.uuid).await.ok();
|
|
}
|
|
|
|
db_run! { conn: {
|
|
diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid)))
|
|
.execute(conn)
|
|
.map_res("Error deleting send")
|
|
}}
|
|
}
|
|
|
|
/// Purge all sends that are past their deletion date.
|
|
pub async fn purge(conn: &DbConn) {
|
|
for send in Self::find_by_past_deletion_date(conn).await {
|
|
send.delete(conn).await.ok();
|
|
}
|
|
}
|
|
|
|
pub async fn update_users_revision(&self, conn: &DbConn) -> Vec<UserId> {
|
|
let mut user_uuids = Vec::new();
|
|
match &self.user_uuid {
|
|
Some(user_uuid) => {
|
|
User::update_uuid_revision(user_uuid, conn).await;
|
|
user_uuids.push(user_uuid.clone())
|
|
}
|
|
None => {
|
|
// Belongs to Organization, not implemented
|
|
}
|
|
};
|
|
user_uuids
|
|
}
|
|
|
|
pub async fn delete_all_by_user(user_uuid: &UserId, conn: &DbConn) -> EmptyResult {
|
|
for send in Self::find_by_user(user_uuid, conn).await {
|
|
send.delete(conn).await?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub async fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> {
|
|
use data_encoding::BASE64URL_NOPAD;
|
|
use uuid::Uuid;
|
|
|
|
let Ok(uuid_vec) = BASE64URL_NOPAD.decode(access_id.as_bytes()) else {
|
|
return None;
|
|
};
|
|
|
|
let uuid = match Uuid::from_slice(&uuid_vec) {
|
|
Ok(u) => SendId::from(u.to_string()),
|
|
Err(_) => return None,
|
|
};
|
|
|
|
Self::find_by_uuid(&uuid, conn).await
|
|
}
|
|
|
|
pub async fn find_by_uuid(uuid: &SendId, conn: &DbConn) -> Option<Self> {
|
|
db_run! { conn: {
|
|
sends::table
|
|
.filter(sends::uuid.eq(uuid))
|
|
.first::<Self>(conn)
|
|
.ok()
|
|
}}
|
|
}
|
|
|
|
pub async fn find_by_uuid_and_user(uuid: &SendId, user_uuid: &UserId, conn: &DbConn) -> Option<Self> {
|
|
db_run! { conn: {
|
|
sends::table
|
|
.filter(sends::uuid.eq(uuid))
|
|
.filter(sends::user_uuid.eq(user_uuid))
|
|
.first::<Self>(conn)
|
|
.ok()
|
|
}}
|
|
}
|
|
|
|
pub async fn find_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
|
|
db_run! { conn: {
|
|
sends::table
|
|
.filter(sends::user_uuid.eq(user_uuid))
|
|
.load::<Self>(conn)
|
|
.expect("Error loading sends")
|
|
}}
|
|
}
|
|
|
|
pub async fn size_by_user(user_uuid: &UserId, conn: &DbConn) -> Option<i64> {
|
|
let sends = Self::find_by_user(user_uuid, conn).await;
|
|
|
|
#[derive(serde::Deserialize)]
|
|
struct FileData {
|
|
#[serde(rename = "size", alias = "Size")]
|
|
size: NumberOrString,
|
|
}
|
|
|
|
let mut total: i64 = 0;
|
|
for send in sends {
|
|
if send.atype == SendType::File as i32 {
|
|
if let Ok(size) =
|
|
serde_json::from_str::<FileData>(&send.data).map_err(Into::into).and_then(|d| d.size.into_i64())
|
|
{
|
|
total = total.checked_add(size)?;
|
|
};
|
|
}
|
|
}
|
|
|
|
Some(total)
|
|
}
|
|
|
|
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
|
|
db_run! { conn: {
|
|
sends::table
|
|
.filter(sends::organization_uuid.eq(org_uuid))
|
|
.load::<Self>(conn)
|
|
.expect("Error loading sends")
|
|
}}
|
|
}
|
|
|
|
pub async fn find_by_past_deletion_date(conn: &DbConn) -> Vec<Self> {
|
|
let now = Utc::now().naive_utc();
|
|
db_run! { conn: {
|
|
sends::table
|
|
.filter(sends::deletion_date.lt(now))
|
|
.load::<Self>(conn)
|
|
.expect("Error loading sends")
|
|
}}
|
|
}
|
|
}
|
|
|
|
// separate namespace to avoid name collision with std::marker::Send
|
|
pub mod id {
|
|
use derive_more::{AsRef, Deref, Display, From};
|
|
use macros::{IdFromParam, UuidFromParam};
|
|
use std::marker::Send;
|
|
use std::path::Path;
|
|
|
|
#[derive(
|
|
Clone,
|
|
Debug,
|
|
AsRef,
|
|
Deref,
|
|
DieselNewType,
|
|
Display,
|
|
From,
|
|
FromForm,
|
|
Hash,
|
|
PartialEq,
|
|
Eq,
|
|
Serialize,
|
|
Deserialize,
|
|
UuidFromParam,
|
|
)]
|
|
pub struct SendId(String);
|
|
|
|
impl AsRef<Path> for SendId {
|
|
#[inline]
|
|
fn as_ref(&self) -> &Path {
|
|
Path::new(&self.0)
|
|
}
|
|
}
|
|
|
|
#[derive(
|
|
Clone, Debug, AsRef, Deref, Display, From, FromForm, Hash, PartialEq, Eq, Serialize, Deserialize, IdFromParam,
|
|
)]
|
|
pub struct SendFileId(String);
|
|
|
|
impl AsRef<Path> for SendFileId {
|
|
#[inline]
|
|
fn as_ref(&self) -> &Path {
|
|
Path::new(&self.0)
|
|
}
|
|
}
|
|
}
|