vw_small

Hardened fork of Vaultwarden (https://github.com/dani-garcia/vaultwarden) with fewer features.
git clone https://git.philomathiclife.com/repos/vw_small
Log | Files | Refs | README

commit 7598997deb9b54409c75db68bd9317824eb27138
parent 3c876dc2028f1834672b06e038bff3445a106267
Author: Daniel GarcĂ­a <dani-garcia@users.noreply.github.com>
Date:   Sun,  4 Sep 2022 23:04:15 +0200

Merge branch 'org-user-revoke-access' of https://github.com/BlackDex/vaultwarden into BlackDex-org-user-revoke-access

Diffstat:
Msrc/api/admin.rs | 21++++++++++++++++-----
Msrc/api/core/ciphers.rs | 2+-
Msrc/api/core/emergency_access.rs | 6+++---
Msrc/api/core/organizations.rs | 318+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------------
Msrc/api/core/sends.rs | 5+++--
Msrc/db/models/mod.rs | 2+-
Msrc/db/models/org_policy.rs | 150++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++--------------
Msrc/db/models/organization.rs | 122+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------
Msrc/db/models/user.rs | 10+++++-----
9 files changed, 487 insertions(+), 149 deletions(-)

diff --git a/src/api/admin.rs b/src/api/admin.rs @@ -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 } diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs @@ -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.") } } diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs @@ -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()), } diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs @@ -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; - - 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?; + // 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(); + + 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(()) +} diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs @@ -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(()) diff --git a/src/db/models/mod.rs b/src/db/models/mod.rs @@ -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}; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs @@ -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), } } } diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs @@ -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 diff --git a/src/db/models/user.rs b/src/db/models/user.rs @@ -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") } }