Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2025-01-08 11:55:42 +01:00
introduce attachment id
Dieser Commit ist enthalten in:
Ursprung
5dc05d6ba4
Commit
53b1c36085
6 geänderte Dateien mit 84 neuen und 29 gelöschten Zeilen
|
@ -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};
|
||||||
|
|
Laden …
In neuem Issue referenzieren