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:
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")
}
}