commit 325039c31695ac981da3b88dbbe6c6f40c6a180d
parent c5b97f4146fd387c6845f52bec6d6ee02cc61413
Author: Daniel GarcĂa <dani-garcia@users.noreply.github.com>
Date: Mon, 17 Feb 2020 22:56:26 +0100
Attachment size limits, per-user and per-organization
Diffstat:
4 files changed, 83 insertions(+), 8 deletions(-)
diff --git 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
@@ -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
@@ -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::<Self>(&**conn)
.expect("Error loading attachments")
}
+
+ pub fn size_by_user(user_uuid: &str, conn: &DbConn) -> i64 {
+ let result: Option<i64> = 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<i64> = 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
@@ -212,6 +212,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) => {{
return Err(($log_value, $expr).into());