@ -1,7 +1,7 @@
use std ::collections ::{ HashMap , HashSet } ;
use std ::collections ::{ HashMap , HashSet } ;
use num_traits ::FromPrimitive ;
use num_traits ::FromPrimitive ;
use rocket ::{ Route , serde ::json ::Json } ;
use rocket ::{ Route , http ::Status , serde ::json ::Json } ;
use serde_json ::Value ;
use serde_json ::Value ;
use crate ::{
use crate ::{
@ -17,7 +17,8 @@ use crate::{
models ::{
models ::{
Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId , CollectionUser , EventType ,
Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId , CollectionUser , EventType ,
Group , GroupId , GroupUser , Invitation , Membership , MembershipId , MembershipStatus , MembershipType ,
Group , GroupId , GroupUser , Invitation , Membership , MembershipId , MembershipStatus , MembershipType ,
OrgPolicy , OrgPolicyType , Organization , OrganizationApiKey , OrganizationId , User , UserId ,
OrgPolicy , OrgPolicyType , Organization , OrganizationApiKey , OrganizationId , TwoFactor , TwoFactorType , User ,
UserId ,
} ,
} ,
} ,
} ,
mail ,
mail ,
@ -132,7 +133,6 @@ struct FullCollectionData {
name : String ,
name : String ,
groups : Vec < CollectionGroupData > ,
groups : Vec < CollectionGroupData > ,
users : Vec < CollectionMembershipData > ,
users : Vec < CollectionMembershipData > ,
id : Option < CollectionId > ,
external_id : Option < String > ,
external_id : Option < String > ,
}
}
@ -391,7 +391,7 @@ async fn get_org_collections(org_id: OrganizationId, headers: ManagerHeadersLoos
}
}
if ! headers . membership . has_full_access ( ) {
if ! headers . membership . has_full_access ( ) {
err_code ! ( "Resource not found." , "User does not have full access" , rocket ::http ::Status ::NotFound . code ) ;
err_code ! ( "Resource not found." , "User does not have full access" , Status ::NotFound . code ) ;
}
}
Ok ( Json ( json ! ( {
Ok ( Json ( json ! ( {
@ -887,11 +887,11 @@ struct OrgIdData {
#[ get( " /ciphers/organization-details?<data..> " ) ]
#[ get( " /ciphers/organization-details?<data..> " ) ]
async fn get_org_details ( data : OrgIdData , headers : ManagerHeadersLoose , conn : DbConn ) -> JsonResult {
async fn get_org_details ( data : OrgIdData , headers : ManagerHeadersLoose , conn : DbConn ) -> JsonResult {
if data . organization_id ! = headers . membership . org_uuid {
if data . organization_id ! = headers . membership . org_uuid {
err_code ! ( "Resource not found." , "Organization id's do not match" , rocket ::http ::Status ::NotFound . code ) ;
err_code ! ( "Resource not found." , "Organization id's do not match" , Status ::NotFound . code ) ;
}
}
if ! headers . membership . has_full_access ( ) {
if ! headers . membership . has_full_access ( ) {
err_code ! ( "Resource not found." , "User does not have full access" , rocket ::http ::Status ::NotFound . code ) ;
err_code ! ( "Resource not found." , "User does not have full access" , Status ::NotFound . code ) ;
}
}
Ok ( Json ( json ! ( {
Ok ( Json ( json ! ( {
@ -959,7 +959,7 @@ async fn get_members(
}
}
if ! headers . membership . has_full_access ( ) {
if ! headers . membership . has_full_access ( ) {
err_code ! ( "Resource not found." , "User does not have full access" , rocket ::http ::Status ::NotFound . code ) ;
err_code ! ( "Resource not found." , "User does not have full access" , Status ::NotFound . code ) ;
}
}
let mut users_json = Vec ::new ( ) ;
let mut users_json = Vec ::new ( ) ;
@ -1079,7 +1079,7 @@ async fn send_invite(
& & data . permissions . get ( "deleteAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "deleteAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "createNewCollections" ) = = Some ( & json ! ( true ) ) ) ;
& & data . permissions . get ( "createNewCollections" ) = = Some ( & json ! ( true ) ) ) ;
let mut user_created : bool = false ;
let mut user_created : bool ;
for email in & data . emails {
for email in & data . emails {
let mut member_status = MembershipStatus ::Invited as i32 ;
let mut member_status = MembershipStatus ::Invited as i32 ;
let user = match User ::find_by_mail ( email , & conn ) . await {
let user = match User ::find_by_mail ( email , & conn ) . await {
@ -1114,6 +1114,7 @@ async fn send_invite(
member_status = MembershipStatus ::Accepted as i32 ;
member_status = MembershipStatus ::Accepted as i32 ;
}
}
}
}
user_created = false ;
user
user
}
}
} ;
} ;
@ -1797,11 +1798,22 @@ async fn bulk_public_keys(
use super ::ciphers ::CipherData ;
use super ::ciphers ::CipherData ;
use super ::ciphers ::update_cipher_from_data ;
use super ::ciphers ::update_cipher_from_data ;
// The import endpoint only ever uses the name/id/external_id of a collection.
// Bitwarden's own server ignores `groups`/`users` here too, so do not make them
// mandatory: clients are free to leave them out.
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct ImportCollectionData {
name : String ,
id : Option < CollectionId > ,
external_id : Option < String > ,
}
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
struct ImportData {
struct ImportData {
ciphers : Vec < CipherData > ,
ciphers : Vec < CipherData > ,
collections : Vec < FullCollectionData > ,
collections : Vec < Import CollectionData> ,
collection_relationships : Vec < RelationsData > ,
collection_relationships : Vec < RelationsData > ,
}
}
@ -2480,7 +2492,7 @@ async fn get_groups_data(
| | Collection ::has_manageable_collection_by_user ( & org_id , & headers . membership . user_uuid , & conn ) . await
| | Collection ::has_manageable_collection_by_user ( & org_id , & headers . membership . user_uuid , & conn ) . await
} ;
} ;
if ! allowed {
if ! allowed {
err_code ! ( "Resource not found." , "User does not have access" , rocket ::http ::Status ::NotFound . code ) ;
err_code ! ( "Resource not found." , "User does not have access" , Status ::NotFound . code ) ;
}
}
let groups : Vec < Value > = if CONFIG . org_groups_enabled ( ) {
let groups : Vec < Value > = if CONFIG . org_groups_enabled ( ) {
@ -2931,8 +2943,8 @@ struct OrganizationUserResetPasswordEnrollmentRequest {
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
struct OrganizationUserRecoverAccountRequest {
struct OrganizationUserRecoverAccountRequest {
new_master_password_hash : String ,
new_master_password_hash : Option < String > ,
key : String ,
key : Option < String > ,
#[ serde(default) ]
#[ serde(default) ]
reset_master_password : bool ,
reset_master_password : bool ,
@ -2976,12 +2988,7 @@ async fn put_recover_account(
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
let req = data . into_inner ( ) ;
recover_account ( org_id , member_id , headers , data . into_inner ( ) , conn , nt ) . await
if req . reset_master_password & & ! req . reset_two_factor {
recover_account ( org_id , member_id , headers , req , conn , nt ) . await
} else {
err ! ( "Unsupported operation" )
}
}
}
// Deprecated since `v2026.4.2`
// Deprecated since `v2026.4.2`
@ -3001,7 +3008,7 @@ async fn recover_account(
org_id : OrganizationId ,
org_id : OrganizationId ,
member_id : MembershipId ,
member_id : MembershipId ,
headers : AdminHeaders ,
headers : AdminHeaders ,
reset_re quest : OrganizationUserRecoverAccountRequest ,
req : OrganizationUserRecoverAccountRequest ,
conn : DbConn ,
conn : DbConn ,
nt : Notify < '_ > ,
nt : Notify < '_ > ,
) -> EmptyResult {
) -> EmptyResult {
@ -3016,7 +3023,7 @@ async fn recover_account(
err ! ( "User to reset isn't member of required organization" )
err ! ( "User to reset isn't member of required organization" )
} ;
} ;
let Some ( user ) = User ::find_by_uuid ( & member . user_uuid , & conn ) . await else {
let Some ( mut user ) = User ::find_by_uuid ( & member . user_uuid , & conn ) . await else {
err ! ( "User not found" )
err ! ( "User not found" )
} ;
} ;
@ -3029,29 +3036,56 @@ async fn recover_account(
err ! ( "Organization user must be confirmed for password reset functionality" ) ;
err ! ( "Organization user must be confirmed for password reset functionality" ) ;
}
}
// Sending email before resetting password to ensure working email configuration and the resulting
let fallback_2fa_email = if req . reset_two_factor & & CONFIG . email_2fa_auto_fallback ( ) {
// user notification. Also this might add some protection against security flaws and misuse
TwoFactor ::find_by_user_and_type ( & user . uuid , TwoFactorType ::Email as i32 , & conn ) . await . is_none ( )
if let Err ( e ) = mail ::send_admin_reset_password ( & user . email , user . display_name ( ) , & org . name ) . await {
} else {
false
} ;
// Sending email first ensure working email configuration and the resulting user notification.
// Also this might add some protection against security flaws and misuse
if let Err ( e ) = mail ::send_admin_account_recovery (
& user . email ,
user . display_name ( ) ,
& org . name ,
req . reset_master_password ,
req . reset_two_factor ,
fallback_2fa_email ,
)
. await
{
err ! ( format ! ( "Error sending user reset password email: {e:#?}" ) ) ;
err ! ( format ! ( "Error sending user reset password email: {e:#?}" ) ) ;
}
}
let mut user = user ;
if req . reset_master_password {
user . set_password ( reset_request . new_master_password_hash . as_str ( ) , Some ( reset_request . key ) , true , None , & conn )
if let Some ( key ) = req . key
& & let Some ( hash ) = req . new_master_password_hash
{
user . set_password ( hash . as_str ( ) , Some ( key ) , true , None , & conn ) . await ? ;
} else {
err_code ! ( "Unprocessable request" , "Missing fields to reset password" , Status ::UnprocessableEntity . code ) ;
}
}
if req . reset_two_factor {
TwoFactor ::delete_all_by_user ( & user . uuid , & conn ) . await ? ;
if ! fallback_2fa_email | | two_factor ::email ::find_and_activate_email_2fa ( & user . uuid , & conn ) . await . is_err ( ) {
two_factor ::enforce_2fa_policy ( & user , & headers . user . uuid , headers . device . atype , & headers . ip . ip , & conn )
. await ? ;
. await ? ;
}
}
user . save ( & conn ) . await ? ;
user . save ( & conn ) . await ? ;
nt . send_logout ( & user , None , & conn ) . await ;
nt . send_logout ( & user , None , & conn ) . await ;
log_event (
if req . reset_master_password {
EventType ::OrganizationUserAdminResetPassword ,
headers . log_event ( EventType ::OrganizationUserAdminResetPassword , & member_id , & org_id , & conn ) . await ;
& member_id ,
}
& org_id ,
& headers . user . uuid ,
if req . reset_two_factor {
headers . device . atype ,
headers . log_event ( EventType ::OrganizationUserAdminResetTwoFactor , & member_id , & org_id , & conn ) . await ;
& headers . ip . ip ,
}
& conn ,
)
. await ;
Ok ( ( ) )
Ok ( ( ) )
}
}