Browse Source

Add sync policy notifications

pull/7758/head
tom27052006 4 days ago
parent
commit
46670df4ba
  1. 12
      src/api/core/organizations.rs
  2. 69
      src/api/notifications.rs
  3. 29
      src/api/push.rs

12
src/api/core/organizations.rs

@ -2082,6 +2082,7 @@ async fn put_policy(
pol_type: i32,
data: Json<PutPolicy>,
headers: AdminHeaders,
nt: Notify<'_>,
conn: DbConn,
) -> JsonResult {
if org_id != headers.org_id {
@ -2093,6 +2094,11 @@ async fn put_policy(
err!("Invalid or unsupported policy type")
};
// Collect the members before applying the policy, since enabling the TwoFactorAuthentication or
// SingleOrg policy revokes members below, and those clients need to know about the change too.
let member_ids: Vec<UserId> =
Membership::find_confirmed_by_org(&org_id, &conn).await.into_iter().map(|m| m.user_uuid).collect();
// Bitwarden only allows the Reset Password policy when Single Org policy is enabled
// Vaultwarden encouraged to use multiple orgs instead of groups because groups were not available in the past
// Now that groups are available we can enforce this option when wanted.
@ -2192,6 +2198,9 @@ async fn put_policy(
)
.await;
// Let the members sync, so the new policy applies without waiting for the next periodic sync
nt.send_policy_update(&policy, &member_ids, &conn).await;
Ok(Json(policy.to_json()))
}
@ -2202,9 +2211,10 @@ async fn put_policy_vnext(
pol_type: i32,
data: Json<PutPolicy>,
headers: AdminHeaders,
nt: Notify<'_>,
conn: DbConn,
) -> JsonResult {
put_policy(org_id, pol_type, data, headers, conn).await
put_policy(org_id, pol_type, data, headers, nt, conn).await
}
#[get("/plans")]

69
src/api/notifications.rs

@ -15,13 +15,16 @@ use crate::{
auth::{ClientIp, WsAccessTokenHeader},
db::{
DbConn,
models::{AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, PushId, Send as DbSend, User, UserId},
models::{
AuthRequestId, Cipher, CollectionId, Device, DeviceId, Folder, OrgPolicy, PushId, Send as DbSend, User,
UserId,
},
},
};
use super::{
push::push_auth_request, push::push_auth_response, push_cipher_update, push_folder_update, push_logout,
push_send_update, push_user_update,
push::push_auth_request, push::push_auth_response, push::push_policy_update, push_cipher_update,
push_folder_update, push_logout, push_send_update, push_user_update,
};
pub static WS_USERS: LazyLock<Arc<WebSocketUsers>> = LazyLock::new(|| {
@ -510,6 +513,33 @@ impl WebSocketUsers {
}
}
pub async fn send_policy_update(&self, policy: &OrgPolicy, user_ids: &[UserId], conn: &DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
return;
}
debug!(
"Sending SyncPolicy ({}) for policy type {} of organization {} to {} member(s)",
UpdateType::SyncPolicy as i32,
policy.atype,
policy.org_uuid,
user_ids.len()
);
if CONFIG.enable_websocket() {
let data = create_policy_update(policy);
for user_id in user_ids {
self.send_update(user_id, &data).await;
}
}
if CONFIG.push_enabled() {
for user_id in user_ids {
push_policy_update(policy, user_id, conn).await;
}
}
}
pub async fn send_auth_request(&self, user_id: &UserId, auth_request_uuid: &str, device: &Device, conn: &DbConn) {
// Skip any processing if both WebSockets and Push are not active
if *NOTIFICATIONS_DISABLED {
@ -642,6 +672,32 @@ fn create_update(payload: Vec<(Value, Value)>, ut: UpdateType, acting_device_id:
serialize(&value)
}
// Follows upstream's `SyncPolicyPushNotification`, with the fields Vaultwarden stores.
// `Data` is sent as the raw string, like upstream's policy entity does, and there is no
// revision date because Vaultwarden does not keep one per policy.
// No acting device: upstream does not exclude it either, so the client which changed the policy syncs too.
fn create_policy_update(policy: &OrgPolicy) -> Vec<u8> {
use rmpv::Value as V;
create_update(
vec![
("OrganizationId".into(), policy.org_uuid.to_string().into()),
(
"Policy".into(),
V::Map(vec![
("Id".into(), policy.uuid.as_ref().as_str().into()),
("OrganizationId".into(), policy.org_uuid.to_string().into()),
("Type".into(), policy.atype.into()),
("Data".into(), policy.data.as_str().into()),
("Enabled".into(), policy.enabled.into()),
]),
),
],
UpdateType::SyncPolicy,
None,
)
}
fn create_anonymous_update(payload: Vec<(Value, Value)>, ut: UpdateType, user_id: &UserId) -> Vec<u8> {
use rmpv::Value as V;
@ -700,6 +756,13 @@ pub enum UpdateType {
// NotificationStatus = 21, // Not supported
// RefreshSecurityTasks = 22, // Not supported
// OrganizationBankAccountVerified = 23, // Not supported
// ProviderBankAccountVerified = 24, // Not supported
// Upstream calls this `PolicyChanged`, the clients call it `SyncPolicy`
SyncPolicy = 25,
None = 100,
}

29
src/api/push.rs

@ -15,7 +15,7 @@ use crate::{
api::{ApiResult, EmptyResult, UpdateType},
db::{
DbConn,
models::{AuthRequestId, Cipher, Device, Folder, PushId, Send, User, UserId},
models::{AuthRequestId, Cipher, Device, Folder, OrgPolicy, PushId, Send, User, UserId},
},
http_client::make_http_request,
util::{format_date, get_uuid},
@ -264,6 +264,33 @@ pub async fn push_send_update(ut: UpdateType, send: &Send, device: &Device, conn
}
}
// Vaultwarden does not register the `organizationIds` of a device with the push relay (see
// `register_push_device`), so the members are notified one by one instead of the organization.
// The payload matches the one sent over the WebSocket, see `create_policy_update`.
pub async fn push_policy_update(policy: &OrgPolicy, user_id: &UserId, conn: &DbConn) {
if Device::check_user_has_push_device(user_id, conn).await {
tokio::task::spawn(send_to_push_relay(json!({
"userId": user_id,
"organizationId": null,
"deviceId": null, // All devices of this user need to know about the change
"identifier": null,
"type": UpdateType::SyncPolicy as i32,
"payload": {
"organizationId": policy.org_uuid,
"policy": {
"id": policy.uuid,
"organizationId": policy.org_uuid,
"type": policy.atype,
"data": policy.data,
"enabled": policy.enabled,
}
},
"clientType": null,
"installationId": null
})));
}
}
async fn send_to_push_relay(notification_data: Value) {
if !CONFIG.push_enabled() {
return;

Loading…
Cancel
Save