@ -48,6 +48,7 @@ pub fn routes() -> Vec<Route> {
confirm_invite ,
bulk_confirm_invite ,
accept_invite ,
get_org_user_mini_details ,
get_user ,
edit_user ,
put_organization_user ,
@ -77,6 +78,7 @@ pub fn routes() -> Vec<Route> {
restore_organization_user ,
bulk_restore_organization_user ,
get_groups ,
get_groups_details ,
post_groups ,
get_group ,
put_group ,
@ -98,6 +100,7 @@ pub fn routes() -> Vec<Route> {
get_org_export ,
api_key ,
rotate_api_key ,
get_billing_metadata ,
]
}
@ -322,7 +325,14 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
} ;
// get all collection memberships for the current organization
let coll_users = CollectionUser ::find_by_organization ( org_id , & mut conn ) . await ;
let coll_users = CollectionUser ::find_by_organization_swap_user_uuid_with_org_user_uuid ( org_id , & mut conn ) . await ;
// Generate a HashMap to get the correct UserOrgType 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 users_org_type : HashMap < String , i32 > = UserOrganization ::find_confirmed_by_org ( org_id , & mut conn )
. await
. into_iter ( )
. map ( | uo | ( uo . uuid , uo . atype ) )
. collect ( ) ;
// check if current user has full access to the organization (either directly or via any group)
let has_full_access_to_org = user_org . access_all
@ -336,11 +346,22 @@ async fn get_org_collections_details(org_id: &str, headers: ManagerHeadersLoose,
| | ( CONFIG . org_groups_enabled ( )
& & GroupUser ::has_access_to_collection_by_member ( & col . uuid , & user_org . uuid , & mut conn ) . await ) ;
// Not assigned collections should not be returned
if ! assigned {
continue ;
}
// get the users assigned directly to the given collection
let users : Vec < Value > = coll_users
. iter ( )
. filter ( | collection_user | collection_user . collection_uuid = = col . uuid )
. map ( | collection_user | SelectionReadOnly ::to_collection_user_details_read_only ( collection_user ) . to_json ( ) )
. map ( | collection_user | {
SelectionReadOnly ::to_collection_user_details_read_only (
collection_user ,
* users_org_type . get ( & collection_user . user_uuid ) . unwrap_or ( & ( UserOrgType ::User as i32 ) ) ,
)
. to_json ( )
} )
. collect ( ) ;
// get the group details for the given collection
@ -645,12 +666,24 @@ async fn get_org_collection_detail(
Vec ::with_capacity ( 0 )
} ;
// Generate a HashMap to get the correct UserOrgType 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 users_org_type : HashMap < String , i32 > = UserOrganization ::find_confirmed_by_org ( org_id , & mut conn )
. await
. into_iter ( )
. map ( | uo | ( uo . uuid , uo . atype ) )
. collect ( ) ;
let users : Vec < Value > =
CollectionUser ::find_by_collection_swap_user_uuid_with_org_user_uuid ( & collection . uuid , & mut conn )
. await
. iter ( )
. map ( | collection_user | {
SelectionReadOnly ::to_collection_user_details_read_only ( collection_user ) . to_json ( )
SelectionReadOnly ::to_collection_user_details_read_only (
collection_user ,
* users_org_type . get ( & collection_user . user_uuid ) . unwrap_or ( & ( UserOrgType ::User as i32 ) ) ,
)
. to_json ( )
} )
. collect ( ) ;
@ -830,13 +863,19 @@ struct InviteData {
collections : Option < Vec < CollectionData > > ,
#[ serde(default) ]
access_all : bool ,
#[ serde(default) ]
permissions : HashMap < String , Value > ,
}
#[ post( " /organizations/<org_id>/users/invite " , data = " <data> " ) ]
async fn send_invite ( org_id : & str , data : Json < InviteData > , headers : AdminHeaders , mut conn : DbConn ) -> EmptyResult {
let data : InviteData = data . into_inner ( ) ;
let mut data : InviteData = data . into_inner ( ) ;
let new_type = match UserOrgType ::from_str ( & data . r#type . into_string ( ) ) {
// 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
let raw_type = & data . r#type . into_string ( ) ;
// UserOrgType::from_str will convert custom (4) to manager (3)
let new_type = match UserOrgType ::from_str ( raw_type ) {
Some ( new_type ) = > new_type as i32 ,
None = > err ! ( "Invalid type" ) ,
} ;
@ -845,6 +884,18 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
err ! ( "Only Owners can invite Managers, Admins or Owners" )
}
// HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
// Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
// If the box is not checked, the user will still be a manager, but not with the access_all permission
if raw_type . eq ( "4" )
& & data . permissions . get ( "editAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "deleteAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "createNewCollections" ) = = Some ( & json ! ( true ) )
{
data . access_all = true ;
}
let mut user_created : bool = false ;
for email in data . emails . iter ( ) {
let mut user_org_status = UserOrgStatus ::Invited as i32 ;
let user = match User ::find_by_mail ( email , & mut conn ) . await {
@ -858,13 +909,13 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}
if ! CONFIG . mail_enabled ( ) {
let invitation = Invitation ::new ( email ) ;
invitation . save ( & mut conn ) . await ? ;
Invitation ::new ( email ) . save ( & mut conn ) . await ? ;
}
let mut user = User ::new ( email . clone ( ) ) ;
user . save ( & mut conn ) . await ? ;
user
let mut new_user = User ::new ( email . clone ( ) ) ;
new_user . save ( & mut conn ) . await ? ;
user_created = true ;
new_user
}
Some ( user ) = > {
if UserOrganization ::find_by_user_and_org ( & user . uuid , org_id , & mut conn ) . await . is_some ( ) {
@ -879,11 +930,49 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}
} ;
let mut new_us er = UserOrganization ::new ( user . uuid . clone ( ) , String ::from ( org_id ) ) ;
let mut new_memb er = UserOrganization ::new ( user . uuid . clone ( ) , String ::from ( org_id ) ) ;
let access_all = data . access_all ;
new_user . access_all = access_all ;
new_user . atype = new_type ;
new_user . status = user_org_status ;
new_member . access_all = access_all ;
new_member . atype = new_type ;
new_member . status = user_org_status ;
new_member . save ( & mut conn ) . await ? ;
if CONFIG . mail_enabled ( ) {
let org_name = match Organization ::find_by_uuid ( org_id , & mut conn ) . await {
Some ( org ) = > org . name ,
None = > err ! ( "Error looking up organization" ) ,
} ;
if let Err ( e ) = mail ::send_invite (
& user ,
Some ( String ::from ( org_id ) ) ,
Some ( new_member . uuid . clone ( ) ) ,
& org_name ,
Some ( headers . user . email . clone ( ) ) ,
)
. await
{
// Upon error delete the user, invite and org member records when needed
if user_created {
user . delete ( & mut conn ) . await ? ;
} else {
new_member . delete ( & mut conn ) . await ? ;
}
err ! ( format ! ( "Error sending invite: {e:?} " ) ) ;
} ;
}
log_event (
EventType ::OrganizationUserInvited as i32 ,
& new_member . uuid . clone ( ) ,
org_id ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& mut conn ,
)
. await ;
// If no accessAll, add the collections received
if ! access_all {
@ -904,39 +993,10 @@ async fn send_invite(org_id: &str, data: Json<InviteData>, headers: AdminHeaders
}
}
new_user . save ( & mut conn ) . await ? ;
for group in data . groups . iter ( ) {
let mut group_entry = GroupUser ::new ( String ::from ( group ) , us er. uuid . clone ( ) ) ;
let mut group_entry = GroupUser ::new ( String ::from ( group ) , new_member . uuid . clone ( ) ) ;
group_entry . save ( & mut conn ) . await ? ;
}
log_event (
EventType ::OrganizationUserInvited as i32 ,
& new_user . uuid ,
org_id ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& mut conn ,
)
. await ;
if CONFIG . mail_enabled ( ) {
let org_name = match Organization ::find_by_uuid ( org_id , & mut conn ) . await {
Some ( org ) = > org . name ,
None = > err ! ( "Error looking up organization" ) ,
} ;
mail ::send_invite (
& user ,
Some ( String ::from ( org_id ) ) ,
Some ( new_user . uuid ) ,
& org_name ,
Some ( headers . user . email . clone ( ) ) ,
)
. await ? ;
}
}
Ok ( ( ) )
@ -1014,7 +1074,7 @@ async fn _reinvite_user(org_id: &str, user_org: &str, invited_by_email: &str, co
let invitation = Invitation ::new ( & user . email ) ;
invitation . save ( conn ) . await ? ;
} else {
let _ = Invitation ::take ( & user . email , conn ) . await ;
Invitation ::take ( & user . email , conn ) . await ;
let mut user_org = user_org ;
user_org . status = UserOrgStatus ::Accepted as i32 ;
user_org . save ( conn ) . await ? ;
@ -1254,7 +1314,21 @@ async fn _confirm_invite(
save_result
}
#[ get( " /organizations/<org_id>/users/<org_user_id>?<data..> " ) ]
#[ get( " /organizations/<org_id>/users/mini-details " , rank = 1) ]
async fn get_org_user_mini_details ( org_id : & str , _headers : ManagerHeadersLoose , mut conn : DbConn ) -> Json < Value > {
let mut users_json = Vec ::new ( ) ;
for u in UserOrganization ::find_by_org ( org_id , & mut conn ) . await {
users_json . push ( u . to_json_mini_details ( & mut conn ) . await ) ;
}
Json ( json ! ( {
"data" : users_json ,
"object" : "list" ,
"continuationToken" : null ,
} ) )
}
#[ get( " /organizations/<org_id>/users/<org_user_id>?<data..> " , rank = 2) ]
async fn get_user (
org_id : & str ,
org_user_id : & str ,
@ -1282,6 +1356,8 @@ struct EditUserData {
groups : Option < Vec < String > > ,
#[ serde(default) ]
access_all : bool ,
#[ serde(default) ]
permissions : HashMap < String , Value > ,
}
#[ put( " /organizations/<org_id>/users/<org_user_id> " , data = " <data> " , rank = 1) ]
@ -1303,14 +1379,30 @@ async fn edit_user(
headers : AdminHeaders ,
mut conn : DbConn ,
) -> EmptyResult {
let data : EditUserData = data . into_inner ( ) ;
let mut data : EditUserData = data . into_inner ( ) ;
let Some ( new_type ) = UserOrgType ::from_str ( & data . r#type . into_string ( ) ) else {
// 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
let raw_type = & data . r#type . into_string ( ) ;
// UserOrgType::from_str will convert custom (4) to manager (3)
let Some ( new_type ) = UserOrgType ::from_str ( raw_type ) else {
err ! ( "Invalid type" )
} ;
let Some ( mut user_to_edit ) = UserOrganization ::find_by_uuid_and_org ( org_user_id , org_id , & mut conn ) . await else {
err ! ( "The specified user isn't member of the organization" )
// HACK: This converts the Custom role which has the `Manage all collections` box checked into an access_all flag
// Since the parent checkbox is not sent to the server we need to check and verify the child checkboxes
// If the box is not checked, the user will still be a manager, but not with the access_all permission
if raw_type . eq ( "4" )
& & data . permissions . get ( "editAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "deleteAnyCollection" ) = = Some ( & json ! ( true ) )
& & data . permissions . get ( "createNewCollections" ) = = Some ( & json ! ( true ) )
{
data . access_all = true ;
}
let mut user_to_edit = match UserOrganization ::find_by_uuid_and_org ( org_user_id , org_id , & mut conn ) . await {
Some ( user ) = > user ,
None = > err ! ( "The specified user isn't member of the organization" ) ,
} ;
if new_type ! = user_to_edit . atype
@ -1901,6 +1993,12 @@ fn get_plans_tax_rates(_headers: Headers) -> Json<Value> {
Json ( _empty_data_json ( ) )
}
#[ get( " /organizations/<_org_id>/billing/metadata " ) ]
fn get_billing_metadata ( _org_id : & str , _headers : Headers ) -> Json < Value > {
// Prevent a 404 error, which also causes Javascript errors.
Json ( _empty_data_json ( ) )
}
fn _empty_data_json ( ) -> Value {
json ! ( {
"object" : "list" ,
@ -1938,6 +2036,9 @@ struct OrgImportData {
users : Vec < OrgImportUserData > ,
}
/// This function seems to be deprected
/// It is only used with older directory connectors
/// TODO: Cleanup Tech debt
#[ post( " /organizations/<org_id>/import " , data = " <data> " ) ]
async fn import ( org_id : & str , data : Json < OrgImportData > , headers : Headers , mut conn : DbConn ) -> EmptyResult {
let data = data . into_inner ( ) ;
@ -1981,23 +2082,10 @@ async fn import(org_id: &str, data: Json<OrgImportData>, headers: Headers, mut c
UserOrgStatus ::Accepted as i32 // Automatically mark user as accepted if no email invites
} ;
let mut new_org_user = UserOrganization ::new ( user . uuid . clone ( ) , String ::from ( org_id ) ) ;
new_org_user . access_all = false ;
new_org_user . atype = UserOrgType ::User as i32 ;
new_org_user . status = user_org_status ;
new_org_user . save ( & mut conn ) . await ? ;
log_event (
EventType ::OrganizationUserInvited as i32 ,
& new_org_user . uuid ,
org_id ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& mut conn ,
)
. await ;
let mut new_member = UserOrganization ::new ( user . uuid . clone ( ) , String ::from ( org_id ) ) ;
new_member . access_all = false ;
new_member . atype = UserOrgType ::User as i32 ;
new_member . status = user_org_status ;
if CONFIG . mail_enabled ( ) {
let org_name = match Organization ::find_by_uuid ( org_id , & mut conn ) . await {
@ -2008,12 +2096,27 @@ async fn import(org_id: &str, data: Json<OrgImportData>, headers: Headers, mut c
mail ::send_invite (
& user ,
Some ( String ::from ( org_id ) ) ,
Some ( new_org_us er . uuid ) ,
Some ( new_memb er . uuid . clone ( ) ) ,
& org_name ,
Some ( headers . user . email . clone ( ) ) ,
)
. await ? ;
}
// Save the member after sending an email
// If sending fails the member will not be saved to the database, and will not result in the admin needing to reinvite the users manually
new_member . save ( & mut conn ) . await ? ;
log_event (
EventType ::OrganizationUserInvited as i32 ,
& new_member . uuid ,
org_id ,
& headers . user . uuid ,
headers . device . atype ,
& headers . ip . ip ,
& mut conn ,
)
. await ;
}
}
}
@ -2299,6 +2402,11 @@ async fn get_groups(org_id: &str, _headers: ManagerHeadersLoose, mut conn: DbCon
} ) ) )
}
#[ get( " /organizations/<org_id>/groups/details " , rank = 1) ]
async fn get_groups_details ( org_id : & str , headers : ManagerHeadersLoose , conn : DbConn ) -> JsonResult {
get_groups ( org_id , headers , conn ) . await
}
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
struct GroupRequest {
@ -2331,6 +2439,7 @@ struct SelectionReadOnly {
id : String ,
read_only : bool ,
hide_passwords : bool ,
manage : bool ,
}
impl SelectionReadOnly {
@ -2339,18 +2448,31 @@ impl SelectionReadOnly {
}
pub fn to_collection_group_details_read_only ( collection_group : & CollectionGroup ) -> SelectionReadOnly {
// If both read_only and hide_passwords are false, then manage should be true
// You can't have an entry with read_only and manage, or hide_passwords and manage
// Or an entry with everything to false
SelectionReadOnly {
id : collection_group . groups_uuid . clone ( ) ,
read_only : collection_group . read_only ,
hide_passwords : collection_group . hide_passwords ,
manage : ! collection_group . read_only & & ! collection_group . hide_passwords ,
}
}
pub fn to_collection_user_details_read_only ( collection_user : & CollectionUser ) -> SelectionReadOnly {
pub fn to_collection_user_details_read_only (
collection_user : & CollectionUser ,
user_org_type : i32 ,
) -> SelectionReadOnly {
// Vaultwarden allows manage access for Admins and Owners by default
// For managers (Or custom role) it depends if they have read_ony or hide_passwords set to true or not
SelectionReadOnly {
id : collection_user . user_uuid . clone ( ) ,
read_only : collection_user . read_only ,
hide_passwords : collection_user . hide_passwords ,
manage : user_org_type > = UserOrgType ::Admin
| | ( user_org_type = = UserOrgType ::Manager
& & ! collection_user . read_only
& & ! collection_user . hide_passwords ) ,
}
}
@ -2534,7 +2656,7 @@ async fn bulk_delete_groups(
Ok ( ( ) )
}
#[ get( " /organizations/<org_id>/groups/<group_id> " ) ]
#[ get( " /organizations/<org_id>/groups/<group_id> " , rank = 2 )]
async fn get_group ( org_id : & str , group_id : & str , _headers : AdminHeaders , mut conn : DbConn ) -> JsonResult {
if ! CONFIG . org_groups_enabled ( ) {
err ! ( "Group support is disabled" ) ;
@ -2904,7 +3026,7 @@ async fn put_reset_password_enrollment(
if reset_request . reset_password_key . is_none ( )
& & OrgPolicy ::org_is_reset_password_auto_enroll ( org_id , & mut conn ) . await
{
err ! ( "Reset password can't be withdrawed due to an enterprise policy" ) ;
err ! ( "Reset password can't be withdrawn due to an enterprise policy" ) ;
}
if reset_request . reset_password_key . is_some ( ) {