introduce attachment id

pull/5320/head
Stefan Melmuk 1 week 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}
#[allow(dead_code)]
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
// 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/<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 {
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/<uuid>/attachment/<attachment_id>", format = "multipart/form-data", data = "<data>", rank = 1)]
async fn post_attachment_v2_data(
uuid: CipherId,
attachment_id: &str,
attachment_id: AttachmentId,
data: Form<UploadData<'_>>,
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/<uuid>/attachment/<attachment_id>/share", format = "multipart/form-data", data = "<data>")]
async fn post_attachment_share(
uuid: CipherId,
attachment_id: &str,
attachment_id: AttachmentId,
data: Form<UploadData<'_>>,
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/<uuid>/attachment/<attachment_id>/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/<uuid>/attachment/<attachment_id>/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/<uuid>/attachment/<attachment_id>")]
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/<uuid>/attachment/<attachment_id>/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/<uuid>/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<'_>,

@ -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<Option<NamedFile>> {
}
#[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 {
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.

@ -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(),

@ -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.

@ -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<Self> {
pub async fn find_by_id(id: &AttachmentId, conn: &mut DbConn) -> Option<Self> {
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<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 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};

Loading…
Cancel
Save