@ -20,7 +20,8 @@ use crate::{
DbConn ,
} ,
mail ,
util ::{ convert_json_key_lcase_first , get_uuid , NumberOrString } ,
sso ::FAKE_SSO_IDENTIFIER ,
util ::{ convert_json_key_lcase_first , NumberOrString } ,
CONFIG ,
} ;
@ -64,6 +65,7 @@ pub fn routes() -> Vec<Route> {
post_org_import ,
list_policies ,
list_policies_token ,
get_dummy_master_password_policy ,
get_master_password_policy ,
get_policy ,
put_policy ,
@ -76,6 +78,7 @@ pub fn routes() -> Vec<Route> {
revoke_member ,
bulk_revoke_members ,
restore_member ,
restore_member_vnext ,
bulk_restore_members ,
get_groups ,
get_groups_details ,
@ -99,6 +102,7 @@ pub fn routes() -> Vec<Route> {
get_billing_metadata ,
get_billing_warnings ,
get_auto_enroll_status ,
get_self_host_billing_metadata ,
]
}
@ -131,6 +135,24 @@ struct FullCollectionData {
external_id : Option < String > ,
}
impl FullCollectionData {
pub async fn validate ( & self , org_id : & OrganizationId , conn : & DbConn ) -> EmptyResult {
let org_groups = Group ::find_by_organization ( org_id , conn ) . await ;
let org_group_ids : HashSet < & GroupId > = org_groups . iter ( ) . map ( | c | & c . uuid ) . collect ( ) ;
if let Some ( e ) = self . groups . iter ( ) . find ( | g | ! org_group_ids . contains ( & g . id ) ) {
err ! ( "Invalid group" , format ! ( "Group {} does not belong to organization {}!" , e . id , org_id ) )
}
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 ) = self . users . iter ( ) . find ( | m | ! org_membership_ids . contains ( & m . id ) ) {
err ! ( "Invalid member" , format ! ( "Member {} does not belong to organization {}!" , e . id , org_id ) )
}
Ok ( ( ) )
}
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct CollectionGroupData {
@ -233,11 +255,13 @@ async fn post_delete_organization(
}
#[ post( " /organizations/<org_id>/leave " ) ]
async fn leave_organization ( org_id : OrganizationId , headers : Headers , conn : DbConn ) -> EmptyResult {
match Membership ::find_by_user_and_org ( & headers . user . uuid , & org_id , & conn ) . await {
None = > err ! ( "User not part of organization" ) ,
Some ( member ) = > {
if member . atype = = MembershipType ::Owner
async fn leave_organization ( org_id : OrganizationId , headers : OrgMemberHeaders , conn : DbConn ) -> EmptyResult {
if headers . membership . status ! = MembershipStatus ::Confirmed as i32 {
err ! ( "You need to be a Member of the Organization to call this endpoint" )
}
let membership = headers . membership ;
if membership . atype = = MembershipType ::Owner
& & Membership ::count_confirmed_by_org_and_type ( & org_id , MembershipType ::Owner , & conn ) . await < = 1
{
err ! ( "The last owner can't leave" )
@ -245,7 +269,7 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbCo
log_event (
EventType ::OrganizationUserLeft as i32 ,
& member . uuid ,
& membership . uuid ,
& org_id ,
& headers . user . uuid ,
headers . device . atype ,
@ -254,9 +278,7 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbCo
)
. await ;
member . delete ( & conn ) . await
}
}
membership . delete ( & conn ) . await
}
#[ get( " /organizations/<org_id> " ) ]
@ -335,7 +357,7 @@ async fn get_user_collections(headers: Headers, conn: DbConn) -> Json<Value> {
// The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it
#[ get( " /organizations/<identifier>/auto-enroll-status " ) ]
async fn get_auto_enroll_status ( identifier : & str , headers : Headers , conn : DbConn ) -> JsonResult {
let org = if identifier = = crate ::sso ::FAKE_IDENTIFIER {
let org = if identifier = = FAKE_SSO _IDENTIFIER {
match Membership ::find_main_user_org ( & headers . user . uuid , & conn ) . await {
Some ( member ) = > Organization ::find_by_uuid ( & member . org_uuid , & conn ) . await ,
None = > None ,
@ -345,7 +367,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn
} ;
let ( id , identifier , rp_auto_enroll ) = match org {
None = > ( get_uuid ( ) , identifier . to_string ( ) , false ) ,
None = > ( identifier . to_string ( ) , identifier . to_string ( ) , false ) ,
Some ( org ) = > (
org . uuid . to_string ( ) ,
org . uuid . to_string ( ) ,
@ -395,7 +417,7 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
Membership ::find_confirmed_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 . access_all
let has_full_access_to_org = member . has_full_access ( )
| | ( CONFIG . org_groups_enabled ( ) & & GroupUser ::has_full_access_by_member ( & org_id , & member . uuid , & conn ) . await ) ;
// Get all admins, owners and managers who can manage/access all
@ -421,6 +443,11 @@ async fn get_org_collections_details(org_id: OrganizationId, headers: ManagerHea
| | ( CONFIG . org_groups_enabled ( )
& & GroupUser ::has_access_to_collection_by_member ( & col . uuid , & member . uuid , & conn ) . await ) ;
// If the user is a manager, and is not assigned to this collection, skip this and continue with the next collection
if ! assigned {
continue ;
}
// get the users assigned directly to the given collection
let mut users : Vec < Value > = col_users
. iter ( )
@ -475,12 +502,13 @@ async fn post_organization_collections(
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
let data : FullCollectionData = data . into_inner ( ) ;
data . validate ( & org_id , & conn ) . await ? ;
let Some ( org ) = Organization ::find_by_uuid ( & org_id , & conn ) . await else {
err ! ( "Can't find organization detail s" )
} ;
if headers . membership . atype = = MembershipType ::Manager & & ! headers . membership . access_all {
err ! ( "You don't have permission to create collection s" )
}
let collection = Collection ::new ( org . uuid , data . name , data . external_id ) ;
let collection = Collection ::new ( org_id . clone ( ) , data . name , data . external_id ) ;
collection . save ( & conn ) . await ? ;
log_event (
@ -496,7 +524,7 @@ async fn post_organization_collections(
for group in data . groups {
CollectionGroup ::new ( collection . uuid . clone ( ) , group . id , group . read_only , group . hide_passwords , group . manage )
. save ( & conn )
. save ( & org_id , & conn )
. await ? ;
}
@ -520,10 +548,6 @@ async fn post_organization_collections(
. await ? ;
}
if headers . membership . atype = = MembershipType ::Manager & & ! headers . membership . access_all {
CollectionUser ::save ( & headers . membership . user_uuid , & collection . uuid , false , false , false , & conn ) . await ? ;
}
Ok ( Json ( collection . to_json_details ( & headers . membership . user_uuid , None , & conn ) . await ) )
}
@ -574,10 +598,10 @@ async fn post_bulk_access_collections(
)
. await ;
CollectionGroup ::delete_all_by_collection ( & col_id , & conn ) . await ? ;
CollectionGroup ::delete_all_by_collection ( & col_id , & org_id , & conn ) . await ? ;
for group in & data . groups {
CollectionGroup ::new ( col_id . clone ( ) , group . id . clone ( ) , group . read_only , group . hide_passwords , group . manage )
. save ( & conn )
. save ( & org_id , & conn )
. await ? ;
}
@ -622,6 +646,7 @@ async fn post_organization_collection_update(
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
let data : FullCollectionData = data . into_inner ( ) ;
data . validate ( & org_id , & conn ) . await ? ;
if Organization ::find_by_uuid ( & org_id , & conn ) . await . is_none ( ) {
err ! ( "Can't find organization details" )
@ -650,11 +675,11 @@ async fn post_organization_collection_update(
)
. await ;
CollectionGroup ::delete_all_by_collection ( & col_id , & conn ) . await ? ;
CollectionGroup ::delete_all_by_collection ( & col_id , & org_id , & conn ) . await ? ;
for group in data . groups {
CollectionGroup ::new ( col_id . clone ( ) , group . id , group . read_only , group . hide_passwords , group . manage )
. save ( & conn )
. save ( & org_id , & conn )
. await ? ;
}
@ -883,36 +908,21 @@ async fn _get_org_details(
Ok ( json ! ( ciphers_json ) )
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct OrgDomainDetails {
email : String ,
}
// Returning a Domain/Organization here allow to prefill it and prevent prompting the user
// So we either return an Org name associated to the user or a dummy value.
// So we return a dummy value, since we only support a single SSO integration, and do not use the response anywhere
// In use since `v2025.6.0`, appears to use only the first `organizationIdentifier`
#[ post( " /organizations/domain/sso/verified " , data = " <data> " ) ]
async fn get_org_domain_sso_verified ( data : Json < OrgDomainDetails > , conn : DbConn ) -> JsonResult {
let data : OrgDomainDetails = data . into_inner ( ) ;
let identifiers = match Organization ::find_org_user_email ( & data . email , & conn )
. await
. into_iter ( )
. map ( | o | ( o . name , o . uuid . to_string ( ) ) )
. collect ::< Vec < ( String , String ) > > ( )
{
v if ! v . is_empty ( ) = > v ,
_ = > vec ! [ ( crate ::sso ::FAKE_IDENTIFIER . to_string ( ) , crate ::sso ::FAKE_IDENTIFIER . to_string ( ) ) ] ,
} ;
#[ post( " /organizations/domain/sso/verified " ) ]
fn get_org_domain_sso_verified ( ) -> JsonResult {
// Always return a dummy value, no matter if SSO is enabled or not
Ok ( Json ( json ! ( {
"object" : "list" ,
"data" : identifiers . into_iter ( ) . map ( | ( name , identifier ) | json ! ( {
"organizationName" : name , // appear unused
"organizationIdentifier" : identifier ,
"domainName" : CONFIG . domain ( ) , // appear unused
} ) ) . collect ::< Vec < Value > > ( )
"data" : [ {
"organizationIdentifier" : FAKE_SSO_IDENTIFIER ,
// These appear to be unused
"organizationName" : FAKE_SSO_IDENTIFIER ,
"domainName" : CONFIG . domain ( )
} ] ,
"continuationToken" : null
} ) ) )
}
@ -998,6 +1008,24 @@ struct InviteData {
permissions : HashMap < String , Value > ,
}
impl InviteData {
async fn validate ( & self , org_id : & OrganizationId , conn : & DbConn ) -> EmptyResult {
let org_collections = Collection ::find_by_organization ( org_id , conn ) . await ;
let org_collection_ids : HashSet < & CollectionId > = org_collections . iter ( ) . map ( | c | & c . uuid ) . collect ( ) ;
if let Some ( e ) = self . collections . iter ( ) . flatten ( ) . find ( | c | ! org_collection_ids . contains ( & c . id ) ) {
err ! ( "Invalid collection" , format ! ( "Collection {} does not belong to organization {}!" , e . id , org_id ) )
}
let org_groups = Group ::find_by_organization ( org_id , conn ) . await ;
let org_group_ids : HashSet < & GroupId > = org_groups . iter ( ) . map ( | c | & c . uuid ) . collect ( ) ;
if let Some ( e ) = self . groups . iter ( ) . find ( | g | ! org_group_ids . contains ( g ) ) {
err ! ( "Invalid group" , format ! ( "Group {} does not belong to organization {}!" , e , org_id ) )
}
Ok ( ( ) )
}
}
#[ post( " /organizations/<org_id>/users/invite " , data = " <data> " ) ]
async fn send_invite (
org_id : OrganizationId ,
@ -1009,6 +1037,7 @@ async fn send_invite(
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
let data : InviteData = data . into_inner ( ) ;
data . validate ( & org_id , & conn ) . await ? ;
// HACK: We need the raw user-type to be sure custom role is selected to determine the access_all permission
// The from_str() will convert the custom role type into a manager role type
@ -1268,20 +1297,20 @@ async fn accept_invite(
// skip invitation logic when we were invited via the /admin panel
if * * member_id ! = FAKE_ADMIN_UUID {
let Some ( mut member ) = Membership ::find_by_uuid_and_org ( member_id , & claims . org_id , & conn ) . await else {
let Some ( mut membership ) = Membership ::find_by_uuid_and_org ( member_id , & claims . org_id , & conn ) . await else {
err ! ( "Error accepting the invitation" )
} ;
let reset_password_key = match OrgPolicy ::org_is_reset_password_auto_enroll ( & member . org_uuid , & conn ) . await {
let reset_password_key = match OrgPolicy ::org_is_reset_password_auto_enroll ( & membership . org_uuid , & conn ) . await {
true if data . reset_password_key . is_none ( ) = > err ! ( "Reset password key is required, but not provided." ) ,
true = > data . reset_password_key ,
false = > None ,
} ;
// In case the user was invited before the mail was saved in db.
member . invited_by_email = member . invited_by_email . or ( claims . invited_by_email ) ;
membership . invited_by_email = membership . invited_by_email . or ( claims . invited_by_email ) ;
accept_org_invite ( & headers . user , member , reset_password_key , & conn ) . await ? ;
accept_org_invite ( & headers . user , membership , reset_password_key , & conn ) . await ? ;
} else if CONFIG . mail_enabled ( ) {
// User was invited from /admin, so they are automatically confirmed
let org_name = CONFIG . invitation_org_name ( ) ;
@ -1420,7 +1449,7 @@ async fn _confirm_invite(
let save_result = member_to_confirm . save ( conn ) . await ;
if let Some ( user ) = User ::find_by_uuid ( & member_to_confirm . user_uuid , conn ) . await {
nt . send_user_update ( UpdateType ::SyncOrgKeys , & user , & headers . device . push_uuid , conn ) . await ;
nt . send_user_update ( UpdateType ::SyncOrgKeys , & user , headers . device . push_uuid . as_ref ( ) , conn ) . await ;
}
save_result
@ -1515,9 +1544,8 @@ async fn edit_member(
& & data . permissions . get ( "deleteAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "createNewCollections" ) = = Some ( & json ! ( true ) ) ) ;
let mut member_to_edit = match Membership ::find_by_uuid_and_org ( & member_id , & org_id , & conn ) . await {
Some ( member ) = > member ,
None = > err ! ( "The specified user isn't member of the organization" ) ,
let Some ( mut member_to_edit ) = Membership ::find_by_uuid_and_org ( & member_id , & org_id , & conn ) . await else {
err ! ( "The specified user isn't member of the organization" )
} ;
if new_type ! = member_to_edit . atype
@ -1679,7 +1707,7 @@ async fn _delete_member(
. await ;
if let Some ( user ) = User ::find_by_uuid ( & member_to_delete . user_uuid , conn ) . await {
nt . send_user_update ( UpdateType ::SyncOrgKeys , & user , & headers . device . push_uuid , conn ) . await ;
nt . send_user_update ( UpdateType ::SyncOrgKeys , & user , headers . device . push_uuid . as_ref ( ) , conn ) . await ;
}
member_to_delete . delete ( conn ) . await
@ -1834,7 +1862,6 @@ async fn post_org_import(
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ allow(dead_code) ]
struct BulkCollectionsData {
organization_id : OrganizationId ,
cipher_ids : Vec < CipherId > ,
@ -1848,6 +1875,10 @@ struct BulkCollectionsData {
async fn post_bulk_collections ( data : Json < BulkCollectionsData > , headers : Headers , conn : DbConn ) -> EmptyResult {
let data : BulkCollectionsData = data . into_inner ( ) ;
if Membership ::find_confirmed_by_user_and_org ( & headers . user . uuid , & data . organization_id , & conn ) . await . is_none ( ) {
err ! ( "You need to be a Member of the Organization to call this endpoint" )
}
// Get all the collection available to the user in one query
// Also filter based upon the provided collections
let user_collections : HashMap < CollectionId , Collection > =
@ -1863,7 +1894,7 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
} )
. collect ( ) ;
// Verify if all the collections requested exists and are write able for the user, else abort
// Verify if all the collections requested exists and are writable for the user, else abort
for collection_uuid in & data . collection_ids {
match user_collections . get ( collection_uuid ) {
Some ( collection ) if collection . is_writable_by_user ( & headers . user . uuid , & conn ) . await = > ( ) ,
@ -1933,10 +1964,20 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn)
} ) ) )
}
// Called during the SSO enrollment.
// Return the org policy if it exists, otherwise use the default one.
#[ get( " /organizations/<org_id>/policies/master-password " , rank = 1) ]
async fn get_master_password_policy ( org_id : OrganizationId , _headers : Headers , conn : DbConn ) -> JsonResult {
// Called during the SSO enrollment return the default policy
#[ get( " /organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password " , rank = 1) ]
fn get_dummy_master_password_policy ( ) -> JsonResult {
let ( enabled , data ) = match CONFIG . sso_master_password_policy_value ( ) {
Some ( policy ) if CONFIG . sso_enabled ( ) = > ( true , policy . to_string ( ) ) ,
_ = > ( false , "null" . to_string ( ) ) ,
} ;
let policy = OrgPolicy ::new ( FAKE_SSO_IDENTIFIER . into ( ) , OrgPolicyType ::MasterPassword , enabled , data ) ;
Ok ( Json ( policy . to_json ( ) ) )
}
// Called during the SSO enrollment return the org policy if it exists
#[ get( " /organizations/<org_id>/policies/master-password " , rank = 2) ]
async fn get_master_password_policy ( org_id : OrganizationId , _headers : OrgMemberHeaders , conn : DbConn ) -> JsonResult {
let policy =
OrgPolicy ::find_by_org_and_type ( & org_id , OrgPolicyType ::MasterPassword , & conn ) . await . unwrap_or_else ( | | {
let ( enabled , data ) = match CONFIG . sso_master_password_policy_value ( ) {
@ -1950,7 +1991,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, c
Ok ( Json ( policy . to_json ( ) ) )
}
#[ get( " /organizations/<org_id>/policies/<pol_type> " , rank = 2 ) ]
#[ get( " /organizations/<org_id>/policies/<pol_type> " , rank = 3 ) ]
async fn get_policy ( org_id : OrganizationId , pol_type : i32 , headers : AdminHeaders , conn : DbConn ) -> JsonResult {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
@ -2144,13 +2185,13 @@ fn get_plans() -> Json<Value> {
}
#[ get( " /organizations/<_org_id>/billing/metadata " ) ]
fn get_billing_metadata ( _org_id : OrganizationId , _headers : Headers ) -> Json < Value > {
fn get_billing_metadata ( _org_id : OrganizationId , _headers : OrgMember Headers) -> 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 : Headers ) -> Json < Value > {
fn get_billing_warnings ( _org_id : OrganizationId , _headers : OrgMember Headers) -> Json < Value > {
Json ( json ! ( {
"freeTrial" :null ,
"inactiveSubscription" :null ,
@ -2159,6 +2200,15 @@ fn get_billing_warnings(_org_id: OrganizationId, _headers: Headers) -> Json<Valu
} ) )
}
#[ get( " /organizations/<_org_id>/billing/vnext/self-host/metadata " ) ]
fn get_self_host_billing_metadata ( _org_id : OrganizationId , _headers : OrgMemberHeaders ) -> Json < Value > {
// Prevent a 404 error, which also causes Javascript errors.
Json ( json ! ( {
"isOnSecretsManagerStandalone" : false , // Secrets Manager is not supported by Vaultwarden
"organizationOccupiedSeats" : 0 // Vaultwarden does not count seats
} ) )
}
fn _empty_data_json ( ) -> Value {
json ! ( {
"object" : "list" ,
@ -2266,6 +2316,18 @@ async fn _revoke_member(
Ok ( ( ) )
}
#[ put( " /organizations/<org_id>/users/<member_id>/restore/vnext " ) ]
async fn restore_member_vnext (
org_id : OrganizationId ,
member_id : MembershipId ,
headers : AdminHeaders ,
conn : DbConn ,
) -> EmptyResult {
// Vaultwarden does not (yet) support the per User Collection linked to the `Enforce organization data ownership` policy.
// Therefor we ignore the `defaultUserCollectionName` data sent and just call restore_member
_restore_member ( & org_id , & member_id , & headers , & conn ) . await
}
#[ put( " /organizations/<org_id>/users/<member_id>/restore " ) ]
async fn restore_member (
org_id : OrganizationId ,
@ -2422,6 +2484,23 @@ impl GroupRequest {
group
}
/// Validate if all the collections and members belong to the provided organization
pub async fn validate ( & self , org_id : & OrganizationId , conn : & DbConn ) -> EmptyResult {
let org_collections = Collection ::find_by_organization ( org_id , conn ) . await ;
let org_collection_ids : HashSet < & CollectionId > = org_collections . iter ( ) . map ( | c | & c . uuid ) . collect ( ) ;
if let Some ( e ) = self . collections . iter ( ) . find ( | c | ! org_collection_ids . contains ( & c . id ) ) {
err ! ( "Invalid collection" , format ! ( "Collection {} does not belong to organization {}!" , e . id , org_id ) )
}
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 ) = self . users . iter ( ) . find ( | m | ! org_membership_ids . contains ( m ) ) {
err ! ( "Invalid member" , format ! ( "Member {} does not belong to organization {}!" , e , org_id ) )
}
Ok ( ( ) )
}
}
#[ derive(Deserialize, Serialize) ]
@ -2465,6 +2544,8 @@ async fn post_groups(
}
let group_request = data . into_inner ( ) ;
group_request . validate ( & org_id , & conn ) . await ? ;
let group = group_request . to_group ( & org_id ) ;
log_event (
@ -2501,10 +2582,12 @@ async fn put_group(
} ;
let group_request = data . into_inner ( ) ;
group_request . validate ( & org_id , & conn ) . await ? ;
let updated_group = group_request . update_group ( group ) ;
CollectionGroup ::delete_all_by_group ( & group_id , & conn ) . await ? ;
GroupUser ::delete_all_by_group ( & group_id , & conn ) . await ? ;
CollectionGroup ::delete_all_by_group ( & group_id , & org_id , & conn ) . await ? ;
GroupUser ::delete_all_by_group ( & group_id , & org_id , & conn ) . await ? ;
log_event (
EventType ::GroupUpdated as i32 ,
@ -2532,7 +2615,7 @@ async fn add_update_group(
for col_selection in collections {
let mut collection_group = col_selection . to_collection_group ( group . uuid . clone ( ) ) ;
collection_group . save ( conn ) . await ? ;
collection_group . save ( & org_id , conn ) . await ? ;
}
for assigned_member in members {
@ -2625,7 +2708,7 @@ async fn _delete_group(
)
. await ;
group . delete ( conn ) . await
group . delete ( org_id , conn ) . await
}
#[ delete( " /organizations/<org_id>/groups " , data = " <data> " ) ]
@ -2684,7 +2767,7 @@ async fn get_group_members(
err ! ( "Group could not be found!" , "Group uuid is invalid or does not belong to the organization" )
} ;
let group_members : Vec < MembershipId > = GroupUser ::find_by_group ( & group_id , & conn )
let group_members : Vec < MembershipId > = GroupUser ::find_by_group ( & group_id , & org_id , & conn )
. await
. iter ( )
. map ( | entry | entry . users_organizations_uuid . clone ( ) )
@ -2712,9 +2795,15 @@ async fn put_group_members(
err ! ( "Group could not be found!" , "Group uuid is invalid or does not belong to the organization" )
} ;
GroupUser ::delete_all_by_group ( & group_id , & conn ) . await ? ;
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 ? ;
@ -2853,7 +2942,8 @@ async fn put_reset_password(
let reset_request = data . into_inner ( ) ;
let mut user = user ;
user . set_password ( reset_request . new_master_password_hash . as_str ( ) , Some ( reset_request . key ) , true , None ) ;
user . set_password ( reset_request . new_master_password_hash . as_str ( ) , Some ( reset_request . key ) , true , None , & conn )
. await ? ;
user . save ( & conn ) . await ? ;
nt . send_logout ( & user , None , & conn ) . await ;
@ -2945,17 +3035,19 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn)
Ok ( ( ) )
}
#[ put( " /organizations/<org_id>/users/<memb er_id>/reset-password-enrollment " , data = " <data> " ) ]
#[ put( " /organizations/<org_id>/users/<us er_id>/reset-password-enrollment " , data = " <data> " ) ]
async fn put_reset_password_enrollment (
org_id : OrganizationId ,
member_id : Membership Id,
headers : Headers ,
user_id : User Id,
headers : OrgMember Headers,
data : Json < OrganizationUserResetPasswordEnrollmentRequest > ,
conn : DbConn ,
) -> EmptyResult {
let Some ( mut member ) = Membership ::find_by_user_and_org ( & headers . user . uuid , & org_id , & conn ) . await else {
err ! ( "User to enroll isn't member of required organization" )
} ;
if user_id ! = headers . user . uuid {
err ! ( "User to enroll isn't member of required organization" , "The user_id and acting user do not match" ) ;
}
let mut membership = headers . membership ;
check_reset_password_applicable ( & org_id , & conn ) . await ? ;
@ -2980,16 +3072,17 @@ async fn put_reset_password_enrollment(
. await ? ;
}
member . reset_password_key = reset_password_key ;
member . save ( & conn ) . await ? ;
membership . reset_password_key = reset_password_key ;
membership . save ( & conn ) . await ? ;
let log_id = if member . reset_password_key . is_some ( ) {
let event_type = if membership . reset_password_key . is_some ( ) {
EventType ::OrganizationUserResetPasswordEnroll as i32
} else {
EventType ::OrganizationUserResetPasswordWithdraw as i32
} ;
log_event ( log_id , & member_id , & org_id , & headers . user . uuid , headers . device . atype , & headers . ip . ip , & conn ) . await ;
log_event ( event_type , & membership . uuid , & org_id , & headers . user . uuid , headers . device . atype , & headers . ip . ip , & conn )
. await ;
Ok ( ( ) )
}