@ -2,30 +2,30 @@ use std::collections::{HashMap, HashSet};
use chrono ::{ NaiveDateTime , Utc } ;
use num_traits ::ToPrimitive ;
use rocket ::fs ::TempFile ;
use rocket ::serde ::json ::Json ;
use rocket ::{
form ::{ Form , FromForm } ,
Route ,
form ::{ Form , FromForm } ,
fs ::TempFile ,
serde ::json ::Json ,
} ;
use serde_json ::Value ;
use crate ::auth ::ClientVersion ;
use crate ::util ::{ save_temp_file , NumberOrString } ;
use crate ::{
api ::{ self , core ::log_event , EmptyResult , JsonResult , Notify , PasswordOrOtpData , UpdateType } ,
auth ::Headers ,
CONFIG ,
api ::{ self , EmptyResult , JsonResult , Notify , PasswordOrOtpData , UpdateType , core ::log_event } ,
auth ::ClientVersion ,
auth ::{ Headers , OrgIdGuard , OwnerHeaders } ,
config ::PathType ,
crypto ,
db ::{
DbConn , DbPool ,
models ::{
Attachment , AttachmentId , Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId ,
CollectionUser , EventType , Favorite , Folder , FolderCipher , FolderId , Group , Membership , MembershipType ,
OrgPolicy , OrgPolicyType , OrganizationId , RepromptType , Send , UserId ,
Archive , A ttachment , AttachmentId , Cipher , CipherId , Collection , CollectionCipher , CollectionGroup ,
CollectionId , Collection User , EventType , Favorite , Folder , FolderCipher , FolderId , Group , Membership ,
MembershipType , OrgPolicy , OrgPolicyType , OrganizationId , RepromptType , Send , UserId ,
} ,
DbConn , DbPool ,
} ,
CONFIG ,
util ::{ NumberOrString , deser_opt_nonempty_str , save_temp_file } ,
} ;
use super ::folders ::FolderData ;
@ -86,7 +86,8 @@ pub fn routes() -> Vec<Route> {
restore_cipher_put_admin ,
restore_cipher_selected ,
restore_cipher_selected_admin ,
delete_all ,
purge_org_vault ,
purge_personal_vault ,
move_cipher_selected ,
move_cipher_selected_put ,
put_collections2_update ,
@ -95,6 +96,10 @@ pub fn routes() -> Vec<Route> {
post_collections_update ,
post_collections_admin ,
put_collections_admin ,
archive_cipher_put ,
archive_cipher_selected ,
unarchive_cipher_put ,
unarchive_cipher_selected ,
]
}
@ -103,7 +108,7 @@ pub async fn purge_trashed_ciphers(pool: DbPool) {
if let Ok ( conn ) = pool . get ( ) . await {
Cipher ::purge_trash ( & conn ) . await ;
} else {
error ! ( "Failed to get DB connection while purging trashed ciphers" )
error ! ( "Failed to get DB connection while purging trashed ciphers" ) ;
}
}
@ -159,7 +164,7 @@ async fn sync(data: SyncData, headers: Headers, client_version: Option<ClientVer
let domains_json = if data . exclude_domains {
Value ::Null
} else {
api ::core ::_ get_eq_domains( & headers , true ) . into_inner ( )
api ::core ::get_eq_domains ( & headers , true ) . into_inner ( )
} ;
// This is very similar to the the userDecryptionOptions sent in connect/token,
@ -247,6 +252,7 @@ pub struct CipherData {
// Id is optional as it is included only in bulk share
pub id : Option < CipherId > ,
// Folder id is not included in import
#[ serde(default, deserialize_with = " deser_opt_nonempty_str " ) ]
pub folder_id : Option < FolderId > ,
// TODO: Some of these might appear all the time, no need for Option
#[ serde(alias = " organizationID " ) ]
@ -291,11 +297,13 @@ pub struct CipherData {
// when using older client versions, or if the operation doesn't involve
// updating an existing cipher.
last_known_revision_date : Option < String > ,
archived_date : Option < String > ,
}
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
pub struct PartialCipherData {
#[ serde(default, deserialize_with = " deser_opt_nonempty_str " ) ]
folder_id : Option < FolderId > ,
favorite : bool ,
}
@ -393,20 +401,34 @@ pub async fn update_cipher_from_data(
nt : & Notify < '_ > ,
ut : UpdateType ,
) -> EmptyResult {
// Cleanup cipher data, like removing the 'Response' key.
// This key is somewhere generated during Javascript so no way for us this fix this.
// Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.
// We do not mind which data is in it, the keep our model more flexible when there are upstream changes.
// But, we at least know we do not need to store and return this specific key.
fn clean_cipher_data ( mut json_data : Value ) -> Value {
if json_data . is_array ( ) {
json_data . as_array_mut ( ) . unwrap ( ) . iter_mut ( ) . for_each ( | ref mut f | {
f . as_object_mut ( ) . unwrap ( ) . remove ( "response" ) ;
} ) ;
}
json_data
}
enforce_personal_ownership_policy ( Some ( & data ) , headers , conn ) . await ? ;
// Check that the client isn't updating an existing cipher with stale data.
// And only perform this check when not importing ciphers, else the date/time check will fail.
if ut ! = UpdateType ::None {
if let Some ( dt ) = data . last_known_revision_date {
match NaiveDateTime ::parse_from_str ( & dt , "%+" ) {
// ISO 8601 format
Err ( err ) = > warn ! ( "Error parsing LastKnownRevisionDate '{dt}': {err}" ) ,
Ok ( dt ) if cipher . updated_at . signed_duration_since ( dt ) . num_seconds ( ) > 1 = > {
err ! ( "The client copy of this cipher is out of date. Resync the client and try again." )
}
Ok ( _ ) = > ( ) ,
if ut ! = UpdateType ::None
& & let Some ( dt ) = data . last_known_revision_date
{
match NaiveDateTime ::parse_from_str ( & dt , "%+" ) {
// ISO 8601 format
Err ( err ) = > warn ! ( "Error parsing LastKnownRevisionDate '{dt}': {err}" ) ,
Ok ( dt ) if cipher . updated_at . signed_duration_since ( dt ) . num_seconds ( ) > 1 = > {
err ! ( "The client copy of this cipher is out of date. Resync the client and try again." )
}
Ok ( _ ) = > ( ) ,
}
}
@ -425,7 +447,7 @@ pub async fn update_cipher_from_data(
let transfer_cipher = cipher . organization_uuid . is_none ( ) & & data . organization_id . is_some ( ) ;
if let Some ( org_id ) = data . organization_id {
match Membership ::find_by_user_and_org ( & headers . user . uuid , & org_id , conn ) . await {
match Membership ::find_confirmed_ by_user_and_org ( & headers . user . uuid , & org_id , conn ) . await {
None = > err ! ( "You don't have permission to add item to organization" ) ,
Some ( member ) = > {
if shared_to_collections . is_some ( )
@ -448,25 +470,22 @@ pub async fn update_cipher_from_data(
cipher . user_uuid = Some ( headers . user . uuid . clone ( ) ) ;
}
if let Some ( ref folder_id ) = data . folder_id {
if Folder ::find_by_uuid_and_user ( folder_id , & headers . user . uuid , conn ) . await . is_none ( ) {
err ! ( "Invalid folder" , "Folder does not exist or belongs to another user" ) ;
}
if let Some ( ref folder_id ) = data . folder_id
& & Folder ::find_by_uuid_and_user ( folder_id , & headers . user . uuid , conn ) . await . is_none ( )
{
err ! ( "Invalid folder" , "Folder does not exist or belongs to another user" ) ;
}
// Modify attachments name and keys when rotating
if let Some ( attachments ) = data . attachments2 {
for ( id , attachment ) in attachments {
let mut saved_att = match Attachment ::find_by_id ( & id , conn ) . await {
Some ( att ) = > att ,
None = > {
// Warn and continue here.
// A missing attachment means it was removed via an other client.
// Also the Desktop Client supports removing attachments and save an update afterwards.
// Bitwarden it self ignores these mismatches server side.
warn ! ( "Attachment {id} doesn't exist" ) ;
continue ;
}
let Some ( mut saved_att ) = Attachment ::find_by_id ( & id , conn ) . await else {
// Warn and continue here.
// A missing attachment means it was removed via an other client.
// Also the Desktop Client supports removing attachments and save an update afterwards.
// Bitwarden it self ignores these mismatches server side.
warn ! ( "Attachment {id} doesn't exist" ) ;
continue ;
} ;
if saved_att . cipher_uuid ! = cipher . uuid {
@ -483,20 +502,6 @@ pub async fn update_cipher_from_data(
}
}
// Cleanup cipher data, like removing the 'Response' key.
// This key is somewhere generated during Javascript so no way for us this fix this.
// Also, upstream only retrieves keys they actually want to store, and thus skip the 'Response' key.
// We do not mind which data is in it, the keep our model more flexible when there are upstream changes.
// But, we at least know we do not need to store and return this specific key.
fn _clean_cipher_data ( mut json_data : Value ) -> Value {
if json_data . is_array ( ) {
json_data . as_array_mut ( ) . unwrap ( ) . iter_mut ( ) . for_each ( | ref mut f | {
f . as_object_mut ( ) . unwrap ( ) . remove ( "response" ) ;
} ) ;
} ;
json_data
}
let type_data_opt = match data . r#type {
1 = > data . login ,
2 = > data . secure_note ,
@ -506,23 +511,22 @@ pub async fn update_cipher_from_data(
_ = > err ! ( "Invalid type" ) ,
} ;
let type_data = match type_data_opt {
Some ( mut data ) = > {
// Remove the 'Response' key from the base object.
data . as_object_mut ( ) . unwrap ( ) . remove ( "response" ) ;
// Remove the 'Response' key from every Uri.
if data [ "uris" ] . is_array ( ) {
data [ "uris" ] = _clean_cipher_data ( data [ "uris" ] . clone ( ) ) ;
}
data
let type_data = if let Some ( mut data ) = type_data_opt {
// Remove the 'Response' key from the base object.
data . as_object_mut ( ) . unwrap ( ) . remove ( "response" ) ;
// Remove the 'Response' key from every Uri.
if data [ "uris" ] . is_array ( ) {
data [ "uris" ] = clean_cipher_data ( data [ "uris" ] . clone ( ) ) ;
}
None = > err ! ( "Data missing" ) ,
data
} else {
err ! ( "Data missing" )
} ;
cipher . key = data . key ;
cipher . name = data . name ;
cipher . notes = data . notes ;
cipher . fields = data . fields . map ( | f | _ clean_cipher_data( f ) . to_string ( ) ) ;
cipher . fields = data . fields . map ( | f | clean_cipher_data ( f ) . to_string ( ) ) ;
cipher . data = type_data . to_string ( ) ;
cipher . password_history = data . password_history . map ( | f | f . to_string ( ) ) ;
cipher . reprompt = data . reprompt . filter ( | r | * r = = RepromptType ::None as i32 | | * r = = RepromptType ::Password as i32 ) ;
@ -531,6 +535,13 @@ pub async fn update_cipher_from_data(
cipher . move_to_folder ( data . folder_id , & headers . user . uuid , conn ) . await ? ;
cipher . set_favorite ( data . favorite , & headers . user . uuid , conn ) . await ? ;
if let Some ( dt_str ) = data . archived_date {
match NaiveDateTime ::parse_from_str ( & dt_str , "%+" ) {
Ok ( dt ) = > cipher . set_archived_at ( dt , & headers . user . uuid , conn ) . await ? ,
Err ( err ) = > warn ! ( "Error parsing ArchivedDate '{dt_str}': {err}" ) ,
}
}
if ut ! = UpdateType ::None {
// Only log events for organizational ciphers
if let Some ( org_id ) = & cipher . organization_uuid {
@ -597,7 +608,7 @@ async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbC
let existing_folders : HashSet < Option < FolderId > > =
Folder ::find_by_user ( & headers . user . uuid , & conn ) . await . into_iter ( ) . map ( | f | Some ( f . uuid ) ) . collect ( ) ;
let mut folders : Vec < FolderId > = Vec ::with_capacity ( data . folders . len ( ) ) ;
for folder in data . folders . into_iter ( ) {
for folder in data . folders {
let folder_id = if existing_folders . contains ( & folder . id ) {
folder . id . unwrap ( )
} else {
@ -627,7 +638,7 @@ async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbC
let mut user = headers . user ;
user . update_revision ( & conn ) . await ? ;
nt . send_user_update ( UpdateType ::SyncVault , & user , & headers . device . push_uuid , & conn ) . await ;
nt . send_user_update ( UpdateType ::SyncVault , & user , headers . device . push_uuid . as_ref ( ) , & conn ) . await ;
Ok ( ( ) )
}
@ -722,10 +733,10 @@ async fn put_cipher_partial(
err ! ( "Cipher does not exist" , "Cipher is not accessible for the current user" )
}
if let Some ( ref folder_id ) = data . folder_id {
if Folder ::find_by_uuid_and_user ( folder_id , & headers . user . uuid , & conn ) . await . is_none ( ) {
err ! ( "Invalid folder" , "Folder does not exist or belongs to another user" ) ;
}
if let Some ( ref folder_id ) = data . folder_id
& & Folder ::find_by_uuid_and_user ( folder_id , & headers . user . uuid , & conn ) . await . is_none ( )
{
err ! ( "Invalid folder" , "Folder does not exist or belongs to another user" ) ;
}
// Move cipher
@ -799,12 +810,16 @@ async fn post_collections_update(
err ! ( "Collection cannot be changed" )
}
let Some ( ref org_uuid ) = cipher . organization_uuid else {
err ! ( "Cipher is not owned by an organization" )
} ;
let posted_collections = HashSet ::< CollectionId > ::from_iter ( data . collection_ids ) ;
let current_collections =
HashSet ::< CollectionId > ::from_iter ( cipher . get_collections ( headers . user . uuid . clone ( ) , & conn ) . await ) ;
for collection in posted_collections . symmetric_difference ( & current_collections ) {
match Collection ::find_by_uuid_and_org ( collection , cipher . organization _uuid . as_ref ( ) . unwrap ( ) , & conn ) . await {
match Collection ::find_by_uuid_and_org ( collection , org_uuid , & conn ) . await {
None = > err ! ( "Invalid collection ID provided" ) ,
Some ( collection ) = > {
if collection . is_writable_by_user ( & headers . user . uuid , & conn ) . await {
@ -835,7 +850,7 @@ async fn post_collections_update(
log_event (
EventType ::CipherUpdatedCollections as i32 ,
& cipher . uuid ,
& cipher . organization _uuid . clone ( ) . unwrap ( ) ,
org_uuid ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
@ -875,12 +890,16 @@ async fn post_collections_admin(
err ! ( "Collection cannot be changed" )
}
let Some ( ref org_uuid ) = cipher . organization_uuid else {
err ! ( "Cipher is not owned by an organization" )
} ;
let posted_collections = HashSet ::< CollectionId > ::from_iter ( data . collection_ids ) ;
let current_collections =
HashSet ::< CollectionId > ::from_iter ( cipher . get_admin_collections ( headers . user . uuid . clone ( ) , & conn ) . await ) ;
for collection in posted_collections . symmetric_difference ( & current_collections ) {
match Collection ::find_by_uuid_and_org ( collection , cipher . organization _uuid . as_ref ( ) . unwrap ( ) , & conn ) . await {
match Collection ::find_by_uuid_and_org ( collection , org_uuid , & conn ) . await {
None = > err ! ( "Invalid collection ID provided" ) ,
Some ( collection ) = > {
if collection . is_writable_by_user ( & headers . user . uuid , & conn ) . await {
@ -911,7 +930,7 @@ async fn post_collections_admin(
log_event (
EventType ::CipherUpdatedCollections as i32 ,
& cipher . uuid ,
& cipher . organization _uuid . unwrap ( ) ,
org_uuid ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
@ -981,7 +1000,7 @@ async fn put_cipher_share_selected(
err ! ( "You must select at least one collection." )
}
for cipher in data . ciphers . iter ( ) {
for cipher in & data . ciphers {
if cipher . id . is_none ( ) {
err ! ( "Request missing ids field" )
}
@ -993,16 +1012,15 @@ async fn put_cipher_share_selected(
collection_ids : data . collection_ids . clone ( ) ,
} ;
match shared_cipher_data . cipher . id . take ( ) {
Some ( id ) = > {
share_cipher_by_uuid ( & id , shared_cipher_data , & headers , & conn , & nt , Some ( UpdateType ::None ) ) . await ?
}
None = > err ! ( "Request missing ids field" ) ,
if let Some ( id ) = shared_cipher_data . cipher . id . take ( ) {
share_cipher_by_uuid ( & id , shared_cipher_data , & headers , & conn , & nt , Some ( UpdateType ::None ) ) . await ?
} else {
err ! ( "Request missing ids field" )
} ;
}
// Multi share actions do not send out a push for each cipher, we need to send a general sync here
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , & headers . device . push_uuid , & conn ) . await ;
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , headers . device . push_uuid . as_ref ( ) , & conn ) . await ;
Ok ( ( ) )
}
@ -1015,15 +1033,14 @@ async fn share_cipher_by_uuid(
nt : & Notify < '_ > ,
override_ut : Option < UpdateType > ,
) -> JsonResult {
let mut cipher = match Cipher ::find_by_uuid ( cipher_id , conn ) . await {
Some ( cipher ) = > {
if cipher . is_write_accessible_to_user ( & headers . user . uuid , conn ) . await {
cipher
} else {
err ! ( "Cipher is not write accessible" )
}
let mut cipher = if let Some ( cipher ) = Cipher ::find_by_uuid ( cipher_id , conn ) . await {
if cipher . is_write_accessible_to_user ( & headers . user . uuid , conn ) . await {
cipher
} else {
err ! ( "Cipher is not write accessible" )
}
None = > err ! ( "Cipher doesn't exist" ) ,
} else {
err ! ( "Cipher doesn't exist" )
} ;
let mut shared_to_collections = vec ! [ ] ;
@ -1042,7 +1059,7 @@ async fn share_cipher_by_uuid(
}
}
}
} ;
}
// When LastKnownRevisionDate is None, it is a new cipher, so send CipherCreate.
// If there is an override, like when handling multiple items, we want to prevent a push notification for every single item
@ -1240,10 +1257,10 @@ async fn save_attachment(
err ! ( "Cipher is neither owned by a user nor an organization" ) ;
} ;
if let Some ( size_limit ) = size_limit {
if size > size_limit {
err ! ( "Attachment storage limit exceeded with this file" ) ;
}
if let Some ( size_limit ) = size_limit
& & size > size_limit
{
err ! ( "Attachment storage limit exceeded with this file" ) ;
}
let file_id = match & attachment {
@ -1385,7 +1402,7 @@ async fn post_attachment_share(
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
_ delete_cipher_attachment_by_id( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await ? ;
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await ? ;
post_attachment ( cipher_id , data , headers , conn , nt ) . await
}
@ -1419,7 +1436,7 @@ async fn delete_attachment(
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
_ delete_cipher_attachment_by_id( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await
}
#[ delete( " /ciphers/<cipher_id>/attachment/<attachment_id>/admin " ) ]
@ -1430,42 +1447,42 @@ async fn delete_attachment_admin(
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
_ delete_cipher_attachment_by_id( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await
}
#[ post( " /ciphers/<cipher_id>/delete " ) ]
async fn delete_cipher_post ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
_ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
// permanent delete
}
#[ post( " /ciphers/<cipher_id>/delete-admin " ) ]
async fn delete_cipher_post_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
_ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
// permanent delete
}
#[ put( " /ciphers/<cipher_id>/delete " ) ]
async fn delete_cipher_put ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
_ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & CipherDeleteOptions ::SoftSingle , & nt ) . await
delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & CipherDeleteOptions ::SoftSingle , & nt ) . await
// soft delete
}
#[ put( " /ciphers/<cipher_id>/delete-admin " ) ]
async fn delete_cipher_put_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
_ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & CipherDeleteOptions ::SoftSingle , & nt ) . await
delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & CipherDeleteOptions ::SoftSingle , & nt ) . await
// soft delete
}
#[ delete( " /ciphers/<cipher_id> " ) ]
async fn delete_cipher ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
_ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
// permanent delete
}
#[ delete( " /ciphers/<cipher_id>/admin " ) ]
async fn delete_cipher_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
_ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & CipherDeleteOptions ::HardSingle , & nt ) . await
// permanent delete
}
@ -1476,7 +1493,7 @@ async fn delete_cipher_selected(
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
_ delete_multiple_ciphers( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
// permanent delete
}
@ -1487,7 +1504,7 @@ async fn delete_cipher_selected_post(
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
_ delete_multiple_ciphers( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
// permanent delete
}
@ -1498,7 +1515,7 @@ async fn delete_cipher_selected_put(
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
_ delete_multiple_ciphers( data , headers , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
// soft delete
}
@ -1509,7 +1526,7 @@ async fn delete_cipher_selected_admin(
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
_ delete_multiple_ciphers( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
// permanent delete
}
@ -1520,7 +1537,7 @@ async fn delete_cipher_selected_post_admin(
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
_ delete_multiple_ciphers( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
// permanent delete
}
@ -1531,18 +1548,18 @@ async fn delete_cipher_selected_put_admin(
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
_ delete_multiple_ciphers( data , headers , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
// soft delete
}
#[ put( " /ciphers/<cipher_id>/restore " ) ]
async fn restore_cipher_put ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
_ restore_cipher_by_uuid( & cipher_id , & headers , false , & conn , & nt ) . await
restore_cipher_by_uuid ( & cipher_id , & headers , false , & conn , & nt ) . await
}
#[ put( " /ciphers/<cipher_id>/restore-admin " ) ]
async fn restore_cipher_put_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
_ restore_cipher_by_uuid( & cipher_id , & headers , false , & conn , & nt ) . await
restore_cipher_by_uuid ( & cipher_id , & headers , false , & conn , & nt ) . await
}
#[ put( " /ciphers/restore-admin " , data = " <data> " ) ]
@ -1552,7 +1569,7 @@ async fn restore_cipher_selected_admin(
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
_ restore_multiple_ciphers( data , & headers , & conn , & nt ) . await
restore_multiple_ciphers ( data , & headers , & conn , & nt ) . await
}
#[ put( " /ciphers/restore " , data = " <data> " ) ]
@ -1562,12 +1579,13 @@ async fn restore_cipher_selected(
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
_ restore_multiple_ciphers( data , & headers , & conn , & nt ) . await
restore_multiple_ciphers ( data , & headers , & conn , & nt ) . await
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct MoveCipherData {
#[ serde(default, deserialize_with = " deser_opt_nonempty_str " ) ]
folder_id : Option < FolderId > ,
ids : Vec < CipherId > ,
}
@ -1582,10 +1600,10 @@ async fn move_cipher_selected(
let data = data . into_inner ( ) ;
let user_id = & headers . user . uuid ;
if let Some ( ref folder_id ) = data . folder_id {
if Folder ::find_by_uuid_and_user ( folder_id , user_id , & conn ) . await . is_none ( ) {
err ! ( "Invalid folder" , "Folder does not exist or belongs to another user" ) ;
}
if let Some ( ref folder_id ) = data . folder_id
& & Folder ::find_by_uuid_and_user ( folder_id , user_id , & conn ) . await . is_none ( )
{
err ! ( "Invalid folder" , "Folder does not exist or belongs to another user" ) ;
}
let cipher_count = data . ids . len ( ) ;
@ -1614,7 +1632,7 @@ async fn move_cipher_selected(
. await ;
} else {
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , & headers . device . push_uuid , & conn ) . await ;
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , headers . device . push_uuid . as_ref ( ) , & conn ) . await ;
}
if cipher_count ! = accessible_ciphers_count {
@ -1642,67 +1660,105 @@ struct OrganizationIdData {
org_id : OrganizationId ,
}
// Use the OrgIdGuard here, to ensure there an organization id present.
// If there is no organization id present, it should be forwarded to purge_personal_vault.
// This guard needs to be the first argument, else OwnerHeaders will be triggered which will logout the user.
#[ post( " /ciphers/purge?<organization..> " , data = " <data> " ) ]
async fn delete_all (
organization : Option < OrganizationIdData > ,
async fn purge_org_vault (
_org_id_guard : OrgIdGuard ,
organization : OrganizationIdData ,
data : Json < PasswordOrOtpData > ,
headers : Headers ,
headers : Owner Headers,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
if organization . org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
let data : PasswordOrOtpData = data . into_inner ( ) ;
let mut user = headers . user ;
let user = headers . user ;
data . validate ( & user , true , & conn ) . await ? ;
match organization {
Some ( org_data ) = > {
// Organization ID in query params, purging organization vault
match Membership ::find_by_user_and_org ( & user . uuid , & org_data . org_id , & conn ) . await {
None = > err ! ( "You don't have permission to purge the organization vault" ) ,
Some ( member ) = > {
if member . atype = = MembershipType ::Owner {
Cipher ::delete_all_by_organization ( & org_data . org_id , & conn ) . await ? ;
nt . send_user_update ( UpdateType ::SyncVault , & user , & headers . device . push_uuid , & conn ) . await ;
log_event (
EventType ::OrganizationPurgedVault as i32 ,
& org_data . org_id ,
& org_data . org_id ,
& user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& conn ,
)
. await ;
Ok ( ( ) )
} else {
err ! ( "You don't have permission to purge the organization vault" ) ;
}
}
}
}
None = > {
// No organization ID in query params, purging user vault
// Delete ciphers and their attachments
for cipher in Cipher ::find_owned_by_user ( & user . uuid , & conn ) . await {
cipher . delete ( & conn ) . await ? ;
}
match Membership ::find_confirmed_by_user_and_org ( & user . uuid , & organization . org_id , & conn ) . await {
Some ( member ) if member . atype = = MembershipType ::Owner = > {
Cipher ::delete_all_by_organization ( & organization . org_id , & conn ) . await ? ;
nt . send_user_update ( UpdateType ::SyncVault , & user , headers . device . push_uuid . as_ref ( ) , & conn ) . await ;
// Delete folders
for f in Folder ::find_by_user ( & user . uuid , & conn ) . await {
f . delete ( & conn ) . await ? ;
}
user . update_revision ( & conn ) . await ? ;
nt . send_user_update ( UpdateType ::SyncVault , & user , & headers . device . push_uuid , & conn ) . await ;
log_event (
EventType ::OrganizationPurgedVault as i32 ,
& organization . org_id ,
& organization . org_id ,
& user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& conn ,
)
. await ;
Ok ( ( ) )
}
_ = > err ! ( "You don't have permission to purge the organization vault" ) ,
}
}
#[ post( " /ciphers/purge " , data = " <data> " ) ]
async fn purge_personal_vault (
data : Json < PasswordOrOtpData > ,
headers : Headers ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
let data : PasswordOrOtpData = data . into_inner ( ) ;
let mut user = headers . user ;
data . validate ( & user , true , & conn ) . await ? ;
for cipher in Cipher ::find_owned_by_user ( & user . uuid , & conn ) . await {
cipher . delete ( & conn ) . await ? ;
}
for f in Folder ::find_by_user ( & user . uuid , & conn ) . await {
f . delete ( & conn ) . await ? ;
}
user . update_revision ( & conn ) . await ? ;
nt . send_user_update ( UpdateType ::SyncVault , & user , headers . device . push_uuid . as_ref ( ) , & conn ) . await ;
Ok ( ( ) )
}
#[ put( " /ciphers/<cipher_id>/archive " ) ]
async fn archive_cipher_put ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
archive_cipher ( & cipher_id , & headers , false , & conn , & nt ) . await
}
#[ put( " /ciphers/archive " , data = " <data> " ) ]
async fn archive_cipher_selected (
data : Json < CipherIdsData > ,
headers : Headers ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
archive_multiple_ciphers ( data , & headers , & conn , & nt ) . await
}
#[ put( " /ciphers/<cipher_id>/unarchive " ) ]
async fn unarchive_cipher_put ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
unarchive_cipher ( & cipher_id , & headers , false , & conn , & nt ) . await
}
#[ put( " /ciphers/unarchive " , data = " <data> " ) ]
async fn unarchive_cipher_selected (
data : Json < CipherIdsData > ,
headers : Headers ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
unarchive_multiple_ciphers ( data , & headers , & conn , & nt ) . await
}
#[ derive(PartialEq) ]
pub enum CipherDeleteOptions {
SoftSingle ,
@ -1711,7 +1767,7 @@ pub enum CipherDeleteOptions {
HardMulti ,
}
async fn _ delete_cipher_by_uuid(
async fn delete_cipher_by_uuid (
cipher_id : & CipherId ,
headers : & Headers ,
conn : & DbConn ,
@ -1777,7 +1833,7 @@ struct CipherIdsData {
ids : Vec < CipherId > ,
}
async fn _ delete_multiple_ciphers(
async fn delete_multiple_ciphers (
data : Json < CipherIdsData > ,
headers : Headers ,
conn : DbConn ,
@ -1787,18 +1843,18 @@ async fn _delete_multiple_ciphers(
let data = data . into_inner ( ) ;
for cipher_id in data . ids {
if let error @ Err ( _ ) = _ delete_cipher_by_uuid( & cipher_id , & headers , & conn , & delete_options , & nt ) . await {
if let error @ Err ( _ ) = delete_cipher_by_uuid ( & cipher_id , & headers , & conn , & delete_options , & nt ) . await {
return error ;
} ;
}
}
// Multi delete actions do not send out a push for each cipher, we need to send a general sync here
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , & headers . device . push_uuid , & conn ) . await ;
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , headers . device . push_uuid . as_ref ( ) , & conn ) . await ;
Ok ( ( ) )
}
async fn _ restore_cipher_by_uuid(
async fn restore_cipher_by_uuid (
cipher_id : & CipherId ,
headers : & Headers ,
multi_restore : bool ,
@ -1844,7 +1900,7 @@ async fn _restore_cipher_by_uuid(
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await ? ) )
}
async fn _ restore_multiple_ciphers(
async fn restore_multiple_ciphers (
data : Json < CipherIdsData > ,
headers : & Headers ,
conn : & DbConn ,
@ -1854,14 +1910,14 @@ async fn _restore_multiple_ciphers(
let mut ciphers : Vec < Value > = Vec ::new ( ) ;
for cipher_id in data . ids {
match _ restore_cipher_by_uuid( & cipher_id , headers , true , conn , nt ) . await {
match restore_cipher_by_uuid ( & cipher_id , headers , true , conn , nt ) . await {
Ok ( json ) = > ciphers . push ( json . into_inner ( ) ) ,
err = > return err ,
}
}
// Multi move actions do not send out a push for each cipher, we need to send a general sync here
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , & headers . device . push_uuid , conn ) . await ;
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , headers . device . push_uuid . as_ref ( ) , conn ) . await ;
Ok ( Json ( json ! ( {
"data" : ciphers ,
@ -1870,7 +1926,7 @@ async fn _restore_multiple_ciphers(
} ) ) )
}
async fn _ delete_cipher_attachment_by_id(
async fn delete_cipher_attachment_by_id (
cipher_id : & CipherId ,
attachment_id : & AttachmentId ,
headers : & Headers ,
@ -1921,6 +1977,122 @@ async fn _delete_cipher_attachment_by_id(
Ok ( Json ( json ! ( { "cipher" :cipher_json } ) ) )
}
async fn archive_cipher (
cipher_id : & CipherId ,
headers : & Headers ,
multi_archive : bool ,
conn : & DbConn ,
nt : & Notify < '_ > ,
) -> JsonResult {
let Some ( cipher ) = Cipher ::find_by_uuid ( cipher_id , conn ) . await else {
err ! ( "Cipher doesn't exist" )
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , conn ) . await {
err ! ( "Cipher is not accessible for the current user" )
}
cipher . set_archived_at ( Utc ::now ( ) . naive_utc ( ) , & headers . user . uuid , conn ) . await ? ;
if ! multi_archive {
nt . send_cipher_update (
UpdateType ::SyncCipherUpdate ,
& cipher ,
& cipher . update_users_revision ( conn ) . await ,
& headers . device ,
None ,
conn ,
)
. await ;
}
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await ? ) )
}
async fn unarchive_cipher (
cipher_id : & CipherId ,
headers : & Headers ,
multi_unarchive : bool ,
conn : & DbConn ,
nt : & Notify < '_ > ,
) -> JsonResult {
let Some ( cipher ) = Cipher ::find_by_uuid ( cipher_id , conn ) . await else {
err ! ( "Cipher doesn't exist" )
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , conn ) . await {
err ! ( "Cipher is not accessible for the current user" )
}
cipher . unarchive ( & headers . user . uuid , conn ) . await ? ;
if ! multi_unarchive {
nt . send_cipher_update (
UpdateType ::SyncCipherUpdate ,
& cipher ,
& cipher . update_users_revision ( conn ) . await ,
& headers . device ,
None ,
conn ,
)
. await ;
}
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await ? ) )
}
async fn archive_multiple_ciphers (
data : Json < CipherIdsData > ,
headers : & Headers ,
conn : & DbConn ,
nt : & Notify < '_ > ,
) -> JsonResult {
let data = data . into_inner ( ) ;
let mut ciphers : Vec < Value > = Vec ::new ( ) ;
for cipher_id in data . ids {
match archive_cipher ( & cipher_id , headers , true , conn , nt ) . await {
Ok ( json ) = > ciphers . push ( json . into_inner ( ) ) ,
err = > return err ,
}
}
// Multi archive does not send out a push for each cipher, we need to send a general sync here
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , headers . device . push_uuid . as_ref ( ) , conn ) . await ;
Ok ( Json ( json ! ( {
"data" : ciphers ,
"object" : "list" ,
"continuationToken" : null
} ) ) )
}
async fn unarchive_multiple_ciphers (
data : Json < CipherIdsData > ,
headers : & Headers ,
conn : & DbConn ,
nt : & Notify < '_ > ,
) -> JsonResult {
let data = data . into_inner ( ) ;
let mut ciphers : Vec < Value > = Vec ::new ( ) ;
for cipher_id in data . ids {
match unarchive_cipher ( & cipher_id , headers , true , conn , nt ) . await {
Ok ( json ) = > ciphers . push ( json . into_inner ( ) ) ,
err = > return err ,
}
}
// Multi unarchive does not send out a push for each cipher, we need to send a general sync here
nt . send_user_update ( UpdateType ::SyncCiphers , & headers . user , headers . device . push_uuid . as_ref ( ) , conn ) . await ;
Ok ( Json ( json ! ( {
"data" : ciphers ,
"object" : "list" ,
"continuationToken" : null
} ) ) )
}
/// This will hold all the necessary data to improve a full sync of all the ciphers
/// It can be used during the `Cipher::to_json()` call.
/// It will prevent the so called N+1 SQL issue by running just a few queries which will hold all the data needed.
@ -1930,6 +2102,7 @@ pub struct CipherSyncData {
pub cipher_folders : HashMap < CipherId , FolderId > ,
pub cipher_favorites : HashSet < CipherId > ,
pub cipher_collections : HashMap < CipherId , Vec < CollectionId > > ,
pub cipher_archives : HashMap < CipherId , NaiveDateTime > ,
pub members : HashMap < OrganizationId , Membership > ,
pub user_collections : HashMap < CollectionId , CollectionUser > ,
pub user_collections_groups : HashMap < CollectionId , CollectionGroup > ,
@ -1946,20 +2119,25 @@ impl CipherSyncData {
pub async fn new ( user_id : & UserId , sync_type : CipherSyncType , conn : & DbConn ) -> Self {
let cipher_folders : HashMap < CipherId , FolderId > ;
let cipher_favorites : HashSet < CipherId > ;
let cipher_archives : HashMap < CipherId , NaiveDateTime > ;
match sync_type {
// User Sync supports Folders and Favorit es
// User Sync supports Folders, Favorites, and Archiv es
CipherSyncType ::User = > {
// Generate a HashMap with the Cipher UUID as key and the Folder UUID as value
cipher_folders = FolderCipher ::find_by_user ( user_id , conn ) . await . into_iter ( ) . collect ( ) ;
// Generate a HashSet of all the Cipher UUID's which are marked as favorite
cipher_favorites = Favorite ::get_all_cipher_uuid_by_user ( user_id , conn ) . await . into_iter ( ) . collect ( ) ;
// Generate a HashMap with the Cipher UUID as key and the archived date time as value
cipher_archives = Archive ::find_by_user ( user_id , conn ) . await . into_iter ( ) . collect ( ) ;
}
// Organization Sync does not support Folders and Favorites.
// Organization Sync does not support Folders, Favorites, or Archiv es.
// If these are set, it will cause issues in the web-vault.
CipherSyncType ::Organization = > {
cipher_folders = HashMap ::with_capacity ( 0 ) ;
cipher_favorites = HashSet ::with_capacity ( 0 ) ;
cipher_archives = HashMap ::with_capacity ( 0 ) ;
}
}
@ -1980,8 +2158,11 @@ impl CipherSyncData {
}
// Generate a HashMap with the Organization UUID as key and the Membership record
let members : HashMap < OrganizationId , Membership > =
Membership ::find_by_user ( user_id , conn ) . await . into_iter ( ) . map ( | m | ( m . org_uuid . clone ( ) , m ) ) . collect ( ) ;
let members : HashMap < OrganizationId , Membership > = Membership ::find_confirmed_by_user ( user_id , conn )
. await
. into_iter ( )
. map ( | m | ( m . org_uuid . clone ( ) , m ) )
. collect ( ) ;
// Generate a HashMap with the User_Collections UUID as key and the CollectionUser record
let user_collections : HashMap < CollectionId , CollectionUser > = CollectionUser ::find_by_user ( user_id , conn )
@ -2023,6 +2204,7 @@ impl CipherSyncData {
cipher_folders ,
cipher_favorites ,
cipher_collections ,
cipher_archives ,
members ,
user_collections ,
user_collections_groups ,