mirror of
				https://github.com/dani-garcia/vaultwarden.git
				synced 2025-10-31 05:41:13 +00:00 
			
		
		
		
	Merge branch 'BlackDex-org-user-revoke-access' into main
This commit is contained in:
		
				commit
				
					
						518d74ce21
					
				
			
		
					 9 changed files with 486 additions and 148 deletions
				
			
		|  | @ -418,15 +418,26 @@ async fn update_user_org_type(data: Json<UserOrgTypeData>, _token: AdminToken, c | |||
|     }; | ||||
| 
 | ||||
|     if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { | ||||
|         // Removing owner permmission, check that there are at least another owner
 | ||||
|         let num_owners = | ||||
|             UserOrganization::find_by_org_and_type(&data.org_uuid, UserOrgType::Owner as i32, &conn).await.len(); | ||||
| 
 | ||||
|         if num_owners <= 1 { | ||||
|         // Removing owner permmission, check that there is at least one other confirmed owner
 | ||||
|         if UserOrganization::count_confirmed_by_org_and_type(&data.org_uuid, UserOrgType::Owner, &conn).await <= 1 { | ||||
|             err!("Can't change the type of the last owner") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // This check is also done at api::organizations::{accept_invite(), _confirm_invite, _activate_user(), edit_user()}, update_user_org_type
 | ||||
|     // It returns different error messages per function.
 | ||||
|     if new_type < UserOrgType::Admin { | ||||
|         match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &user_to_edit.org_uuid, true, &conn).await { | ||||
|             Ok(_) => {} | ||||
|             Err(OrgPolicyErr::TwoFactorMissing) => { | ||||
|                 err!("You cannot modify this user to this type because it has no two-step login method activated"); | ||||
|             } | ||||
|             Err(OrgPolicyErr::SingleOrgEnforced) => { | ||||
|                 err!("You cannot modify this user to this type because it is a member of an organization which forbids it"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     user_to_edit.atype = new_type; | ||||
|     user_to_edit.save(&conn).await | ||||
| } | ||||
|  |  | |||
|  | @ -328,7 +328,7 @@ async fn enforce_personal_ownership_policy(data: Option<&CipherData>, headers: & | |||
|     if data.is_none() || data.unwrap().OrganizationId.is_none() { | ||||
|         let user_uuid = &headers.user.uuid; | ||||
|         let policy_type = OrgPolicyType::PersonalOwnership; | ||||
|         if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn).await { | ||||
|         if OrgPolicy::is_applicable_to_user(user_uuid, policy_type, None, conn).await { | ||||
|             err!("Due to an Enterprise Policy, you are restricted from saving items to your personal vault.") | ||||
|         } | ||||
|     } | ||||
|  |  | |||
|  | @ -258,7 +258,7 @@ async fn send_invite(data: JsonUpcase<EmergencyAccessInviteData>, headers: Heade | |||
|         match User::find_by_mail(&email, &conn).await { | ||||
|             Some(user) => { | ||||
|                 match accept_invite_process(user.uuid, new_emergency_access.uuid, Some(email), conn.borrow()).await { | ||||
|                     Ok(v) => (v), | ||||
|                     Ok(v) => v, | ||||
|                     Err(e) => err!(e.to_string()), | ||||
|                 } | ||||
|             } | ||||
|  | @ -317,7 +317,7 @@ async fn resend_invite(emer_id: String, headers: Headers, conn: DbConn) -> Empty | |||
|         match accept_invite_process(grantee_user.uuid, emergency_access.uuid, emergency_access.email, conn.borrow()) | ||||
|             .await | ||||
|         { | ||||
|             Ok(v) => (v), | ||||
|             Ok(v) => v, | ||||
|             Err(e) => err!(e.to_string()), | ||||
|         } | ||||
|     } | ||||
|  | @ -363,7 +363,7 @@ async fn accept_invite(emer_id: String, data: JsonUpcase<AcceptData>, conn: DbCo | |||
|         && (claims.grantor_email.is_some() && grantor_user.email == claims.grantor_email.unwrap()) | ||||
|     { | ||||
|         match accept_invite_process(grantee_user.uuid.clone(), emer_id, Some(grantee_user.email.clone()), &conn).await { | ||||
|             Ok(v) => (v), | ||||
|             Ok(v) => v, | ||||
|             Err(e) => err!(e.to_string()), | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -61,6 +61,10 @@ pub fn routes() -> Vec<Route> { | |||
|         import, | ||||
|         post_org_keys, | ||||
|         bulk_public_keys, | ||||
|         deactivate_organization_user, | ||||
|         bulk_deactivate_organization_user, | ||||
|         activate_organization_user, | ||||
|         bulk_activate_organization_user | ||||
|     ] | ||||
| } | ||||
| 
 | ||||
|  | @ -107,7 +111,7 @@ async fn create_organization(headers: Headers, data: JsonUpcase<OrgData>, conn: | |||
|     if !CONFIG.is_org_creation_allowed(&headers.user.email) { | ||||
|         err!("User not allowed to create organizations") | ||||
|     } | ||||
|     if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, &conn).await { | ||||
|     if OrgPolicy::is_applicable_to_user(&headers.user.uuid, OrgPolicyType::SingleOrg, None, &conn).await { | ||||
|         err!( | ||||
|             "You may not create an organization. You belong to an organization which has a policy that prohibits you from being a member of any other organization." | ||||
|         ) | ||||
|  | @ -172,13 +176,10 @@ async fn leave_organization(org_id: String, headers: Headers, conn: DbConn) -> E | |||
|     match UserOrganization::find_by_user_and_org(&headers.user.uuid, &org_id, &conn).await { | ||||
|         None => err!("User not part of organization"), | ||||
|         Some(user_org) => { | ||||
|             if user_org.atype == UserOrgType::Owner { | ||||
|                 let num_owners = | ||||
|                     UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).await.len(); | ||||
| 
 | ||||
|                 if num_owners <= 1 { | ||||
|                     err!("The last owner can't leave") | ||||
|                 } | ||||
|             if user_org.atype == UserOrgType::Owner | ||||
|                 && UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &conn).await <= 1 | ||||
|             { | ||||
|                 err!("The last owner can't leave") | ||||
|             } | ||||
| 
 | ||||
|             user_org.delete(&conn).await | ||||
|  | @ -749,17 +750,16 @@ struct AcceptData { | |||
|     Token: String, | ||||
| } | ||||
| 
 | ||||
| #[post("/organizations/<_org_id>/users/<_org_user_id>/accept", data = "<data>")] | ||||
| #[post("/organizations/<org_id>/users/<_org_user_id>/accept", data = "<data>")] | ||||
| async fn accept_invite( | ||||
|     _org_id: String, | ||||
|     org_id: String, | ||||
|     _org_user_id: String, | ||||
|     data: JsonUpcase<AcceptData>, | ||||
|     conn: DbConn, | ||||
| ) -> EmptyResult { | ||||
|     // The web-vault passes org_id and org_user_id in the URL, but we are just reading them from the JWT instead
 | ||||
|     let data: AcceptData = data.into_inner().data; | ||||
|     let token = &data.Token; | ||||
|     let claims = decode_invite(token)?; | ||||
|     let claims = decode_invite(&data.Token)?; | ||||
| 
 | ||||
|     match User::find_by_mail(&claims.email, &conn).await { | ||||
|         Some(_) => { | ||||
|  | @ -775,46 +775,20 @@ async fn accept_invite( | |||
|                     err!("User already accepted the invitation") | ||||
|                 } | ||||
| 
 | ||||
|                 let user_twofactor_disabled = TwoFactor::find_by_user(&user_org.user_uuid, &conn).await.is_empty(); | ||||
| 
 | ||||
|                 let policy = OrgPolicyType::TwoFactorAuthentication as i32; | ||||
|                 let org_twofactor_policy_enabled = | ||||
|                     match OrgPolicy::find_by_org_and_type(&user_org.org_uuid, policy, &conn).await { | ||||
|                         Some(p) => p.enabled, | ||||
|                         None => false, | ||||
|                     }; | ||||
| 
 | ||||
|                 if org_twofactor_policy_enabled && user_twofactor_disabled { | ||||
|                     err!("You cannot join this organization until you enable two-step login on your user account.") | ||||
|                 } | ||||
| 
 | ||||
|                 // Enforce Single Organization Policy of organization user is trying to join
 | ||||
|                 let single_org_policy_enabled = | ||||
|                     match OrgPolicy::find_by_org_and_type(&user_org.org_uuid, OrgPolicyType::SingleOrg as i32, &conn) | ||||
|                         .await | ||||
|                     { | ||||
|                         Some(p) => p.enabled, | ||||
|                         None => false, | ||||
|                     }; | ||||
|                 if single_org_policy_enabled && user_org.atype < UserOrgType::Admin { | ||||
|                     let is_member_of_another_org = UserOrganization::find_any_state_by_user(&user_org.user_uuid, &conn) | ||||
|                         .await | ||||
|                         .into_iter() | ||||
|                         .filter(|uo| uo.org_uuid != user_org.org_uuid) | ||||
|                         .count() | ||||
|                         > 1; | ||||
|                     if is_member_of_another_org { | ||||
|                         err!("You may not join this organization until you leave or remove all other organizations.") | ||||
|                 // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
 | ||||
|                 // It returns different error messages per function.
 | ||||
|                 if user_org.atype < UserOrgType::Admin { | ||||
|                     match OrgPolicy::is_user_allowed(&user_org.user_uuid, &org_id, false, &conn).await { | ||||
|                         Ok(_) => {} | ||||
|                         Err(OrgPolicyErr::TwoFactorMissing) => { | ||||
|                             err!("You cannot join this organization until you enable two-step login on your user account"); | ||||
|                         } | ||||
|                         Err(OrgPolicyErr::SingleOrgEnforced) => { | ||||
|                             err!("You cannot join this organization because you are a member of an organization which forbids it"); | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 // Enforce Single Organization Policy of other organizations user is a member of
 | ||||
|                 if OrgPolicy::is_applicable_to_user(&user_org.user_uuid, OrgPolicyType::SingleOrg, &conn).await { | ||||
|                     err!( | ||||
|                         "You cannot join this organization because you are a member of an organization which forbids it" | ||||
|                     ) | ||||
|                 } | ||||
| 
 | ||||
|                 user_org.status = UserOrgStatus::Accepted as i32; | ||||
|                 user_org.save(&conn).await?; | ||||
|             } | ||||
|  | @ -918,6 +892,20 @@ async fn _confirm_invite( | |||
|         err!("User in invalid state") | ||||
|     } | ||||
| 
 | ||||
|     // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
 | ||||
|     // It returns different error messages per function.
 | ||||
|     if user_to_confirm.atype < UserOrgType::Admin { | ||||
|         match OrgPolicy::is_user_allowed(&user_to_confirm.user_uuid, org_id, true, conn).await { | ||||
|             Ok(_) => {} | ||||
|             Err(OrgPolicyErr::TwoFactorMissing) => { | ||||
|                 err!("You cannot confirm this user because it has no two-step login method activated"); | ||||
|             } | ||||
|             Err(OrgPolicyErr::SingleOrgEnforced) => { | ||||
|                 err!("You cannot confirm this user because it is a member of an organization which forbids it"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     user_to_confirm.status = UserOrgStatus::Confirmed as i32; | ||||
|     user_to_confirm.akey = key.to_string(); | ||||
| 
 | ||||
|  | @ -997,14 +985,26 @@ async fn edit_user( | |||
|     } | ||||
| 
 | ||||
|     if user_to_edit.atype == UserOrgType::Owner && new_type != UserOrgType::Owner { | ||||
|         // Removing owner permmission, check that there are at least another owner
 | ||||
|         let num_owners = UserOrganization::find_by_org_and_type(&org_id, UserOrgType::Owner as i32, &conn).await.len(); | ||||
| 
 | ||||
|         if num_owners <= 1 { | ||||
|         // Removing owner permmission, check that there is at least one other confirmed owner
 | ||||
|         if UserOrganization::count_confirmed_by_org_and_type(&org_id, UserOrgType::Owner, &conn).await <= 1 { | ||||
|             err!("Can't delete the last owner") | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
 | ||||
|     // It returns different error messages per function.
 | ||||
|     if new_type < UserOrgType::Admin { | ||||
|         match OrgPolicy::is_user_allowed(&user_to_edit.user_uuid, &org_id, true, &conn).await { | ||||
|             Ok(_) => {} | ||||
|             Err(OrgPolicyErr::TwoFactorMissing) => { | ||||
|                 err!("You cannot modify this user to this type because it has no two-step login method activated"); | ||||
|             } | ||||
|             Err(OrgPolicyErr::SingleOrgEnforced) => { | ||||
|                 err!("You cannot modify this user to this type because it is a member of an organization which forbids it"); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     user_to_edit.access_all = data.AccessAll; | ||||
|     user_to_edit.atype = new_type as i32; | ||||
| 
 | ||||
|  | @ -1083,10 +1083,8 @@ async fn _delete_user(org_id: &str, org_user_id: &str, headers: &AdminHeaders, c | |||
|     } | ||||
| 
 | ||||
|     if user_to_delete.atype == UserOrgType::Owner { | ||||
|         // Removing owner, check that there are at least another owner
 | ||||
|         let num_owners = UserOrganization::find_by_org_and_type(org_id, UserOrgType::Owner as i32, conn).await.len(); | ||||
| 
 | ||||
|         if num_owners <= 1 { | ||||
|         // Removing owner, check that there is at least one other confirmed owner
 | ||||
|         if UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 { | ||||
|             err!("Can't delete the last owner") | ||||
|         } | ||||
|     } | ||||
|  | @ -1255,7 +1253,7 @@ async fn get_policy(org_id: String, pol_type: i32, _headers: AdminHeaders, conn: | |||
|         None => err!("Invalid or unsupported policy type"), | ||||
|     }; | ||||
| 
 | ||||
|     let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn).await { | ||||
|     let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { | ||||
|         Some(p) => p, | ||||
|         None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), | ||||
|     }; | ||||
|  | @ -1283,15 +1281,16 @@ async fn put_policy( | |||
| 
 | ||||
|     let pol_type_enum = match OrgPolicyType::from_i32(pol_type) { | ||||
|         Some(pt) => pt, | ||||
|         None => err!("Invalid policy type"), | ||||
|         None => err!("Invalid or unsupported policy type"), | ||||
|     }; | ||||
| 
 | ||||
|     // If enabling the TwoFactorAuthentication policy, remove this org's members that do have 2FA
 | ||||
|     // When enabling the TwoFactorAuthentication policy, remove this org's members that do have 2FA
 | ||||
|     if pol_type_enum == OrgPolicyType::TwoFactorAuthentication && data.enabled { | ||||
|         for member in UserOrganization::find_by_org(&org_id, &conn).await.into_iter() { | ||||
|             let user_twofactor_disabled = TwoFactor::find_by_user(&member.user_uuid, &conn).await.is_empty(); | ||||
| 
 | ||||
|             // Policy only applies to non-Owner/non-Admin members who have accepted joining the org
 | ||||
|             // Invited users still need to accept the invite and will get an error when they try to accept the invite.
 | ||||
|             if user_twofactor_disabled | ||||
|                 && member.atype < UserOrgType::Admin | ||||
|                 && member.status != UserOrgStatus::Invited as i32 | ||||
|  | @ -1307,33 +1306,29 @@ async fn put_policy( | |||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // If enabling the SingleOrg policy, remove this org's members that are members of other orgs
 | ||||
|     // When enabling the SingleOrg policy, remove this org's members that are members of other orgs
 | ||||
|     if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { | ||||
|         for member in UserOrganization::find_by_org(&org_id, &conn).await.into_iter() { | ||||
|             // Policy only applies to non-Owner/non-Admin members who have accepted joining the org
 | ||||
|             if member.atype < UserOrgType::Admin && member.status != UserOrgStatus::Invited as i32 { | ||||
|                 let is_member_of_another_org = UserOrganization::find_any_state_by_user(&member.user_uuid, &conn) | ||||
|                     .await | ||||
|                     .into_iter() | ||||
|                     // Other UserOrganization's where they have accepted being a member of
 | ||||
|                     .filter(|uo| uo.uuid != member.uuid && uo.status != UserOrgStatus::Invited as i32) | ||||
|                     .count() | ||||
|                     > 1; | ||||
|             // Exclude invited and revoked users when checking for this policy.
 | ||||
|             // Those users will not be allowed to accept or be activated because of the policy checks done there.
 | ||||
|             // We check if the count is larger then 1, because it includes this organization also.
 | ||||
|             if member.atype < UserOrgType::Admin | ||||
|                 && member.status != UserOrgStatus::Invited as i32 | ||||
|                 && UserOrganization::count_accepted_and_confirmed_by_user(&member.user_uuid, &conn).await > 1 | ||||
|             { | ||||
|                 if CONFIG.mail_enabled() { | ||||
|                     let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap(); | ||||
|                     let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap(); | ||||
| 
 | ||||
|                 if is_member_of_another_org { | ||||
|                     if CONFIG.mail_enabled() { | ||||
|                         let org = Organization::find_by_uuid(&member.org_uuid, &conn).await.unwrap(); | ||||
|                         let user = User::find_by_uuid(&member.user_uuid, &conn).await.unwrap(); | ||||
| 
 | ||||
|                         mail::send_single_org_removed_from_org(&user.email, &org.name).await?; | ||||
|                     } | ||||
|                     member.delete(&conn).await?; | ||||
|                     mail::send_single_org_removed_from_org(&user.email, &org.name).await?; | ||||
|                 } | ||||
|                 member.delete(&conn).await?; | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn).await { | ||||
|     let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { | ||||
|         Some(p) => p, | ||||
|         None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), | ||||
|     }; | ||||
|  | @ -1473,7 +1468,7 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header | |||
| 
 | ||||
|     // If this flag is enabled, any user that isn't provided in the Users list will be removed (by default they will be kept unless they have Deleted == true)
 | ||||
|     if data.OverwriteExisting { | ||||
|         for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User as i32, &conn).await { | ||||
|         for user_org in UserOrganization::find_by_org_and_type(&org_id, UserOrgType::User, &conn).await { | ||||
|             if let Some(user_email) = User::find_by_uuid(&user_org.user_uuid, &conn).await.map(|u| u.email) { | ||||
|                 if !data.Users.iter().any(|u| u.Email == user_email) { | ||||
|                     user_org.delete(&conn).await?; | ||||
|  | @ -1484,3 +1479,166 @@ async fn import(org_id: String, data: JsonUpcase<OrgImportData>, headers: Header | |||
| 
 | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[put("/organizations/<org_id>/users/<org_user_id>/deactivate")] | ||||
| async fn deactivate_organization_user( | ||||
|     org_id: String, | ||||
|     org_user_id: String, | ||||
|     headers: AdminHeaders, | ||||
|     conn: DbConn, | ||||
| ) -> EmptyResult { | ||||
|     _deactivate_organization_user(&org_id, &org_user_id, &headers, &conn).await | ||||
| } | ||||
| 
 | ||||
| #[put("/organizations/<org_id>/users/deactivate", data = "<data>")] | ||||
| async fn bulk_deactivate_organization_user( | ||||
|     org_id: String, | ||||
|     data: JsonUpcase<Value>, | ||||
|     headers: AdminHeaders, | ||||
|     conn: DbConn, | ||||
| ) -> Json<Value> { | ||||
|     let data = data.into_inner().data; | ||||
| 
 | ||||
|     let mut bulk_response = Vec::new(); | ||||
|     match data["Ids"].as_array() { | ||||
|         Some(org_users) => { | ||||
|             for org_user_id in org_users { | ||||
|                 let org_user_id = org_user_id.as_str().unwrap_or_default(); | ||||
|                 let err_msg = match _deactivate_organization_user(&org_id, org_user_id, &headers, &conn).await { | ||||
|                     Ok(_) => String::from(""), | ||||
|                     Err(e) => format!("{:?}", e), | ||||
|                 }; | ||||
| 
 | ||||
|                 bulk_response.push(json!( | ||||
|                     { | ||||
|                         "Object": "OrganizationUserBulkResponseModel", | ||||
|                         "Id": org_user_id, | ||||
|                         "Error": err_msg | ||||
|                     } | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         None => error!("No users to revoke"), | ||||
|     } | ||||
| 
 | ||||
|     Json(json!({ | ||||
|         "Data": bulk_response, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": null | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
| async fn _deactivate_organization_user( | ||||
|     org_id: &str, | ||||
|     org_user_id: &str, | ||||
|     headers: &AdminHeaders, | ||||
|     conn: &DbConn, | ||||
| ) -> EmptyResult { | ||||
|     match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { | ||||
|         Some(mut user_org) if user_org.status > UserOrgStatus::Revoked as i32 => { | ||||
|             if user_org.user_uuid == headers.user.uuid { | ||||
|                 err!("You cannot revoke yourself") | ||||
|             } | ||||
|             if user_org.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { | ||||
|                 err!("Only owners can revoke other owners") | ||||
|             } | ||||
|             if user_org.atype == UserOrgType::Owner | ||||
|                 && UserOrganization::count_confirmed_by_org_and_type(org_id, UserOrgType::Owner, conn).await <= 1 | ||||
|             { | ||||
|                 err!("Organization must have at least one confirmed owner") | ||||
|             } | ||||
| 
 | ||||
|             user_org.revoke(); | ||||
|             user_org.save(conn).await?; | ||||
|         } | ||||
|         Some(_) => err!("User is already revoked"), | ||||
|         None => err!("User not found in organization"), | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
| 
 | ||||
| #[put("/organizations/<org_id>/users/<org_user_id>/activate")] | ||||
| async fn activate_organization_user( | ||||
|     org_id: String, | ||||
|     org_user_id: String, | ||||
|     headers: AdminHeaders, | ||||
|     conn: DbConn, | ||||
| ) -> EmptyResult { | ||||
|     _activate_organization_user(&org_id, &org_user_id, &headers, &conn).await | ||||
| } | ||||
| 
 | ||||
| #[put("/organizations/<org_id>/users/activate", data = "<data>")] | ||||
| async fn bulk_activate_organization_user( | ||||
|     org_id: String, | ||||
|     data: JsonUpcase<Value>, | ||||
|     headers: AdminHeaders, | ||||
|     conn: DbConn, | ||||
| ) -> Json<Value> { | ||||
|     let data = data.into_inner().data; | ||||
| 
 | ||||
|     let mut bulk_response = Vec::new(); | ||||
|     match data["Ids"].as_array() { | ||||
|         Some(org_users) => { | ||||
|             for org_user_id in org_users { | ||||
|                 let org_user_id = org_user_id.as_str().unwrap_or_default(); | ||||
|                 let err_msg = match _activate_organization_user(&org_id, org_user_id, &headers, &conn).await { | ||||
|                     Ok(_) => String::from(""), | ||||
|                     Err(e) => format!("{:?}", e), | ||||
|                 }; | ||||
| 
 | ||||
|                 bulk_response.push(json!( | ||||
|                     { | ||||
|                         "Object": "OrganizationUserBulkResponseModel", | ||||
|                         "Id": org_user_id, | ||||
|                         "Error": err_msg | ||||
|                     } | ||||
|                 )); | ||||
|             } | ||||
|         } | ||||
|         None => error!("No users to restore"), | ||||
|     } | ||||
| 
 | ||||
|     Json(json!({ | ||||
|         "Data": bulk_response, | ||||
|         "Object": "list", | ||||
|         "ContinuationToken": null | ||||
|     })) | ||||
| } | ||||
| 
 | ||||
| async fn _activate_organization_user( | ||||
|     org_id: &str, | ||||
|     org_user_id: &str, | ||||
|     headers: &AdminHeaders, | ||||
|     conn: &DbConn, | ||||
| ) -> EmptyResult { | ||||
|     match UserOrganization::find_by_uuid_and_org(org_user_id, org_id, conn).await { | ||||
|         Some(mut user_org) if user_org.status < UserOrgStatus::Accepted as i32 => { | ||||
|             if user_org.user_uuid == headers.user.uuid { | ||||
|                 err!("You cannot restore yourself") | ||||
|             } | ||||
|             if user_org.atype == UserOrgType::Owner && headers.org_user_type != UserOrgType::Owner { | ||||
|                 err!("Only owners can restore other owners") | ||||
|             } | ||||
| 
 | ||||
|             // This check is also done at accept_invite(), _confirm_invite, _activate_user(), edit_user(), admin::update_user_org_type
 | ||||
|             // It returns different error messages per function.
 | ||||
|             if user_org.atype < UserOrgType::Admin { | ||||
|                 match OrgPolicy::is_user_allowed(&user_org.user_uuid, org_id, false, conn).await { | ||||
|                     Ok(_) => {} | ||||
|                     Err(OrgPolicyErr::TwoFactorMissing) => { | ||||
|                         err!("You cannot restore this user because it has no two-step login method activated"); | ||||
|                     } | ||||
|                     Err(OrgPolicyErr::SingleOrgEnforced) => { | ||||
|                         err!("You cannot restore this user because it is a member of an organization which forbids it"); | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             user_org.activate(); | ||||
|             user_org.save(conn).await?; | ||||
|         } | ||||
|         Some(_) => err!("User is already active"), | ||||
|         None => err!("User not found in organization"), | ||||
|     } | ||||
|     Ok(()) | ||||
| } | ||||
|  |  | |||
|  | @ -70,8 +70,9 @@ struct SendData { | |||
| /// controls this policy globally.
 | ||||
| async fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult { | ||||
|     let user_uuid = &headers.user.uuid; | ||||
|     let policy_type = OrgPolicyType::DisableSend; | ||||
|     if !CONFIG.sends_allowed() || OrgPolicy::is_applicable_to_user(user_uuid, policy_type, conn).await { | ||||
|     if !CONFIG.sends_allowed() | ||||
|         || OrgPolicy::is_applicable_to_user(user_uuid, OrgPolicyType::DisableSend, None, conn).await | ||||
|     { | ||||
|         err!("Due to an Enterprise Policy, you are only able to delete an existing Send.") | ||||
|     } | ||||
|     Ok(()) | ||||
|  |  | |||
|  | @ -19,7 +19,7 @@ pub use self::device::Device; | |||
| pub use self::emergency_access::{EmergencyAccess, EmergencyAccessStatus, EmergencyAccessType}; | ||||
| pub use self::favorite::Favorite; | ||||
| pub use self::folder::{Folder, FolderCipher}; | ||||
| pub use self::org_policy::{OrgPolicy, OrgPolicyType}; | ||||
| pub use self::org_policy::{OrgPolicy, OrgPolicyErr, OrgPolicyType}; | ||||
| pub use self::organization::{Organization, UserOrgStatus, UserOrgType, UserOrganization}; | ||||
| pub use self::send::{Send, SendType}; | ||||
| pub use self::two_factor::{TwoFactor, TwoFactorType}; | ||||
|  |  | |||
|  | @ -6,7 +6,7 @@ use crate::db::DbConn; | |||
| use crate::error::MapResult; | ||||
| use crate::util::UpCase; | ||||
| 
 | ||||
| use super::{UserOrgStatus, UserOrgType, UserOrganization}; | ||||
| use super::{TwoFactor, UserOrgStatus, UserOrgType, UserOrganization}; | ||||
| 
 | ||||
| db_object! { | ||||
|     #[derive(Identifiable, Queryable, Insertable, AsChangeset)] | ||||
|  | @ -21,25 +21,37 @@ db_object! { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| // https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/PolicyType.cs
 | ||||
| #[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)] | ||||
| pub enum OrgPolicyType { | ||||
|     TwoFactorAuthentication = 0, | ||||
|     MasterPassword = 1, | ||||
|     PasswordGenerator = 2, | ||||
|     SingleOrg = 3, | ||||
|     // RequireSso = 4, // Not currently supported.
 | ||||
|     // RequireSso = 4, // Not supported
 | ||||
|     PersonalOwnership = 5, | ||||
|     DisableSend = 6, | ||||
|     SendOptions = 7, | ||||
|     // ResetPassword = 8, // Not supported
 | ||||
|     // MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
 | ||||
|     // DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
 | ||||
| } | ||||
| 
 | ||||
| // https://github.com/bitwarden/server/blob/master/src/Core/Models/Data/SendOptionsPolicyData.cs
 | ||||
| // https://github.com/bitwarden/server/blob/5cbdee137921a19b1f722920f0fa3cd45af2ef0f/src/Core/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs
 | ||||
| #[derive(Deserialize)] | ||||
| #[allow(non_snake_case)] | ||||
| pub struct SendOptionsPolicyData { | ||||
|     pub DisableHideEmail: bool, | ||||
| } | ||||
| 
 | ||||
| pub type OrgPolicyResult = Result<(), OrgPolicyErr>; | ||||
| 
 | ||||
| #[derive(Debug)] | ||||
| pub enum OrgPolicyErr { | ||||
|     TwoFactorMissing, | ||||
|     SingleOrgEnforced, | ||||
| } | ||||
| 
 | ||||
| /// Local methods
 | ||||
| impl OrgPolicy { | ||||
|     pub fn new(org_uuid: String, atype: OrgPolicyType, data: String) -> Self { | ||||
|  | @ -160,11 +172,11 @@ impl OrgPolicy { | |||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Option<Self> { | ||||
|     pub async fn find_by_org_and_type(org_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> Option<Self> { | ||||
|         db_run! { conn: { | ||||
|             org_policies::table | ||||
|                 .filter(org_policies::org_uuid.eq(org_uuid)) | ||||
|                 .filter(org_policies::atype.eq(atype)) | ||||
|                 .filter(org_policies::atype.eq(policy_type as i32)) | ||||
|                 .first::<OrgPolicyDb>(conn) | ||||
|                 .ok() | ||||
|                 .from_db() | ||||
|  | @ -179,40 +191,128 @@ impl OrgPolicy { | |||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_accepted_and_confirmed_by_user_and_active_policy( | ||||
|         user_uuid: &str, | ||||
|         policy_type: OrgPolicyType, | ||||
|         conn: &DbConn, | ||||
|     ) -> Vec<Self> { | ||||
|         db_run! { conn: { | ||||
|             org_policies::table | ||||
|                 .inner_join( | ||||
|                     users_organizations::table.on( | ||||
|                         users_organizations::org_uuid.eq(org_policies::org_uuid) | ||||
|                             .and(users_organizations::user_uuid.eq(user_uuid))) | ||||
|                 ) | ||||
|                 .filter( | ||||
|                     users_organizations::status.eq(UserOrgStatus::Accepted as i32) | ||||
|                 ) | ||||
|                 .or_filter( | ||||
|                     users_organizations::status.eq(UserOrgStatus::Confirmed as i32) | ||||
|                 ) | ||||
|                 .filter(org_policies::atype.eq(policy_type as i32)) | ||||
|                 .filter(org_policies::enabled.eq(true)) | ||||
|                 .select(org_policies::all_columns) | ||||
|                 .load::<OrgPolicyDb>(conn) | ||||
|                 .expect("Error loading org_policy") | ||||
|                 .from_db() | ||||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_confirmed_by_user_and_active_policy( | ||||
|         user_uuid: &str, | ||||
|         policy_type: OrgPolicyType, | ||||
|         conn: &DbConn, | ||||
|     ) -> Vec<Self> { | ||||
|         db_run! { conn: { | ||||
|             org_policies::table | ||||
|                 .inner_join( | ||||
|                     users_organizations::table.on( | ||||
|                         users_organizations::org_uuid.eq(org_policies::org_uuid) | ||||
|                             .and(users_organizations::user_uuid.eq(user_uuid))) | ||||
|                 ) | ||||
|                 .filter( | ||||
|                     users_organizations::status.eq(UserOrgStatus::Confirmed as i32) | ||||
|                 ) | ||||
|                 .filter(org_policies::atype.eq(policy_type as i32)) | ||||
|                 .filter(org_policies::enabled.eq(true)) | ||||
|                 .select(org_policies::all_columns) | ||||
|                 .load::<OrgPolicyDb>(conn) | ||||
|                 .expect("Error loading org_policy") | ||||
|                 .from_db() | ||||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     /// Returns true if the user belongs to an org that has enabled the specified policy type,
 | ||||
|     /// and the user is not an owner or admin of that org. This is only useful for checking
 | ||||
|     /// applicability of policy types that have these particular semantics.
 | ||||
|     pub async fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool { | ||||
|         // TODO: Should check confirmed and accepted users
 | ||||
|         for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn).await { | ||||
|             if policy.enabled && policy.has_type(policy_type) { | ||||
|                 let org_uuid = &policy.org_uuid; | ||||
|                 if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await { | ||||
|                     if user.atype < UserOrgType::Admin { | ||||
|                         return true; | ||||
|                     } | ||||
|     pub async fn is_applicable_to_user( | ||||
|         user_uuid: &str, | ||||
|         policy_type: OrgPolicyType, | ||||
|         exclude_org_uuid: Option<&str>, | ||||
|         conn: &DbConn, | ||||
|     ) -> bool { | ||||
|         for policy in | ||||
|             OrgPolicy::find_accepted_and_confirmed_by_user_and_active_policy(user_uuid, policy_type, conn).await | ||||
|         { | ||||
|             // Check if we need to skip this organization.
 | ||||
|             if exclude_org_uuid.is_some() && exclude_org_uuid.unwrap() == policy.org_uuid { | ||||
|                 continue; | ||||
|             } | ||||
| 
 | ||||
|             if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { | ||||
|                 if user.atype < UserOrgType::Admin { | ||||
|                     return true; | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         false | ||||
|     } | ||||
| 
 | ||||
|     pub async fn is_user_allowed( | ||||
|         user_uuid: &str, | ||||
|         org_uuid: &str, | ||||
|         exclude_current_org: bool, | ||||
|         conn: &DbConn, | ||||
|     ) -> OrgPolicyResult { | ||||
|         // Enforce TwoFactor/TwoStep login
 | ||||
|         if TwoFactor::find_by_user(user_uuid, conn).await.is_empty() { | ||||
|             match Self::find_by_org_and_type(org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await { | ||||
|                 Some(p) if p.enabled => { | ||||
|                     return Err(OrgPolicyErr::TwoFactorMissing); | ||||
|                 } | ||||
|                 _ => {} | ||||
|             }; | ||||
|         } | ||||
| 
 | ||||
|         // Enforce Single Organization Policy of other organizations user is a member of
 | ||||
|         // This check here needs to exclude this current org-id, else an accepted user can not be confirmed.
 | ||||
|         let exclude_org = if exclude_current_org { | ||||
|             Some(org_uuid) | ||||
|         } else { | ||||
|             None | ||||
|         }; | ||||
|         if Self::is_applicable_to_user(user_uuid, OrgPolicyType::SingleOrg, exclude_org, conn).await { | ||||
|             return Err(OrgPolicyErr::SingleOrgEnforced); | ||||
|         } | ||||
| 
 | ||||
|         Ok(()) | ||||
|     } | ||||
| 
 | ||||
|     /// Returns true if the user belongs to an org that has enabled the `DisableHideEmail`
 | ||||
|     /// option of the `Send Options` policy, and the user is not an owner or admin of that org.
 | ||||
|     pub async fn is_hide_email_disabled(user_uuid: &str, conn: &DbConn) -> bool { | ||||
|         for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn).await { | ||||
|             if policy.enabled && policy.has_type(OrgPolicyType::SendOptions) { | ||||
|                 let org_uuid = &policy.org_uuid; | ||||
|                 if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, org_uuid, conn).await { | ||||
|                     if user.atype < UserOrgType::Admin { | ||||
|                         match serde_json::from_str::<UpCase<SendOptionsPolicyData>>(&policy.data) { | ||||
|                             Ok(opts) => { | ||||
|                                 if opts.data.DisableHideEmail { | ||||
|                                     return true; | ||||
|                                 } | ||||
|         for policy in | ||||
|             OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await | ||||
|         { | ||||
|             if let Some(user) = UserOrganization::find_by_user_and_org(user_uuid, &policy.org_uuid, conn).await { | ||||
|                 if user.atype < UserOrgType::Admin { | ||||
|                     match serde_json::from_str::<UpCase<SendOptionsPolicyData>>(&policy.data) { | ||||
|                         Ok(opts) => { | ||||
|                             if opts.data.DisableHideEmail { | ||||
|                                 return true; | ||||
|                             } | ||||
|                             _ => error!("Failed to deserialize policy data: {}", policy.data), | ||||
|                         } | ||||
|                         _ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data), | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|  |  | |||
|  | @ -31,7 +31,9 @@ db_object! { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| // https://github.com/bitwarden/server/blob/b86a04cef9f1e1b82cf18e49fc94e017c641130c/src/Core/Enums/OrganizationUserStatusType.cs
 | ||||
| pub enum UserOrgStatus { | ||||
|     Revoked = -1, | ||||
|     Invited = 0, | ||||
|     Accepted = 1, | ||||
|     Confirmed = 2, | ||||
|  | @ -133,26 +135,29 @@ impl Organization { | |||
|             public_key, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     // https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/Organizations/OrganizationResponseModel.cs
 | ||||
|     pub fn to_json(&self) -> Value { | ||||
|         json!({ | ||||
|             "Id": self.uuid, | ||||
|             "Identifier": null, // not supported by us
 | ||||
|             "Name": self.name, | ||||
|             "Seats": 10, // The value doesn't matter, we don't check server-side
 | ||||
|             // "MaxAutoscaleSeats": null, // The value doesn't matter, we don't check server-side
 | ||||
|             "MaxCollections": 10, // The value doesn't matter, we don't check server-side
 | ||||
|             "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
 | ||||
|             "Use2fa": true, | ||||
|             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
 | ||||
|             "UseEvents": false, // not supported by us
 | ||||
|             "UseGroups": false, // not supported by us
 | ||||
|             "UseEvents": false, // Not supported
 | ||||
|             "UseGroups": false, // Not supported
 | ||||
|             "UseTotp": true, | ||||
|             "UsePolicies": true, | ||||
|             "UseSso": false, // We do not support SSO
 | ||||
|             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
 | ||||
|             "UseSso": false, // Not supported
 | ||||
|             // "UseKeyConnector": false, // Not supported
 | ||||
|             "SelfHost": true, | ||||
|             "UseApi": false, // not supported by us
 | ||||
|             "UseApi": false, // Not supported
 | ||||
|             "HasPublicAndPrivateKeys": self.private_key.is_some() && self.public_key.is_some(), | ||||
|             "ResetPasswordEnrolled": false, // not supported by us
 | ||||
|             "UseResetPassword": false, // Not supported
 | ||||
| 
 | ||||
|             "BusinessName": null, | ||||
|             "BusinessAddress1": null, | ||||
|  | @ -170,6 +175,12 @@ impl Organization { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| // Used to either subtract or add to the current status
 | ||||
| // The number 128 should be fine, it is well within the range of an i32
 | ||||
| // The same goes for the database where we only use INTEGER (the same as an i32)
 | ||||
| // It should also provide enough room for 100+ types, which i doubt will ever happen.
 | ||||
| static ACTIVATE_REVOKE_DIFF: i32 = 128; | ||||
| 
 | ||||
| impl UserOrganization { | ||||
|     pub fn new(user_uuid: String, org_uuid: String) -> Self { | ||||
|         Self { | ||||
|  | @ -184,6 +195,18 @@ impl UserOrganization { | |||
|             atype: UserOrgType::User as i32, | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn activate(&mut self) { | ||||
|         if self.status < UserOrgStatus::Accepted as i32 { | ||||
|             self.status += ACTIVATE_REVOKE_DIFF; | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     pub fn revoke(&mut self) { | ||||
|         if self.status > UserOrgStatus::Revoked as i32 { | ||||
|             self.status -= ACTIVATE_REVOKE_DIFF; | ||||
|         } | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| use crate::db::DbConn; | ||||
|  | @ -265,9 +288,10 @@ impl UserOrganization { | |||
|     pub async fn to_json(&self, conn: &DbConn) -> Value { | ||||
|         let org = Organization::find_by_uuid(&self.org_uuid, conn).await.unwrap(); | ||||
| 
 | ||||
|         // https://github.com/bitwarden/server/blob/13d1e74d6960cf0d042620b72d85bf583a4236f7/src/Api/Models/Response/ProfileOrganizationResponseModel.cs
 | ||||
|         json!({ | ||||
|             "Id": self.org_uuid, | ||||
|             "Identifier": null, // not supported by us
 | ||||
|             "Identifier": null, // Not supported
 | ||||
|             "Name": org.name, | ||||
|             "Seats": 10, // The value doesn't matter, we don't check server-side
 | ||||
|             "MaxCollections": 10, // The value doesn't matter, we don't check server-side
 | ||||
|  | @ -275,44 +299,48 @@ impl UserOrganization { | |||
| 
 | ||||
|             "Use2fa": true, | ||||
|             "UseDirectory": false, // Is supported, but this value isn't checked anywhere (yet)
 | ||||
|             "UseEvents": false, // not supported by us
 | ||||
|             "UseGroups": false, // not supported by us
 | ||||
|             "UseEvents": false, // Not supported
 | ||||
|             "UseGroups": false, // Not supported
 | ||||
|             "UseTotp": true, | ||||
|             // "UseScim": false, // Not supported (Not AGPLv3 Licensed)
 | ||||
|             "UsePolicies": true, | ||||
|             "UseApi": false, // not supported by us
 | ||||
|             "UseApi": false, // Not supported
 | ||||
|             "SelfHost": true, | ||||
|             "HasPublicAndPrivateKeys": org.private_key.is_some() && org.public_key.is_some(), | ||||
|             "ResetPasswordEnrolled": false, // not supported by us
 | ||||
|             "SsoBound": false, // We do not support SSO
 | ||||
|             "UseSso": false, // We do not support SSO
 | ||||
|             // TODO: Add support for Business Portal
 | ||||
|             // Upstream is moving Policies and SSO management outside of the web-vault to /portal
 | ||||
|             // For now they still have that code also in the web-vault, but they will remove it at some point.
 | ||||
|             // https://github.com/bitwarden/server/tree/master/bitwarden_license/src/
 | ||||
|             "UseBusinessPortal": false, // Disable BusinessPortal Button
 | ||||
|             "ResetPasswordEnrolled": false, // Not supported
 | ||||
|             "SsoBound": false, // Not supported
 | ||||
|             "UseSso": false, // Not supported
 | ||||
|             "ProviderId": null, | ||||
|             "ProviderName": null, | ||||
|             // "KeyConnectorEnabled": false,
 | ||||
|             // "KeyConnectorUrl": null,
 | ||||
| 
 | ||||
|             // TODO: Add support for Custom User Roles
 | ||||
|             // See: https://bitwarden.com/help/article/user-types-access-control/#custom-role
 | ||||
|             // "Permissions": {
 | ||||
|             //     "AccessBusinessPortal": false,
 | ||||
|             //     "AccessEventLogs": false,
 | ||||
|             //     "AccessEventLogs": false, // Not supported
 | ||||
|             //     "AccessImportExport": false,
 | ||||
|             //     "AccessReports": false,
 | ||||
|             //     "ManageAllCollections": false,
 | ||||
|             //     "CreateNewCollections": false,
 | ||||
|             //     "EditAnyCollection": false,
 | ||||
|             //     "DeleteAnyCollection": false,
 | ||||
|             //     "ManageAssignedCollections": false,
 | ||||
|             //     "editAssignedCollections": false,
 | ||||
|             //     "deleteAssignedCollections": false,
 | ||||
|             //     "ManageCiphers": false,
 | ||||
|             //     "ManageGroups": false,
 | ||||
|             //     "ManageGroups": false, // Not supported
 | ||||
|             //     "ManagePolicies": false,
 | ||||
|             //     "ManageResetPassword": false,
 | ||||
|             //     "ManageSso": false,
 | ||||
|             //     "ManageResetPassword": false, // Not supported
 | ||||
|             //     "ManageSso": false, // Not supported
 | ||||
|             //     "ManageUsers": false,
 | ||||
|             //     "ManageScim": false, // Not supported (Not AGPLv3 Licensed)
 | ||||
|             // },
 | ||||
| 
 | ||||
|             "MaxStorageGb": 10, // The value doesn't matter, we don't check server-side
 | ||||
| 
 | ||||
|             // These are per user
 | ||||
|             "UserId": self.user_uuid, | ||||
|             "Key": self.akey, | ||||
|             "Status": self.status, | ||||
|             "Type": self.atype, | ||||
|  | @ -325,13 +353,21 @@ impl UserOrganization { | |||
|     pub async fn to_json_user_details(&self, conn: &DbConn) -> Value { | ||||
|         let user = User::find_by_uuid(&self.user_uuid, conn).await.unwrap(); | ||||
| 
 | ||||
|         // Because BitWarden want the status to be -1 for revoked users we need to catch that here.
 | ||||
|         // We subtract/add a number so we can restore/activate the user to it's previouse state again.
 | ||||
|         let status = if self.status < UserOrgStatus::Revoked as i32 { | ||||
|             UserOrgStatus::Revoked as i32 | ||||
|         } else { | ||||
|             self.status | ||||
|         }; | ||||
| 
 | ||||
|         json!({ | ||||
|             "Id": self.uuid, | ||||
|             "UserId": self.user_uuid, | ||||
|             "Name": user.name, | ||||
|             "Email": user.email, | ||||
| 
 | ||||
|             "Status": self.status, | ||||
|             "Status": status, | ||||
|             "Type": self.atype, | ||||
|             "AccessAll": self.access_all, | ||||
| 
 | ||||
|  | @ -365,11 +401,19 @@ impl UserOrganization { | |||
|                 .collect() | ||||
|         }; | ||||
| 
 | ||||
|         // Because BitWarden want the status to be -1 for revoked users we need to catch that here.
 | ||||
|         // We subtract/add a number so we can restore/activate the user to it's previouse state again.
 | ||||
|         let status = if self.status < UserOrgStatus::Revoked as i32 { | ||||
|             UserOrgStatus::Revoked as i32 | ||||
|         } else { | ||||
|             self.status | ||||
|         }; | ||||
| 
 | ||||
|         json!({ | ||||
|             "Id": self.uuid, | ||||
|             "UserId": self.user_uuid, | ||||
| 
 | ||||
|             "Status": self.status, | ||||
|             "Status": status, | ||||
|             "Type": self.atype, | ||||
|             "AccessAll": self.access_all, | ||||
|             "Collections": coll_uuids, | ||||
|  | @ -507,6 +551,18 @@ impl UserOrganization { | |||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn count_accepted_and_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> i64 { | ||||
|         db_run! { conn: { | ||||
|             users_organizations::table | ||||
|                 .filter(users_organizations::user_uuid.eq(user_uuid)) | ||||
|                 .filter(users_organizations::status.eq(UserOrgStatus::Accepted as i32)) | ||||
|                 .or_filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) | ||||
|                 .count() | ||||
|                 .first::<i64>(conn) | ||||
|                 .unwrap_or(0) | ||||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_by_org(org_uuid: &str, conn: &DbConn) -> Vec<Self> { | ||||
|         db_run! { conn: { | ||||
|             users_organizations::table | ||||
|  | @ -527,16 +583,28 @@ impl UserOrganization { | |||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_by_org_and_type(org_uuid: &str, atype: i32, conn: &DbConn) -> Vec<Self> { | ||||
|     pub async fn find_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &DbConn) -> Vec<Self> { | ||||
|         db_run! { conn: { | ||||
|             users_organizations::table | ||||
|                 .filter(users_organizations::org_uuid.eq(org_uuid)) | ||||
|                 .filter(users_organizations::atype.eq(atype)) | ||||
|                 .filter(users_organizations::atype.eq(atype as i32)) | ||||
|                 .load::<UserOrganizationDb>(conn) | ||||
|                 .expect("Error loading user organizations").from_db() | ||||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn count_confirmed_by_org_and_type(org_uuid: &str, atype: UserOrgType, conn: &DbConn) -> i64 { | ||||
|         db_run! { conn: { | ||||
|             users_organizations::table | ||||
|                 .filter(users_organizations::org_uuid.eq(org_uuid)) | ||||
|                 .filter(users_organizations::atype.eq(atype as i32)) | ||||
|                 .filter(users_organizations::status.eq(UserOrgStatus::Confirmed as i32)) | ||||
|                 .count() | ||||
|                 .first::<i64>(conn) | ||||
|                 .unwrap_or(0) | ||||
|         }} | ||||
|     } | ||||
| 
 | ||||
|     pub async fn find_by_user_and_org(user_uuid: &str, org_uuid: &str, conn: &DbConn) -> Option<Self> { | ||||
|         db_run! { conn: { | ||||
|             users_organizations::table | ||||
|  |  | |||
|  | @ -275,11 +275,11 @@ impl User { | |||
| 
 | ||||
|     pub async fn delete(self, conn: &DbConn) -> EmptyResult { | ||||
|         for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn).await { | ||||
|             if user_org.atype == UserOrgType::Owner { | ||||
|                 let owner_type = UserOrgType::Owner as i32; | ||||
|                 if UserOrganization::find_by_org_and_type(&user_org.org_uuid, owner_type, conn).await.len() <= 1 { | ||||
|                     err!("Can't delete last owner") | ||||
|                 } | ||||
|             if user_org.atype == UserOrgType::Owner | ||||
|                 && UserOrganization::count_confirmed_by_org_and_type(&user_org.org_uuid, UserOrgType::Owner, conn).await | ||||
|                     <= 1 | ||||
|             { | ||||
|                 err!("Can't delete last owner") | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue