From 325039c31695ac981da3b88dbbe6c6f40c6a180d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Daniel=20Garc=C3=ADa?= Date: Mon, 17 Feb 2020 22:56:26 +0100 Subject: [PATCH] Attachment size limits, per-user and per-organization --- src/api/core/ciphers.rs | 50 +++++++++++++++++++++++++++++++------ src/config.rs | 5 ++++ src/db/models/attachment.rs | 24 +++++++++++++++++- src/error.rs | 12 +++++++++ 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 2ffa694b..7793c623 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -642,20 +642,49 @@ fn post_attachment( ) -> JsonResult { let cipher = match Cipher::find_by_uuid(&uuid, &conn) { Some(cipher) => cipher, - None => err!("Cipher doesn't exist"), + None => err_discard!("Cipher doesn't exist", data), }; if !cipher.is_write_accessible_to_user(&headers.user.uuid, &conn) { - err!("Cipher is not write accessible") + err_discard!("Cipher is not write accessible", data) } - + let mut params = content_type.params(); let boundary_pair = params.next().expect("No boundary provided"); let boundary = boundary_pair.1; + let size_limit = if let Some(ref user_uuid) = cipher.user_uuid { + match CONFIG.user_attachment_limit() { + Some(0) => err_discard!("Attachments are disabled", data), + Some(limit) => { + let left = limit - Attachment::size_by_user(user_uuid, &conn); + if left <= 0 { + err_discard!("Attachment size limit reached! Delete some files to open space", data) + } + Some(left as u64) + } + None => None, + } + } else if let Some(ref org_uuid) = cipher.organization_uuid { + match CONFIG.org_attachment_limit() { + Some(0) => err_discard!("Attachments are disabled", data), + Some(limit) => { + let left = limit - Attachment::size_by_org(org_uuid, &conn); + if left <= 0 { + err_discard!("Attachment size limit reached! Delete some files to open space", data) + } + Some(left as u64) + } + None => None, + } + } else { + err_discard!("Cipher is neither owned by a user nor an organization", data); + }; + let base_path = Path::new(&CONFIG.attachments_folder()).join(&cipher.uuid); let mut attachment_key = None; + let mut error = None; Multipart::with_body(data.open(), boundary) .foreach_entry(|mut field| { @@ -674,18 +703,21 @@ fn post_attachment( let file_name = HEXLOWER.encode(&crypto::get_random(vec![0; 10])); let path = base_path.join(&file_name); - let size = match field.data.save().memory_threshold(0).size_limit(None).with_path(path) { + let size = match field.data.save().memory_threshold(0).size_limit(size_limit).with_path(path.clone()) { SaveResult::Full(SavedData::File(_, size)) => size as i32, SaveResult::Full(other) => { - error!("Attachment is not a file: {:?}", other); + std::fs::remove_file(path).ok(); + error = Some(format!("Attachment is not a file: {:?}", other)); return; } SaveResult::Partial(_, reason) => { - error!("Partial result: {:?}", reason); + std::fs::remove_file(path).ok(); + error = Some(format!("Attachment size limit exceeded with this file: {:?}", reason)); return; } SaveResult::Error(e) => { - error!("Error: {:?}", e); + std::fs::remove_file(path).ok(); + error = Some(format!("Error: {:?}", e)); return; } }; @@ -699,6 +731,10 @@ fn post_attachment( }) .expect("Error processing multipart data"); + if let Some(ref e) = error { + err!(e); + } + nt.send_cipher_update(UpdateType::CipherUpdate, &cipher, &cipher.update_users_revision(&conn)); Ok(Json(cipher.to_json(&headers.host, &headers.user.uuid, &conn))) diff --git a/src/config.rs b/src/config.rs index 2551d21e..2c53a342 100644 --- a/src/config.rs +++ b/src/config.rs @@ -246,6 +246,11 @@ make_config! { /// HIBP Api Key |> HaveIBeenPwned API Key, request it here: https://haveibeenpwned.com/API/Key hibp_api_key: Pass, true, option; + /// Per-user attachment limit (KB) |> Limit in kilobytes for a users attachments, once the limit is exceeded it won't be possible to upload more + user_attachment_limit: i64, true, option; + /// Per-organization attachment limit (KB) |> Limit in kilobytes for an organization attachments, once the limit is exceeded it won't be possible to upload more + org_attachment_limit: i64, true, option; + /// Disable icon downloads |> Set to true to disable icon downloading, this would still serve icons from /// $ICON_CACHE_FOLDER, but it won't produce any external network request. Needs to set $ICON_CACHE_TTL to 0, /// otherwise it will delete them and they won't be downloaded again. diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 03064863..e43a6a82 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -49,7 +49,7 @@ impl Attachment { } } -use crate::db::schema::attachments; +use crate::db::schema::{attachments, ciphers}; use crate::db::DbConn; use diesel; use diesel::prelude::*; @@ -118,4 +118,26 @@ impl Attachment { .load::(&**conn) .expect("Error loading attachments") } + + pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 { + let result: Option = attachments::table + .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) + .filter(ciphers::user_uuid.eq(user_uuid)) + .select(diesel::dsl::sum(attachments::file_size)) + .first(&**conn) + .expect("Error loading user attachment total size"); + + result.unwrap_or(0) + } + + pub fn size_by_org(org_uuid: &str, conn: &DbConn) -> i64 { + let result: Option = attachments::table + .left_join(ciphers::table.on(ciphers::uuid.eq(attachments::cipher_uuid))) + .filter(ciphers::organization_uuid.eq(org_uuid)) + .select(diesel::dsl::sum(attachments::file_size)) + .first(&**conn) + .expect("Error loading user attachment total size"); + + result.unwrap_or(0) + } } diff --git a/src/error.rs b/src/error.rs index ab700d70..7cd0bd82 100644 --- a/src/error.rs +++ b/src/error.rs @@ -211,6 +211,18 @@ macro_rules! err { }}; } +#[macro_export] +macro_rules! err_discard { + ($msg:expr, $data:expr) => {{ + std::io::copy(&mut $data.open(), &mut std::io::sink()).ok(); + return Err(crate::error::Error::new($msg, $msg)); + }}; + ($usr_msg:expr, $log_value:expr, $data:expr) => {{ + std::io::copy(&mut $data.open(), &mut std::io::sink()).ok(); + return Err(crate::error::Error::new($usr_msg, $log_value)); + }}; +} + #[macro_export] macro_rules! err_json { ($expr:expr, $log_value:expr) => {{