Spiegel von
https://github.com/dani-garcia/vaultwarden.git
synchronisiert 2024-11-22 05:10:29 +01:00
Merge branch 'org-user-revoke-access' of https://github.com/BlackDex/vaultwarden into BlackDex-org-user-revoke-access
Dieser Commit ist enthalten in:
Commit
7598997deb
9 geänderte Dateien mit 486 neuen und 148 gelöschten Zeilen
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Laden …
In neuem Issue referenzieren