vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 8da5b994828cedad67c2d32df8d89fa79749b04f
parent 2969e87b5262b0c75d9298f8a2bf4a82336e918d
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Sun, 14 Mar 2021 23:35:55 +0100

Send API

Diffstat:
M.env.template | 1+
Amigrations/mysql/2021-03-11-190243_add_sends/down.sql | 1+
Amigrations/mysql/2021-03-11-190243_add_sends/up.sql | 26++++++++++++++++++++++++++
Amigrations/postgresql/2021-03-11-190243_add_sends/down.sql | 1+
Amigrations/postgresql/2021-03-11-190243_add_sends/up.sql | 26++++++++++++++++++++++++++
Amigrations/sqlite/2021-03-11-190243_add_sends/down.sql | 1+
Amigrations/sqlite/2021-03-11-190243_add_sends/up.sql | 26++++++++++++++++++++++++++
Msrc/api/core/ciphers.rs | 7+++++++
Msrc/api/core/mod.rs | 2++
Asrc/api/core/sends.rs | 383+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/api/notifications.rs | 4++++
Msrc/api/web.rs | 7++++++-
Msrc/config.rs | 2++
Msrc/db/models/mod.rs | 3+++
Asrc/db/models/send.rs | 235+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/db/models/user.rs | 3++-
Msrc/db/schemas/mysql/schema.rs | 26++++++++++++++++++++++++++
Msrc/db/schemas/postgresql/schema.rs | 26++++++++++++++++++++++++++
Msrc/db/schemas/sqlite/schema.rs | 26++++++++++++++++++++++++++
Msrc/error.rs | 12++++++++++++
20 files changed, 816 insertions(+), 2 deletions(-)

