@ -21,9 +21,9 @@ use crate::{
db ::{
db ::{
DbConn , DbPool ,
DbConn , DbPool ,
models ::{
models ::{
Archive , Attachment , AttachmentId , Cipher , CipherId , Collection , CollectionCipher , CollectionGroup ,
Archive , Attachment , AttachmentId , Cipher , CipherAccessScope , Cipher Id , Collection , CollectionCipher ,
CollectionId , CollectionUser , EventType , Favorite , Folder , FolderCipher , FolderId , Group , KeyId ,
CollectionGroup , Collection Id , CollectionUser , EventType , Favorite , Folder , FolderCipher , FolderId , Group ,
Membership , MembershipType , OrgPolicy , OrgPolicyType , OrganizationId , RepromptType , Send , UserId ,
KeyId , Membership , MembershipType , OrgPolicy , OrgPolicyType , OrganizationId , RepromptType , Send , UserId ,
} ,
} ,
} ,
} ,
util ::{ NumberOrString , deser_opt_nonempty_str , save_temp_file } ,
util ::{ NumberOrString , deser_opt_nonempty_str , save_temp_file } ,
@ -233,21 +233,12 @@ async fn get_ciphers(headers: Headers, conn: DbConn) -> JsonResult {
#[ get( " /ciphers/<cipher_id> " ) ]
#[ get( " /ciphers/<cipher_id> " ) ]
async fn get_cipher ( cipher_id : CipherId , headers : Headers , conn : DbConn ) -> JsonResult {
async fn get_cipher ( cipher_id : CipherId , headers : Headers , conn : DbConn ) -> JsonResult {
let Some ( cipher ) = Cipher ::find_by_uuid ( & cipher_id , & conn ) . await else {
get_cipher_impl ( cipher_id , & headers , CipherAccessScope ::User , & conn ) . await
err ! ( "Cipher doesn't exist" )
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , & conn ) . await {
err ! ( "Cipher is not owned by user" )
}
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , & conn ) . await ? ) )
}
}
#[ get( " /ciphers/<cipher_id>/admin " ) ]
#[ get( " /ciphers/<cipher_id>/admin " ) ]
async fn get_cipher_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn ) -> JsonResult {
async fn get_cipher_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn ) -> JsonResult {
// TODO: Implement this correctly
get_cipher_impl ( cipher_id , & headers , CipherAccessScope ::OrganizationAdmin , & conn ) . await
get_cipher ( cipher_id , headers , conn ) . await
}
}
#[ get( " /ciphers/<cipher_id>/details " ) ]
#[ get( " /ciphers/<cipher_id>/details " ) ]
@ -255,6 +246,42 @@ async fn get_cipher_details(cipher_id: CipherId, headers: Headers, conn: DbConn)
get_cipher ( cipher_id , headers , conn ) . await
get_cipher ( cipher_id , headers , conn ) . await
}
}
async fn get_cipher_impl (
cipher_id : CipherId ,
headers : & Headers ,
scope : CipherAccessScope ,
conn : & DbConn ,
) -> 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 , scope , conn ) . await {
err ! ( "Cipher is not owned by user" )
}
Ok ( Json ( cipher_json_for_scope ( & cipher , headers , scope , conn ) . await ? ) )
}
/// Serialize a cipher the caller has just been authorized for at `scope`.
///
/// The admin routes have to report the access they were authorized with, otherwise an
/// organization-wide caller without a personal assignment is answered `edit: false` for a cipher
/// they may in fact edit.
async fn cipher_json_for_scope (
cipher : & Cipher ,
headers : & Headers ,
scope : CipherAccessScope ,
conn : & DbConn ,
) -> Result < Value , crate ::Error > {
match scope {
CipherAccessScope ::User = > {
cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await
}
CipherAccessScope ::OrganizationAdmin = > cipher . to_json_org_admin ( & headers . host , & headers . user . uuid , conn ) . await ,
}
}
#[ derive(Debug, Deserialize) ]
#[ derive(Debug, Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
pub struct CipherData {
pub struct CipherData {
@ -337,7 +364,11 @@ pub struct Attachments2Data {
/// Called when an org admin clones an org cipher.
/// Called when an org admin clones an org cipher.
#[ post( " /ciphers/admin " , data = " <data> " ) ]
#[ post( " /ciphers/admin " , data = " <data> " ) ]
async fn post_ciphers_admin ( data : Json < ShareCipherData > , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
async fn post_ciphers_admin ( data : Json < ShareCipherData > , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
post_ciphers_create ( data , headers , conn , nt ) . await
// Only the response differs from `/ciphers/create`: the cipher is created owned by the caller
// and then shared, so the authorization along the way is the regular one either way. Without
// this, an administrative caller without a personal assignment to the target collection is
// answered `edit: false` for the cipher they just created.
post_ciphers_create_impl ( data , headers , CipherAccessScope ::OrganizationAdmin , conn , nt ) . await
}
}
/// Called when creating a new org-owned cipher, or cloning a cipher (whether
/// Called when creating a new org-owned cipher, or cloning a cipher (whether
@ -349,6 +380,16 @@ async fn post_ciphers_create(
headers : Headers ,
headers : Headers ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
post_ciphers_create_impl ( data , headers , CipherAccessScope ::User , conn , nt ) . await
}
async fn post_ciphers_create_impl (
data : Json < ShareCipherData > ,
headers : Headers ,
response_scope : CipherAccessScope ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
let mut data : ShareCipherData = data . into_inner ( ) ;
let mut data : ShareCipherData = data . into_inner ( ) ;
@ -373,7 +414,7 @@ async fn post_ciphers_create(
// or otherwise), we can just ignore this field entirely.
// or otherwise), we can just ignore this field entirely.
data . cipher . last_known_revision_date = None ;
data . cipher . last_known_revision_date = None ;
let res = share_cipher_by_uuid ( & cipher . uuid , data , & headers , & conn , & nt , None ) . await ;
let res = share_cipher_by_uuid ( & cipher . uuid , data , & headers , response_scope , & conn , & nt , None ) . await ;
if res . is_err ( ) {
if res . is_err ( ) {
cipher . delete ( & conn ) . await ? ;
cipher . delete ( & conn ) . await ? ;
}
}
@ -403,7 +444,16 @@ async fn post_ciphers(data: Json<CipherData>, headers: Headers, conn: DbConn, nt
data . last_known_revision_date = None ;
data . last_known_revision_date = None ;
let mut cipher = Cipher ::new ( data . r#type , data . name . clone ( ) ) ;
let mut cipher = Cipher ::new ( data . r#type , data . name . clone ( ) ) ;
update_cipher_from_data ( & mut cipher , data , & headers , None , & conn , & nt , UpdateType ::SyncCipherCreate ) . await ? ;
update_cipher_from_data (
& mut cipher ,
data ,
& headers ,
CipherUpdateAuthorization ::default ( ) ,
& conn ,
& nt ,
UpdateType ::SyncCipherCreate ,
)
. await ? ;
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , & conn ) . await ? ) )
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , & conn ) . await ? ) )
}
}
@ -426,11 +476,43 @@ async fn enforce_personal_ownership_policy(data: Option<&CipherData>, headers: &
Ok ( ( ) )
Ok ( ( ) )
}
}
fn has_prevalidated_organization_write_authority (
shared_to_collections : Option < & Vec < CollectionId > > ,
member_has_full_access : bool ,
organization_write_authorized : bool ,
) -> bool {
organization_write_authorized
| | shared_to_collections . is_some_and ( | collections | ! collections . is_empty ( ) )
| | member_has_full_access
}
#[ derive(Default) ]
pub struct CipherUpdateAuthorization {
shared_to_collections : Option < Vec < CollectionId > > ,
organization_write_authorized : bool ,
}
impl CipherUpdateAuthorization {
pub fn shared_to ( collections : Vec < CollectionId > ) -> Self {
Self {
shared_to_collections : Some ( collections ) ,
organization_write_authorized : false ,
}
}
pub fn organization_import ( collections : Vec < CollectionId > , organization_write_authorized : bool ) -> Self {
Self {
shared_to_collections : Some ( collections ) ,
organization_write_authorized ,
}
}
}
pub async fn update_cipher_from_data (
pub async fn update_cipher_from_data (
cipher : & mut Cipher ,
cipher : & mut Cipher ,
data : CipherData ,
data : CipherData ,
headers : & Headers ,
headers : & Headers ,
shared_to_collections : Option < Vec < CollectionId > > ,
authorization : CipherUpdateAuthorization ,
conn : & DbConn ,
conn : & DbConn ,
nt : & Notify < '_ > ,
nt : & Notify < '_ > ,
ut : UpdateType ,
ut : UpdateType ,
@ -449,6 +531,11 @@ pub async fn update_cipher_from_data(
json_data
json_data
}
}
let CipherUpdateAuthorization {
shared_to_collections ,
organization_write_authorized ,
} = authorization ;
enforce_personal_ownership_policy ( Some ( & data ) , headers , conn ) . await ? ;
enforce_personal_ownership_policy ( Some ( & data ) , headers , conn ) . await ? ;
// Check that the client isn't updating an existing cipher with stale data.
// Check that the client isn't updating an existing cipher with stale data.
@ -486,9 +573,11 @@ pub async fn update_cipher_from_data(
Some ( member ) = > {
Some ( member ) = > {
// A non-empty list of collections implies the caller already validated the user's write
// A non-empty list of collections implies the caller already validated the user's write
// access to them, so we can move the cipher into the organization on that basis.
// access to them, so we can move the cipher into the organization on that basis.
if shared_to_collections . as_ref ( ) . is_some_and ( | cols | ! cols . is_empty ( ) )
if has_prevalidated_organization_write_authority (
| | member . has_full_access ( )
shared_to_collections . as_ref ( ) ,
| | cipher . is_write_accessible_to_user ( & headers . user . uuid , conn ) . await
member . has_full_access ( ) ,
organization_write_authorized ,
) | | cipher . is_write_accessible_to_user ( & headers . user . uuid , CipherAccessScope ::User , conn ) . await
{
{
cipher . organization_uuid = Some ( org_id ) ;
cipher . organization_uuid = Some ( org_id ) ;
// After some discussion in PR #1329 re-added the user_uuid = None again.
// After some discussion in PR #1329 re-added the user_uuid = None again.
@ -665,7 +754,16 @@ async fn post_ciphers_import(data: Json<ImportData>, headers: Headers, conn: DbC
cipher_data . folder_id = folder_id ;
cipher_data . folder_id = folder_id ;
let mut cipher = Cipher ::new ( cipher_data . r#type , cipher_data . name . clone ( ) ) ;
let mut cipher = Cipher ::new ( cipher_data . r#type , cipher_data . name . clone ( ) ) ;
update_cipher_from_data ( & mut cipher , cipher_data , & headers , None , & conn , & nt , UpdateType ::None ) . await ? ;
update_cipher_from_data (
& mut cipher ,
cipher_data ,
& headers ,
CipherUpdateAuthorization ::default ( ) ,
& conn ,
& nt ,
UpdateType ::None ,
)
. await ? ;
}
}
let mut user = headers . user ;
let mut user = headers . user ;
@ -684,7 +782,7 @@ async fn put_cipher_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
put_cipher ( cipher_id , data , headers , conn , nt ) . await
put_cipher_impl ( cipher_id , data , headers , CipherAccessScope ::OrganizationAdmin , conn , nt ) . await
}
}
#[ post( " /ciphers/<cipher_id>/admin " , data = " <data> " ) ]
#[ post( " /ciphers/<cipher_id>/admin " , data = " <data> " ) ]
@ -695,7 +793,7 @@ async fn post_cipher_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
post_cipher ( cipher_id , data , headers , conn , nt ) . await
put_cipher_impl ( cipher_id , data , headers , CipherAccessScope ::OrganizationAdmin , conn , nt ) . await
}
}
#[ post( " /ciphers/<cipher_id> " , data = " <data> " ) ]
#[ post( " /ciphers/<cipher_id> " , data = " <data> " ) ]
@ -716,6 +814,17 @@ async fn put_cipher(
headers : Headers ,
headers : Headers ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
put_cipher_impl ( cipher_id , data , headers , CipherAccessScope ::User , conn , nt ) . await
}
async fn put_cipher_impl (
cipher_id : CipherId ,
data : Json < CipherData > ,
headers : Headers ,
scope : CipherAccessScope ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
let data : CipherData = data . into_inner ( ) ;
let data : CipherData = data . into_inner ( ) ;
@ -728,13 +837,22 @@ async fn put_cipher(
// cipher itself, so the user shouldn't need write access to change these.
// cipher itself, so the user shouldn't need write access to change these.
// Interestingly, upstream Bitwarden doesn't properly handle this either.
// Interestingly, upstream Bitwarden doesn't properly handle this either.
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , & conn ) . await {
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , scope , & conn ) . await {
err ! ( "Cipher is not write accessible" )
err ! ( "Cipher is not write accessible" )
}
}
update_cipher_from_data ( & mut cipher , data , & headers , None , & conn , & nt , UpdateType ::SyncCipherUpdate ) . await ? ;
update_cipher_from_data (
& mut cipher ,
data ,
& headers ,
CipherUpdateAuthorization ::default ( ) ,
& conn ,
& nt ,
UpdateType ::SyncCipherUpdate ,
)
. await ? ;
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , & conn ) . await ? ) )
Ok ( Json ( cipher_json_for_scope ( & cip her, & headers , scope , & conn ) . await ? ) )
}
}
#[ post( " /ciphers/<cipher_id>/partial " , data = " <data> " ) ]
#[ post( " /ciphers/<cipher_id>/partial " , data = " <data> " ) ]
@ -761,7 +879,7 @@ async fn put_cipher_partial(
err ! ( "Cipher does not exist" )
err ! ( "Cipher does not exist" )
} ;
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , & conn ) . await {
if ! cipher . is_accessible_to_user ( & headers . user . uuid , CipherAccessScope ::User , & conn ) . await {
err ! ( "Cipher does not exist" , "Cipher is not accessible for the current user" )
err ! ( "Cipher does not exist" , "Cipher is not accessible for the current user" )
}
}
@ -838,7 +956,7 @@ async fn post_collections_update(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_in_editable_collection_by_user ( & headers . user . uuid , & conn ) . await {
if ! cipher . is_in_editable_collection_by_user ( & headers . user . uuid , CipherAccessScope ::User , & conn ) . await {
err ! ( "Collection cannot be changed" )
err ! ( "Collection cannot be changed" )
}
}
@ -918,7 +1036,10 @@ async fn post_collections_admin(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_in_editable_collection_by_user ( & headers . user . uuid , & conn ) . await {
// Upstream guards this route with `CanEditCipherAsAdminAsync`, so a member holding
// organization-wide cipher authority reaches every cipher of the organization here.
if ! cipher . is_in_editable_collection_by_user ( & headers . user . uuid , CipherAccessScope ::OrganizationAdmin , & conn ) . await
{
err ! ( "Collection cannot be changed" )
err ! ( "Collection cannot be changed" )
}
}
@ -992,7 +1113,7 @@ async fn post_cipher_share(
) -> JsonResult {
) -> JsonResult {
let data : ShareCipherData = data . into_inner ( ) ;
let data : ShareCipherData = data . into_inner ( ) ;
share_cipher_by_uuid ( & cipher_id , data , & headers , & conn , & nt , None ) . await
share_cipher_by_uuid ( & cipher_id , data , & headers , CipherAccessScope ::User , & conn , & nt , None ) . await
}
}
#[ put( " /ciphers/<cipher_id>/share " , data = " <data> " ) ]
#[ put( " /ciphers/<cipher_id>/share " , data = " <data> " ) ]
@ -1005,7 +1126,7 @@ async fn put_cipher_share(
) -> JsonResult {
) -> JsonResult {
let data : ShareCipherData = data . into_inner ( ) ;
let data : ShareCipherData = data . into_inner ( ) ;
share_cipher_by_uuid ( & cipher_id , data , & headers , & conn , & nt , None ) . await
share_cipher_by_uuid ( & cipher_id , data , & headers , CipherAccessScope ::User , & conn , & nt , None ) . await
}
}
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
@ -1045,7 +1166,16 @@ async fn put_cipher_share_selected(
} ;
} ;
if let Some ( id ) = shared_cipher_data . cipher . id . take ( ) {
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 ?
share_cipher_by_uuid (
& id ,
shared_cipher_data ,
& headers ,
CipherAccessScope ::User ,
& conn ,
& nt ,
Some ( UpdateType ::None ) ,
)
. await ?
} else {
} else {
err ! ( "Request missing ids field" )
err ! ( "Request missing ids field" )
} ;
} ;
@ -1061,12 +1191,16 @@ async fn share_cipher_by_uuid(
cipher_id : & CipherId ,
cipher_id : & CipherId ,
data : ShareCipherData ,
data : ShareCipherData ,
headers : & Headers ,
headers : & Headers ,
// Only the response is serialized with this. The entry check below deliberately stays
// `CipherAccessScope::User`: sharing is a regular vault operation, and `/ciphers/admin` reaches
// it with a cipher it has just created and therefore owns.
response_scope : CipherAccessScope ,
conn : & DbConn ,
conn : & DbConn ,
nt : & Notify < '_ > ,
nt : & Notify < '_ > ,
override_ut : Option < UpdateType > ,
override_ut : Option < UpdateType > ,
) -> JsonResult {
) -> JsonResult {
let mut cipher = if let Some ( cipher ) = Cipher ::find_by_uuid ( cipher_id , conn ) . await {
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 {
if cipher . is_write_accessible_to_user ( & headers . user . uuid , CipherAccessScope ::User , conn ) . await {
cipher
cipher
} else {
} else {
err ! ( "Cipher is not write accessible" )
err ! ( "Cipher is not write accessible" )
@ -1110,9 +1244,18 @@ async fn share_cipher_by_uuid(
UpdateType ::SyncCipherCreate
UpdateType ::SyncCipherCreate
} ;
} ;
update_cipher_from_data ( & mut cipher , data . cipher , headers , Some ( shared_to_collections ) , conn , nt , ut ) . await ? ;
update_cipher_from_data (
& mut cipher ,
data . cipher ,
headers ,
CipherUpdateAuthorization ::shared_to ( shared_to_collections ) ,
conn ,
nt ,
ut ,
)
. await ? ;
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await ? ) )
Ok ( Json ( cipher_json_for_scope ( & cipher , headers , response_scope , conn ) . await ? ) )
}
}
/// v2 API for downloading an attachment. This just redirects the client to
/// v2 API for downloading an attachment. This just redirects the client to
@ -1132,7 +1275,7 @@ async fn get_attachment(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , & conn ) . await {
if ! cipher . is_accessible_to_user ( & headers . user . uuid , CipherAccessScope ::User , & conn ) . await {
err ! ( "Cipher is not accessible" )
err ! ( "Cipher is not accessible" )
}
}
@ -1168,15 +1311,22 @@ async fn post_attachment_v2(
headers : Headers ,
headers : Headers ,
conn : DbConn ,
conn : DbConn ,
) -> JsonResult {
) -> JsonResult {
let data : AttachmentRequestData = data . into_inner ( ) ;
// Upstream's `PostAttachment` branches on `adminRequest`: it authorizes the administrative
// request with `CanEditCipherAsAdminAsync` and answers it with a `CipherMiniResponse`. The flag
// picks the predicate, not its answer -- a caller without organization-wide cipher authority is
// refused here either way.
let scope = CipherAccessScope ::requested ( data . admin_request ) ;
let Some ( cipher ) = Cipher ::find_by_uuid ( & cipher_id , & conn ) . await else {
let Some ( cipher ) = Cipher ::find_by_uuid ( & cipher_id , & conn ) . await else {
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , & conn ) . await {
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , scope , & conn ) . await {
err ! ( "Cipher is not write accessible" )
err ! ( "Cipher is not write accessible" )
}
}
let data : AttachmentRequestData = data . into_inner ( ) ;
let file_size = data . file_size . into_i64 ( ) ? ;
let file_size = data . file_size . into_i64 ( ) ? ;
if file_size < 0 {
if file_size < 0 {
@ -1188,9 +1338,11 @@ async fn post_attachment_v2(
attachment . save ( & conn ) . await . expect ( "Error saving attachment" ) ;
attachment . save ( & conn ) . await . expect ( "Error saving attachment" ) ;
let url = format ! ( "/ciphers/{}/attachment/{attachment_id}" , cipher . uuid ) ;
let url = format ! ( "/ciphers/{}/attachment/{attachment_id}" , cipher . uuid ) ;
let response_key = match data . admin_request {
// Derived from the same `scope` the request was authorized with, so the response key and the
Some ( b ) if b = > "cipherMiniResponse" ,
// serialization below can never disagree about which flow this is.
_ = > "cipherResponse" ,
let response_key = match scope {
CipherAccessScope ::OrganizationAdmin = > "cipherMiniResponse" ,
CipherAccessScope ::User = > "cipherResponse" ,
} ;
} ;
Ok ( Json ( json ! ( { // AttachmentUploadDataResponseModel
Ok ( Json ( json ! ( { // AttachmentUploadDataResponseModel
@ -1198,7 +1350,7 @@ async fn post_attachment_v2(
"attachmentId" : attachment_id ,
"attachmentId" : attachment_id ,
"url" : url ,
"url" : url ,
"fileUploadType" : FileUploadType ::Direct as i32 ,
"fileUploadType" : FileUploadType ::Direct as i32 ,
response_key : cipher . to_json ( & heade rs . host , & headers . user . uuid , None , CipherSyncType ::User , & conn ) . await ? ,
response_key : cipher_json_for_scope ( & cip her, & headers , scope , & conn ) . await ? ,
} ) ) )
} ) ) )
}
}
@ -1221,6 +1373,7 @@ async fn save_attachment(
cipher_id : CipherId ,
cipher_id : CipherId ,
data : Form < UploadData < '_ > > ,
data : Form < UploadData < '_ > > ,
headers : & Headers ,
headers : & Headers ,
scope : CipherAccessScope ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> Result < ( Cipher , DbConn ) , crate ::error ::Error > {
) -> Result < ( Cipher , DbConn ) , crate ::error ::Error > {
@ -1237,7 +1390,7 @@ async fn save_attachment(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , & conn ) . await {
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , scope , & conn ) . await {
err ! ( "Cipher is not write accessible" )
err ! ( "Cipher is not write accessible" )
}
}
@ -1398,11 +1551,31 @@ async fn post_attachment_v2_data(
None = > err ! ( "Attachment doesn't exist" ) ,
None = > err ! ( "Attachment doesn't exist" ) ,
} ;
} ;
save_attachment ( attachment , cipher_id , data , & headers , conn , nt ) . await ? ;
// This leg of the v2 upload carries no `adminRequest` field, so the administrative context is
// recomputed from the caller's own membership in *this cipher's* organization, exactly as
// upstream's `PostFileForExistingAttachment` does. Nothing in the request feeds into it, so the
// upload route cannot be talked into an administrative scope.
let Some ( cipher ) = Cipher ::find_by_uuid ( & cipher_id , & conn ) . await else {
err ! ( "Cipher doesn't exist" )
} ;
let scope = cipher_scope_for_member ( & cipher , & headers . user . uuid , & conn ) . await ;
save_attachment ( attachment , cipher_id , data , & headers , scope , conn , nt ) . await ? ;
Ok ( ( ) )
Ok ( ( ) )
}
}
/// The [`CipherAccessScope`] for a route the client cannot state one for: resolved from the
/// caller's own confirmed membership in the cipher's organization, never from the request.
async fn cipher_scope_for_member ( cipher : & Cipher , user_id : & UserId , conn : & DbConn ) -> CipherAccessScope {
let Some ( org_id ) = cipher . organization_uuid . as_ref ( ) else {
// A personal cipher has no organization to administer.
return CipherAccessScope ::User ;
} ;
CipherAccessScope ::for_member ( Membership ::find_confirmed_by_user_and_org ( user_id , org_id , conn ) . await . as_ref ( ) )
}
/// Legacy API for creating an attachment associated with a cipher.
/// Legacy API for creating an attachment associated with a cipher.
#[ post( " /ciphers/<cipher_id>/attachment " , format = " multipart/form-data " , data = " <data> " ) ]
#[ post( " /ciphers/<cipher_id>/attachment " , format = " multipart/form-data " , data = " <data> " ) ]
async fn post_attachment (
async fn post_attachment (
@ -1412,13 +1585,7 @@ async fn post_attachment(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
// Setting this as None signifies to save_attachment() that it should create
post_attachment_impl ( cipher_id , data , headers , CipherAccessScope ::User , conn , nt ) . await
// the attachment database record as well as saving the data to disk.
let attachment = None ;
let ( cipher , conn ) = save_attachment ( attachment , cipher_id , data , & headers , conn , nt ) . await ? ;
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , & conn ) . await ? ) )
}
}
#[ post( " /ciphers/<cipher_id>/attachment-admin " , format = " multipart/form-data " , data = " <data> " ) ]
#[ post( " /ciphers/<cipher_id>/attachment-admin " , format = " multipart/form-data " , data = " <data> " ) ]
@ -1429,7 +1596,24 @@ async fn post_attachment_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
post_attachment ( cipher_id , data , headers , conn , nt ) . await
post_attachment_impl ( cipher_id , data , headers , CipherAccessScope ::OrganizationAdmin , conn , nt ) . await
}
async fn post_attachment_impl (
cipher_id : CipherId ,
data : Form < UploadData < '_ > > ,
headers : Headers ,
scope : CipherAccessScope ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> JsonResult {
// Setting this as None signifies to save_attachment() that it should create
// the attachment database record as well as saving the data to disk.
let attachment = None ;
let ( cipher , conn ) = save_attachment ( attachment , cipher_id , data , & headers , scope , conn , nt ) . await ? ;
Ok ( Json ( cipher_json_for_scope ( & cipher , & headers , scope , & conn ) . await ? ) )
}
}
#[ post( " /ciphers/<cipher_id>/attachment/<attachment_id>/share " , format = " multipart/form-data " , data = " <data> " ) ]
#[ post( " /ciphers/<cipher_id>/attachment/<attachment_id>/share " , format = " multipart/form-data " , data = " <data> " ) ]
@ -1441,7 +1625,7 @@ async fn post_attachment_share(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await ? ;
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , CipherAccessScope ::User , & conn , & nt ) . await ? ;
post_attachment ( cipher_id , data , headers , conn , nt ) . await
post_attachment ( cipher_id , data , headers , conn , nt ) . await
}
}
@ -1453,7 +1637,15 @@ async fn delete_attachment_post_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
delete_attachment ( cipher_id , attachment_id , headers , conn , nt ) . await
delete_cipher_attachment_by_id (
& cipher_id ,
& attachment_id ,
& headers ,
CipherAccessScope ::OrganizationAdmin ,
& conn ,
& nt ,
)
. await
}
}
#[ post( " /ciphers/<cipher_id>/attachment/<attachment_id>/delete " ) ]
#[ post( " /ciphers/<cipher_id>/attachment/<attachment_id>/delete " ) ]
@ -1475,7 +1667,7 @@ async fn delete_attachment(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , CipherAccessScope ::User , & conn , & nt ) . await
}
}
#[ delete( " /ciphers/<cipher_id>/attachment/<attachment_id>/admin " ) ]
#[ delete( " /ciphers/<cipher_id>/attachment/<attachment_id>/admin " ) ]
@ -1486,42 +1678,77 @@ async fn delete_attachment_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
delete_cipher_attachment_by_id ( & cipher_id , & attachment_id , & headers , & conn , & nt ) . await
delete_cipher_attachment_by_id (
& cipher_id ,
& attachment_id ,
& headers ,
CipherAccessScope ::OrganizationAdmin ,
& conn ,
& nt ,
)
. await
}
}
#[ post( " /ciphers/<cipher_id>/delete " ) ]
#[ post( " /ciphers/<cipher_id>/delete " ) ]
async fn delete_cipher_post ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
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 , CipherAccessScope ::User , & conn , & CipherDeleteOptions ::HardSingle , & nt )
. await
// permanent delete
// permanent delete
}
}
#[ post( " /ciphers/<cipher_id>/delete-admin " ) ]
#[ post( " /ciphers/<cipher_id>/delete-admin " ) ]
async fn delete_cipher_post_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
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 ,
CipherAccessScope ::OrganizationAdmin ,
& conn ,
& CipherDeleteOptions ::HardSingle ,
& nt ,
)
. await
// permanent delete
// permanent delete
}
}
#[ put( " /ciphers/<cipher_id>/delete " ) ]
#[ put( " /ciphers/<cipher_id>/delete " ) ]
async fn delete_cipher_put ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
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 , CipherAccessScope ::User , & conn , & CipherDeleteOptions ::SoftSingle , & nt )
. await
// soft delete
// soft delete
}
}
#[ put( " /ciphers/<cipher_id>/delete-admin " ) ]
#[ put( " /ciphers/<cipher_id>/delete-admin " ) ]
async fn delete_cipher_put_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
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 ,
CipherAccessScope ::OrganizationAdmin ,
& conn ,
& CipherDeleteOptions ::SoftSingle ,
& nt ,
)
. await
// soft delete
// soft delete
}
}
#[ delete( " /ciphers/<cipher_id> " ) ]
#[ delete( " /ciphers/<cipher_id> " ) ]
async fn delete_cipher ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
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 , CipherAccessScope ::User , & conn , & CipherDeleteOptions ::HardSingle , & nt )
. await
// permanent delete
// permanent delete
}
}
#[ delete( " /ciphers/<cipher_id>/admin " ) ]
#[ delete( " /ciphers/<cipher_id>/admin " ) ]
async fn delete_cipher_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> EmptyResult {
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 ,
CipherAccessScope ::OrganizationAdmin ,
& conn ,
& CipherDeleteOptions ::HardSingle ,
& nt ,
)
. await
// permanent delete
// permanent delete
}
}
@ -1532,7 +1759,7 @@ async fn delete_cipher_selected(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers ( data , headers , CipherAccessScope ::User , conn , CipherDeleteOptions ::HardMulti , nt ) . await
// permanent delete
// permanent delete
}
}
@ -1543,7 +1770,7 @@ async fn delete_cipher_selected_post(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers ( data , headers , CipherAccessScope ::User , conn , CipherDeleteOptions ::HardMulti , nt ) . await
// permanent delete
// permanent delete
}
}
@ -1554,7 +1781,7 @@ async fn delete_cipher_selected_put(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
delete_multiple_ciphers ( data , headers , CipherAccessScope ::User , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
// soft delete
// soft delete
}
}
@ -1565,7 +1792,15 @@ async fn delete_cipher_selected_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers (
data ,
headers ,
CipherAccessScope ::OrganizationAdmin ,
conn ,
CipherDeleteOptions ::HardMulti ,
nt ,
)
. await
// permanent delete
// permanent delete
}
}
@ -1576,7 +1811,15 @@ async fn delete_cipher_selected_post_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::HardMulti , nt ) . await
delete_multiple_ciphers (
data ,
headers ,
CipherAccessScope ::OrganizationAdmin ,
conn ,
CipherDeleteOptions ::HardMulti ,
nt ,
)
. await
// permanent delete
// permanent delete
}
}
@ -1587,18 +1830,26 @@ async fn delete_cipher_selected_put_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
delete_multiple_ciphers ( data , headers , conn , CipherDeleteOptions ::SoftMulti , nt ) . await
delete_multiple_ciphers (
data ,
headers ,
CipherAccessScope ::OrganizationAdmin ,
conn ,
CipherDeleteOptions ::SoftMulti ,
nt ,
)
. await
// soft delete
// soft delete
}
}
#[ put( " /ciphers/<cipher_id>/restore " ) ]
#[ put( " /ciphers/<cipher_id>/restore " ) ]
async fn restore_cipher_put ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
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 , CipherAccessScope ::User , & conn , & nt ) . await
}
}
#[ put( " /ciphers/<cipher_id>/restore-admin " ) ]
#[ put( " /ciphers/<cipher_id>/restore-admin " ) ]
async fn restore_cipher_put_admin ( cipher_id : CipherId , headers : Headers , conn : DbConn , nt : Notify < '_ > ) -> JsonResult {
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 , CipherAccessScope ::OrganizationAdmin , & conn , & nt ) . await
}
}
#[ put( " /ciphers/restore-admin " , data = " <data> " ) ]
#[ put( " /ciphers/restore-admin " , data = " <data> " ) ]
@ -1608,7 +1859,7 @@ async fn restore_cipher_selected_admin(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
restore_multiple_ciphers ( data , & headers , & conn , & nt ) . await
restore_multiple_ciphers ( data , & headers , CipherAccessScope ::OrganizationAdmin , & conn , & nt ) . await
}
}
#[ put( " /ciphers/restore " , data = " <data> " ) ]
#[ put( " /ciphers/restore " , data = " <data> " ) ]
@ -1618,7 +1869,7 @@ async fn restore_cipher_selected(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
restore_multiple_ciphers ( data , & headers , & conn , & nt ) . await
restore_multiple_ciphers ( data , & headers , CipherAccessScope ::User , & conn , & nt ) . await
}
}
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
@ -1809,6 +2060,7 @@ pub enum CipherDeleteOptions {
async fn delete_cipher_by_uuid (
async fn delete_cipher_by_uuid (
cipher_id : & CipherId ,
cipher_id : & CipherId ,
headers : & Headers ,
headers : & Headers ,
scope : CipherAccessScope ,
conn : & DbConn ,
conn : & DbConn ,
delete_options : & CipherDeleteOptions ,
delete_options : & CipherDeleteOptions ,
nt : & Notify < '_ > ,
nt : & Notify < '_ > ,
@ -1817,7 +2069,7 @@ async fn delete_cipher_by_uuid(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , conn ) . await {
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , scope , conn ) . await {
err ! ( "Cipher can't be deleted by user" )
err ! ( "Cipher can't be deleted by user" )
}
}
@ -1875,6 +2127,7 @@ struct CipherIdsData {
async fn delete_multiple_ciphers (
async fn delete_multiple_ciphers (
data : Json < CipherIdsData > ,
data : Json < CipherIdsData > ,
headers : Headers ,
headers : Headers ,
scope : CipherAccessScope ,
conn : DbConn ,
conn : DbConn ,
delete_options : CipherDeleteOptions ,
delete_options : CipherDeleteOptions ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
@ -1882,7 +2135,7 @@ async fn delete_multiple_ciphers(
let data = data . into_inner ( ) ;
let data = data . into_inner ( ) ;
for cipher_id in data . ids {
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 , scope , & conn , & delete_options , & nt ) . await {
return error ;
return error ;
}
}
}
}
@ -1897,6 +2150,7 @@ async fn restore_cipher_by_uuid(
cipher_id : & CipherId ,
cipher_id : & CipherId ,
headers : & Headers ,
headers : & Headers ,
multi_restore : bool ,
multi_restore : bool ,
scope : CipherAccessScope ,
conn : & DbConn ,
conn : & DbConn ,
nt : & Notify < '_ > ,
nt : & Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
@ -1904,7 +2158,7 @@ async fn restore_cipher_by_uuid(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , conn ) . await {
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , scope , conn ) . await {
err ! ( "Cipher can't be restored by user" )
err ! ( "Cipher can't be restored by user" )
}
}
@ -1936,12 +2190,15 @@ async fn restore_cipher_by_uuid(
. await ;
. await ;
}
}
Ok ( Json ( cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await ? ) )
// Answer with the scope the restore was authorized under, so an administrative caller without a
// personal assignment is not told `edit: false` for a cipher they just restored.
Ok ( Json ( cipher_json_for_scope ( & cipher , headers , scope , conn ) . await ? ) )
}
}
async fn restore_multiple_ciphers (
async fn restore_multiple_ciphers (
data : Json < CipherIdsData > ,
data : Json < CipherIdsData > ,
headers : & Headers ,
headers : & Headers ,
scope : CipherAccessScope ,
conn : & DbConn ,
conn : & DbConn ,
nt : & Notify < '_ > ,
nt : & Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
@ -1949,7 +2206,7 @@ async fn restore_multiple_ciphers(
let mut ciphers : Vec < Value > = Vec ::new ( ) ;
let mut ciphers : Vec < Value > = Vec ::new ( ) ;
for cipher_id in data . ids {
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 , scope , conn , nt ) . await {
Ok ( json ) = > ciphers . push ( json . into_inner ( ) ) ,
Ok ( json ) = > ciphers . push ( json . into_inner ( ) ) ,
err = > return err ,
err = > return err ,
}
}
@ -1969,6 +2226,7 @@ async fn delete_cipher_attachment_by_id(
cipher_id : & CipherId ,
cipher_id : & CipherId ,
attachment_id : & AttachmentId ,
attachment_id : & AttachmentId ,
headers : & Headers ,
headers : & Headers ,
scope : CipherAccessScope ,
conn : & DbConn ,
conn : & DbConn ,
nt : & Notify < '_ > ,
nt : & Notify < '_ > ,
) -> JsonResult {
) -> JsonResult {
@ -1984,7 +2242,7 @@ async fn delete_cipher_attachment_by_id(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , conn ) . await {
if ! cipher . is_write_accessible_to_user ( & headers . user . uuid , scope , conn ) . await {
err ! ( "Cipher cannot be deleted by user" )
err ! ( "Cipher cannot be deleted by user" )
}
}
@ -2012,7 +2270,8 @@ async fn delete_cipher_attachment_by_id(
)
)
. await ;
. await ;
}
}
let cipher_json = cipher . to_json ( & headers . host , & headers . user . uuid , None , CipherSyncType ::User , conn ) . await ? ;
// Same scope the deletion was authorized under; see `cipher_json_for_scope`.
let cipher_json = cipher_json_for_scope ( & cipher , headers , scope , conn ) . await ? ;
Ok ( Json ( json ! ( { "cipher" :cipher_json } ) ) )
Ok ( Json ( json ! ( { "cipher" :cipher_json } ) ) )
}
}
@ -2027,7 +2286,7 @@ async fn archive_cipher(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , conn ) . await {
if ! cipher . is_accessible_to_user ( & headers . user . uuid , CipherAccessScope ::User , conn ) . await {
err ! ( "Cipher is not accessible for the current user" )
err ! ( "Cipher is not accessible for the current user" )
}
}
@ -2059,7 +2318,7 @@ async fn unarchive_cipher(
err ! ( "Cipher doesn't exist" )
err ! ( "Cipher doesn't exist" )
} ;
} ;
if ! cipher . is_accessible_to_user ( & headers . user . uuid , conn ) . await {
if ! cipher . is_accessible_to_user ( & headers . user . uuid , CipherAccessScope ::User , conn ) . await {
err ! ( "Cipher is not accessible for the current user" )
err ! ( "Cipher is not accessible for the current user" )
}
}