|
|
@ -8,7 +8,7 @@ use crate::{ |
|
|
CONFIG, |
|
|
CONFIG, |
|
|
api::admin::FAKE_ADMIN_UUID, |
|
|
api::admin::FAKE_ADMIN_UUID, |
|
|
api::{ |
|
|
api::{ |
|
|
EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, |
|
|
ApiResult, EmptyResult, JsonResult, Notify, PasswordOrOtpData, UpdateType, |
|
|
core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor}, |
|
|
core::{CipherSyncData, CipherSyncType, accept_org_invite, log_event, two_factor}, |
|
|
}, |
|
|
}, |
|
|
auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite}, |
|
|
auth::{AdminHeaders, Headers, ManagerHeaders, ManagerHeadersLoose, OrgMemberHeaders, OwnerHeaders, decode_invite}, |
|
|
@ -17,7 +17,8 @@ use crate::{ |
|
|
models::{ |
|
|
models::{ |
|
|
Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, |
|
|
Cipher, CipherId, Collection, CollectionCipher, CollectionGroup, CollectionId, CollectionUser, EventType, |
|
|
Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, |
|
|
Group, GroupId, GroupUser, Invitation, Membership, MembershipId, MembershipStatus, MembershipType, |
|
|
OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, User, UserId, |
|
|
OrgPolicy, OrgPolicyType, Organization, OrganizationApiKey, OrganizationId, SendControlsPolicyData, |
|
|
|
|
|
SendOptionsPolicyData, SendWhoCanAccessType, User, UserId, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
}, |
|
|
mail, |
|
|
mail, |
|
|
@ -2160,6 +2161,10 @@ async fn put_policy( |
|
|
} |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
if pol_type_enum == OrgPolicyType::SendControls && data.enabled { |
|
|
|
|
|
validate_send_controls(&parse_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()), |
|
|
@ -2169,6 +2174,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 as i32, |
|
|
EventType::PolicyUpdated as i32, |
|
|
policy.uuid.as_ref(), |
|
|
policy.uuid.as_ref(), |
|
|
@ -2183,6 +2190,124 @@ async fn put_policy( |
|
|
Ok(Json(policy.to_json())) |
|
|
Ok(Json(policy.to_json())) |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn parse_send_controls(data: Option<&Value>) -> ApiResult<SendControlsPolicyData> { |
|
|
|
|
|
match data { |
|
|
|
|
|
None | Some(Value::Null) => Ok(SendControlsPolicyData::default()), |
|
|
|
|
|
Some(value) => match serde_json::from_value::<SendControlsPolicyData>(value.clone()) { |
|
|
|
|
|
Ok(parsed) => Ok(parsed), |
|
|
|
|
|
Err(e) => err!(format!("Invalid Send controls policy data: {e}")), |
|
|
|
|
|
}, |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
fn validate_send_controls(data: &SendControlsPolicyData) -> EmptyResult { |
|
|
|
|
|
// Vaultwarden rejects Sends that carry recipient emails, so requiring email verification would
|
|
|
|
|
|
// leave the members of this organization unable to create any Send at all.
|
|
|
|
|
|
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 accepts allowed domains together with the specific people access type, which
|
|
|
|
|
|
// the check above already rules out.
|
|
|
|
|
|
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(()) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
/// The `Send controls` policy is a container that absorbs the older `DisableSend` and
|
|
|
|
|
|
/// `Send Options` policies. Upstream mirrors changes in both directions with dedicated policy event
|
|
|
|
|
|
/// handlers, so that clients which only know the legacy policies keep enforcing the same rules and
|
|
|
|
|
|
/// so that a rollback stays safe. We do the same, which also keeps the existing enforcement in
|
|
|
|
|
|
/// `sends.rs` 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.
|
|
|
|
|
|
upsert_mirrored_policy(org_id, OrgPolicyType::DisableSend, saved.enabled && data.disable_send, None, conn) |
|
|
|
|
|
.await?; |
|
|
|
|
|
|
|
|
|
|
|
let send_options = SendOptionsPolicyData { |
|
|
|
|
|
disable_hide_email: data.disable_hide_email, |
|
|
|
|
|
}; |
|
|
|
|
|
upsert_mirrored_policy( |
|
|
|
|
|
org_id, |
|
|
|
|
|
OrgPolicyType::SendOptions, |
|
|
|
|
|
saved.enabled && data.disable_hide_email, |
|
|
|
|
|
Some(serde_json::to_string(&send_options)?), |
|
|
|
|
|
conn, |
|
|
|
|
|
) |
|
|
|
|
|
.await?; |
|
|
|
|
|
} |
|
|
|
|
|
OrgPolicyType::DisableSend | OrgPolicyType::SendOptions => { |
|
|
|
|
|
// Keep every restriction that only exists on the Send controls policy.
|
|
|
|
|
|
let mut data = match OrgPolicy::find_by_org_and_type(org_id, OrgPolicyType::SendControls, conn).await { |
|
|
|
|
|
Some(p) => p.send_controls_data(), |
|
|
|
|
|
None => SendControlsPolicyData::default(), |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
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; |
|
|
|
|
|
|
|
|
|
|
|
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); |
|
|
|
|
|
|
|
|
|
|
|
let enabled = disable_send || send_options.is_some_and(|p| p.enabled); |
|
|
|
|
|
upsert_mirrored_policy( |
|
|
|
|
|
org_id, |
|
|
|
|
|
OrgPolicyType::SendControls, |
|
|
|
|
|
enabled, |
|
|
|
|
|
Some(serde_json::to_string(&data)?), |
|
|
|
|
|
conn, |
|
|
|
|
|
) |
|
|
|
|
|
.await?; |
|
|
|
|
|
} |
|
|
|
|
|
_ => (), |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
Ok(()) |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
async fn upsert_mirrored_policy( |
|
|
|
|
|
org_id: &OrganizationId, |
|
|
|
|
|
pol_type: OrgPolicyType, |
|
|
|
|
|
enabled: bool, |
|
|
|
|
|
data: Option<String>, |
|
|
|
|
|
conn: &DbConn, |
|
|
|
|
|
) -> EmptyResult { |
|
|
|
|
|
let mut policy = 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()), |
|
|
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
policy.enabled = enabled; |
|
|
|
|
|
if let Some(data) = data { |
|
|
|
|
|
policy.data = data; |
|
|
|
|
|
} |
|
|
|
|
|
policy.save(conn).await |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
// Deprecated with client v2026.5.0
|
|
|
// Deprecated with client v2026.5.0
|
|
|
#[put("/organizations/<org_id>/policies/<pol_type>/vnext", data = "<data>")] |
|
|
#[put("/organizations/<org_id>/policies/<pol_type>/vnext", data = "<data>")] |
|
|
async fn put_policy_vnext( |
|
|
async fn put_policy_vnext( |
|
|
@ -3251,3 +3376,30 @@ async fn rotate_api_key( |
|
|
) -> JsonResult { |
|
|
) -> JsonResult { |
|
|
api_key(&org_id, data, true, headers, conn).await |
|
|
api_key(&org_id, data, true, headers, conn).await |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[cfg(test)] |
|
|
|
|
|
mod tests { |
|
|
|
|
|
use super::*; |
|
|
|
|
|
|
|
|
|
|
|
fn send_controls(json: &str) -> SendControlsPolicyData { |
|
|
|
|
|
serde_json::from_str(json).expect("valid Send controls payload") |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
fn send_controls_rejects_restrictions_vaultwarden_cannot_satisfy() { |
|
|
|
|
|
// Requiring recipient emails would lock every member out of creating Sends.
|
|
|
|
|
|
assert!(validate_send_controls(&send_controls(r#"{"whoCanAccess":2}"#)).is_err()); |
|
|
|
|
|
assert!(validate_send_controls(&send_controls(r#"{"allowedDomains":"example.com"}"#)).is_err()); |
|
|
|
|
|
assert!(validate_send_controls(&send_controls(r#"{"deletionHours":0}"#)).is_err()); |
|
|
|
|
|
} |
|
|
|
|
|
|
|
|
|
|
|
#[test] |
|
|
|
|
|
fn send_controls_accepts_the_supported_restrictions() { |
|
|
|
|
|
assert!(validate_send_controls(&send_controls(r#"{"disableSend":true,"disableHideEmail":true}"#)).is_ok()); |
|
|
|
|
|
assert!( |
|
|
|
|
|
validate_send_controls(&send_controls(r#"{"whoCanAccess":1,"deletionHours":24,"allowedSendTypes":[0]}"#)) |
|
|
|
|
|
.is_ok() |
|
|
|
|
|
); |
|
|
|
|
|
assert!(validate_send_controls(&SendControlsPolicyData::default()).is_ok()); |
|
|
|
|
|
} |
|
|
|
|
|
} |
|
|
|