From f78268e371a6acd56261744d8e6768d22bc48aaf Mon Sep 17 00:00:00 2001 From: tom27052006 <83423411+tom27052006@users.noreply.github.com> Date: Tue, 10 Nov 2026 11:35:00 +0100 Subject: [PATCH] Add the Send controls policy (policy 21) --- .env.template | 2 + src/api/core/organizations.rs | 14 +- src/api/core/sends.rs | 59 +---- src/config.rs | 2 + src/db/models/org_policy.rs | 54 +--- src/db/models/org_policy/send_controls.rs | 289 ++++++++++++++++++++++ 6 files changed, 324 insertions(+), 96 deletions(-) create mode 100644 src/db/models/org_policy/send_controls.rs diff --git a/.env.template b/.env.template index 2d5c5d8e..b5a0c1f2 100644 --- a/.env.template +++ b/.env.template @@ -406,6 +406,8 @@ ## - "pm-30529-webauthn-related-origins": Allow passkeys stored in the vault to be used on related origins listed in a site's /.well-known/webauthn file (Browser >= 2026.2.0) ## - "pm-32009-new-item-types": Enable new item types: Bank Account, Driver's License, and Passport (Clients >= 2026.4.0) ## - "pm-34171-card-scanner": Enable the new card scanner feature on mobile (Android >= 2026.4.1, iOS >= 2026.4.1) +## - "pm-31885-send-controls": Use the Send controls policy, which replaces the Remove Send and Send options policies (Web vault >= 2026.4.0, Android >= 2026.8.0, iOS >= 2026.7.1) +## The web vault, browser, desktop and CLI clients after 2026.10 always use it, only older versions and the mobile apps still read this flag. ## - "enable-basic-auth-response": Enable HTTP Basic Auth autofill in the browser extension (Browser >= 2026.9.0) ## - "undetermined-cipher-scenario-logic": Enable the rewritten add/update login notification triggering logic in the browser extension (Browser >= 2026.2.0) ## - "biometrics-sdk-ipc": Enable biometric unlock over the SDK IPC framework (Desktop >= 2026.9.0, Browser >= 2026.9.0) diff --git a/src/api/core/organizations.rs b/src/api/core/organizations.rs index 607db45b..0972754b 100644 --- a/src/api/core/organizations.rs +++ b/src/api/core/organizations.rs @@ -1963,7 +1963,10 @@ async fn list_policies(org_id: OrganizationId, headers: AdminHeaders, conn: DbCo if org_id != headers.org_id { err!("Organization not found", "Organization id's do not match"); } - let policies = OrgPolicy::find_by_org(&org_id, &conn).await; + let mut policies = OrgPolicy::find_by_org(&org_id, &conn).await; + if !policies.iter().any(|p| p.has_type(OrgPolicyType::SendControls)) { + policies.push(OrgPolicy::synthesize_send_controls(&org_id, &policies)); + } let policies_json: Vec = policies.iter().map(OrgPolicy::to_json).collect(); Ok(Json(json!({ @@ -2036,6 +2039,9 @@ async fn get_policy(org_id: OrganizationId, pol_type: i32, headers: AdminHeaders let policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { Some(p) => p, + None if pol_type_enum == OrgPolicyType::SendControls => { + OrgPolicy::synthesize_send_controls(&org_id, &OrgPolicy::find_by_org(&org_id, &conn).await) + } None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "null".to_owned()), }; @@ -2207,13 +2213,15 @@ async fn put_policy( } } + let policy_data = OrgPolicy::send_policy_data(pol_type_enum, data.data)?; + let mut policy = match current_policy { Some(p) => p, None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()), }; policy.enabled = data.enabled; - policy.data = serde_json::to_string(&data.data)?; + policy.data = serde_json::to_string(&policy_data)?; policy.save(&conn).await?; log_event( @@ -2227,6 +2235,8 @@ async fn put_policy( ) .await; + OrgPolicy::sync_send_policies(&policy, &conn).await?; + Ok(Json(policy.to_json())) } diff --git a/src/api/core/sends.rs b/src/api/core/sends.rs index f8cc92bc..34f800f7 100644 --- a/src/api/core/sends.rs +++ b/src/api/core/sends.rs @@ -16,7 +16,7 @@ use crate::{ config::PathType, db::{ DbConn, DbPool, - models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId}, + models::{Device, OrgPolicy, Send, SendFileId, SendId, SendType, UserId}, }, util::{NumberOrString, save_temp_file}, }; @@ -90,40 +90,16 @@ pub struct SendData { pub id: Option, } -/// Enforces the `Disable Send` policy. A non-owner/admin user belonging to -/// an org with this policy enabled isn't allowed to create new Sends or -/// modify existing ones, but is allowed to delete them. -/// -/// Ref: https://bitwarden.com/help/article/policies/#disable-send +/// Enforces the Send policies (`Send controls` and the legacy `Disable Send` and `Send Options` it +/// absorbs) on the Send as it would be saved. Deleting a Send is always allowed. /// /// There is also a Vaultwarden-specific `sends_allowed` config setting that /// controls this policy globally. -async fn enforce_disable_send_policy(headers: &Headers, conn: &DbConn) -> EmptyResult { - let user_id = &headers.user.uuid; - if !CONFIG.sends_allowed() - || OrgPolicy::is_applicable_to_user(user_id, OrgPolicyType::DisableSend, None, conn).await - { +async fn enforce_send_policy(send: &Send, headers: &Headers, conn: &DbConn) -> EmptyResult { + if !CONFIG.sends_allowed() { err!("Due to an Enterprise Policy, you are only able to delete an existing Send.") } - Ok(()) -} - -/// Enforces the `DisableHideEmail` option of the `Send Options` policy. -/// A non-owner/admin user belonging to an org with this option enabled isn't -/// allowed to hide their email address from the recipient of a Bitwarden Send, -/// but is allowed to remove this option from an existing Send. -/// -/// Ref: https://bitwarden.com/help/article/policies/#send-options -async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, conn: &DbConn) -> EmptyResult { - let user_id = &headers.user.uuid; - let hide_email = data.hide_email.unwrap_or(false); - if hide_email && OrgPolicy::is_hide_email_disabled(user_id, conn).await { - err!( - "Due to an Enterprise Policy, you are not allowed to hide your email address \ - from recipients when creating or editing a Send." - ) - } - Ok(()) + OrgPolicy::check_send(&headers.user.uuid, send, conn).await } fn create_send(data: SendData, user_id: UserId) -> ApiResult { @@ -192,16 +168,14 @@ async fn get_send(send_id: SendId, headers: Headers, conn: DbConn) -> JsonResult #[post("/sends", data = "")] async fn post_send(data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - enforce_disable_send_policy(&headers, &conn).await?; - let data: SendData = data.into_inner(); - enforce_disable_hide_email_policy(&data, &headers, &conn).await?; if data.r#type == SendType::File as i32 { err!("File sends should use /api/sends/file/v2") } - let mut send = create_send(data, headers.user.uuid)?; + let mut send = create_send(data, headers.user.uuid.clone())?; + enforce_send_policy(&send, &headers, &conn).await?; send.save(&conn).await?; nt.send_send_update( UpdateType::SyncSendCreate, @@ -223,16 +197,12 @@ struct UploadDataV2<'f> { // Upstream: https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Api/Tools/Controllers/SendsController.cs#L165 #[post("/sends/file/v2", data = "")] async fn post_send_file_v2(data: Json, headers: Headers, conn: DbConn) -> JsonResult { - enforce_disable_send_policy(&headers, &conn).await?; - let data = data.into_inner(); if data.r#type != SendType::File as i32 { err!("Send content is not a file"); } - enforce_disable_hide_email_policy(&data, &headers, &conn).await?; - let file_length = if let Some(m) = &data.file_length { m.into_i64()? } else { @@ -263,7 +233,8 @@ async fn post_send_file_v2(data: Json, headers: Headers, conn: DbConn) err!("Send storage limit exceeded with this file"); } - let mut send = create_send(data, headers.user.uuid)?; + let mut send = create_send(data, headers.user.uuid.clone())?; + enforce_send_policy(&send, &headers, &conn).await?; let file_id = crate::crypto::generate_send_file_id(); @@ -303,8 +274,6 @@ async fn post_send_file_v2_data( conn: DbConn, nt: Notify<'_>, ) -> EmptyResult { - enforce_disable_send_policy(&headers, &conn).await?; - let data = data.into_inner(); let Some(send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else { @@ -314,6 +283,7 @@ async fn post_send_file_v2_data( if send.atype != SendType::File as i32 { err!("Send is not a file type send."); } + enforce_send_policy(&send, &headers, &conn).await?; let Ok(send_data) = serde_json::from_str::(&send.data) else { err!("Unable to decode send data as json.") @@ -465,10 +435,7 @@ async fn download_send(send_id: SendId, file_id: SendFileId, t: &str) -> Option< #[put("/sends/", data = "")] async fn put_send(send_id: SendId, data: Json, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - enforce_disable_send_policy(&headers, &conn).await?; - let data: SendData = data.into_inner(); - enforce_disable_hide_email_policy(&data, &headers, &conn).await?; let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else { err!("Send not found", "Send send_id is invalid or does not belong to user") @@ -534,6 +501,7 @@ async fn update_send_from_data( send.set_password(Some(&password)); } + enforce_send_policy(send, headers, conn).await?; send.save(conn).await?; if ut != UpdateType::None { nt.send_send_update(ut, send, &send.update_users_revision(conn).await, &headers.device, conn).await; @@ -562,13 +530,12 @@ async fn delete_send(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify #[put("/sends//remove-password")] async fn put_remove_password(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { - enforce_disable_send_policy(&headers, &conn).await?; - let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else { err!("Send not found", "Invalid send uuid, or does not belong to user") }; send.set_password(None); + enforce_send_policy(&send, &headers, &conn).await?; send.save(&conn).await?; nt.send_send_update( UpdateType::SyncSendUpdate, diff --git a/src/config.rs b/src/config.rs index a565a93e..418af8b4 100644 --- a/src/config.rs +++ b/src/config.rs @@ -1520,6 +1520,8 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[ "pm-34171-card-scanner", // Platform Team "pm-30529-webauthn-related-origins", + // Tools Team: read by the clients up to 2026.10 and by the Android and iOS apps + "pm-31885-send-controls", // Vault Team "pm-32009-new-item-types", ]; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 554f51fc..68bb087b 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -17,7 +17,9 @@ use crate::{ use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId}; -#[derive(Identifiable, Queryable, Insertable, AsChangeset)] +mod send_controls; + +#[derive(Clone, Identifiable, Queryable, Insertable, AsChangeset)] #[diesel(table_name = org_policies)] #[diesel(primary_key(uuid))] pub struct OrgPolicy { @@ -53,13 +55,13 @@ pub enum OrgPolicyType { // AutoConfirm = 18, // Not supported (not implemented yet) // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed) OrganizationUserNotification = 20, - // SendControls = 21, // Not supported yet + SendControls = 21, 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")] +#[derive(Default, Deserialize, Serialize)] +#[serde(default, rename_all = "camelCase")] pub struct SendOptionsPolicyData { #[serde(rename = "disableHideEmail", alias = "DisableHideEmail")] pub disable_hide_email: bool, @@ -270,28 +272,6 @@ impl OrgPolicy { .await } - pub async fn find_confirmed_by_user_and_active_policy( - user_uuid: &UserId, - policy_type: OrgPolicyType, - conn: &DbConn, - ) -> Vec { - 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::(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. @@ -375,28 +355,6 @@ impl OrgPolicy { 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::(&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 diff --git a/src/db/models/org_policy/send_controls.rs b/src/db/models/org_policy/send_controls.rs new file mode 100644 index 00000000..c3fd5d6a --- /dev/null +++ b/src/db/models/org_policy/send_controls.rs @@ -0,0 +1,289 @@ +//! The Send controls policy (21) absorbs the legacy `Disable Send` (6) and `Send Options` (7) policies. +//! Like upstream, the three are kept in sync in both directions, so clients that only know the legacy +//! policies keep enforcing the same rules, and Sends are checked against all of them. +//! +//! https://github.com/bitwarden/server/tree/04a4d7548b07d18ab7327ddaae81a05fd893a820/src/Core/AdminConsole/OrganizationFeatures/Policies + +use chrono::TimeDelta; +use diesel::prelude::*; +use num_traits::FromPrimitive; +use serde::Deserialize; +use serde_json::Value; + +use super::{OrgPolicy, OrgPolicyType, SendOptionsPolicyData}; +use crate::{ + api::{ApiResult, EmptyResult}, + db::{ + DbConn, + models::{MembershipStatus, MembershipType, OrganizationId, Send, SendType, UserId}, + schema::{org_policies, users_organizations}, + }, +}; + +const SEND_POLICIES: [i32; 3] = + [OrgPolicyType::DisableSend as i32, OrgPolicyType::SendOptions as i32, OrgPolicyType::SendControls as i32]; + +// SendWhoCanAccessType. Specific people requires Sends with email verification, which are not supported. +const ACCESS_ANY: i32 = 0; +const ACCESS_PASSWORD: i32 = 1; +const ACCESS_SPECIFIC_PEOPLE: i32 = 2; + +// https://github.com/bitwarden/server/blob/04a4d7548b07d18ab7327ddaae81a05fd893a820/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs +#[derive(Default, Deserialize, Serialize)] +#[serde(default, rename_all = "camelCase")] +struct SendControlsPolicyData { + #[serde(alias = "DisableSend")] + disable_send: bool, + #[serde(alias = "DisableHideEmail")] + disable_hide_email: bool, + #[serde(alias = "WhoCanAccess")] + who_can_access: Option, + #[serde(alias = "AllowedDomains")] + allowed_domains: Option, + #[serde(alias = "DeletionHours")] + deletion_hours: Option, + #[serde(alias = "AllowedSendTypes")] + allowed_send_types: Option>, +} + +impl SendControlsPolicyData { + /// Validates the data an organization saves. Requiring email verification, which allowed domains + /// depend on, would leave members unable to create any Send, so it is refused. + fn parse(data: Option<&Value>) -> ApiResult { + let data = match Option::::deserialize(data.unwrap_or(&Value::Null)) { + Ok(data) => data.unwrap_or_default(), + Err(e) => err!(format!("Invalid Send controls policy data: {e}")), + }; + match data.who_can_access { + None | Some(ACCESS_ANY | ACCESS_PASSWORD) => (), + Some(ACCESS_SPECIFIC_PEOPLE) => { + err!("Sends with email verification are not supported, so that access type cannot be required") + } + Some(_) => err!("Invalid Send controls access type"), + } + if data.allowed_domains.is_some() { + err!("Allowed domains require Sends with email verification, which are not supported") + } + if data.deletion_hours.is_some_and(|hours| hours < 1) { + err!("The maximum lifetime of a Send has to be at least one hour") + } + // Current clients may also list Item Sends (2), which can not be created here. + if let Some(types) = &data.allowed_send_types + && (types.iter().any(|t| !(0..=2).contains(t)) + || !types.iter().any(|t| *t == SendType::Text as i32 || *t == SendType::File as i32)) + { + err!("The allowed Send types have to include Text or File") + } + Ok(data) + } + + /// Every organization's restrictions apply, so a less strict organization never relaxes a stricter + /// one, independent of the order the policies are loaded in. + fn merge(&mut self, other: Self) { + self.disable_send |= other.disable_send; + self.disable_hide_email |= other.disable_hide_email; + self.who_can_access = self.who_can_access.max(other.who_can_access); + self.deletion_hours = match (self.deletion_hours, other.deletion_hours) { + (Some(a), Some(b)) => Some(a.min(b)), + (a, b) => a.or(b), + }; + self.allowed_send_types = match (self.allowed_send_types.take(), other.allowed_send_types) { + (Some(a), Some(b)) => Some(a.into_iter().filter(|t| b.contains(t)).collect()), + (a, b) => a.or(b), + }; + } + + /// Checks the Send as it would be saved, like upstream's `SendValidationService`. + fn check_send(&self, send: &Send) -> EmptyResult { + if self.disable_send { + err!("Due to an Enterprise Policy, you are only able to delete an existing Send.") + } + if self.disable_hide_email && send.hide_email.unwrap_or(false) { + err!( + "Due to an Enterprise Policy, you are not allowed to hide your email address \ + from recipients when creating or editing a Send." + ) + } + if self.who_can_access == Some(ACCESS_PASSWORD) && send.password_hash.is_none() { + err!("Due to an Enterprise policy your Sends must be protected by password") + } + if let Some(types) = &self.allowed_send_types + && !types.contains(&send.atype) + { + let names: Vec<&str> = types + .iter() + .map(|t| match *t { + 0 => "Text", + 1 => "File", + _ => "Unknown", + }) + .collect(); + err!(format!("Due to an Enterprise policy your Sends must be of the following types: {}", names.join(", "))) + } + // Upstream allows for up to a minute of skew between the deletion and the creation date. + if let Some(hours) = self.deletion_hours + && send.deletion_date - send.creation_date - TimeDelta::minutes(1) > TimeDelta::hours(i64::from(hours)) + { + let (duration, unit) = if hours >= 24 { + (hours / 24, "day") + } else { + (hours, "hour") + }; + let plural = if duration > 1 { + "s" + } else { + "" + }; + err!(format!( + "Due to an Enterprise policy your Sends must have deletion dates no more than {duration} {unit}{plural} from their creation dates" + )) + } + Ok(()) + } +} + +/// Whether an enabled `Send Options` policy disables hiding the email. Unreadable data keeps the option on. +fn hide_email_disabled(policy: &OrgPolicy) -> bool { + policy.enabled + && serde_json::from_str::>(&policy.data).map_or_else( + |_| { + error!("Failed to deserialize SendOptionsPolicyData: {}", policy.data); + true + }, + |data| data.is_some_and(|d| d.disable_hide_email), + ) +} + +/// The options of the legacy policies, as Send controls holds them. A disabled `Send Options` policy +/// counts as off, so a later save of Send controls does not switch its stored option back on. +fn legacy_send_options(policies: &[OrgPolicy]) -> (bool, bool) { + let disable_send = policies.iter().any(|p| p.has_type(OrgPolicyType::DisableSend) && p.enabled); + let disable_hide_email = policies.iter().any(|p| p.has_type(OrgPolicyType::SendOptions) && hide_email_disabled(p)); + (disable_send, disable_hide_email) +} + +/// Mirrors a saved Send policy into the others, like upstream's `SendControlsSyncPolicyEvent`, +/// `DisableSendSyncPolicyEvent` and `SendOptionsSyncPolicyEvent`, and returns the policies to save. +/// `policies` are the organization's policies, including the saved one. +fn mirror_send_policies(saved: &OrgPolicy, policies: &[OrgPolicy]) -> ApiResult> { + let find_or_new = |policy_type| { + policies + .iter() + .find(|p| p.has_type(policy_type)) + .cloned() + .unwrap_or_else(|| OrgPolicy::new(saved.org_uuid.clone(), policy_type, false, String::from("null"))) + }; + + if saved.has_type(OrgPolicyType::SendControls) { + let data = saved.send_controls_data(); + let mut disable_send = find_or_new(OrgPolicyType::DisableSend); + disable_send.enabled = saved.enabled && data.disable_send; + let mut send_options = find_or_new(OrgPolicyType::SendOptions); + send_options.enabled = saved.enabled && data.disable_hide_email; + send_options.data = serde_json::to_string(&SendOptionsPolicyData { + disable_hide_email: data.disable_hide_email, + })?; + return Ok(vec![disable_send, send_options]); + } + + let mut controls = find_or_new(OrgPolicyType::SendControls); + let mut data = controls.send_controls_data(); + (data.disable_send, data.disable_hide_email) = legacy_send_options(policies); + // Turning a legacy policy off must not switch off the restrictions only Send controls has. + controls.enabled |= data.disable_send || data.disable_hide_email; + controls.data = serde_json::to_string(&data)?; + Ok(vec![controls]) +} + +impl OrgPolicy { + /// Stored data was validated when saved; data that can not be read blocks Sends instead of allowing them. + fn send_controls_data(&self) -> SendControlsPolicyData { + let data = serde_json::from_str::(&self.data).ok(); + data.and_then(|data| SendControlsPolicyData::parse(Some(&data)).ok()).unwrap_or_else(|| { + error!("Invalid Send controls policy data, blocking Sends: {}", self.data); + SendControlsPolicyData { + disable_send: true, + ..Default::default() + } + }) + } + + /// Validates the data of a Send policy before it is saved and returns the data to store. + pub fn send_policy_data(policy_type: OrgPolicyType, data: Option) -> ApiResult> { + match policy_type { + OrgPolicyType::SendControls => { + Ok(Some(serde_json::to_value(SendControlsPolicyData::parse(data.as_ref())?)?)) + } + OrgPolicyType::SendOptions => { + if let Some(value) = &data + && let Err(e) = Option::::deserialize(value) + { + err!(format!("Invalid Send options policy data: {e}")) + } + Ok(data) + } + _ => Ok(data), + } + } + + /// Keeps Send controls and the legacy Send policies in sync after one of them was saved. + pub async fn sync_send_policies(saved: &Self, conn: &DbConn) -> EmptyResult { + if !SEND_POLICIES.contains(&saved.atype) { + return Ok(()); + } + for mut policy in mirror_send_policies(saved, &Self::find_by_org(&saved.org_uuid, conn).await)? { + policy.save(conn).await?; + } + Ok(()) + } + + /// Upstream's `PolicyQuery`: until Send controls is saved, it is derived from the legacy policies, so the + /// current policy editor shows what is enforced instead of turning it off on the next save. + pub fn synthesize_send_controls(org_id: &OrganizationId, policies: &[Self]) -> Self { + let (disable_send, disable_hide_email) = legacy_send_options(policies); + let data = SendControlsPolicyData { + disable_send, + disable_hide_email, + ..Default::default() + }; + Self::new( + org_id.clone(), + OrgPolicyType::SendControls, + disable_send || disable_hide_email, + json!(data).to_string(), + ) + } + + /// Enforces the Send policies of every organization in which the user is an accepted or confirmed + /// member below Admin on the Send as it would be saved. Deleting a Send stays possible. + pub async fn check_send(user_id: &UserId, send: &Send, conn: &DbConn) -> EmptyResult { + let policies = conn + .run(move |conn| { + org_policies::table + .inner_join(users_organizations::table.on(users_organizations::org_uuid.eq(org_policies::org_uuid))) + .filter(users_organizations::user_uuid.eq(user_id)) + .filter( + users_organizations::status + .eq_any([MembershipStatus::Accepted as i32, MembershipStatus::Confirmed as i32]), + ) + // Owners and admins are exempt. The numbers of MembershipType are not its order. + .filter(users_organizations::atype.ne(MembershipType::Owner as i32)) + .filter(users_organizations::atype.ne(MembershipType::Admin as i32)) + .filter(org_policies::enabled.eq(true)) + .filter(org_policies::atype.eq_any(SEND_POLICIES)) + .select(org_policies::all_columns) + .load::(conn) + }) + .await?; + + let mut controls = SendControlsPolicyData::default(); + for policy in policies { + match OrgPolicyType::from_i32(policy.atype) { + Some(OrgPolicyType::DisableSend) => controls.disable_send = true, + Some(OrgPolicyType::SendOptions) => controls.disable_hide_email |= hide_email_disabled(&policy), + _ => controls.merge(policy.send_controls_data()), + } + } + controls.check_send(send) + } +}