mirror of
https://github.com/dani-garcia/vaultwarden.git
synced 2026-10-11 21:59:59 +00:00
Send API cleanup: remove legacy endpoints and align access with upstream (#7806)
This commit is contained in:
@@ -32,7 +32,7 @@ use crate::{
|
||||
|
||||
use super::{
|
||||
ciphers::{CipherData, update_cipher_from_data},
|
||||
sends::{SendData, update_send_from_data},
|
||||
sends::SendData,
|
||||
};
|
||||
|
||||
pub fn routes() -> Vec<rocket::Route> {
|
||||
@@ -982,11 +982,13 @@ async fn post_rotatekey(data: Json<KeyData>, headers: Headers, conn: DbConn, nt:
|
||||
|
||||
// Update send data
|
||||
for send_data in data.account_data.sends {
|
||||
let Some(send) = existing_sends.iter_mut().find(|s| &s.uuid == send_data.id.as_ref().unwrap()) else {
|
||||
let Some(send) = send_data.id.as_ref().and_then(|id| existing_sends.iter_mut().find(|s| &s.uuid == id)) else {
|
||||
err!("Send doesn't exist")
|
||||
};
|
||||
|
||||
update_send_from_data(send, send_data, &headers, &conn, &nt, UpdateType::None).await?;
|
||||
// Like upstream, only the key changes on rotation
|
||||
send.akey = send_data.key;
|
||||
send.save(&conn).await?;
|
||||
}
|
||||
|
||||
// Update cipher data
|
||||
|
||||
+18
-178
@@ -12,7 +12,7 @@ use serde_json::Value;
|
||||
use crate::{
|
||||
CONFIG,
|
||||
api::{ApiResult, EmptyResult, JsonResult, Notify, UpdateType},
|
||||
auth::{ClientIp, Headers, Host, SendHeaders},
|
||||
auth::{Headers, Host, SendHeaders},
|
||||
config::PathType,
|
||||
db::{
|
||||
DbConn, DbPool,
|
||||
@@ -46,11 +46,8 @@ pub fn routes() -> Vec<rocket::Route> {
|
||||
get_sends,
|
||||
get_send,
|
||||
post_send,
|
||||
post_send_file,
|
||||
post_access,
|
||||
post_access_legacy,
|
||||
post_access_file,
|
||||
post_access_file_legacy,
|
||||
put_send,
|
||||
delete_send,
|
||||
put_remove_password,
|
||||
@@ -73,7 +70,7 @@ pub async fn purge_sends(pool: DbPool) {
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendData {
|
||||
r#type: i32,
|
||||
key: String,
|
||||
pub key: String,
|
||||
password: Option<String>,
|
||||
max_access_count: Option<NumberOrString>,
|
||||
expiration_date: Option<DateTime<Utc>>,
|
||||
@@ -201,7 +198,7 @@ async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Not
|
||||
enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
|
||||
|
||||
if data.r#type == SendType::File as i32 {
|
||||
err!("File sends should use /api/sends/file")
|
||||
err!("File sends should use /api/sends/file/v2")
|
||||
}
|
||||
|
||||
let mut send = create_send(data, headers.user.uuid)?;
|
||||
@@ -218,93 +215,11 @@ async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Not
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct UploadData<'f> {
|
||||
model: Json<SendData>,
|
||||
data: TempFile<'f>,
|
||||
}
|
||||
|
||||
#[derive(FromForm)]
|
||||
struct UploadDataV2<'f> {
|
||||
data: TempFile<'f>,
|
||||
}
|
||||
|
||||
// @deprecated Mar 25 2021: This method has been deprecated in favor of direct uploads (v2).
|
||||
// This method still exists to support older clients, probably need to remove it sometime.
|
||||
// Upstream: https://github.com/bitwarden/server/blob/d0c793c95181dfb1b447eb450f85ba0bfd7ef643/src/Api/Controllers/SendsController.cs#L164-L167
|
||||
// 2025: This endpoint doesn't seem to exists anymore in the latest version
|
||||
// See: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs
|
||||
#[post("/sends/file", format = "multipart/form-data", data = "<data>")]
|
||||
async fn post_send_file(data: Form<UploadData<'_>>, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
enforce_disable_send_policy(&headers, &conn).await?;
|
||||
|
||||
let UploadData {
|
||||
model,
|
||||
data,
|
||||
} = data.into_inner();
|
||||
let model = model.into_inner();
|
||||
|
||||
let Some(size) = data.len().to_i64() else {
|
||||
err!("Invalid send size");
|
||||
};
|
||||
if size < 0 {
|
||||
err!("Send size can't be negative")
|
||||
}
|
||||
|
||||
enforce_disable_hide_email_policy(&model, &headers, &conn).await?;
|
||||
|
||||
let size_limit = match CONFIG.user_send_limit() {
|
||||
Some(0) => err!("File uploads are disabled"),
|
||||
Some(limit_kb) => {
|
||||
let Some(already_used) = Send::size_by_user(&headers.user.uuid, &conn).await else {
|
||||
err!("Existing sends overflow")
|
||||
};
|
||||
let Some(left) = limit_kb.checked_mul(1024).and_then(|l| l.checked_sub(already_used)) else {
|
||||
err!("Send size overflow");
|
||||
};
|
||||
if left <= 0 {
|
||||
err!("Send storage limit reached! Delete some sends to free up space")
|
||||
}
|
||||
i64::clamp(left, 0, SIZE_525_MB)
|
||||
}
|
||||
None => SIZE_525_MB,
|
||||
};
|
||||
|
||||
if size > size_limit {
|
||||
err!("Send storage limit exceeded with this file");
|
||||
}
|
||||
|
||||
let mut send = create_send(model, headers.user.uuid)?;
|
||||
if send.atype != SendType::File as i32 {
|
||||
err!("Send content is not a file");
|
||||
}
|
||||
|
||||
let file_id = crate::crypto::generate_send_file_id();
|
||||
|
||||
save_temp_file(&PathType::Sends, &format!("{}/{file_id}", send.uuid), data, true).await?;
|
||||
|
||||
let mut data_value: Value = serde_json::from_str(&send.data)?;
|
||||
if let Some(o) = data_value.as_object_mut() {
|
||||
o.insert(String::from("id"), Value::String(file_id));
|
||||
o.insert(String::from("size"), Value::Number(size.into()));
|
||||
o.insert(String::from("sizeName"), Value::String(crate::util::get_display_size(size)));
|
||||
}
|
||||
send.data = serde_json::to_string(&data_value)?;
|
||||
|
||||
// Save the changes in the database
|
||||
send.save(&conn).await?;
|
||||
nt.send_send_update(
|
||||
UpdateType::SyncSendCreate,
|
||||
&send,
|
||||
&send.update_users_revision(&conn).await,
|
||||
&headers.device,
|
||||
&conn,
|
||||
)
|
||||
.await;
|
||||
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
|
||||
// Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165
|
||||
#[post("/sends/file/v2", data = "<data>")]
|
||||
async fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn) -> JsonResult {
|
||||
@@ -450,63 +365,16 @@ async fn post_send_file_v2_data(
|
||||
|
||||
#[post("/sends/access")]
|
||||
async fn post_access(headers: SendHeaders, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
let Some(send) = Send::find_by_uuid(&headers.send_id, &conn).await else {
|
||||
let Some(mut send) = Send::find_by_uuid(&headers.send_id, &conn).await else {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
};
|
||||
if !send.is_accessible() {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
process_access(send, conn, nt).await
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct SendAccessData {
|
||||
pub password: Option<String>,
|
||||
}
|
||||
|
||||
// Legacy since web-2026.6.0
|
||||
#[post("/sends/access/<access_id>", data = "<data>")]
|
||||
async fn post_access_legacy(
|
||||
access_id: &str,
|
||||
data: Json<SendAccessData>,
|
||||
conn: DbConn,
|
||||
ip: ClientIp,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
crate::ratelimit::check_limit_unauthenticated(&ip.ip)?;
|
||||
|
||||
let Some(mut send) = Send::find_by_access_id(access_id, &conn).await else {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
};
|
||||
|
||||
if let Some(max_access_count) = send.max_access_count
|
||||
&& send.access_count >= max_access_count
|
||||
{
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404);
|
||||
}
|
||||
|
||||
if !send.is_accessible() {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
|
||||
if send.password_hash.is_some() {
|
||||
match data.into_inner().password {
|
||||
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||
Some(_) => err!("Invalid password", format!("IP: {}.", ip.ip)),
|
||||
None => err_code!("Password not provided", format!("IP: {}.", ip.ip), 401),
|
||||
}
|
||||
}
|
||||
|
||||
// Files are incremented during the download
|
||||
if send.atype == SendType::Text as i32 {
|
||||
if !send.register_access(&conn).await? {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
} else {
|
||||
send.save(&conn).await?;
|
||||
if send.atype == SendType::Text as i32 && !send.register_access(&conn).await? {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
|
||||
process_access(send, conn, nt).await
|
||||
}
|
||||
|
||||
@@ -531,55 +399,27 @@ async fn post_access_file(
|
||||
conn: DbConn,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
let Some(send) = Send::find_by_uuid(&headers.send_id, &conn).await else {
|
||||
let Some(mut send) = Send::find_by_uuid(&headers.send_id, &conn).await else {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
};
|
||||
if !send.is_accessible() {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
check_send_file_id(&send, &file_id)?;
|
||||
if !send.register_access(&conn).await? {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
process_access_file(send, file_id, host, conn, nt).await
|
||||
}
|
||||
|
||||
// Legacy since web-2026.6.0
|
||||
#[post("/sends/<send_id>/access/file/<file_id>", data = "<data>")]
|
||||
async fn post_access_file_legacy(
|
||||
send_id: SendId,
|
||||
file_id: SendFileId,
|
||||
data: Json<SendAccessData>,
|
||||
host: Host,
|
||||
conn: DbConn,
|
||||
ip: ClientIp,
|
||||
nt: Notify<'_>,
|
||||
) -> JsonResult {
|
||||
crate::ratelimit::check_limit_unauthenticated(&ip.ip)?;
|
||||
|
||||
let Some(mut send) = Send::find_by_uuid(&send_id, &conn).await else {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
};
|
||||
|
||||
if let Some(max_access_count) = send.max_access_count
|
||||
&& send.access_count >= max_access_count
|
||||
{
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
fn check_send_file_id(send: &Send, file_id: &SendFileId) -> EmptyResult {
|
||||
if send.atype != SendType::File as i32 {
|
||||
err!("Send is not a file type send.");
|
||||
}
|
||||
|
||||
if !send.is_accessible() {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
match serde_json::from_str::<SendFileData>(&send.data) {
|
||||
Ok(data) if &data.id == file_id => Ok(()),
|
||||
_ => err_code!(SEND_INACCESSIBLE_MSG, 404),
|
||||
}
|
||||
|
||||
if send.password_hash.is_some() {
|
||||
match data.into_inner().password {
|
||||
Some(ref p) if send.check_password(p) => { /* Nothing to do here */ }
|
||||
Some(_) => err!("Invalid password."),
|
||||
None => err_code!("Password not provided", 401),
|
||||
}
|
||||
}
|
||||
|
||||
if !send.register_access(&conn).await? {
|
||||
err_code!(SEND_INACCESSIBLE_MSG, 404)
|
||||
}
|
||||
|
||||
process_access_file(send, file_id, host, conn, nt).await
|
||||
}
|
||||
|
||||
async fn process_access_file(send: Send, file_id: SendFileId, host: Host, conn: DbConn, nt: Notify<'_>) -> JsonResult {
|
||||
@@ -642,7 +482,7 @@ async fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn:
|
||||
Ok(Json(send.to_json()))
|
||||
}
|
||||
|
||||
pub async fn update_send_from_data(
|
||||
async fn update_send_from_data(
|
||||
send: &mut Send,
|
||||
data: SendData,
|
||||
headers: &Headers,
|
||||
|
||||
+1
-5
@@ -75,7 +75,7 @@ impl SendTokens {
|
||||
return Self::invalid_error(&format!("Can't convert {access_id}"), "send_id_invalid", false);
|
||||
};
|
||||
|
||||
let Some(mut send) = Send::find_by_uuid(&send_id, conn).await else {
|
||||
let Some(send) = Send::find_by_uuid(&send_id, conn).await else {
|
||||
return Self::invalid_error(&format!("Can't find {send_id}"), "send_id_invalid", false);
|
||||
};
|
||||
|
||||
@@ -103,10 +103,6 @@ impl SendTokens {
|
||||
}
|
||||
}
|
||||
|
||||
if !send.register_access(conn).await? {
|
||||
return Self::invalid_error(&format!("Send {send_id}, max access reached"), "send_id_invalid", true);
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
access_claims: generate_send_access_claims(&send_id),
|
||||
})
|
||||
|
||||
@@ -711,6 +711,7 @@ impl Cipher {
|
||||
// Only allow group access via a confirmed membership, since
|
||||
// groups_users rows are kept when a membership is revoked.
|
||||
.filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
|
||||
.filter(ciphers::organization_uuid.eq(users_organizations::org_uuid.nullable()))
|
||||
.select((collections_groups::read_only, collections_groups::hide_passwords, collections_groups::manage))
|
||||
.load::<(bool, bool, bool)>(conn)
|
||||
.expect("Error getting group access restrictions")
|
||||
|
||||
+7
-23
@@ -140,6 +140,10 @@ impl Send {
|
||||
None
|
||||
}
|
||||
|
||||
fn access_id(&self) -> String {
|
||||
BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes())
|
||||
}
|
||||
|
||||
pub fn to_json(&self) -> Value {
|
||||
let mut data = serde_json::from_str::<LowerCase<Value>>(&self.data).map(|d| d.data).unwrap_or_default();
|
||||
|
||||
@@ -150,7 +154,7 @@ impl Send {
|
||||
|
||||
json!({
|
||||
"id": self.uuid,
|
||||
"accessId": BASE64URL_NOPAD.encode(Uuid::parse_str(&self.uuid).unwrap_or_default().as_bytes()),
|
||||
"accessId": self.access_id(),
|
||||
"type": self.atype,
|
||||
|
||||
"name": self.name,
|
||||
@@ -182,7 +186,7 @@ impl Send {
|
||||
}
|
||||
|
||||
json!({
|
||||
"id": self.uuid,
|
||||
"id": self.access_id(),
|
||||
"type": self.atype,
|
||||
|
||||
"name": self.name,
|
||||
@@ -256,7 +260,7 @@ impl Send {
|
||||
|
||||
/// Whether the Send is currently within its validity window: not disabled, not past its
|
||||
/// expiration date, and not past its deletion date. Does not consider `max_access_count`
|
||||
/// (consumed at token issuance) or the password.
|
||||
/// (counted on each access) or the password.
|
||||
pub fn is_accessible(&self) -> bool {
|
||||
let now = Utc::now().naive_utc();
|
||||
if self.disabled {
|
||||
@@ -309,19 +313,6 @@ impl Send {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn find_by_access_id(access_id: &str, conn: &DbConn) -> Option<Self> {
|
||||
let Ok(uuid_vec) = BASE64URL_NOPAD.decode(access_id.as_bytes()) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let uuid = match Uuid::from_slice(&uuid_vec) {
|
||||
Ok(u) => SendId::from(u.to_string()),
|
||||
Err(_) => return None,
|
||||
};
|
||||
|
||||
Self::find_by_uuid(&uuid, conn).await
|
||||
}
|
||||
|
||||
pub async fn find_by_uuid(uuid: &SendId, conn: &DbConn) -> Option<Self> {
|
||||
conn.run(move |conn| sends::table.filter(sends::uuid.eq(uuid)).first::<Self>(conn).ok()).await
|
||||
}
|
||||
@@ -361,13 +352,6 @@ impl Send {
|
||||
Some(total)
|
||||
}
|
||||
|
||||
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
|
||||
conn.run(move |conn| {
|
||||
sends::table.filter(sends::organization_uuid.eq(org_uuid)).load::<Self>(conn).expect("Error loading sends")
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn find_by_past_deletion_date(conn: &DbConn) -> Vec<Self> {
|
||||
let now = Utc::now().naive_utc();
|
||||
conn.run(move |conn| {
|
||||
|
||||
Reference in New Issue
Block a user