@ -20,7 +20,8 @@ use crate::{
DbConn ,
DbConn ,
} ,
} ,
mail ,
mail ,
util ::{ convert_json_key_lcase_first , get_uuid , NumberOrString } ,
sso ::FAKE_SSO_IDENTIFIER ,
util ::{ convert_json_key_lcase_first , NumberOrString } ,
CONFIG ,
CONFIG ,
} ;
} ;
@ -64,6 +65,7 @@ pub fn routes() -> Vec<Route> {
post_org_import ,
post_org_import ,
list_policies ,
list_policies ,
list_policies_token ,
list_policies_token ,
get_dummy_master_password_policy ,
get_master_password_policy ,
get_master_password_policy ,
get_policy ,
get_policy ,
put_policy ,
put_policy ,
@ -99,6 +101,7 @@ pub fn routes() -> Vec<Route> {
get_billing_metadata ,
get_billing_metadata ,
get_billing_warnings ,
get_billing_warnings ,
get_auto_enroll_status ,
get_auto_enroll_status ,
get_self_host_billing_metadata ,
]
]
}
}
@ -131,6 +134,24 @@ struct FullCollectionData {
external_id : Option < String > ,
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) ]
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
struct CollectionGroupData {
struct CollectionGroupData {
@ -233,11 +254,13 @@ async fn post_delete_organization(
}
}
#[ post( " /organizations/<org_id>/leave " ) ]
#[ post( " /organizations/<org_id>/leave " ) ]
async fn leave_organization ( org_id : OrganizationId , headers : Headers , conn : DbConn ) -> EmptyResult {
async fn leave_organization ( org_id : OrganizationId , headers : OrgMemberHeaders , conn : DbConn ) -> EmptyResult {
match Membership ::find_by_user_and_org ( & headers . user . uuid , & org_id , & conn ) . await {
if headers . membership . status ! = MembershipStatus ::Confirmed as i32 {
None = > err ! ( "User not part of organization" ) ,
err ! ( "You need to be a Member of the Organization to call this endpoint" )
Some ( member ) = > {
}
if member . atype = = MembershipType ::Owner
let membership = headers . membership ;
if membership . atype = = MembershipType ::Owner
& & Membership ::count_confirmed_by_org_and_type ( & org_id , MembershipType ::Owner , & conn ) . await < = 1
& & Membership ::count_confirmed_by_org_and_type ( & org_id , MembershipType ::Owner , & conn ) . await < = 1
{
{
err ! ( "The last owner can't leave" )
err ! ( "The last owner can't leave" )
@ -245,7 +268,7 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbCo
log_event (
log_event (
EventType ::OrganizationUserLeft as i32 ,
EventType ::OrganizationUserLeft as i32 ,
& member . uuid ,
& membership . uuid ,
& org_id ,
& org_id ,
& headers . user . uuid ,
& headers . user . uuid ,
headers . device . atype ,
headers . device . atype ,
@ -254,9 +277,7 @@ async fn leave_organization(org_id: OrganizationId, headers: Headers, conn: DbCo
)
)
. await ;
. await ;
member . delete ( & conn ) . await
membership . delete ( & conn ) . await
}
}
}
}
#[ get( " /organizations/<org_id> " ) ]
#[ get( " /organizations/<org_id> " ) ]
@ -335,7 +356,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
// The returned `Id` will then be passed to `get_master_password_policy` which will mainly ignore it
#[ get( " /organizations/<identifier>/auto-enroll-status " ) ]
#[ get( " /organizations/<identifier>/auto-enroll-status " ) ]
async fn get_auto_enroll_status ( identifier : & str , headers : Headers , conn : DbConn ) -> JsonResult {
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 {
match Membership ::find_main_user_org ( & headers . user . uuid , & conn ) . await {
Some ( member ) = > Organization ::find_by_uuid ( & member . org_uuid , & conn ) . await ,
Some ( member ) = > Organization ::find_by_uuid ( & member . org_uuid , & conn ) . await ,
None = > None ,
None = > None ,
@ -345,7 +366,7 @@ async fn get_auto_enroll_status(identifier: &str, headers: Headers, conn: DbConn
} ;
} ;
let ( id , identifier , rp_auto_enroll ) = match org {
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 ) = > (
Some ( org ) = > (
org . uuid . to_string ( ) ,
org . uuid . to_string ( ) ,
org . uuid . to_string ( ) ,
org . uuid . to_string ( ) ,
@ -480,12 +501,13 @@ async fn post_organization_collections(
err ! ( "Organization not found" , "Organization id's do not match" ) ;
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
}
let data : FullCollectionData = data . into_inner ( ) ;
let data : FullCollectionData = data . into_inner ( ) ;
data . validate ( & org_id , & conn ) . await ? ;
let Some ( org ) = Organization ::find_by_uuid ( & org_id , & conn ) . await else {
if headers . membership . atype = = MembershipType ::Manager & & ! headers . membership . access_all {
err ! ( "Can't find organization detail s" )
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 ? ;
collection . save ( & conn ) . await ? ;
log_event (
log_event (
@ -501,7 +523,7 @@ async fn post_organization_collections(
for group in data . groups {
for group in data . groups {
CollectionGroup ::new ( collection . uuid . clone ( ) , group . id , group . read_only , group . hide_passwords , group . manage )
CollectionGroup ::new ( collection . uuid . clone ( ) , group . id , group . read_only , group . hide_passwords , group . manage )
. save ( & conn )
. save ( & org_id , & conn )
. await ? ;
. await ? ;
}
}
@ -525,10 +547,6 @@ async fn post_organization_collections(
. await ? ;
. 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 ) )
Ok ( Json ( collection . to_json_details ( & headers . membership . user_uuid , None , & conn ) . await ) )
}
}
@ -579,10 +597,10 @@ async fn post_bulk_access_collections(
)
)
. await ;
. 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 {
for group in & data . groups {
CollectionGroup ::new ( col_id . clone ( ) , group . id . clone ( ) , group . read_only , group . hide_passwords , group . manage )
CollectionGroup ::new ( col_id . clone ( ) , group . id . clone ( ) , group . read_only , group . hide_passwords , group . manage )
. save ( & conn )
. save ( & org_id , & conn )
. await ? ;
. await ? ;
}
}
@ -627,6 +645,7 @@ async fn post_organization_collection_update(
err ! ( "Organization not found" , "Organization id's do not match" ) ;
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
}
let data : FullCollectionData = data . into_inner ( ) ;
let data : FullCollectionData = data . into_inner ( ) ;
data . validate ( & org_id , & conn ) . await ? ;
if Organization ::find_by_uuid ( & org_id , & conn ) . await . is_none ( ) {
if Organization ::find_by_uuid ( & org_id , & conn ) . await . is_none ( ) {
err ! ( "Can't find organization details" )
err ! ( "Can't find organization details" )
@ -655,11 +674,11 @@ async fn post_organization_collection_update(
)
)
. await ;
. 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 {
for group in data . groups {
CollectionGroup ::new ( col_id . clone ( ) , group . id , group . read_only , group . hide_passwords , group . manage )
CollectionGroup ::new ( col_id . clone ( ) , group . id , group . read_only , group . hide_passwords , group . manage )
. save ( & conn )
. save ( & org_id , & conn )
. await ? ;
. await ? ;
}
}
@ -888,36 +907,21 @@ async fn _get_org_details(
Ok ( json ! ( ciphers_json ) )
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
// 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`
// In use since `v2025.6.0`, appears to use only the first `organizationIdentifier`
#[ post( " /organizations/domain/sso/verified " , data = " <data> " ) ]
#[ post( " /organizations/domain/sso/verified " ) ]
async fn get_org_domain_sso_verified ( data : Json < OrgDomainDetails > , conn : DbConn ) -> JsonResult {
fn get_org_domain_sso_verified ( ) -> JsonResult {
let data : OrgDomainDetails = data . into_inner ( ) ;
// Always return a dummy value, no matter if SSO is enabled or not
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 ( ) ) ] ,
} ;
Ok ( Json ( json ! ( {
Ok ( Json ( json ! ( {
"object" : "list" ,
"object" : "list" ,
"data" : identifiers . into_iter ( ) . map ( | ( name , identifier ) | json ! ( {
"data" : [ {
"organizationName" : name , // appear unused
"organizationIdentifier" : FAKE_SSO_IDENTIFIER ,
"organizationIdentifier" : identifier ,
// These appear to be unused
"domainName" : CONFIG . domain ( ) , // appear unused
"organizationName" : FAKE_SSO_IDENTIFIER ,
} ) ) . collect ::< Vec < Value > > ( )
"domainName" : CONFIG . domain ( )
} ] ,
"continuationToken" : null
} ) ) )
} ) ) )
}
}
@ -1003,6 +1007,24 @@ struct InviteData {
permissions : HashMap < String , Value > ,
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> " ) ]
#[ post( " /organizations/<org_id>/users/invite " , data = " <data> " ) ]
async fn send_invite (
async fn send_invite (
org_id : OrganizationId ,
org_id : OrganizationId ,
@ -1014,6 +1036,7 @@ async fn send_invite(
err ! ( "Organization not found" , "Organization id's do not match" ) ;
err ! ( "Organization not found" , "Organization id's do not match" ) ;
}
}
let data : InviteData = data . into_inner ( ) ;
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
// 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
// The from_str() will convert the custom role type into a manager role type
@ -1273,20 +1296,20 @@ async fn accept_invite(
// skip invitation logic when we were invited via the /admin panel
// skip invitation logic when we were invited via the /admin panel
if * * member_id ! = FAKE_ADMIN_UUID {
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" )
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 if data . reset_password_key . is_none ( ) = > err ! ( "Reset password key is required, but not provided." ) ,
true = > data . reset_password_key ,
true = > data . reset_password_key ,
false = > None ,
false = > None ,
} ;
} ;
// In case the user was invited before the mail was saved in db.
// 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 ( ) {
} else if CONFIG . mail_enabled ( ) {
// User was invited from /admin, so they are automatically confirmed
// User was invited from /admin, so they are automatically confirmed
let org_name = CONFIG . invitation_org_name ( ) ;
let org_name = CONFIG . invitation_org_name ( ) ;
@ -1425,7 +1448,7 @@ async fn _confirm_invite(
let save_result = member_to_confirm . save ( conn ) . await ;
let save_result = member_to_confirm . save ( conn ) . await ;
if let Some ( user ) = User ::find_by_uuid ( & member_to_confirm . user_uuid , 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
save_result
@ -1520,9 +1543,8 @@ async fn edit_member(
& & 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 member_to_edit = match Membership ::find_by_uuid_and_org ( & member_id , & org_id , & conn ) . await {
let Some ( mut member_to_edit ) = Membership ::find_by_uuid_and_org ( & member_id , & org_id , & conn ) . await else {
Some ( member ) = > member ,
err ! ( "The specified user isn't member of the organization" )
None = > err ! ( "The specified user isn't member of the organization" ) ,
} ;
} ;
if new_type ! = member_to_edit . atype
if new_type ! = member_to_edit . atype
@ -1684,7 +1706,7 @@ async fn _delete_member(
. await ;
. await ;
if let Some ( user ) = User ::find_by_uuid ( & member_to_delete . user_uuid , conn ) . 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
member_to_delete . delete ( conn ) . await
@ -1839,7 +1861,6 @@ async fn post_org_import(
#[ derive(Deserialize) ]
#[ derive(Deserialize) ]
#[ serde(rename_all = " camelCase " ) ]
#[ serde(rename_all = " camelCase " ) ]
#[ allow(dead_code) ]
struct BulkCollectionsData {
struct BulkCollectionsData {
organization_id : OrganizationId ,
organization_id : OrganizationId ,
cipher_ids : Vec < CipherId > ,
cipher_ids : Vec < CipherId > ,
@ -1853,6 +1874,10 @@ struct BulkCollectionsData {
async fn post_bulk_collections ( data : Json < BulkCollectionsData > , headers : Headers , conn : DbConn ) -> EmptyResult {
async fn post_bulk_collections ( data : Json < BulkCollectionsData > , headers : Headers , conn : DbConn ) -> EmptyResult {
let data : BulkCollectionsData = data . into_inner ( ) ;
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
// Get all the collection available to the user in one query
// Also filter based upon the provided collections
// Also filter based upon the provided collections
let user_collections : HashMap < CollectionId , Collection > =
let user_collections : HashMap < CollectionId , Collection > =
@ -1868,7 +1893,7 @@ async fn post_bulk_collections(data: Json<BulkCollectionsData>, headers: Headers
} )
} )
. collect ( ) ;
. 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 {
for collection_uuid in & data . collection_ids {
match user_collections . get ( collection_uuid ) {
match user_collections . get ( collection_uuid ) {
Some ( collection ) if collection . is_writable_by_user ( & headers . user . uuid , & conn ) . await = > ( ) ,
Some ( collection ) if collection . is_writable_by_user ( & headers . user . uuid , & conn ) . await = > ( ) ,
@ -1938,10 +1963,20 @@ async fn list_policies_token(org_id: OrganizationId, token: &str, conn: DbConn)
} ) ) )
} ) ) )
}
}
// Called during the SSO enrollment.
// Called during the SSO enrollment return the default policy
// Return the org policy if it exists, otherwise use the default one.
#[ get( " /organizations/00000000-01DC-01DC-01DC-000000000000/policies/master-password " , rank = 1) ]
#[ get( " /organizations/<org_id>/policies/master-password " , rank = 1) ]
fn get_dummy_master_password_policy ( ) -> JsonResult {
async fn get_master_password_policy ( org_id : OrganizationId , _headers : Headers , conn : DbConn ) -> 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 =
let policy =
OrgPolicy ::find_by_org_and_type ( & org_id , OrgPolicyType ::MasterPassword , & conn ) . await . unwrap_or_else ( | | {
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 ( ) {
let ( enabled , data ) = match CONFIG . sso_master_password_policy_value ( ) {
@ -1955,7 +1990,7 @@ async fn get_master_password_policy(org_id: OrganizationId, _headers: Headers, c
Ok ( Json ( policy . to_json ( ) ) )
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 {
async fn get_policy ( org_id : OrganizationId , pol_type : i32 , headers : AdminHeaders , conn : DbConn ) -> JsonResult {
if org_id ! = headers . org_id {
if org_id ! = headers . org_id {
err ! ( "Organization not found" , "Organization id's do not match" ) ;
err ! ( "Organization not found" , "Organization id's do not match" ) ;
@ -2149,13 +2184,13 @@ fn get_plans() -> Json<Value> {
}
}
#[ get( " /organizations/<_org_id>/billing/metadata " ) ]
#[ 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.
// Prevent a 404 error, which also causes Javascript errors.
Json ( _empty_data_json ( ) )
Json ( _empty_data_json ( ) )
}
}
#[ get( " /organizations/<_org_id>/billing/vnext/warnings " ) ]
#[ 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 ! ( {
Json ( json ! ( {
"freeTrial" :null ,
"freeTrial" :null ,
"inactiveSubscription" :null ,
"inactiveSubscription" :null ,
@ -2164,6 +2199,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 {
fn _empty_data_json ( ) -> Value {
json ! ( {
json ! ( {
"object" : "list" ,
"object" : "list" ,
@ -2427,6 +2471,23 @@ impl GroupRequest {
group
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) ]
#[ derive(Deserialize, Serialize) ]
@ -2470,6 +2531,8 @@ async fn post_groups(
}
}
let group_request = data . into_inner ( ) ;
let group_request = data . into_inner ( ) ;
group_request . validate ( & org_id , & conn ) . await ? ;
let group = group_request . to_group ( & org_id ) ;
let group = group_request . to_group ( & org_id ) ;
log_event (
log_event (
@ -2506,10 +2569,12 @@ async fn put_group(
} ;
} ;
let group_request = data . into_inner ( ) ;
let group_request = data . into_inner ( ) ;
group_request . validate ( & org_id , & conn ) . await ? ;
let updated_group = group_request . update_group ( group ) ;
let updated_group = group_request . update_group ( group ) ;
CollectionGroup ::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 , & conn ) . await ? ;
GroupUser ::delete_all_by_group ( & group_id , & org_id , & conn ) . await ? ;
log_event (
log_event (
EventType ::GroupUpdated as i32 ,
EventType ::GroupUpdated as i32 ,
@ -2537,7 +2602,7 @@ async fn add_update_group(
for col_selection in collections {
for col_selection in collections {
let mut collection_group = col_selection . to_collection_group ( group . uuid . clone ( ) ) ;
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 {
for assigned_member in members {
@ -2630,7 +2695,7 @@ async fn _delete_group(
)
)
. await ;
. await ;
group . delete ( conn ) . await
group . delete ( org_id , conn ) . await
}
}
#[ delete( " /organizations/<org_id>/groups " , data = " <data> " ) ]
#[ delete( " /organizations/<org_id>/groups " , data = " <data> " ) ]
@ -2689,7 +2754,7 @@ async fn get_group_members(
err ! ( "Group could not be found!" , "Group uuid is invalid or does not belong to the organization" )
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
. await
. iter ( )
. iter ( )
. map ( | entry | entry . users_organizations_uuid . clone ( ) )
. map ( | entry | entry . users_organizations_uuid . clone ( ) )
@ -2717,9 +2782,15 @@ async fn put_group_members(
err ! ( "Group could not be found!" , "Group uuid is invalid or does not belong to the organization" )
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 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 {
for assigned_member in assigned_members {
let mut user_entry = GroupUser ::new ( group_id . clone ( ) , assigned_member . clone ( ) ) ;
let mut user_entry = GroupUser ::new ( group_id . clone ( ) , assigned_member . clone ( ) ) ;
user_entry . save ( & conn ) . await ? ;
user_entry . save ( & conn ) . await ? ;
@ -2858,7 +2929,8 @@ async fn put_reset_password(
let reset_request = data . into_inner ( ) ;
let reset_request = data . into_inner ( ) ;
let mut user = user ;
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 ? ;
user . save ( & conn ) . await ? ;
nt . send_logout ( & user , None , & conn ) . await ;
nt . send_logout ( & user , None , & conn ) . await ;
@ -2950,17 +3022,19 @@ async fn check_reset_password_applicable(org_id: &OrganizationId, conn: &DbConn)
Ok ( ( ) )
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 (
async fn put_reset_password_enrollment (
org_id : OrganizationId ,
org_id : OrganizationId ,
member_id : Membership Id,
user_id : User Id,
headers : Headers ,
headers : OrgMember Headers,
data : Json < OrganizationUserResetPasswordEnrollmentRequest > ,
data : Json < OrganizationUserResetPasswordEnrollmentRequest > ,
conn : DbConn ,
conn : DbConn ,
) -> EmptyResult {
) -> EmptyResult {
let Some ( mut member ) = Membership ::find_by_user_and_org ( & headers . user . uuid , & org_id , & conn ) . await else {
if user_id ! = headers . user . uuid {
err ! ( "User to enroll isn't member of required organization" )
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 ? ;
check_reset_password_applicable ( & org_id , & conn ) . await ? ;
@ -2985,16 +3059,17 @@ async fn put_reset_password_enrollment(
. await ? ;
. await ? ;
}
}
member . reset_password_key = reset_password_key ;
membership . reset_password_key = reset_password_key ;
member . save ( & conn ) . await ? ;
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
EventType ::OrganizationUserResetPasswordEnroll as i32
} else {
} else {
EventType ::OrganizationUserResetPasswordWithdraw as i32
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 ( ( ) )
Ok ( ( ) )
}
}