From 73db51845eadcd55da7b9e8ddce5eb75592046f7 Mon Sep 17 00:00:00 2001 From: Will Lotto Date: Mon, 10 Aug 2026 09:01:31 +1000 Subject: [PATCH 1/2] Add revision_date to org policies Org policy JSON responses never included a revisionDate field, unlike every other synced entity. The official Bitwarden server always sets it, and current official clients (recently migrated to the WASM SDK's typed PolicyView) now assume it is always a valid date string. Its absence deserializes to undefined client-side, which then throws (RangeError: Invalid time value) when the client formats it, breaking the whole policy sync pipeline and silently emptying the vault view in the browser and desktop apps. Adds a revision_date column (mirroring the existing pattern used by Send), stamped on creation and bumped on every save. --- .../down.sql | 0 .../up.sql | 2 ++ .../down.sql | 0 .../up.sql | 2 ++ .../down.sql | 0 .../up.sql | 2 ++ src/db/models/org_policy.rs | 17 ++++++++++++----- src/db/schema.rs | 1 + 8 files changed, 19 insertions(+), 5 deletions(-) create mode 100644 migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql create mode 100644 migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql create mode 100644 migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql create mode 100644 migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql create mode 100644 migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/down.sql create mode 100644 migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql diff --git a/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql new file mode 100644 index 00000000..fc5a5f44 --- /dev/null +++ b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE org_policies +ADD COLUMN revision_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql new file mode 100644 index 00000000..ed39a1d9 --- /dev/null +++ b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE org_policies +ADD COLUMN revision_date TIMESTAMP NOT NULL DEFAULT now(); diff --git a/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/down.sql b/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/down.sql new file mode 100644 index 00000000..e69de29b diff --git a/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql b/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql new file mode 100644 index 00000000..4ab5d2e5 --- /dev/null +++ b/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql @@ -0,0 +1,2 @@ +ALTER TABLE org_policies +ADD COLUMN revision_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; diff --git a/src/db/models/org_policy.rs b/src/db/models/org_policy.rs index 88b7872c..095ca7b4 100644 --- a/src/db/models/org_policy.rs +++ b/src/db/models/org_policy.rs @@ -1,3 +1,4 @@ +use chrono::{NaiveDateTime, Utc}; use derive_more::{AsRef, From}; use diesel::prelude::*; use serde::Deserialize; @@ -11,6 +12,7 @@ use crate::{ schema::{org_policies, users_organizations}, }, error::MapResult, + util::format_date, }; use super::{Membership, MembershipId, MembershipStatus, MembershipType, OrganizationId, TwoFactor, UserId}; @@ -24,6 +26,7 @@ pub struct OrgPolicy { 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 @@ -76,6 +79,7 @@ impl OrgPolicy { atype: atype as i32, enabled, data, + revision_date: Utc::now().naive_utc(), } } @@ -91,6 +95,7 @@ impl OrgPolicy { "type": self.atype, "data": data_json, "enabled": self.enabled, + "revisionDate": format_date(&self.revision_date), "object": "policy", }); @@ -108,11 +113,13 @@ impl OrgPolicy { /// Database methods impl OrgPolicy { - pub async fn save(&self, conn: &DbConn) -> EmptyResult { + 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) + .values(&*self) .execute(conn) { Ok(_) => Ok(()), @@ -120,7 +127,7 @@ impl OrgPolicy { Err(diesel::result::Error::DatabaseError(diesel::result::DatabaseErrorKind::ForeignKeyViolation, _)) => { diesel::update(org_policies::table) .filter(org_policies::uuid.eq(&self.uuid)) - .set(self) + .set(&*self) .execute(conn) .map_res("Error saving org_policy") } @@ -140,10 +147,10 @@ impl OrgPolicy { .map_res("Error deleting org_policy for insert")?; diesel::insert_into(org_policies::table) - .values(self) + .values(&*self) .on_conflict(org_policies::uuid) .do_update() - .set(self) + .set(&*self) .execute(conn) .map_res("Error saving org_policy") } diff --git a/src/db/schema.rs b/src/db/schema.rs index af342186..acf12554 100644 --- a/src/db/schema.rs +++ b/src/db/schema.rs @@ -116,6 +116,7 @@ table! { atype -> Integer, enabled -> Bool, data -> Text, + revision_date -> Timestamp, } } From 7049ec72a7e3e86dd22f5f1180fad89aa9fea6d5 Mon Sep 17 00:00:00 2001 From: Will Lotto Date: Mon, 10 Aug 2026 09:58:55 +1000 Subject: [PATCH 2/2] Fix migration correctness for org policy revision_date Caught by review: SQLite rejects non-constant defaults in ALTER TABLE ADD COLUMN, so the sqlite migration would fail outright on every existing installation (vaultwarden's default backend) rather than the postgresql setup this was developed against. - sqlite/mysql: add the column with a constant placeholder default, then backfill via UPDATE, since neither allows a volatile default in ALTER TABLE ADD COLUMN (mysql does allow it, but a constant keeps the three backends' migrations symmetric). - mysql: use DATETIME instead of TIMESTAMP, matching this repo's existing revision_date columns, and backfill with UTC_TIMESTAMP() instead of a session-timezone-dependent CURRENT_TIMESTAMP. - postgresql: backfill with `now() AT TIME ZONE 'utc'` instead of a DEFAULT now(), since assigning timestamptz now() into this naive TIMESTAMP column would otherwise cast through the server's TimeZone GUC and store local wall-clock instead of UTC on non-UTC servers. - mysql/postgresql: add a real down.sql (DROP COLUMN) instead of leaving it empty. --- .../down.sql | 1 + .../up.sql | 7 ++++++- .../down.sql | 1 + .../up.sql | 9 ++++++++- .../up.sql | 6 +++++- 5 files changed, 21 insertions(+), 3 deletions(-) diff --git a/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql index e69de29b..27c33623 100644 --- a/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql +++ b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/down.sql @@ -0,0 +1 @@ +ALTER TABLE org_policies DROP COLUMN revision_date; diff --git a/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql index fc5a5f44..97a84117 100644 --- a/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql +++ b/migrations/mysql/2026-08-10-120000_add_org_policy_revision_date/up.sql @@ -1,2 +1,7 @@ +-- DATETIME (not TIMESTAMP) to match this repo's convention for revision_date +-- columns elsewhere, and to avoid MySQL's implicit session-timezone +-- conversion and 2038 range limit on TIMESTAMP. ALTER TABLE org_policies -ADD COLUMN revision_date TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP; +ADD COLUMN revision_date DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00'; + +UPDATE org_policies SET revision_date = UTC_TIMESTAMP(); diff --git a/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql index e69de29b..27c33623 100644 --- a/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql +++ b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/down.sql @@ -0,0 +1 @@ +ALTER TABLE org_policies DROP COLUMN revision_date; diff --git a/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql index ed39a1d9..44229de2 100644 --- a/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql +++ b/migrations/postgresql/2026-08-10-120000_add_org_policy_revision_date/up.sql @@ -1,2 +1,9 @@ +-- Backfill via `now() AT TIME ZONE 'utc'` rather than a DEFAULT of now(): +-- assigning timestamptz now() into a naive TIMESTAMP column casts through +-- the server's TimeZone GUC, so a DEFAULT now() would store local wall-clock +-- instead of UTC on non-UTC servers, unlike every other naive-UTC timestamp +-- column in this schema. ALTER TABLE org_policies -ADD COLUMN revision_date TIMESTAMP NOT NULL DEFAULT now(); +ADD COLUMN revision_date TIMESTAMP NOT NULL DEFAULT '1970-01-01 00:00:00'; + +UPDATE org_policies SET revision_date = (now() AT TIME ZONE 'utc'); diff --git a/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql b/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql index 4ab5d2e5..86595711 100644 --- a/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql +++ b/migrations/sqlite/2026-08-10-120000_add_org_policy_revision_date/up.sql @@ -1,2 +1,6 @@ +-- SQLite forbids non-constant defaults in ALTER TABLE ... ADD COLUMN, so add +-- the column with a constant placeholder and backfill separately. ALTER TABLE org_policies -ADD COLUMN revision_date DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP; +ADD COLUMN revision_date DATETIME NOT NULL DEFAULT '1970-01-01 00:00:00'; + +UPDATE org_policies SET revision_date = CURRENT_TIMESTAMP;