@ -15,10 +15,10 @@ use crate::{
db ::{
DbConn ,
models ::{
Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId , CollectionUser , EventTyp e,
Group , GroupId , GroupUser , Invitation , MaximumVaultTimeoutPolicyData , Membership , MembershipId ,
MembershipStatus , MembershipType , OrgPolicy , OrgPolicyType , Organization , OrganizationApiKey ,
OrganizationId , TwoFactor , TwoFactorType , User , UserId ,
Cipher , CipherId , Collection , CollectionCipher , CollectionGroup , CollectionId , CollectionUser , Devic e,
EventType , Group , GroupId , GroupUser , Invitation , MaximumVaultTimeoutPolicyData , Membership , MembershipId ,
MembershipStatus , MembershipType , OrgPolicy , OrgPolicyType , Organization , OrganizationApiKey , OrganizationId ,
TwoFactor , TwoFactorType , User , UserId ,
} ,
} ,
mail ,
@ -31,7 +31,6 @@ pub fn routes() -> Vec<Route> {
get_organization ,
create_organization ,
delete_organization ,
post_delete_organization ,
leave_organization ,
get_user_collections ,
get_org_collections ,
@ -39,13 +38,10 @@ pub fn routes() -> Vec<Route> {
get_org_collection_detail ,
get_collection_users ,
put_organization ,
post_organization ,
post_organization_collections ,
post_bulk_access_collections ,
post_organization_collection_update ,
put_organization_collection_update ,
delete_organization_collection ,
post_organization_collection_delete ,
bulk_delete_organization_collections ,
post_bulk_collections ,
get_org_details ,
@ -60,7 +56,6 @@ pub fn routes() -> Vec<Route> {
get_org_user_mini_details ,
get_user ,
edit_member ,
put_member ,
delete_member ,
bulk_delete_member ,
post_org_import ,
@ -70,7 +65,6 @@ pub fn routes() -> Vec<Route> {
get_master_password_policy ,
get_policy ,
put_policy ,
put_policy_vnext ,
get_plans ,
post_org_keys ,
get_organization_keys ,
@ -86,22 +80,16 @@ pub fn routes() -> Vec<Route> {
post_groups ,
get_group ,
put_group ,
post_group ,
get_group_details ,
delete_group ,
post_delete_group ,
bulk_delete_groups ,
get_group_members ,
put_group_members ,
post_delete_group_member ,
put_reset_password_enrollment ,
get_reset_password_details ,
put_reset_password ,
put_recover_account ,
get_org_export ,
post_api_key ,
rotate_api_key ,
get_billing_metadata ,
get_billing_warnings ,
get_auto_enroll_status ,
get_self_host_billing_metadata ,
@ -245,16 +233,6 @@ async fn delete_organization(
}
}
#[ post( " /organizations/<org_id>/delete " , data = " <data> " ) ]
async fn post_delete_organization (
org_id : OrganizationId ,
data : Json < PasswordOrOtpData > ,
headers : OwnerHeaders ,
conn : DbConn ,
) -> EmptyResult {
delete_organization ( org_id , data , headers , conn ) . await
}
#[ post( " /organizations/<org_id>/leave " ) ]
async fn leave_organization ( org_id : OrganizationId , headers : OrgMemberHeaders , conn : DbConn ) -> EmptyResult {
if headers . membership . status ! = MembershipStatus ::Confirmed as i32 {
@ -300,16 +278,6 @@ async fn put_organization(
headers : OwnerHeaders ,
data : Json < OrganizationUpdateData > ,
conn : DbConn ,
) -> JsonResult {
post_organization ( org_id , headers , data , conn ) . await
}
#[ post( " /organizations/<org_id> " , data = " <data> " ) ]
async fn post_organization (
org_id : OrganizationId ,
headers : OwnerHeaders ,
data : Json < OrganizationUpdateData > ,
conn : DbConn ,
) -> JsonResult {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
@ -415,8 +383,9 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
let col_users = CollectionUser ::find_by_organization_swap_user_uuid_with_member_uuid ( & org_id , & conn ) . await ;
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
// This lists other members for admins, so it must not depend on the membership status
let membership_type : HashMap < MembershipId , i32 > =
Membership ::find_confirmed_ by_org ( & org_id , & conn ) . await . into_iter ( ) . map ( | m | ( m . uuid , m . atype ) ) . collect ( ) ;
Membership ::find_by_org ( & org_id , & conn ) . await . into_iter ( ) . map ( | m | ( m . uuid , m . atype ) ) . collect ( ) ;
// check if current user has full access to the organization (either directly or via any group)
let has_full_access_to_org = member . has_full_access ( )
@ -639,17 +608,6 @@ async fn put_organization_collection_update(
headers : ManagerHeaders ,
data : Json < FullCollectionData > ,
conn : DbConn ,
) -> JsonResult {
post_organization_collection_update ( org_id , col_id , headers , data , conn ) . await
}
#[ post( " /organizations/<org_id>/collections/<col_id> " , data = " <data> " , rank = 2) ]
async fn post_organization_collection_update (
org_id : OrganizationId ,
col_id : CollectionId ,
headers : ManagerHeaders ,
data : Json < FullCollectionData > ,
conn : DbConn ,
) -> JsonResult {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
@ -745,16 +703,6 @@ async fn delete_organization_collection(
delete_organization_collection_impl ( & org_id , & col_id , & headers , & conn ) . await
}
#[ post( " /organizations/<org_id>/collections/<col_id>/delete " ) ]
async fn post_organization_collection_delete (
org_id : OrganizationId ,
col_id : CollectionId ,
headers : ManagerHeaders ,
conn : DbConn ,
) -> EmptyResult {
delete_organization_collection_impl ( & org_id , & col_id , & headers , & conn ) . await
}
#[ derive(Deserialize, Debug) ]
#[ serde(rename_all = " camelCase " ) ]
struct BulkCollectionIds {
@ -818,11 +766,9 @@ async fn get_org_collection_detail(
// Generate a HashMap to get the correct MembershipType per user to determine the manage permission
// We use the uuid instead of the user_uuid here, since that is what is used in CollectionUser
let membership_type : HashMap < MembershipId , i32 > = Membership ::find_confirmed_by_org ( & org_id , & conn )
. await
. into_iter ( )
. map ( | m | ( m . uuid , m . atype ) )
. collect ( ) ;
// This lists other members for admins, so it must not depend on the membership status
let membership_type : HashMap < MembershipId , i32 > =
Membership ::find_by_org ( & org_id , & conn ) . await . into_iter ( ) . map ( | m | ( m . uuid , m . atype ) ) . collect ( ) ;
let users : Vec < Value > =
CollectionUser ::find_by_org_and_coll_swap_user_uuid_with_member_uuid ( & org_id , & collection . uuid , & conn )
@ -960,8 +906,9 @@ async fn get_members(
let mut users_json = Vec ::new ( ) ;
for u in Membership ::find_by_org ( & org_id , & conn ) . await {
// The user can be a manager instead of an admin, but we've checked above that they have full access
users_json . push (
u . to_json_user_ details (
u . to_json_details_for_admin (
data . include_collections . unwrap_or ( false ) ,
data . include_groups . unwrap_or ( false ) ,
& conn ,
@ -1516,7 +1463,9 @@ async fn get_user(
// In this case, when groups are requested we also need to include collections.
// Else these will not be shown in the interface, and could lead to missing collections when saved.
let include_groups = data . include_groups . unwrap_or ( false ) ;
Ok ( Json ( user . to_json_user_details ( data . include_collections . unwrap_or ( include_groups ) , include_groups , & conn ) . await ) )
Ok ( Json (
user . to_json_details_for_admin ( data . include_collections . unwrap_or ( include_groups ) , include_groups , & conn ) . await ,
) )
}
#[ derive(Deserialize) ]
@ -1530,17 +1479,6 @@ struct EditUserData {
}
#[ put( " /organizations/<org_id>/users/<member_id> " , data = " <data> " , rank = 1) ]
async fn put_member (
org_id : OrganizationId ,
member_id : MembershipId ,
data : Json < EditUserData > ,
headers : AdminHeaders ,
conn : DbConn ,
) -> EmptyResult {
edit_member ( org_id , member_id , data , headers , conn ) . await
}
#[ post( " /organizations/<org_id>/users/<member_id> " , data = " <data> " , rank = 1) ]
async fn edit_member (
org_id : OrganizationId ,
member_id : MembershipId ,
@ -1835,6 +1773,10 @@ async fn post_org_import(
if org_id ! = headers . membership . org_uuid {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
// OrgMemberHeaders also allows invited and accepted members, which are not allowed to import
if headers . membership . status ! = MembershipStatus ::Confirmed as i32 {
err ! ( "You need to be a Member of the Organization to call this endpoint" )
}
let data : ImportData = data . into_inner ( ) ;
// Validate the import before continuing
@ -1916,7 +1858,7 @@ async fn post_org_import(
#[ serde(rename_all = " camelCase " ) ]
struct BulkCollectionsData {
organization_id : OrganizationId ,
cipher_ids : Vec < CipherId > ,
cipher_ids : HashSet < CipherId > ,
collection_ids : HashSet < CollectionId > ,
remove_collections : bool ,
}
@ -2207,18 +2149,6 @@ async fn put_policy(
Ok ( Json ( policy . to_json ( ) ) )
}
// Deprecated with client v2026.5.0
#[ put( " /organizations/<org_id>/policies/<pol_type>/vnext " , data = " <data> " ) ]
async fn put_policy_vnext (
org_id : OrganizationId ,
pol_type : i32 ,
data : Json < PutPolicy > ,
headers : AdminHeaders ,
conn : DbConn ,
) -> JsonResult {
put_policy ( org_id , pol_type , data , headers , conn ) . await
}
#[ get( " /plans " ) ]
fn get_plans ( ) -> Json < Value > {
// Respond with a minimal json just enough to allow the creation of an new organization.
@ -2247,12 +2177,6 @@ fn get_plans() -> Json<Value> {
} ) )
}
#[ get( " /organizations/<_org_id>/billing/metadata " ) ]
fn get_billing_metadata ( _org_id : OrganizationId , _headers : OrgMemberHeaders ) -> Json < Value > {
// Prevent a 404 error, which also causes Javascript errors.
Json ( empty_data_json ( ) )
}
#[ get( " /organizations/<_org_id>/billing/vnext/warnings " ) ]
fn get_billing_warnings ( _org_id : OrganizationId , _headers : OrgMemberHeaders ) -> Json < Value > {
Json ( json ! ( {
@ -2272,14 +2196,6 @@ fn get_self_host_billing_metadata(_org_id: OrganizationId, _headers: OrgMemberHe
} ) )
}
fn empty_data_json ( ) -> Value {
json ! ( {
"object" : "list" ,
"data" : [ ] ,
"continuationToken" : null
} )
}
#[ derive(Deserialize, Debug) ]
#[ serde(rename_all = " camelCase " ) ]
struct BulkRevokeMembershipIds {
@ -2598,17 +2514,6 @@ impl CollectionData {
}
}
#[ post( " /organizations/<org_id>/groups/<group_id> " , data = " <data> " ) ]
async fn post_group (
org_id : OrganizationId ,
group_id : GroupId ,
data : Json < GroupRequest > ,
headers : AdminHeaders ,
conn : DbConn ,
) -> JsonResult {
put_group ( org_id , group_id , data , headers , conn ) . await
}
#[ post( " /organizations/<org_id>/groups " , data = " <data> " ) ]
async fn post_groups (
org_id : OrganizationId ,
@ -2718,7 +2623,6 @@ async fn add_update_group(
"id" : group . uuid ,
"organizationId" : group . organizations_uuid ,
"name" : group . name ,
"accessAll" : group . access_all ,
"externalId" : group . external_id ,
"object" : "group"
} ) ) )
@ -2745,16 +2649,6 @@ async fn get_group_details(
Ok ( Json ( group . to_json_details ( & conn ) . await ) )
}
#[ post( " /organizations/<org_id>/groups/<group_id>/delete " ) ]
async fn post_delete_group (
org_id : OrganizationId ,
group_id : GroupId ,
headers : AdminHeaders ,
conn : DbConn ,
) -> EmptyResult {
delete_group_impl ( & org_id , & group_id , & headers , & conn ) . await
}
#[ delete( " /organizations/<org_id>/groups/<group_id> " ) ]
async fn delete_group ( org_id : OrganizationId , group_id : GroupId , headers : AdminHeaders , conn : DbConn ) -> EmptyResult {
delete_group_impl ( & org_id , & group_id , & headers , & conn ) . await
@ -2856,90 +2750,6 @@ async fn get_group_members(
Ok ( Json ( json ! ( group_members ) ) )
}
#[ put( " /organizations/<org_id>/groups/<group_id>/users " , data = " <data> " ) ]
async fn put_group_members (
org_id : OrganizationId ,
group_id : GroupId ,
headers : AdminHeaders ,
data : Json < Vec < MembershipId > > ,
conn : DbConn ,
) -> EmptyResult {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
if ! CONFIG . org_groups_enabled ( ) {
err ! ( "Group support is disabled" ) ;
}
if Group ::find_by_uuid_and_org ( & group_id , & org_id , & conn ) . await . is_none ( ) {
err ! ( "Group could not be found!" , "Group uuid is invalid or does not belong to the organization" )
}
let assigned_members = data . into_inner ( ) ;
let org_memberships = Membership ::find_by_org ( & org_id , & conn ) . await ;
let org_membership_ids : HashSet < & MembershipId > = org_memberships . iter ( ) . map ( | m | & m . uuid ) . collect ( ) ;
if let Some ( e ) = assigned_members . iter ( ) . find ( | m | ! org_membership_ids . contains ( m ) ) {
err ! ( "Invalid member" , format ! ( "Member {} does not belong to organization {}!" , e , org_id ) )
}
GroupUser ::delete_all_by_group ( & group_id , & org_id , & conn ) . await ? ;
for assigned_member in assigned_members {
let mut user_entry = GroupUser ::new ( group_id . clone ( ) , assigned_member . clone ( ) ) ;
user_entry . save ( & conn ) . await ? ;
log_event (
EventType ::OrganizationUserUpdatedGroups ,
& assigned_member ,
& org_id ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& conn ,
)
. await ;
}
Ok ( ( ) )
}
#[ post( " /organizations/<org_id>/groups/<group_id>/delete-user/<member_id> " ) ]
async fn post_delete_group_member (
org_id : OrganizationId ,
group_id : GroupId ,
member_id : MembershipId ,
headers : AdminHeaders ,
conn : DbConn ,
) -> EmptyResult {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
if ! CONFIG . org_groups_enabled ( ) {
err ! ( "Group support is disabled" ) ;
}
if Membership ::find_by_uuid_and_org ( & member_id , & org_id , & conn ) . await . is_none ( ) {
err ! ( "User could not be found or does not belong to the organization." ) ;
}
if Group ::find_by_uuid_and_org ( & group_id , & org_id , & conn ) . await . is_none ( ) {
err ! ( "Group could not be found or does not belong to the organization." ) ;
}
log_event (
EventType ::OrganizationUserUpdatedGroups ,
& member_id ,
& org_id ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& conn ,
)
. await ;
GroupUser ::delete_by_group_and_member ( & group_id , & member_id , & conn ) . await
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct OrganizationUserResetPasswordEnrollmentRequest {
@ -2999,19 +2809,6 @@ async fn put_recover_account(
recover_account ( org_id , member_id , headers , data . into_inner ( ) , conn , nt ) . await
}
// Deprecated since `v2026.4.2`
#[ put( " /organizations/<org_id>/users/<member_id>/reset-password " , data = " <data> " ) ]
async fn put_reset_password (
org_id : OrganizationId ,
member_id : MembershipId ,
headers : AdminHeaders ,
data : Json < OrganizationUserRecoverAccountRequest > ,
conn : DbConn ,
nt : Notify < '_ > ,
) -> EmptyResult {
recover_account ( org_id , member_id , headers , data . into_inner ( ) , conn , nt ) . await
}
async fn recover_account (
org_id : OrganizationId ,
member_id : MembershipId ,
@ -3044,11 +2841,12 @@ async fn recover_account(
err ! ( "Organization user must be confirmed for password reset functionality" ) ;
}
let fallback_2fa_email = if req . reset_two_factor & & CONFIG . email_2fa_auto_fallback ( ) {
TwoFactor ::find_by_user_and_type ( & user . uuid , TwoFactorType ::Email as i32 , & conn ) . await . is_none ( )
} else {
false
} ;
let fallback_2fa_email =
if req . reset_two_factor & & CONFIG . mail_enabled ( ) & & CONFIG . email_2fa_auto_fallback ( ) & & user . verified ( ) {
TwoFactor ::find_by_user_and_type ( & user . uuid , TwoFactorType ::Email as i32 , & conn ) . await . is_none ( )
} 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
@ -3077,6 +2875,7 @@ async fn recover_account(
if req . reset_two_factor {
TwoFactor ::delete_all_by_user ( & user . uuid , & conn ) . await ? ;
Device ::clear_twofactor_remember_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 ? ;
@ -3245,7 +3044,7 @@ async fn api_key(
org_id : & OrganizationId ,
data : Json < PasswordOrOtpData > ,
rotate : bool ,
headers : Admin Headers,
headers : Owner Headers,
conn : DbConn ,
) -> JsonResult {
if org_id ! = & headers . org_id {
@ -3254,7 +3053,7 @@ async fn api_key(
let data : PasswordOrOtpData = data . into_inner ( ) ;
let user = headers . user ;
// Validate the admin users password/otp
// Validate the owner users password/otp
data . validate ( & user , true , & conn ) . await ? ;
let org_api_key = if let Some ( mut org_api_key ) = OrganizationApiKey ::find_by_org_uuid ( org_id , & conn ) . await {
@ -3282,7 +3081,7 @@ async fn api_key(
async fn post_api_key (
org_id : OrganizationId ,
data : Json < PasswordOrOtpData > ,
headers : Admin Headers,
headers : Owner Headers,
conn : DbConn ,
) -> JsonResult {
api_key ( & org_id , data , false , headers , conn ) . await
@ -3292,7 +3091,7 @@ async fn post_api_key(
async fn rotate_api_key (
org_id : OrganizationId ,
data : Json < PasswordOrOtpData > ,
headers : Admin Headers,
headers : Owner Headers,
conn : DbConn ,
) -> JsonResult {
api_key ( & org_id , data , true , headers , conn ) . await