From 53b1c36085ce9f49abf22c2ac7a7e62f6e92b24a Mon Sep 17 00:00:00 2001 From: Stefan Melmuk Date: Sat, 21 Dec 2024 16:06:34 +0100 Subject: [PATCH] introduce attachment id --- src/api/core/ciphers.rs | 30 +++++++++---------- src/api/web.rs | 10 +++---- src/auth.rs | 6 ++-- src/crypto.rs | 5 ++-- src/db/models/attachment.rs | 60 +++++++++++++++++++++++++++++++++++-- src/db/models/mod.rs | 2 +- 6 files changed, 84 insertions(+), 29 deletions(-) diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index 758ec3b7..6e73e2dc 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -256,7 +256,7 @@ pub struct CipherData { // 'Attachments' is unused, contains map of {id: filename} #[allow(dead_code)] attachments: Option, - attachments2: Option>, + attachments2: Option>, // The revision datetime (in ISO 8601 format) of the client's local copy // of the cipher. This is used to prevent a client from updating a cipher @@ -1040,7 +1040,7 @@ async fn share_cipher_by_uuid( /// their object storage service. For self-hosted instances, it basically just /// redirects to the same location as before the v2 API. #[get("/ciphers//attachment/")] -async fn get_attachment(uuid: CipherId, attachment_id: &str, headers: Headers, mut conn: DbConn) -> JsonResult { +async fn get_attachment(uuid: CipherId, attachment_id: AttachmentId, headers: Headers, mut conn: DbConn) -> JsonResult { let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else { err!("Cipher doesn't exist") }; @@ -1049,7 +1049,7 @@ async fn get_attachment(uuid: CipherId, attachment_id: &str, headers: Headers, m err!("Cipher is not accessible") } - match Attachment::find_by_id(attachment_id, &mut conn).await { + match Attachment::find_by_id(&attachment_id, &mut conn).await { Some(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), @@ -1265,7 +1265,7 @@ async fn save_attachment( } let folder_path = tokio::fs::canonicalize(&CONFIG.attachments_folder()).await?.join(cipher_uuid.as_ref()); - let file_path = folder_path.join(&file_id); + let file_path = folder_path.join(file_id.as_ref()); tokio::fs::create_dir_all(&folder_path).await?; if let Err(_err) = data.data.persist_to(&file_path).await { @@ -1305,13 +1305,13 @@ async fn save_attachment( #[post("/ciphers//attachment/", format = "multipart/form-data", data = "", rank = 1)] async fn post_attachment_v2_data( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - let attachment = match Attachment::find_by_id(attachment_id, &mut conn).await { + let attachment = match Attachment::find_by_id(&attachment_id, &mut conn).await { Some(attachment) if uuid == attachment.cipher_uuid => Some(attachment), Some(_) => err!("Attachment doesn't belong to cipher"), None => err!("Attachment doesn't exist"), @@ -1354,20 +1354,20 @@ async fn post_attachment_admin( #[post("/ciphers//attachment//share", format = "multipart/form-data", data = "")] async fn post_attachment_share( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, data: Form>, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> JsonResult { - _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await?; + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await?; post_attachment(uuid, data, headers, conn, nt).await } #[post("/ciphers//attachment//delete-admin")] async fn delete_attachment_post_admin( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, conn: DbConn, nt: Notify<'_>, @@ -1378,7 +1378,7 @@ async fn delete_attachment_post_admin( #[post("/ciphers//attachment//delete")] async fn delete_attachment_post( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, conn: DbConn, nt: Notify<'_>, @@ -1389,23 +1389,23 @@ async fn delete_attachment_post( #[delete("/ciphers//attachment/")] async fn delete_attachment( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await } #[delete("/ciphers//attachment//admin")] async fn delete_attachment_admin( uuid: CipherId, - attachment_id: &str, + attachment_id: AttachmentId, headers: Headers, mut conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - _delete_cipher_attachment_by_id(&uuid, attachment_id, &headers, &mut conn, &nt).await + _delete_cipher_attachment_by_id(&uuid, &attachment_id, &headers, &mut conn, &nt).await } #[post("/ciphers//delete")] @@ -1789,7 +1789,7 @@ async fn _restore_multiple_ciphers( async fn _delete_cipher_attachment_by_id( uuid: &CipherId, - attachment_id: &str, + attachment_id: &AttachmentId, headers: &Headers, conn: &mut DbConn, nt: &Notify<'_>, diff --git a/src/api/web.rs b/src/api/web.rs index 1439fb1e..608af26d 100644 --- a/src/api/web.rs +++ b/src/api/web.rs @@ -13,9 +13,9 @@ use serde_json::Value; use crate::{ api::{core::now, ApiResult, EmptyResult}, auth::decode_file_download, - db::models::CipherId, + db::models::{AttachmentId, CipherId}, error::Error, - util::{get_web_vault_version, Cached, SafeString}, + util::{get_web_vault_version, Cached}, CONFIG, }; @@ -197,15 +197,15 @@ async fn web_files(p: PathBuf) -> Cached> { } #[get("/attachments//?")] -async fn attachments(uuid: CipherId, file_id: SafeString, token: String) -> Option { +async fn attachments(uuid: CipherId, file_id: AttachmentId, token: String) -> Option { let Ok(claims) = decode_file_download(&token) else { return None; }; - if claims.sub != uuid || claims.file_id != *file_id { + if claims.sub != uuid || claims.file_id != file_id { return None; } - NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id)).await.ok() + NamedFile::open(Path::new(&CONFIG.attachments_folder()).join(uuid.as_ref()).join(file_id.as_ref())).await.ok() } // We use DbConn here to let the alive healthcheck also verify the database connection. diff --git a/src/auth.rs b/src/auth.rs index 565c3630..a7f403fb 100644 --- a/src/auth.rs +++ b/src/auth.rs @@ -14,7 +14,7 @@ use std::{ net::IpAddr, }; -use crate::db::models::{CipherId, CollectionId, MembershipId, OrganizationId, UserId}; +use crate::db::models::{AttachmentId, CipherId, CollectionId, MembershipId, OrganizationId, UserId}; use crate::{error::Error, CONFIG}; const JWT_ALGORITHM: Algorithm = Algorithm::RS256; @@ -295,10 +295,10 @@ pub struct FileDownloadClaims { // Subject pub sub: CipherId, - pub file_id: String, + pub file_id: AttachmentId, } -pub fn generate_file_download_claims(uuid: CipherId, file_id: String) -> FileDownloadClaims { +pub fn generate_file_download_claims(uuid: CipherId, file_id: AttachmentId) -> FileDownloadClaims { let time_now = Utc::now(); FileDownloadClaims { nbf: time_now.timestamp(), diff --git a/src/crypto.rs b/src/crypto.rs index 99f0fb91..c9db1a4b 100644 --- a/src/crypto.rs +++ b/src/crypto.rs @@ -89,9 +89,10 @@ pub fn generate_send_id() -> String { generate_id::<32>() // 256 bits } -pub fn generate_attachment_id() -> String { +use crate::db::models::AttachmentId; +pub fn generate_attachment_id() -> AttachmentId { // Attachment IDs are scoped to a cipher, so they can be smaller. - generate_id::<10>() // 80 bits + AttachmentId(generate_id::<10>()) // 80 bits } /// Generates a numeric token for email-based verifications. diff --git a/src/db/models/attachment.rs b/src/db/models/attachment.rs index 156cdadb..90d90ad7 100644 --- a/src/db/models/attachment.rs +++ b/src/db/models/attachment.rs @@ -1,7 +1,13 @@ use std::io::ErrorKind; use bigdecimal::{BigDecimal, ToPrimitive}; +use rocket::request::FromParam; use serde_json::Value; +use std::{ + borrow::Borrow, + fmt::{Display, Formatter}, + ops::Deref, +}; use super::{CipherId, OrganizationId, UserId}; use crate::CONFIG; @@ -12,7 +18,7 @@ db_object! { #[diesel(treat_none_as_null = true)] #[diesel(primary_key(id))] pub struct Attachment { - pub id: String, + pub id: AttachmentId, pub cipher_uuid: CipherId, pub file_name: String, // encrypted pub file_size: i64, @@ -23,7 +29,7 @@ db_object! { /// Local methods impl Attachment { pub const fn new( - id: String, + id: AttachmentId, cipher_uuid: CipherId, file_name: String, file_size: i64, @@ -131,7 +137,7 @@ impl Attachment { Ok(()) } - pub async fn find_by_id(id: &str, conn: &mut DbConn) -> Option { + pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option { db_run! { conn: { attachments::table .filter(attachments::id.eq(id.to_lowercase())) @@ -227,3 +233,51 @@ impl Attachment { }} } } + +#[derive(DieselNewType, FromForm, Clone, Debug, Hash, PartialEq, Eq, Serialize, Deserialize)] +pub struct AttachmentId(pub String); + +impl AsRef for AttachmentId { + fn as_ref(&self) -> &str { + &self.0 + } +} + +impl Deref for AttachmentId { + type Target = str; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Borrow for AttachmentId { + fn borrow(&self) -> &str { + &self.0 + } +} + +impl Display for AttachmentId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +impl From for AttachmentId { + fn from(raw: String) -> Self { + Self(raw) + } +} + +impl<'r> FromParam<'r> for AttachmentId { + type Error = (); + + #[inline(always)] + fn from_param(param: &'r str) -> Result { + if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) { + Ok(Self(param.to_string())) + } else { + Err(()) + } + } +} diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs index d832c04d..c59c6f13 100644 --- a/src/db/models/mod.rs +++ b/src/db/models/mod.rs @@ -16,7 +16,7 @@ mod two_factor_duo_context; mod two_factor_incomplete; mod user; -pub use self::attachment::Attachment; +pub use self::attachment::{Attachment, AttachmentId}; pub use self::auth_request::AuthRequest; pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};