Browse Source

Merge 661f2adc84 into 58a980f850

pull/7519/merge
Tom 2 days ago
committed by GitHub
parent
commit
bed9550587
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 95
      src/api/core/organizations.rs
  2. 79
      src/api/core/sends.rs
  3. 2
      src/config.rs
  4. 4
      src/db/models/mod.rs
  5. 71
      src/db/models/org_policy.rs

95
src/api/core/organizations.rs

@ -17,8 +17,9 @@ use crate::{
models::{ models::{
Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, Device, Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, Device,
EventType, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, EventType, Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus,
MembershipType, OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, TwoFactor, MembershipType, OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId,
TwoFactorType, User, UserId, SendControlsPolicyData, SendOptionsPolicyData, SendWhoCanAccessType, TwoFactor, TwoFactorType, User,
UserId,
}, },
}, },
mail, mail,
@ -2118,6 +2119,10 @@ async fn put_policy(
} }
} }
if pol_type_enum == OrgPolicyType::SendControls {
validate_send_controls(data.data.as_ref())?;
}
let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await { let mut policy = match OrgPolicy::find_by_org_and_type(&org_id, pol_type_enum, &conn).await {
Some(p) => p, Some(p) => p,
None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()), None => OrgPolicy::new(org_id.clone(), pol_type_enum, false, "{}".to_owned()),
@ -2127,6 +2132,8 @@ async fn put_policy(
policy.data = serde_json::to_string(&data.data)?; policy.data = serde_json::to_string(&data.data)?;
policy.save(&conn).await?; policy.save(&conn).await?;
sync_send_policies(pol_type_enum, &policy, &org_id, &conn).await?;
log_event( log_event(
EventType::PolicyUpdated, EventType::PolicyUpdated,
policy.uuid.as_ref(), policy.uuid.as_ref(),
@ -2141,6 +2148,90 @@ async fn put_policy(
Ok(Json(policy.to_json())) Ok(Json(policy.to_json()))
} }
fn validate_send_controls(data: Option<&Value>) -> EmptyResult {
let data = match serde_json::from_value::<Option<SendControlsPolicyData>>(data.cloned().unwrap_or_default()) {
Ok(data) => data.unwrap_or_default(),
Err(e) => err!(format!("Invalid Send controls policy data: {e}")),
};
// Vaultwarden rejects Sends carrying recipient emails, so members could not create any Send.
if data.required_access_type() == Some(SendWhoCanAccessType::SpecificPeople) {
err!("Sends with email verification are not supported, so that access type cannot be required")
}
// Upstream only allows domains together with the specific people type, ruled out above.
if data.allowed_domains.is_some() {
err!("Allowed domains can only be set when the required access type is set to specific people")
}
// A non positive value would mean no Send could ever satisfy the policy.
if data.deletion_hours.is_some_and(|hours| hours < 1) {
err!("The maximum lifetime of a Send has to be at least one hour")
}
Ok(())
}
/// `Send controls` absorbs the legacy `DisableSend` and `Send Options` policies. Like upstream we
/// mirror changes in both directions so older clients keep enforcing the same rules and a rollback
/// stays safe; enforcement in `sends.rs` therefore stays authoritative for those two flags.
///
/// Ref: https://github.com/bitwarden/server/blob/main/src/Core/AdminConsole/OrganizationFeatures/Policies/PolicyEventHandlers/SendControlsSyncPolicyEvent.cs
async fn sync_send_policies(
pol_type: OrgPolicyType,
saved: &OrgPolicy,
org_id: &OrganizationId,
conn: &DbConn,
) -> EmptyResult {
match pol_type {
OrgPolicyType::SendControls => {
let data = saved.send_controls_data();
// Upstream leaves the data of the DisableSend policy untouched, it carries no options.
let mut disable_send = find_or_new_policy(org_id, OrgPolicyType::DisableSend, conn).await;
disable_send.enabled = saved.enabled && data.disable_send;
disable_send.save(conn).await?;
let mut send_options = find_or_new_policy(org_id, OrgPolicyType::SendOptions, conn).await;
send_options.enabled = saved.enabled && data.disable_hide_email;
send_options.data = serde_json::to_string(&SendOptionsPolicyData {
disable_hide_email: data.disable_hide_email,
})?;
send_options.save(conn).await?;
}
OrgPolicyType::DisableSend | OrgPolicyType::SendOptions => {
let disable_send = OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::DisableSend, conn)
.await
.is_some_and(|p| p.enabled);
let send_options = OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::SendOptions, conn).await;
// Keep every restriction that only exists on the Send controls policy.
let mut controls = find_or_new_policy(org_id, OrgPolicyType::SendControls, conn).await;
let mut data = controls.send_controls_data();
data.disable_send = disable_send;
// Upstream reads this out of the data of the legacy policy regardless of whether that
// policy is enabled, the enabled flag is only folded into the container below.
data.disable_hide_email = send_options
.as_ref()
.and_then(|p| serde_json::from_str::<SendOptionsPolicyData>(&p.data).ok())
.is_some_and(|d| d.disable_hide_email);
controls.enabled = disable_send || send_options.is_some_and(|p| p.enabled);
controls.data = serde_json::to_string(&data)?;
controls.save(conn).await?;
}
_ => (),
}
Ok(())
}
async fn find_or_new_policy(org_id: &OrganizationId, pol_type: OrgPolicyType, conn: &DbConn) -> OrgPolicy {
match OrgPolicy::find_by_org_and_type(org_id, pol_type, conn).await {
Some(p) => p,
None => OrgPolicy::new(org_id.clone(), pol_type, false, "null".to_owned()),
}
}
#[get("/plans")] #[get("/plans")]
fn get_plans() -> Json<Value> { fn get_plans() -> Json<Value> {
// Respond with a minimal json just enough to allow the creation of an new organization. // Respond with a minimal json just enough to allow the creation of an new organization.

79
src/api/core/sends.rs

@ -16,7 +16,7 @@ use crate::{
config::PathType, config::PathType,
db::{ db::{
DbConn, DbPool, DbConn, DbPool,
models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, UserId}, models::{Device, OrgPolicy, OrgPolicyType, Send, SendFileId, SendId, SendType, SendWhoCanAccessType, UserId},
}, },
util::{NumberOrString, save_temp_file}, util::{NumberOrString, save_temp_file},
}; };
@ -126,6 +126,72 @@ async fn enforce_disable_hide_email_policy(data: &SendData, headers: &Headers, c
Ok(()) Ok(())
} }
/// Enforces the restrictions that only exist on the `Send controls` policy. `DisableSend` and
/// `DisableHideEmail` stay with the two functions above, which are mirrored into `Send controls`.
/// `existing` is the Send being updated: the client may leave the password out to keep the existing
/// one, and the deletion date is measured against the date the Send was originally created on.
///
/// Ref: https://github.com/bitwarden/server/blob/main/src/Core/Tools/SendFeatures/Services/SendValidationService.cs
async fn enforce_send_controls_policy(
data: &SendData,
existing: Option<&Send>,
headers: &Headers,
conn: &DbConn,
) -> EmptyResult {
let has_password = data.password.is_some() || existing.is_some_and(|s| s.password_hash.is_some());
let creation_date = existing.map_or_else(Utc::now, |s| s.creation_date.and_utc());
let controls = OrgPolicy::send_controls_for_user(&headers.user.uuid, conn).await;
match controls.required_access_type() {
Some(SendWhoCanAccessType::PasswordProtected) if !has_password => {
err!("Due to an Enterprise Policy, your Sends have to be protected by a password.")
}
// Vaultwarden rejects Sends carrying recipient emails, so this can never be satisfied.
// `put_policy` refuses to store such a policy; this only guards rows written outside it.
Some(SendWhoCanAccessType::SpecificPeople) => {
err!(
"Due to an Enterprise Policy, your Sends have to be protected by email verification, \
which is not supported."
)
}
_ => (),
}
if let Some(allowed_types) = &controls.allowed_send_types
&& !allowed_types.contains(&data.r#type)
{
err!("Due to an Enterprise Policy, your Sends have to be of a type the organization allows.")
}
if let Some(hours) = controls.deletion_hours {
// Upstream allows for up to a minute of skew between the deletion and the creation date.
if send_exceeds_max_lifetime(data.deletion_date, creation_date, hours) {
err!(format!(
"Due to an Enterprise Policy, the deletion date of your Sends has to be within {hours} hours of their creation date."
))
}
}
Ok(())
}
fn send_exceeds_max_lifetime(deletion_date: DateTime<Utc>, creation_date: DateTime<Utc>, max_hours: i32) -> bool {
deletion_date - creation_date - TimeDelta::minutes(1) > TimeDelta::hours(i64::from(max_hours))
}
#[cfg(test)]
mod send_controls_tests {
use super::*;
#[test]
fn send_max_lifetime_respects_exact_one_minute_tolerance() {
let created = Utc::now();
let limit = created + TimeDelta::hours(1) + TimeDelta::minutes(1);
assert!(!send_exceeds_max_lifetime(limit, created, 1));
assert!(send_exceeds_max_lifetime(limit + TimeDelta::milliseconds(1), created, 1));
}
}
fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> { fn create_send(data: SendData, user_id: UserId) -> ApiResult<Send> {
let data_val = if data.r#type == SendType::Text as i32 { let data_val = if data.r#type == SendType::Text as i32 {
data.text data.text
@ -196,6 +262,7 @@ async fn post_send(data: Json<SendData>, headers: Headers, conn: DbConn, nt: Not
let data: SendData = data.into_inner(); let data: SendData = data.into_inner();
enforce_disable_hide_email_policy(&data, &headers, &conn).await?; enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
enforce_send_controls_policy(&data, None, &headers, &conn).await?;
if data.r#type == SendType::File as i32 { if data.r#type == SendType::File as i32 {
err!("File sends should use /api/sends/file/v2") err!("File sends should use /api/sends/file/v2")
@ -232,6 +299,7 @@ async fn post_send_file_v2(data: Json<SendData>, headers: Headers, conn: DbConn)
} }
enforce_disable_hide_email_policy(&data, &headers, &conn).await?; enforce_disable_hide_email_policy(&data, &headers, &conn).await?;
enforce_send_controls_policy(&data, None, &headers, &conn).await?;
let file_length = if let Some(m) = &data.file_length { let file_length = if let Some(m) = &data.file_length {
m.into_i64()? m.into_i64()?
@ -477,6 +545,8 @@ async fn put_send(send_id: SendId, data: Json<SendData>, headers: Headers, conn:
err!("Sends with email verification is not supported"); err!("Sends with email verification is not supported");
} }
enforce_send_controls_policy(&data, Some(&send), &headers, &conn).await?;
update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?; update_send_from_data(&mut send, data, &headers, &conn, &nt, UpdateType::SyncSendUpdate).await?;
Ok(Json(send.to_json())) Ok(Json(send.to_json()))
@ -563,6 +633,13 @@ async fn delete_send(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify
async fn put_remove_password(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult { async fn put_remove_password(send_id: SendId, headers: Headers, conn: DbConn, nt: Notify<'_>) -> JsonResult {
enforce_disable_send_policy(&headers, &conn).await?; enforce_disable_send_policy(&headers, &conn).await?;
// Removing the password would leave the Send non compliant with a policy that requires one.
if OrgPolicy::send_controls_for_user(&headers.user.uuid, &conn).await.required_access_type()
== Some(SendWhoCanAccessType::PasswordProtected)
{
err!("Due to an Enterprise Policy, your Sends have to be protected by a password.")
}
let Some(mut send) = Send::find_by_uuid_and_user(&send_id, &headers.user.uuid, &conn).await else { 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") err!("Send not found", "Invalid send uuid, or does not belong to user")
}; };

2
src/config.rs

@ -1439,6 +1439,8 @@ pub const SUPPORTED_FEATURE_FLAGS: &[&str] = &[
"pm-34171-card-scanner", "pm-34171-card-scanner",
// Platform Team // Platform Team
"pm-30529-webauthn-related-origins", "pm-30529-webauthn-related-origins",
// Tools Team
"pm-31885-send-controls",
// Vault Team // Vault Team
"pm-32009-new-item-types", "pm-32009-new-item-types",
]; ];

4
src/db/models/mod.rs

@ -29,7 +29,9 @@ pub use self::event::{Event, EventType};
pub use self::favorite::Favorite; pub use self::favorite::Favorite;
pub use self::folder::{Folder, FolderCipher, FolderId}; pub use self::folder::{Folder, FolderCipher, FolderId};
pub use self::group::{CollectionGroup, Group, GroupId, GroupUser}; pub use self::group::{CollectionGroup, Group, GroupId, GroupUser};
pub use self::org_policy::{OrgPolicy, OrgPolicyId, OrgPolicyType}; pub use self::org_policy::{
OrgPolicy, OrgPolicyId, OrgPolicyType, SendControlsPolicyData, SendOptionsPolicyData, SendWhoCanAccessType,
};
pub use self::organization::{ pub use self::organization::{
Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey, Membership, MembershipId, MembershipStatus, MembershipType, OrgApiKeyId, Organization, OrganizationApiKey,
OrganizationId, OrganizationId,

71
src/db/models/org_policy.rs

@ -53,16 +53,52 @@ pub enum OrgPolicyType {
// AutoConfirm = 18, // Not supported (not implemented yet) // AutoConfirm = 18, // Not supported (not implemented yet)
// BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed) // BlockClaimedDomainAccountCreation = 19, // Not supported (Not AGPLv3 Licensed)
OrganizationUserNotification = 20, OrganizationUserNotification = 20,
SendControls = 21,
} }
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5 // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendOptionsPolicyData.cs#L5
#[derive(Deserialize)] #[derive(Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct SendOptionsPolicyData { pub struct SendOptionsPolicyData {
#[serde(rename = "disableHideEmail", alias = "DisableHideEmail")] #[serde(rename = "disableHideEmail", alias = "DisableHideEmail")]
pub disable_hide_email: bool, pub disable_hide_email: bool,
} }
// https://github.com/bitwarden/server/blob/main/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsAllowedAccessControl.cs
#[derive(Copy, Clone, Eq, PartialEq, num_derive::FromPrimitive)]
pub enum SendWhoCanAccessType {
Any = 0,
PasswordProtected = 1,
SpecificPeople = 2,
}
// https://github.com/bitwarden/server/blob/main/src/Core/AdminConsole/Models/Data/Organizations/Policies/SendControlsPolicyData.cs
//
// The shipped web vault only renders the first four fields; the last two already exist upstream and
// are parsed and enforced here as well.
#[derive(Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct SendControlsPolicyData {
#[serde(rename = "disableSend", alias = "DisableSend", default)]
pub disable_send: bool,
#[serde(rename = "disableHideEmail", alias = "DisableHideEmail", default)]
pub disable_hide_email: bool,
#[serde(rename = "whoCanAccess", alias = "WhoCanAccess", default)]
pub who_can_access: Option<i32>,
#[serde(rename = "allowedDomains", alias = "AllowedDomains", default)]
pub allowed_domains: Option<String>,
#[serde(rename = "deletionHours", alias = "DeletionHours", default)]
pub deletion_hours: Option<i32>,
#[serde(rename = "allowedSendTypes", alias = "AllowedSendTypes", default)]
pub allowed_send_types: Option<Vec<i32>>,
}
impl SendControlsPolicyData {
pub fn required_access_type(&self) -> Option<SendWhoCanAccessType> {
self.who_can_access.and_then(num_traits::FromPrimitive::from_i32)
}
}
// https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs // https://github.com/bitwarden/server/blob/9ebe16587175b1c0e9208f84397bb75d0d595510/src/Core/AdminConsole/Models/Data/Organizations/Policies/ResetPasswordDataModel.cs
#[derive(Deserialize)] #[derive(Deserialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
@ -387,6 +423,39 @@ impl OrgPolicy {
false false
} }
/// Reads the `Send controls` data, falling back to the defaults when the stored data is missing
/// or unreadable: a broken payload must not accidentally lock users out of creating Sends.
pub fn send_controls_data(&self) -> SendControlsPolicyData {
if let Ok(data) = serde_json::from_str::<SendControlsPolicyData>(&self.data) {
return data;
}
if self.data != "null" && !self.data.is_empty() {
error!("Failed to deserialize SendControlsPolicyData: {}", self.data);
}
SendControlsPolicyData::default()
}
/// Combines the `Send controls` policies of every organization the user is a plain member of. Only
/// the restrictions enforced in `sends.rs` are merged, each from the first organization that sets
/// it like upstreams `SendControlsPolicyRequirementFactory`.
pub async fn send_controls_for_user(user_uuid: &UserId, conn: &DbConn) -> SendControlsPolicyData {
let mut result = SendControlsPolicyData::default();
for policy in
OrgPolicy::find_confirmed_by_user_and_active_policy(user_uuid, OrgPolicyType::SendControls, conn).await
{
if let Some(user) = Membership::find_confirmed_by_user_and_org(user_uuid, &policy.org_uuid, conn).await
&& user.atype < MembershipType::Admin
{
let data = policy.send_controls_data();
result.who_can_access = result.who_can_access.or(data.who_can_access);
result.deletion_hours = result.deletion_hours.or(data.deletion_hours);
result.allowed_send_types = result.allowed_send_types.or(data.allowed_send_types);
}
}
result
}
pub async fn is_enabled_for_member(member_uuid: &MembershipId, policy_type: OrgPolicyType, conn: &DbConn) -> bool { 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 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 && let Some(policy) = OrgPolicy::find_by_org_and_type(&member.org_uuid, policy_type, conn).await

Loading…
Cancel
Save