introduce attachment id

Stefan Melmuk 2 weeks ago
parent 5dc05d6ba4
commit 53b1c36085
No known key found for this signature in database
GPG Key ID: 817020C608FE9C09

@ -256,7 +256,7 @@ pub struct CipherData {
// 'Attachments' is unused, contains map of {id: filename} // 'Attachments' is unused, contains map of {id: filename}
#[allow(dead_code)] #[allow(dead_code)]
attachments: Option<Value>, attachments: Option<Value>,
attachments2: Option<HashMap<CipherId, Attachments2Data>>, attachments2: Option<HashMap<AttachmentId, Attachments2Data>>,
// The revision datetime (in ISO 8601 format) of the client's local copy // 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 // 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 /// their object storage service. For self-hosted instances, it basically just
/// redirects to the same location as before the v2 API. /// redirects to the same location as before the v2 API.
#[get("/ciphers/<uuid>/attachment/<attachment_id>")] #[get("/ciphers/<uuid>/attachment/<attachment_id>")]
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 { let Some(cipher) = Cipher::find_by_uuid(&uuid, &mut conn).await else {
err!("Cipher doesn't exist") 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") 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(attachment) if uuid == attachment.cipher_uuid => Ok(Json(attachment.to_json(&headers.host))),
Some(_) => err!("Attachment doesn't belong to cipher"), Some(_) => err!("Attachment doesn't belong to cipher"),
None => err!("Attachment doesn't exist"), 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 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?; tokio::fs::create_dir_all(&folder_path).await?;
if let Err(_err) = data.data.persist_to(&file_path).await { if let Err(_err) = data.data.persist_to(&file_path).await {
@ -1305,13 +1305,13 @@ async fn save_attachment(
#[post("/ciphers/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)] #[post("/ciphers/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)]
async fn post_attachment_v2_data( async fn post_attachment_v2_data(
uuid: CipherId, uuid: CipherId,
attachment_id: &str, attachment_id: AttachmentId,
data: Form<UploadData<'_>>, data: Form<UploadData<'_>>,
headers: Headers, headers: Headers,
mut conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> EmptyResult { ) -> 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(attachment) if uuid == attachment.cipher_uuid => Some(attachment),
Some(_) => err!("Attachment doesn't belong to cipher"), Some(_) => err!("Attachment doesn't belong to cipher"),
None => err!("Attachment doesn't exist"), None => err!("Attachment doesn't exist"),
@ -1354,20 +1354,20 @@ async fn post_attachment_admin(
#[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")] #[post("/ciphers/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
async fn post_attachment_share( async fn post_attachment_share(
uuid: CipherId, uuid: CipherId,
attachment_id: &str, attachment_id: AttachmentId,
data: Form<UploadData<'_>>, data: Form<UploadData<'_>>,
headers: Headers, headers: Headers,
mut conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> JsonResult { ) -> 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_attachment(uuid, data, headers, conn, nt).await
} }
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")] #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete-admin")]
async fn delete_attachment_post_admin( async fn delete_attachment_post_admin(
uuid: CipherId, uuid: CipherId,
attachment_id: &str, attachment_id: AttachmentId,
headers: Headers, headers: Headers,
conn: DbConn, conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
@ -1378,7 +1378,7 @@ async fn delete_attachment_post_admin(
#[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")] #[post("/ciphers/<uuid>/attachment/<attachment_id>/delete")]
async fn delete_attachment_post( async fn delete_attachment_post(
uuid: CipherId, uuid: CipherId,
attachment_id: &str, attachment_id: AttachmentId,
headers: Headers, headers: Headers,
conn: DbConn, conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
@ -1389,23 +1389,23 @@ async fn delete_attachment_post(
#[delete("/ciphers/<uuid>/attachment/<attachment_id>")] #[delete("/ciphers/<uuid>/attachment/<attachment_id>")]
async fn delete_attachment( async fn delete_attachment(
uuid: CipherId, uuid: CipherId,
attachment_id: &str, attachment_id: AttachmentId,
headers: Headers, headers: Headers,
mut conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> EmptyResult { ) -> 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/<uuid>/attachment/<attachment_id>/admin")] #[delete("/ciphers/<uuid>/attachment/<attachment_id>/admin")]
async fn delete_attachment_admin( async fn delete_attachment_admin(
uuid: CipherId, uuid: CipherId,
attachment_id: &str, attachment_id: AttachmentId,
headers: Headers, headers: Headers,
mut conn: DbConn, mut conn: DbConn,
nt: Notify<'_>, nt: Notify<'_>,
) -> EmptyResult { ) -> 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/<uuid>/delete")] #[post("/ciphers/<uuid>/delete")]
@ -1789,7 +1789,7 @@ async fn _restore_multiple_ciphers(
async fn _delete_cipher_attachment_by_id( async fn _delete_cipher_attachment_by_id(
uuid: &CipherId, uuid: &CipherId,
attachment_id: &str, attachment_id: &AttachmentId,
headers: &Headers, headers: &Headers,
conn: &mut DbConn, conn: &mut DbConn,
nt: &Notify<'_>, nt: &Notify<'_>,

@ -13,9 +13,9 @@ use serde_json::Value;
use crate::{ use crate::{
api::{core::now, ApiResult, EmptyResult}, api::{core::now, ApiResult, EmptyResult},
auth::decode_file_download, auth::decode_file_download,
db::models::CipherId, db::models::{AttachmentId, CipherId},
error::Error, error::Error,
util::{get_web_vault_version, Cached, SafeString}, util::{get_web_vault_version, Cached},
CONFIG, CONFIG,
}; };
@ -197,15 +197,15 @@ async fn web_files(p: PathBuf) -> Cached<Option<NamedFile>> {
} }
#[get("/attachments/<uuid>/<file_id>?<token>")] #[get("/attachments/<uuid>/<file_id>?<token>")]
async fn attachments(uuid: CipherId, file_id: SafeString, token: String) -> Option<NamedFile> { async fn attachments(uuid: CipherId, file_id: AttachmentId, token: String) -> Option<NamedFile> {
let Ok(claims) = decode_file_download(&token) else { let Ok(claims) = decode_file_download(&token) else {
return None; return None;
}; };
if claims.sub != uuid || claims.file_id != *file_id { if claims.sub != uuid || claims.file_id != file_id {
return None; 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. // We use DbConn here to let the alive healthcheck also verify the database connection.

@ -14,7 +14,7 @@ use std::{
net::IpAddr, 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}; use crate::{error::Error, CONFIG};
const JWT_ALGORITHM: Algorithm = Algorithm::RS256; const JWT_ALGORITHM: Algorithm = Algorithm::RS256;
@ -295,10 +295,10 @@ pub struct FileDownloadClaims {
// Subject // Subject
pub sub: CipherId, 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(); let time_now = Utc::now();
FileDownloadClaims { FileDownloadClaims {
nbf: time_now.timestamp(), nbf: time_now.timestamp(),

@ -89,9 +89,10 @@ pub fn generate_send_id() -> String {
generate_id::<32>() // 256 bits 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. // 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. /// Generates a numeric token for email-based verifications.

@ -1,7 +1,13 @@
use std::io::ErrorKind; use std::io::ErrorKind;
use bigdecimal::{BigDecimal, ToPrimitive}; use bigdecimal::{BigDecimal, ToPrimitive};
use rocket::request::FromParam;
use serde_json::Value; use serde_json::Value;
use std::{
borrow::Borrow,
fmt::{Display, Formatter},
ops::Deref,
};
use super::{CipherId, OrganizationId, UserId}; use super::{CipherId, OrganizationId, UserId};
use crate::CONFIG; use crate::CONFIG;
@ -12,7 +18,7 @@ db_object! {
#[diesel(treat_none_as_null = true)] #[diesel(treat_none_as_null = true)]
#[diesel(primary_key(id))] #[diesel(primary_key(id))]
pub struct Attachment { pub struct Attachment {
pub id: String, pub id: AttachmentId,
pub cipher_uuid: CipherId, pub cipher_uuid: CipherId,
pub file_name: String, // encrypted pub file_name: String, // encrypted
pub file_size: i64, pub file_size: i64,
@ -23,7 +29,7 @@ db_object! {
/// Local methods /// Local methods
impl Attachment { impl Attachment {
pub const fn new( pub const fn new(
id: String, id: AttachmentId,
cipher_uuid: CipherId, cipher_uuid: CipherId,
file_name: String, file_name: String,
file_size: i64, file_size: i64,
@ -131,7 +137,7 @@ impl Attachment {
Ok(()) Ok(())
} }
pub async fn find_by_id(id: &str, conn: &mut DbConn) -> Option<Self> { pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option<Self> {
db_run! { conn: { db_run! { conn: {
attachments::table attachments::table
.filter(attachments::id.eq(id.to_lowercase())) .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<str> 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<str> 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<String> 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<Self, Self::Error> {
if param.chars().all(|c| matches!(c, 'a'..='z' | 'A'..='Z' |'0'..='9' | '-')) {
Ok(Self(param.to_string()))
} else {
Err(())
}
}
}

@ -16,7 +16,7 @@ mod two_factor_duo_context;
mod two_factor_incomplete; mod two_factor_incomplete;
mod user; mod user;
pub use self::attachment::Attachment; pub use self::attachment::{Attachment, AttachmentId};
pub use self::auth_request::AuthRequest; pub use self::auth_request::AuthRequest;
pub use self::cipher::{Cipher, CipherId, RepromptType}; pub use self::cipher::{Cipher, CipherId, RepromptType};
pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser}; pub use self::collection::{Collection, CollectionCipher, CollectionId, CollectionUser};

Loading…
Cancel
Save