diff --git a/.env.template b/.env.template @@ -28,6 +28,7 @@ # RSA_KEY_FILENAME=data/rsa_key # ICON_CACHE_FOLDER=data/icon_cache # ATTACHMENTS_FOLDER=data/attachments +# SENDS_FOLDER=data/sends ## Templates data folder, by default uses embedded templates ## Check source code to see the format diff --git a/migrations/mysql/2021-03-11-190243_add_sends/down.sql b/migrations/mysql/2021-03-11-190243_add_sends/down.sql @@ -0,0 +1 @@ +DROP TABLE sends; diff --git a/migrations/mysql/2021-03-11-190243_add_sends/up.sql b/migrations/mysql/2021-03-11-190243_add_sends/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE sends ( + uuid CHAR(36) NOT NULL PRIMARY KEY, + user_uuid CHAR(36) REFERENCES users (uuid), + organization_uuid CHAR(36) REFERENCES organizations (uuid), + + name TEXT NOT NULL, + notes TEXT, + + atype INTEGER NOT NULL, + data TEXT NOT NULL, + key TEXT NOT NULL, + password_hash BLOB, + password_salt BLOB, + password_iter INTEGER, + + max_access_count INTEGER, + access_count INTEGER NOT NULL, + + creation_date DATETIME NOT NULL, + revision_date DATETIME NOT NULL, + expiration_date DATETIME, + deletion_date DATETIME NOT NULL, + + disabled BOOLEAN NOT NULL +); +\ No newline at end of file diff --git a/migrations/postgresql/2021-03-11-190243_add_sends/down.sql b/migrations/postgresql/2021-03-11-190243_add_sends/down.sql @@ -0,0 +1 @@ +DROP TABLE sends; diff --git a/migrations/postgresql/2021-03-11-190243_add_sends/up.sql b/migrations/postgresql/2021-03-11-190243_add_sends/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE sends ( + uuid CHAR(36) NOT NULL PRIMARY KEY, + user_uuid CHAR(36) REFERENCES users (uuid), + organization_uuid CHAR(36) REFERENCES organizations (uuid), + + name TEXT NOT NULL, + notes TEXT, + + atype INTEGER NOT NULL, + data TEXT NOT NULL, + key TEXT NOT NULL, + password_hash BYTEA, + password_salt BYTEA, + password_iter INTEGER, + + max_access_count INTEGER, + access_count INTEGER NOT NULL, + + creation_date TIMESTAMP NOT NULL, + revision_date TIMESTAMP NOT NULL, + expiration_date TIMESTAMP, + deletion_date TIMESTAMP NOT NULL, + + disabled BOOLEAN NOT NULL +); +\ No newline at end of file diff --git a/migrations/sqlite/2021-03-11-190243_add_sends/down.sql b/migrations/sqlite/2021-03-11-190243_add_sends/down.sql @@ -0,0 +1 @@ +DROP TABLE sends; diff --git a/migrations/sqlite/2021-03-11-190243_add_sends/up.sql b/migrations/sqlite/2021-03-11-190243_add_sends/up.sql @@ -0,0 +1,25 @@ +CREATE TABLE sends ( + uuid TEXT NOT NULL PRIMARY KEY, + user_uuid TEXT REFERENCES users (uuid), + organization_uuid TEXT REFERENCES organizations (uuid), + + name TEXT NOT NULL, + notes TEXT, + + atype INTEGER NOT NULL, + data TEXT NOT NULL, + key TEXT NOT NULL, + password_hash BLOB, + password_salt BLOB, + password_iter INTEGER, + + max_access_count INTEGER, + access_count INTEGER NOT NULL, + + creation_date DATETIME NOT NULL, + revision_date DATETIME NOT NULL, + expiration_date DATETIME, + deletion_date DATETIME NOT NULL, + + disabled BOOLEAN NOT NULL +); +\ No newline at end of file diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -104,6 +104,12 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult { .map(|c| c.to_json(&headers.host, &headers.user.uuid, &conn)) .collect(); + let sends = Send::find_by_user(&headers.user.uuid, &conn); + let sends_json: Vec<Value> = sends + .iter() + .map(|s| s.to_json()) + .collect(); + let domains_json = if data.exclude_domains { Value::Null } else { @@ -117,6 +123,7 @@ fn sync(data: Form<SyncData>, headers: Headers, conn: DbConn) -> JsonResult { "Policies": policies_json, "Ciphers": ciphers_json, "Domains": domains_json, + "Sends": sends_json, "Object": "sync" }))) } diff --git a/src/api/core/mod.rs b/src/api/core/mod.rs @@ -3,6 +3,7 @@ mod ciphers; mod folders; mod organizations; pub mod two_factor; +mod sends; pub fn routes() -> Vec<Route> { let mut mod_routes = routes![ @@ -20,6 +21,7 @@ pub fn routes() -> Vec<Route> { routes.append(&mut folders::routes()); routes.append(&mut organizations::routes()); routes.append(&mut two_factor::routes()); + routes.append(&mut sends::routes()); routes.append(&mut mod_routes); routes diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs @@ -0,0 +1,383 @@ +use std::{io::Read, path::Path}; + +use chrono::{DateTime, Duration, Utc}; +use multipart::server::{save::SavedData, Multipart, SaveResult}; +use rocket::{http::ContentType, Data}; +use rocket_contrib::json::Json; +use serde_json::Value; + +use crate::{ + api::{ApiResult, EmptyResult, JsonResult, JsonUpcase, Notify, UpdateType}, + auth::{Headers, Host}, + db::{models::*, DbConn}, + CONFIG, +}; + +pub fn routes() -> Vec<rocket::Route> { + routes![ + post_send, + post_send_file, + post_access, + post_access_file, + put_send, + delete_send, + put_remove_password + ] +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +pub struct SendData { + pub Type: i32, + pub Key: String, + pub Password: Option<String>, + pub MaxAccessCount: Option<i32>, + pub ExpirationDate: Option<DateTime<Utc>>, + pub DeletionDate: DateTime<Utc>, + pub Disabled: bool, + + // Data field + pub Name: String, + pub Notes: Option<String>, + pub Text: Option<Value>, + pub File: Option<Value>, +} + +fn create_send(data: SendData, user_uuid: String) -> ApiResult<Send> { + let data_val = if data.Type == SendType::Text as i32 { + data.Text + } else if data.Type == SendType::File as i32 { + data.File + } else { + err!("Invalid Send type") + }; + + let data_str = if let Some(mut d) = data_val { + d.as_object_mut().and_then(|o| o.remove("Response")); + serde_json::to_string(&d)? + } else { + err!("Send data not provided"); + }; + + if data.DeletionDate > Utc::now() + Duration::days(31) { + err!( + "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." + ); + } + + let mut send = Send::new(data.Type, data.Name, data_str, data.Key, data.DeletionDate.naive_utc()); + send.user_uuid = Some(user_uuid); + send.notes = data.Notes; + send.max_access_count = data.MaxAccessCount; + send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); + send.disabled = data.Disabled; + send.atype = data.Type; + + send.set_password(data.Password.as_deref()); + + Ok(send) +} + +#[post("/sends", data = "<data>")] +fn post_send(data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let data: SendData = data.into_inner().data; + + if data.Type == SendType::File as i32 { + err!("File sends should use /api/sends/file") + } + + let mut send = create_send(data, headers.user.uuid.clone())?; + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); + + Ok(Json(send.to_json())) +} + +#[post("/sends/file", format = "multipart/form-data", data = "<data>")] +fn post_send_file(data: Data, content_type: &ContentType, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let boundary = content_type.params().next().expect("No boundary provided").1; + + let mut mpart = Multipart::with_body(data.open(), boundary); + + // First entry is the SendData JSON + let mut model_entry = match mpart.read_entry()? { + Some(e) if &*e.headers.name == "model" => e, + Some(_) => err!("Invalid entry name"), + None => err!("No model entry present"), + }; + + let mut buf = String::new(); + model_entry.data.read_to_string(&mut buf)?; + let data = serde_json::from_str::<crate::util::UpCase<SendData>>(&buf)?; + + // Get the file length and add an extra 10% to avoid issues + const SIZE_110_MB: u64 = 115_343_360; + + let size_limit = match CONFIG.user_attachment_limit() { + Some(0) => err!("File uploads are disabled"), + Some(limit_kb) => { + let left = (limit_kb * 1024) - Attachment::size_by_user(&headers.user.uuid, &conn); + if left <= 0 { + err!("Attachment size limit reached! Delete some files to open space") + } + std::cmp::Ord::max(left as u64, SIZE_110_MB) + } + None => SIZE_110_MB, + }; + + // Create the Send + let mut send = create_send(data.data, headers.user.uuid.clone())?; + let file_id: String = data_encoding::HEXLOWER.encode(&crate::crypto::get_random(vec![0; 32])); + + if send.atype != SendType::File as i32 { + err!("Send content is not a file"); + } + + let file_path = Path::new(&CONFIG.sends_folder()).join(&send.uuid).join(&file_id); + + // Read the data entry and save the file + let mut data_entry = match mpart.read_entry()? { + Some(e) if &*e.headers.name == "data" => e, + Some(_) => err!("Invalid entry name"), + None => err!("No model entry present"), + }; + + let size = match data_entry + .data + .save() + .memory_threshold(0) + .size_limit(size_limit) + .with_path(&file_path) + { + SaveResult::Full(SavedData::File(_, size)) => size as i32, + SaveResult::Full(other) => { + std::fs::remove_file(&file_path).ok(); + err!(format!("Attachment is not a file: {:?}", other)); + } + SaveResult::Partial(_, reason) => { + std::fs::remove_file(&file_path).ok(); + err!(format!("Attachment size limit exceeded with this file: {:?}", reason)); + } + SaveResult::Error(e) => { + std::fs::remove_file(&file_path).ok(); + err!(format!("Error: {:?}", e)); + } + }; + + // Set ID and sizes + let mut data_value: Value = serde_json::from_str(&send.data)?; + if let Some(o) = data_value.as_object_mut() { + o.insert(String::from("Id"), Value::String(file_id)); + o.insert(String::from("Size"), Value::Number(size.into())); + o.insert( + String::from("SizeName"), + Value::String(crate::util::get_display_size(size)), + ); + } + send.data = serde_json::to_string(&data_value)?; + + // Save the changes in the database + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendCreate, &headers.user); + + Ok(Json(send.to_json())) +} + +#[derive(Deserialize)] +#[allow(non_snake_case)] +pub struct SendAccessData { + pub Password: Option<String>, +} + +#[post("/sends/access/<access_id>", data = "<data>")] +fn post_access(access_id: String, data: JsonUpcase<SendAccessData>, conn: DbConn) -> JsonResult { + let mut send = match Send::find_by_access_id(&access_id, &conn) { + Some(s) => s, + None => err_code!("Send not found", 404), + }; + + if let Some(max_access_count) = send.max_access_count { + if send.access_count > max_access_count { + err_code!("Max access count reached", 404); + } + } + + if let Some(expiration) = send.expiration_date { + if Utc::now().naive_utc() > expiration { + err_code!("Send has expired", 404) + } + } + + if Utc::now().naive_utc() > send.deletion_date { + err_code!("Send has been deleted", 404) + } + + if send.disabled { + err_code!("Send has been disabled", 404) + } + + if send.password_hash.is_some() { + match data.into_inner().data.Password { + Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } + Some(_) => err!("Invalid password."), + None => err_code!("Password not provided", 401), + } + } + + // Files are incremented during the download + if send.atype == SendType::Text as i32 { + send.access_count += 1; + } + + send.save(&conn)?; + + Ok(Json(send.to_json())) +} + +#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")] +fn post_access_file( + send_id: String, + file_id: String, + data: JsonUpcase<SendAccessData>, + host: Host, + conn: DbConn, +) -> JsonResult { + let mut send = match Send::find_by_uuid(&send_id, &conn) { + Some(s) => s, + None => err_code!("Send not found", 404), + }; + + if let Some(max_access_count) = send.max_access_count { + if send.access_count > max_access_count { + err_code!("Max access count reached", 404); + } + } + + if let Some(expiration) = send.expiration_date { + if Utc::now().naive_utc() > expiration { + err_code!("Send has expired", 404) + } + } + + if Utc::now().naive_utc() > send.deletion_date { + err_code!("Send has been deleted", 404) + } + + if send.disabled { + err_code!("Send has been disabled", 404) + } + + if send.password_hash.is_some() { + match data.into_inner().data.Password { + Some(ref p) if send.check_password(p) => { /* Nothing to do here */ } + Some(_) => err!("Invalid password."), + None => err_code!("Password not provided", 401), + } + } + + send.access_count += 1; + + send.save(&conn)?; + + Ok(Json(json!({ + "Object": "send-fileDownload", + "Id": file_id, + "Url": format!("{}/sends/{}/{}", &host.host, send_id, file_id) + }))) +} + +#[put("/sends/<id>", data = "<data>")] +fn put_send(id: String, data: JsonUpcase<SendData>, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let data: SendData = data.into_inner().data; + + let mut send = match Send::find_by_uuid(&id, &conn) { + Some(s) => s, + None => err!("Send not found"), + }; + + if send.user_uuid.as_ref() != Some(&headers.user.uuid) { + err!("Send is not owned by user") + } + + if send.atype != data.Type { + err!("Sends can't change type") + } + + let data_val = if data.Type == SendType::Text as i32 { + data.Text + } else if data.Type == SendType::File as i32 { + data.File + } else { + err!("Invalid Send type") + }; + + let data_str = if let Some(mut d) = data_val { + d.as_object_mut().and_then(|d| d.remove("Response")); + serde_json::to_string(&d)? + } else { + err!("Send data not provided"); + }; + + if data.DeletionDate > Utc::now() + Duration::days(31) { + err!( + "You cannot have a Send with a deletion date that far into the future. Adjust the Deletion Date to a value less than 31 days from now and try again." + ); + } + send.data = data_str; + send.name = data.Name; + send.key = data.Key; + send.deletion_date = data.DeletionDate.naive_utc(); + send.notes = data.Notes; + send.max_access_count = data.MaxAccessCount; + send.expiration_date = data.ExpirationDate.map(|d| d.naive_utc()); + send.disabled = data.Disabled; + + // Only change the value if it's present + if let Some(password) = data.Password { + send.set_password(Some(&password)); + } + + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); + + Ok(Json(send.to_json())) +} + +#[delete("/sends/<id>")] +fn delete_send(id: String, headers: Headers, conn: DbConn, nt: Notify) -> EmptyResult { + let send = match Send::find_by_uuid(&id, &conn) { + Some(s) => s, + None => err!("Send not found"), + }; + + if send.user_uuid.as_ref() != Some(&headers.user.uuid) { + err!("Send is not owned by user") + } + + if send.atype == SendType::File as i32 { + std::fs::remove_dir_all(Path::new(&CONFIG.sends_folder()).join(&send.uuid)).ok(); + } + + send.delete(&conn)?; + nt.send_user_update(UpdateType::SyncSendDelete, &headers.user); + + Ok(()) +} + +#[put("/sends/<id>/remove-password")] +fn put_remove_password(id: String, headers: Headers, conn: DbConn, nt: Notify) -> JsonResult { + let mut send = match Send::find_by_uuid(&id, &conn) { + Some(s) => s, + None => err!("Send not found"), + }; + + if send.user_uuid.as_ref() != Some(&headers.user.uuid) { + err!("Send is not owned by user") + } + + send.set_password(None); + send.save(&conn)?; + nt.send_user_update(UpdateType::SyncSendUpdate, &headers.user); + + Ok(Json(send.to_json())) +} diff --git a/src/api/notifications.rs b/src/api/notifications.rs @@ -394,6 +394,10 @@ pub enum UpdateType { LogOut = 11, + SyncSendCreate = 12, + SyncSendUpdate = 13, + SyncSendDelete = 14, + None = 100, } diff --git a/src/api/web.rs b/src/api/web.rs @@ -10,7 +10,7 @@ pub fn routes() -> Vec<Route> { // If addding more routes here, consider also adding them to // crate::utils::LOGGED_ROUTES to make sure they appear in the log if CONFIG.web_vault_enabled() { - routes![web_index, app_id, web_files, attachments, alive, static_files] + routes![web_index, app_id, web_files, attachments, sends, alive, static_files] } else { routes![attachments, alive, static_files] } @@ -60,6 +60,11 @@ fn attachments(uuid: String, file: PathBuf) -> Option<NamedFile> { NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid).join(file)).ok() } +#[get("/sends/<send_id>/<file_id>")] +fn sends(send_id: String, file_id: String) -> Option<NamedFile> { + NamedFile::open(Path::new(&CONFIG.sends_folder()).join(send_id).join(file_id)).ok() +} + #[get("/alive")] fn alive() -> Json<String> { use crate::util::format_date; diff --git a/src/config.rs b/src/config.rs @@ -299,6 +299,8 @@ make_config! { icon_cache_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "icon_cache"); /// Attachments folder attachments_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "attachments"); + /// Sends folder + sends_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "sends"); /// Templates folder templates_folder: String, false, auto, |c| format!("{}/{}", c.data_folder, "templates"); /// Session JWT key diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs @@ -8,6 +8,7 @@ mod org_policy; mod organization; mod two_factor; mod user; +mod send; pub use self::attachment::Attachment; pub use self::cipher::Cipher; @@ -19,3 +20,4 @@ pub use self::org_policy::{OrgPolicy, OrgPolicyType}; pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; pub use self::two_factor::{TwoFactor, TwoFactorType}; pub use self::user::{Invitation, User, UserStampException}; +pub use self::send::{Send, SendType}; +\ No newline at end of file diff --git a/src/db/models/send.rs b/src/db/models/send.rs @@ -0,0 +1,235 @@ +use chrono::{NaiveDateTime, Utc}; +use serde_json::Value; + +use super::{Organization, User}; + +db_object! { + #[derive(Identifiable, Queryable, Insertable, Associations, AsChangeset)] + #[table_name = "sends"] + #[changeset_options(treat_none_as_null="true")] + #[belongs_to(User, foreign_key = "user_uuid")] + #[belongs_to(Organization, foreign_key = "organization_uuid")] + #[primary_key(uuid)] + pub struct Send { + pub uuid: String, + + pub user_uuid: Option<String>, + pub organization_uuid: Option<String>, + + + pub name: String, + pub notes: Option<String>, + + pub atype: i32, + pub data: String, + pub key: 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, + } +} + +#[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, key: String, deletion_date: NaiveDateTime) -> Self { + let now = Utc::now().naive_utc(); + + Self { + uuid: crate::util::get_uuid(), + user_uuid: None, + organization_uuid: None, + + name, + notes: None, + + atype, + data, + key, + 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, + } + } + + 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_64(); + 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 fn to_json(&self) -> Value { + use crate::util::format_date; + use data_encoding::BASE64URL_NOPAD; + use uuid::Uuid; + + let data: Value = serde_json::from_str(&self.data).unwrap_or_default(); + + 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.key, + "MaxAccessCount": self.max_access_count, + "AccessCount": self.access_count, + "Password": self.password_hash.as_deref().map(|h| BASE64URL_NOPAD.encode(h)), + "Disabled": self.disabled, + + "RevisionDate": format_date(&self.revision_date), + "ExpirationDate": self.expiration_date.as_ref().map(format_date), + "DeletionDate": format_date(&self.deletion_date), + "Object": "send", + }) + } +} + +use crate::db::DbConn; + +use crate::api::EmptyResult; +use crate::error::MapResult; + +impl Send { + pub fn save(&mut self, conn: &DbConn) -> EmptyResult { + // self.update_users_revision(conn); + self.revision_date = Utc::now().naive_utc(); + + db_run! { conn: + sqlite, mysql { + match diesel::replace_into(sends::table) + .values(SendDb::to_db(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(SendDb::to_db(self)) + .execute(conn) + .map_res("Error saving send") + } + Err(e) => Err(e.into()), + }.map_res("Error saving send") + } + postgresql { + let value = SendDb::to_db(self); + diesel::insert_into(sends::table) + .values(&value) + .on_conflict(sends::uuid) + .do_update() + .set(&value) + .execute(conn) + .map_res("Error saving send") + } + } + } + + pub fn delete(&self, conn: &DbConn) -> EmptyResult { + // self.update_users_revision(conn); + + db_run! { conn: { + diesel::delete(sends::table.filter(sends::uuid.eq(&self.uuid))) + .execute(conn) + .map_res("Error deleting send") + }} + } + + pub fn delete_all_by_user(user_uuid: &str, conn: &DbConn) -> EmptyResult { + for send in Self::find_by_user(user_uuid, &conn) { + send.delete(&conn)?; + } + Ok(()) + } + + pub fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> { + use data_encoding::BASE64URL_NOPAD; + use uuid::Uuid; + + let uuid_vec = match BASE64URL_NOPAD.decode(access_id.as_bytes()) { + Ok(v) => v, + Err(_) => return None, + }; + + let uuid = match Uuid::from_slice(&uuid_vec) { + Ok(u) => u.to_string(), + Err(_) => return None, + }; + + Self::find_by_uuid(&uuid, conn) + } + + pub fn find_by_uuid(uuid: &str, conn: &DbConn) -> Option<Self> { + db_run! {conn: { + sends::table + .filter(sends::uuid.eq(uuid)) + .first::<SendDb>(conn) + .ok() + .from_db() + }} + } + + pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec<Self> { + db_run! {conn: { + sends::table + .filter(sends::user_uuid.eq(user_uuid)) + .load::<SendDb>(conn).expect("Error loading sends").from_db() + }} + } + + pub fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { + db_run! {conn: { + sends::table + .filter(sends::organization_uuid.eq(org_uuid)) + .load::<SendDb>(conn).expect("Error loading sends").from_db() + }} + } +} diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -177,7 +177,7 @@ impl User { } } -use super::{Cipher, Device, Favorite, Folder, TwoFactor, UserOrgType, UserOrganization}; +use super::{Cipher, Device, Favorite, Folder, Send, TwoFactor, UserOrgType, UserOrganization}; use crate::db::DbConn; use crate::api::EmptyResult; @@ -263,6 +263,7 @@ impl User { } } + Send::delete_all_by_user(&self.uuid, conn)?; UserOrganization::delete_all_by_user(&self.uuid, conn)?; Cipher::delete_all_by_user(&self.uuid, conn)?; Favorite::delete_all_by_user(&self.uuid, conn)?; diff --git a/src/db/schemas/mysql/schema.rs b/src/db/schemas/mysql/schema.rs @@ -103,6 +103,29 @@ table! { } table! { + sends (uuid) { + uuid -> Text, + user_uuid -> Nullable<Text>, + organization_uuid -> Nullable<Text>, + name -> Text, + notes -> Nullable<Text>, + atype -> Integer, + data -> Text, + key -> Text, + password_hash -> Nullable<Binary>, + password_salt -> Nullable<Binary>, + password_iter -> Nullable<Integer>, + max_access_count -> Nullable<Integer>, + access_count -> Integer, + creation_date -> Datetime, + revision_date -> Datetime, + expiration_date -> Nullable<Datetime>, + deletion_date -> Datetime, + disabled -> Bool, + } +} + +table! { twofactor (uuid) { uuid -> Text, user_uuid -> Text, @@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(org_policies -> organizations (org_uuid)); +joinable!(sends -> organizations (organization_uuid)); +joinable!(sends -> users (user_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!( invitations, org_policies, organizations, + sends, twofactor, users, users_collections, diff --git a/src/db/schemas/postgresql/schema.rs b/src/db/schemas/postgresql/schema.rs @@ -103,6 +103,29 @@ table! { } table! { + sends (uuid) { + uuid -> Text, + user_uuid -> Nullable<Text>, + organization_uuid -> Nullable<Text>, + name -> Text, + notes -> Nullable<Text>, + atype -> Integer, + data -> Text, + key -> Text, + password_hash -> Nullable<Binary>, + password_salt -> Nullable<Binary>, + password_iter -> Nullable<Integer>, + max_access_count -> Nullable<Integer>, + access_count -> Integer, + creation_date -> Timestamp, + revision_date -> Timestamp, + expiration_date -> Nullable<Timestamp>, + deletion_date -> Timestamp, + disabled -> Bool, + } +} + +table! { twofactor (uuid) { uuid -> Text, user_uuid -> Text, @@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(org_policies -> organizations (org_uuid)); +joinable!(sends -> organizations (organization_uuid)); +joinable!(sends -> users (user_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!( invitations, org_policies, organizations, + sends, twofactor, users, users_collections, diff --git a/src/db/schemas/sqlite/schema.rs b/src/db/schemas/sqlite/schema.rs @@ -103,6 +103,29 @@ table! { } table! { + sends (uuid) { + uuid -> Text, + user_uuid -> Nullable<Text>, + organization_uuid -> Nullable<Text>, + name -> Text, + notes -> Nullable<Text>, + atype -> Integer, + data -> Text, + key -> Text, + password_hash -> Nullable<Binary>, + password_salt -> Nullable<Binary>, + password_iter -> Nullable<Integer>, + max_access_count -> Nullable<Integer>, + access_count -> Integer, + creation_date -> Timestamp, + revision_date -> Timestamp, + expiration_date -> Nullable<Timestamp>, + deletion_date -> Timestamp, + disabled -> Bool, + } +} + +table! { twofactor (uuid) { uuid -> Text, user_uuid -> Text, @@ -176,6 +199,8 @@ joinable!(folders -> users (user_uuid)); joinable!(folders_ciphers -> ciphers (cipher_uuid)); joinable!(folders_ciphers -> folders (folder_uuid)); joinable!(org_policies -> organizations (org_uuid)); +joinable!(sends -> organizations (organization_uuid)); +joinable!(sends -> users (user_uuid)); joinable!(twofactor -> users (user_uuid)); joinable!(users_collections -> collections (collection_uuid)); joinable!(users_collections -> users (user_uuid)); @@ -193,6 +218,7 @@ allow_tables_to_appear_in_same_query!( invitations, org_policies, organizations, + sends, twofactor, users, users_collections, diff --git a/src/error.rs b/src/error.rs @@ -221,6 +221,18 @@ macro_rules! err { } #[macro_export] +macro_rules! err_code { + ($msg:expr, $err_code: literal) => {{ + error!("{}", $msg); + return Err(crate::error::Error::new($msg, $msg).with_code($err_code)); + }}; + ($usr_msg:expr, $log_value:expr, $err_code: literal) => {{ + error!("{}. {}", $usr_msg, $log_value); + return Err(crate::error::Error::new($usr_msg, $log_value).with_code($err_code)); + }}; +} + +#[macro_export] macro_rules! err_discard { ($msg:expr, $data:expr) => {{ std::io::copy(&mut $data.open(), &mut std::io::sink()).ok();