From d014eede9a7fa85e4f809656a7f6aed61caafff0 Mon Sep 17 00:00:00 2001 From: Adam Jones Date: Fri, 24 Sep 2021 17:55:49 +0200 Subject: [PATCH] feature: Support single organization policy This adds back-end support for the [single organization policy](https://bitwarden.com/help/article/policies/#single-organization). --- src/api/core/ciphers.rs | 2 +- src/api/core/emergency_access.rs | 2 +- src/api/core/organizations.rs | 56 +++++++++++++++++++ src/api/identity.rs | 4 +- src/config.rs | 1 + src/db/models/org_policy.rs | 11 ++-- src/db/models/organization.rs | 2 +- src/db/models/user.rs | 4 +- src/mail.rs | 12 ++++ .../send_single_org_removed_from_org.hbs | 5 ++ .../send_single_org_removed_from_org.html.hbs | 11 ++++ 11 files changed, 97 insertions(+), 13 deletions(-) create mode 100644 src/static/templates/email/send_single_org_removed_from_org.hbs create mode 100644 src/static/templates/email/send_single_org_removed_from_org.html.hbs diff --git a/src/api/core/ciphers.rs b/src/api/core/ciphers.rs index b5a9737a..ff193a3e 100644 --- a/src/api/core/ciphers.rs +++ b/src/api/core/ciphers.rs @@ -105,7 +105,7 @@ fn sync(data: Form, headers: Headers, conn: DbConn) -> Json { let collections_json: Vec = collections.iter().map(|c| c.to_json_details(&headers.user.uuid, &conn)).collect(); - let policies = OrgPolicy::find_by_user(&headers.user.uuid, &conn); + let policies = OrgPolicy::find_confirmed_by_user(&headers.user.uuid, &conn); let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); let ciphers = Cipher::find_by_user_visible(&headers.user.uuid, &conn); diff --git a/src/api/core/emergency_access.rs b/src/api/core/emergency_access.rs index 0b87a84b..50c885e4 100644 --- a/src/api/core/emergency_access.rs +++ b/src/api/core/emergency_access.rs @@ -683,7 +683,7 @@ fn policies_emergency_access(emer_id: String, headers: Headers, conn: DbConn) -> None => err!("Grantor user not found."), }; - let policies = OrgPolicy::find_by_user(&grantor_user.uuid, &conn); + let policies = OrgPolicy::find_confirmed_by_user(&grantor_user.uuid, &conn); let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); Ok(Json(json!({ diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 82ab552e..99e68234 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -102,6 +102,11 @@ fn create_organization(headers: Headers, data: JsonUpcase, conn: DbConn 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) { + 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." + ) + } let data: OrgData = data.into_inner().data; let (private_key, public_key) = if data.Keys.is_some() { @@ -747,6 +752,30 @@ fn accept_invite(_org_id: String, _org_user_id: String, data: JsonUpcase 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) + .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.") + } + } + + // 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) { + 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)?; } @@ -1219,6 +1248,33 @@ fn put_policy( } } + // If enabling the SingleOrg policy, remove this org's members that are members of other orgs + if pol_type_enum == OrgPolicyType::SingleOrg && data.enabled { + let org_members = UserOrganization::find_by_org(&org_id, &conn); + + for member in org_members.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) + .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).unwrap(); + let user = User::find_by_uuid(&member.user_uuid, &conn).unwrap(); + + mail::send_single_org_removed_from_org(&user.email, &org.name)?; + } + member.delete(&conn)?; + } + } + } + } + let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type, &conn) { Some(p) => p, None => OrgPolicy::new(org_id, pol_type_enum, "{}".to_string()), diff --git a/src/api/identity.rs b/src/api/identity.rs index 1c1ab233..bfc47570 100644 --- a/src/api/identity.rs +++ b/src/api/identity.rs @@ -56,7 +56,7 @@ fn _refresh_login(data: ConnectData, conn: DbConn) -> JsonResult { // COMMON let user = User::find_by_uuid(&device.user_uuid, &conn).unwrap(); - let orgs = UserOrganization::find_by_user(&user.uuid, &conn); + let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn); let (access_token, expires_in) = device.refresh_tokens(&user, orgs); @@ -147,7 +147,7 @@ fn _password_login(data: ConnectData, conn: DbConn, ip: &ClientIp) -> JsonResult } // Common - let orgs = UserOrganization::find_by_user(&user.uuid, &conn); + let orgs = UserOrganization::find_confirmed_by_user(&user.uuid, &conn); let (access_token, expires_in) = device.refresh_tokens(&user, orgs); device.save(&conn)?; diff --git a/src/config.rs b/src/config.rs index 31a57bb3..d4b418bc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -874,6 +874,7 @@ where reg!("email/pw_hint_none", ".html"); reg!("email/pw_hint_some", ".html"); reg!("email/send_2fa_removed_from_org", ".html"); + reg!("email/send_single_org_removed_from_org", ".html"); reg!("email/send_org_invite", ".html"); reg!("email/send_emergency_access_invite", ".html"); reg!("email/twofactor_email", ".html"); diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 34eaedb1..7c6cefd3 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -27,7 +27,7 @@ pub enum OrgPolicyType { TwoFactorAuthentication = 0, MasterPassword = 1, PasswordGenerator = 2, - // SingleOrg = 3, // Not currently supported. + SingleOrg = 3, // RequireSso = 4, // Not currently supported. PersonalOwnership = 5, DisableSend = 6, @@ -143,7 +143,7 @@ impl OrgPolicy { }} } - pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec { + pub fn find_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> Vec { db_run! { conn: { org_policies::table .inner_join( @@ -184,8 +184,8 @@ impl OrgPolicy { /// 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 fn is_applicable_to_user(user_uuid: &str, policy_type: OrgPolicyType, conn: &DbConn) -> bool { - // Returns confirmed users only. - for policy in OrgPolicy::find_by_user(user_uuid, conn) { + // TODO: Should check confirmed and accepted users + for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn) { 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) { @@ -201,8 +201,7 @@ impl OrgPolicy { /// 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 fn is_hide_email_disabled(user_uuid: &str, conn: &DbConn) -> bool { - // Returns confirmed users only. - for policy in OrgPolicy::find_by_user(user_uuid, conn) { + for policy in OrgPolicy::find_confirmed_by_user(user_uuid, conn) { 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) { diff --git a/src/db/models/organization.rs b/src/db/models/organization.rs index 000b63e9..67dd5357 100644 --- a/src/db/models/organization.rs +++ b/src/db/models/organization.rs @@ -477,7 +477,7 @@ impl UserOrganization { }} } - pub fn find_by_user(user_uuid: &str, conn: &DbConn) -> Vec { + pub fn find_confirmed_by_user(user_uuid: &str, conn: &DbConn) -> Vec { db_run! { conn: { users_organizations::table .filter(users_organizations::user_uuid.eq(user_uuid)) diff --git a/src/db/models/user.rs b/src/db/models/user.rs index d382cad7..17cd7fab 100644 --- a/src/db/models/user.rs +++ b/src/db/models/user.rs @@ -185,7 +185,7 @@ use crate::error::MapResult; /// Database methods impl User { pub fn to_json(&self, conn: &DbConn) -> Value { - let orgs = UserOrganization::find_by_user(&self.uuid, conn); + let orgs = UserOrganization::find_confirmed_by_user(&self.uuid, conn); let orgs_json: Vec = orgs.iter().map(|c| c.to_json(conn)).collect(); let twofactor_enabled = !TwoFactor::find_by_user(&self.uuid, conn).is_empty(); @@ -256,7 +256,7 @@ impl User { } pub fn delete(self, conn: &DbConn) -> EmptyResult { - for user_org in UserOrganization::find_by_user(&self.uuid, conn) { + for user_org in UserOrganization::find_confirmed_by_user(&self.uuid, conn) { 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).len() <= 1 { diff --git a/src/mail.rs b/src/mail.rs index f4278bef..001bf301 100644 --- a/src/mail.rs +++ b/src/mail.rs @@ -195,6 +195,18 @@ pub fn send_2fa_removed_from_org(address: &str, org_name: &str) -> EmptyResult { send_email(address, &subject, body_html, body_text) } +pub fn send_single_org_removed_from_org(address: &str, org_name: &str) -> EmptyResult { + let (subject, body_html, body_text) = get_text( + "email/send_single_org_removed_from_org", + json!({ + "url": CONFIG.domain(), + "org_name": org_name, + }), + )?; + + send_email(address, &subject, body_html, body_text) +} + pub fn send_invite( address: &str, uuid: &str, diff --git a/src/static/templates/email/send_single_org_removed_from_org.hbs b/src/static/templates/email/send_single_org_removed_from_org.hbs new file mode 100644 index 00000000..0686d986 --- /dev/null +++ b/src/static/templates/email/send_single_org_removed_from_org.hbs @@ -0,0 +1,5 @@ +You have been removed from {{{org_name}}} + +Your user account has been removed from the *{{org_name}}* organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. +=== +Github: https://github.com/dani-garcia/vaultwarden diff --git a/src/static/templates/email/send_single_org_removed_from_org.html.hbs b/src/static/templates/email/send_single_org_removed_from_org.html.hbs new file mode 100644 index 00000000..e4026628 --- /dev/null +++ b/src/static/templates/email/send_single_org_removed_from_org.html.hbs @@ -0,0 +1,11 @@ +You have been removed from {{{org_name}}} + +{{> email/email_header }} + + + + +
+ Your user account has been removed from the {{org_name}} organization because you are a part of another organization. The {{org_name}} organization has enabled a policy that prevents users from being a part of multiple organizations. Before you can re-join this organization you need to leave all other organizations or join with a different account. +
+{{> email/email_footer }}