You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

526 lines
22 KiB

use chrono::{NaiveDateTime, Utc};
use derive_more::{AsRef, From};
use diesel::prelude::*;
use serde::Deserialize;
use serde_json::Value;
use crate::{
CONFIG,
api::{EmptyResult, core::two_factor},
db::{
DbConn,
schema::{org_policies, users_organizations},
},
error::MapResult,
util::format_date,
};
use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId};
#[derive(Identifiable, Queryable, Insertable, AsChangeset)]
#[diesel(table_name = org_policies)]
#[diesel(primary_key(uuid))]
pub struct OrgPolicy {
pub uuid: OrgPolicyId,
pub org_uuid: OrganizationId,
pub atype: i32,
pub enabled: bool,
pub data: String,
pub revision_date: NaiveDateTime,
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/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 supported
PersonalOwnership = 5,
DisableSend = 6,
SendOptions = 7,
ResetPassword = 8,
// MaximumVaultTimeout = 9, // Not supported (Not AGPLv3 Licensed)
// DisablePersonalVaultExport = 10, // Not supported (Not AGPLv3 Licensed)
// ActivateAutofill = 11,
// AutomaticAppLogIn = 12,
// FreeFamiliesSponsorshipPolicy = 13,
RemoveUnlockWithPin = 14,
RestrictedItemTypes = 15,
UriMatchDefaults = 16,
AutotypeDefaultSetting = 17,
AutomaticUserConfirmation = 18,
// BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed)
OrganizationUserNotification = 20,
// SendControls = 21, // Not supported yet
FillAssist = 22,
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SendOptionsPolicyData {
#[serde(rename = "disableHideEmail", alias = "DisableHideEmail")]
pub disable_hide_email: bool,
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResetPasswordDataModel {
#[serde(rename = "autoEnrollEnabled", alias = "AutoEnrollEnabled")]
pub auto_enroll_enabled: bool,
}
// https://github.com/bitwarden/server/blob/13bc813de8e715a71d3410f1e584e87ccdf76025/src/Core/AdminConsole/Models/Data/Organizations/Policies/FillAssistPolicyData.cs
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct FillAssistPolicyData {
#[serde(alias = "RulesUrl")]
pub rules_url: Option<String>,
}
/// Local methods
impl OrgPolicy {
pub fn new(org_uuid: OrganizationId, atype: OrgPolicyType, enabled: bool, data: String) -> Self {
Self {
uuid: OrgPolicyId(crate::util::get_uuid()),
org_uuid,
atype: atype as i32,
enabled,
data,
revision_date: Utc::now().naive_utc(),
}
}
pub fn has_type(&self, policy_type: OrgPolicyType) -> bool {
self.atype == policy_type as i32
}
pub fn to_json(&self) -> Value {
let data_json: Value = serde_json::from_str(&self.data).unwrap_or(Value::Null);
let mut policy = json!({
"id": self.uuid,
"organizationId": self.org_uuid,
"type": self.atype,
"data": data_json,
"enabled": self.enabled,
"revisionDate": format_date(&self.revision_date),
"object": "policy",
});
// Upstream adds this key/value
// Allow enabling Single Org policy when the organization has claimed domains.
// See: (https://github.com/bitwarden/server/pull/5565)
// We return the same to prevent possible issues
if self.atype == 8i32 {
policy["canToggleState"] = json!(true);
}
policy
}
}
/// Database methods
impl OrgPolicy {
pub async fn save(&mut self, conn: &DbConn) -> EmptyResult {
self.revision_date = Utc::now().naive_utc();
db_run! { conn:
sqlite, mysql {
match diesel::replace_into(org_policies::table)
.values(&*self)
.execute(conn)
{
Ok(_) => Ok(()),
// Record already exists and causes a Foreign Key Violation because replace_into() wants to delete the record first.
Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => {
diesel::update(org_policies::table)
.filter(org_policies::uuid.eq(&self.uuid))
.set(&*self)
.execute(conn)
.map_res("Error saving org_policy")
}
Err(e) => Err(e.into()),
}.map_res("Error saving org_policy")
}
postgresql {
// We need to make sure we're not going to violate the unique constraint on org_uuid and atype.
// This happens automatically on other DBMS backends due to replace_into(). PostgreSQL does
// not support multiple constraints on ON CONFLICT clauses.
let _: () = diesel::delete(
org_policies::table
.filter(org_policies::org_uuid.eq(&self.org_uuid))
.filter(org_policies::atype.eq(&self.atype)),
)
.execute(conn)
.map_res("Error deleting org_policy for insert")?;
diesel::insert_into(org_policies::table)
.values(&*self)
.on_conflict(org_policies::uuid)
.do_update()
.set(&*self)
.execute(conn)
.map_res("Error saving org_policy")
}
}
}
pub async fn delete(self, conn: &DbConn) -> EmptyResult {
conn.run(move |conn| {
diesel::delete(org_policies::table.filter(org_policies::uuid.eq(self.uuid)))
.execute(conn)
.map_res("Error deleting org_policy")
})
.await
}
pub async fn find_by_org(org_uuid: &OrganizationId, conn: &DbConn) -> Vec<Self> {
conn.run(move |conn| {
org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid))
.load::<Self>(conn)
.expect("Error loading org_policy")
})
.await
}
pub async fn find_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
conn.run(move |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(MembershipStatus::Confirmed as i32))
.select(org_policies::all_columns)
.load::<Self>(conn)
.expect("Error loading org_policy")
})
.await
}
pub async fn find_accepted_and_confirmed_by_user(user_uuid: &UserId, conn: &DbConn) -> Vec<Self> {
conn.run(move |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(MembershipStatus::Accepted as i32)
.or(users_organizations::status.eq(MembershipStatus::Confirmed as i32)),
)
.select(org_policies::all_columns)
.load::<Self>(conn)
.expect("Error loading org_policy")
})
.await
}
pub async fn find_by_org_and_type(
org_uuid: &OrganizationId,
policy_type: OrgPolicyType,
conn: &DbConn,
) -> Option<Self> {
conn.run(move |conn| {
org_policies::table
.filter(org_policies::org_uuid.eq(org_uuid))
.filter(org_policies::atype.eq(policy_type as i32))
.first::<Self>(conn)
.ok()
})
.await
}
pub async fn delete_all_by_organization(org_uuid: &OrganizationId, conn: &DbConn) -> EmptyResult {
conn.run(move |conn| {
diesel::delete(org_policies::table.filter(org_policies::org_uuid.eq(org_uuid)))
.execute(conn)
.map_res("Error deleting org_policy")
})
.await
}
pub async fn find_accepted_and_confirmed_by_user_and_active_policy(
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &DbConn,
) -> Vec<Self> {
conn.run(move |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(MembershipStatus::Accepted as i32))
.or_filter(users_organizations::status.eq(MembershipStatus::Confirmed as i32))
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
.select(org_policies::all_columns)
.load::<Self>(conn)
.expect("Error loading org_policy")
})
.await
}
pub async fn find_confirmed_by_user_and_active_policy(
user_uuid: &UserId,
policy_type: OrgPolicyType,
conn: &DbConn,
) -> Vec<Self> {
conn.run(move |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(MembershipStatus::Confirmed as i32))
.filter(org_policies::atype.eq(policy_type as i32))
.filter(org_policies::enabled.eq(true))
.select(org_policies::all_columns)
.load::<Self>(conn)
.expect("Error loading org_policy")
})
.await
}
/// 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: &UserId,
policy_type: OrgPolicyType,
exclude_org_uuid: Option<&OrganizationId>,
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) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await
&& user.atype < MembershipType::Admin
{
return true;
}
}
false
}
/// Whether the policy is stored as enabled for this organization, regardless of any config option.
pub async fn is_enabled(org_uuid: &OrganizationId, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
Self::find_by_org_and_type(org_uuid, policy_type, conn).await.is_some_and(|p| p.enabled)
}
/// Requires both the server-wide option and this organization's policy.
pub async fn is_auto_confirm_enabled(org_uuid: &OrganizationId, conn: &DbConn) -> bool {
CONFIG.org_auto_confirm_enabled()
&& Self::is_enabled(org_uuid, OrgPolicyType::AutomaticUserConfirmation, conn).await
}
pub async fn check_user_allowed(m: &Membership, action: &str, conn: &DbConn) -> EmptyResult {
if m.atype < MembershipType::Admin && m.status > (MembershipStatus::Invited as i32) {
// Enforce TwoFactor/TwoStep login
if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::TwoFactorAuthentication, conn).await
&& p.enabled
&& TwoFactor::find_by_user(&m.user_uuid, conn).await.is_empty()
{
if CONFIG.email_2fa_auto_fallback() {
two_factor::email::find_and_activate_email_2fa(&m.user_uuid, conn).await?;
} else {
err!(format!("Cannot {} because 2FA is required (membership {})", action, m.uuid));
}
}
// Check if the user is part of another Organization with SingleOrg activated
if Self::is_applicable_to_user(&m.user_uuid, OrgPolicyType::SingleOrg, Some(&m.org_uuid), conn).await {
err!(format!(
"Cannot {} because another organization policy forbids it (membership {})",
action, m.uuid
));
}
if let Some(p) = Self::find_by_org_and_type(&m.org_uuid, OrgPolicyType::SingleOrg, conn).await
&& p.enabled
&& Membership::count_accepted_and_confirmed_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0
{
err!(format!(
"Cannot {} because the organization policy forbids being part of other organization (membership {})",
action, m.uuid
));
}
}
// Stricter than the SingleOrg block above: this policy exempts no role and no status.
// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/Enforcement/AutoConfirm/AutomaticUserConfirmationPolicyEnforcementHandler.cs
if AutoConfirmRequirement::for_user(&m.user_uuid, conn).await.forbids_membership_outside(&m.org_uuid) {
err!(format!(
"Cannot {} because another organization confirms its members automatically and forbids other memberships (membership {})",
action, m.uuid
));
}
if Self::is_auto_confirm_enabled(&m.org_uuid, conn).await
&& Membership::count_accepted_confirmed_and_revoked_by_user(&m.user_uuid, &m.org_uuid, conn).await > 0
{
err!(format!(
"Cannot {} because the organization confirms its members automatically and forbids being part of other organizations (membership {})",
action, m.uuid
));
}
Ok(())
}
pub async fn org_is_reset_password_auto_enroll(org_uuid: &OrganizationId, conn: &DbConn) -> bool {
// Account recovery depends on outbound mail. When SMTP is disabled, treat the
// auto-enroll policy as inactive so invites/registration are not forced to
// supply a reset-password key (see check_reset_password_applicable).
if !CONFIG.mail_enabled() {
return false;
}
match OrgPolicy::find_by_org_and_type(org_uuid, OrgPolicyType::ResetPassword, conn).await {
Some(policy) => match serde_json::from_str::<ResetPasswordDataModel>(&policy.data) {
Ok(opts) => {
return policy.enabled && opts.auto_enroll_enabled;
}
_ => error!("Failed to deserialize ResetPasswordDataModel: {}", policy.data),
},
None => return false,
}
false
}
/// 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: &UserId, conn: &DbConn) -> bool {
for policy in
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendOptions, conn).await
{
if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await
&& user.atype < MembershipType::Admin
{
match serde_json::from_str::<SendOptionsPolicyData>(&policy.data) {
Ok(opts) => {
if opts.disable_hide_email {
return true;
}
}
_ => error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data),
}
}
}
false
}
pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool {
if let Some(member) = Membership::find_by_uuid(member_uuid, conn).await
&& let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await
{
return policy.enabled;
}
false
}
}
#[derive(Clone, Debug, AsRef, DieselNewType, From, FromForm, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct OrgPolicyId(String);
/// Active auto-confirm policy memberships. No role or status is exempt; each check below decides which
/// statuses matter for its operation.
/// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/AutomaticUserConfirmationPolicyRequirement.cs
pub struct AutoConfirmRequirement(Vec<Membership>);
impl AutoConfirmRequirement {
/// Loads every role and status in organizations with this policy; empty when the server option is off.
/// https://github.com/bitwarden/server/blob/b3d1eb9a7854322f106efa55c191c1a4da9f8645/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyRequirements/BasePolicyRequirementFactory.cs
pub async fn for_user(user_uuid: &UserId, conn: &DbConn) -> Self {
if !CONFIG.org_auto_confirm_enabled() {
return Self(Vec::new());
}
Self(
conn.run(move |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(org_policies::atype.eq(OrgPolicyType::AutomaticUserConfirmation as i32))
.filter(org_policies::enabled.eq(true))
.select(users_organizations::all_columns)
.load::<Membership>(conn)
.expect("Error loading memberships by org_policy")
})
.await,
)
}
/// Any membership, including Invited and elevated roles, forbids creating another organization.
pub fn forbids_creating_organization(&self) -> bool {
!self.0.is_empty()
}
/// Any policy membership outside `org_uuid` forbids membership in `org_uuid`.
pub fn forbids_membership_outside(&self, org_uuid: &OrganizationId) -> bool {
self.0.iter().any(|m| &m.org_uuid != org_uuid)
}
/// Accepted, Confirmed, or Revoked members may neither grant nor accept emergency access; Invited is exempt.
pub fn forbids_emergency_access(&self) -> bool {
self.0.iter().any(Membership::counts_for_auto_confirm)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn membership(org_uuid: &str, atype: MembershipType, status: MembershipStatus) -> Membership {
let mut member = Membership::new("user".to_owned().into(), org_uuid.to_owned().into(), None);
member.atype = atype as i32;
member.status = status as i32;
member
}
/// Every role and status is covered; only emergency access exempts Invited.
#[test]
fn auto_confirm_requirement() {
let auto_confirm_org = OrganizationId::from(String::from("auto-confirm"));
let other_org = OrganizationId::from(String::from("other"));
let none = AutoConfirmRequirement(Vec::new());
assert!(!none.forbids_creating_organization());
assert!(!none.forbids_membership_outside(&other_org));
assert!(!none.forbids_emergency_access());
let invited_owner =
AutoConfirmRequirement(vec![membership("auto-confirm", MembershipType::Owner, MembershipStatus::Invited)]);
assert!(invited_owner.forbids_creating_organization());
assert!(invited_owner.forbids_membership_outside(&other_org));
assert!(!invited_owner.forbids_membership_outside(&auto_confirm_org));
assert!(!invited_owner.forbids_emergency_access());
// Revoked counts because restore has no second accept step; Invited alone does not restrict emergency access.
let mut revoked_owner = membership("auto-confirm", MembershipType::Owner, MembershipStatus::Confirmed);
assert!(revoked_owner.revoke());
let mut requirement = AutoConfirmRequirement(vec![revoked_owner]);
assert!(requirement.forbids_creating_organization());
requirement.0.push(membership("invited-to", MembershipType::User, MembershipStatus::Invited));
assert!(requirement.forbids_emergency_access());
}
}