Browse Source
Vaultwarden only ever applies pending migrations, so putting an older image back leaves it facing a schema whose `access_all` column is gone -- and it refuses to start. The downgrade guard did not help there either: it only ever blocked `diesel migration revert`, which a self-hosted install cannot run. - tools/custom_role_rollback/ holds a self-contained script per backend that restores the pre-PR schema, folds Custom back onto Manager, and drops the seven ledger rows, so the older version starts again. SQLite rebuilds the table instead of using DROP COLUMN, for the same 3.35 reason as the forward migration. - The guard now states its terms instead of being a dead end: create __vw_allow_custom_role_downgrade and the revert goes through, so the data loss is acknowledged rather than merely prevented. - README documents what the old schema cannot represent, including the two values that cannot be reconstructed: Owner/Admin return with access_all set, and a plain User's former access_all stays materialized as the per-collection assignments the upgrade wrote.pull/7397/head
7 changed files with 305 additions and 6 deletions
@ -1,7 +1,23 @@ |
|||||
-- Nine independent Custom-role permissions cannot be represented losslessly by the legacy |
-- Nine independent Custom-role permissions cannot be represented losslessly by the legacy |
||||
-- role/access_all schema. Always stop before any older down migration removes permission data. |
-- role/access_all schema, so a revert is blocked here -- before any older down migration removes |
||||
|
-- permission data. |
||||
|
-- |
||||
|
-- It is an explicit, acknowledged decision though, not a dead end. Create the marker table below |
||||
|
-- while every Vaultwarden instance is stopped and this guard lets the revert through: |
||||
|
-- |
||||
|
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
||||
|
-- |
||||
|
-- Operators who only need the old server version to start again do not need Diesel at all -- |
||||
|
-- tools/custom_role_rollback/ has a self-contained script per backend. |
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
||||
blocked INTEGER NOT NULL PRIMARY KEY |
blocked INTEGER NOT NULL PRIMARY KEY |
||||
); |
); |
||||
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
||||
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
-- The duplicate key aborts the revert. It is only inserted while the acknowledgement is absent. |
||||
|
INSERT INTO __vw_custom_role_downgrade_guard (blocked) |
||||
|
SELECT 1 FROM DUAL |
||||
|
WHERE NOT EXISTS ( |
||||
|
SELECT 1 FROM information_schema.tables |
||||
|
WHERE table_schema = DATABASE() AND table_name = '__vw_allow_custom_role_downgrade' |
||||
|
); |
||||
|
DROP TABLE __vw_custom_role_downgrade_guard; |
||||
|
|||||
@ -1,7 +1,20 @@ |
|||||
-- Nine independent Custom-role permissions cannot be represented losslessly by the legacy |
-- Nine independent Custom-role permissions cannot be represented losslessly by the legacy |
||||
-- role/access_all schema. Always stop before any older down migration removes permission data. |
-- role/access_all schema, so a revert is blocked here -- before any older down migration removes |
||||
|
-- permission data. |
||||
|
-- |
||||
|
-- It is an explicit, acknowledged decision though, not a dead end. Create the marker table below |
||||
|
-- while every Vaultwarden instance is stopped and this guard lets the revert through: |
||||
|
-- |
||||
|
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
||||
|
-- |
||||
|
-- Operators who only need the old server version to start again do not need Diesel at all -- |
||||
|
-- tools/custom_role_rollback/ has a self-contained script per backend. |
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
||||
blocked INTEGER NOT NULL PRIMARY KEY |
blocked INTEGER NOT NULL PRIMARY KEY |
||||
); |
); |
||||
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
||||
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
-- The duplicate key aborts the revert. It is only inserted while the acknowledgement is absent. |
||||
|
INSERT INTO __vw_custom_role_downgrade_guard (blocked) |
||||
|
SELECT 1 |
||||
|
WHERE to_regclass('__vw_allow_custom_role_downgrade') IS NULL; |
||||
|
DROP TABLE __vw_custom_role_downgrade_guard; |
||||
|
|||||
@ -1,7 +1,23 @@ |
|||||
-- Nine independent Custom-role permissions cannot be represented losslessly by the legacy |
-- Nine independent Custom-role permissions cannot be represented losslessly by the legacy |
||||
-- role/access_all schema. Always stop before any older down migration removes permission data. |
-- role/access_all schema, so a revert is blocked here -- before any older down migration removes |
||||
|
-- permission data. |
||||
|
-- |
||||
|
-- It is an explicit, acknowledged decision though, not a dead end. Create the marker table below |
||||
|
-- while every Vaultwarden instance is stopped and this guard lets the revert through: |
||||
|
-- |
||||
|
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
||||
|
-- |
||||
|
-- Operators who only need the old server version to start again do not need Diesel at all -- |
||||
|
-- tools/custom_role_rollback/ has a self-contained script per backend. |
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
||||
blocked INTEGER NOT NULL PRIMARY KEY |
blocked INTEGER NOT NULL PRIMARY KEY |
||||
); |
); |
||||
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
||||
INSERT INTO __vw_custom_role_downgrade_guard (blocked) VALUES (1); |
-- The duplicate key aborts the revert. It is only inserted while the acknowledgement is absent. |
||||
|
INSERT INTO __vw_custom_role_downgrade_guard (blocked) |
||||
|
SELECT 1 |
||||
|
WHERE NOT EXISTS ( |
||||
|
SELECT 1 FROM sqlite_master |
||||
|
WHERE type = 'table' AND name = '__vw_allow_custom_role_downgrade' |
||||
|
); |
||||
|
DROP TABLE __vw_custom_role_downgrade_guard; |
||||
|
|||||
@ -0,0 +1,77 @@ |
|||||
|
# Rolling back the Custom-role change |
||||
|
|
||||
|
The Custom-role change removes the membership `access_all` column and adds nine permission columns. |
||||
|
A Vaultwarden version from before that change cannot start against the new schema, because its |
||||
|
`schema.rs` still expects `access_all` to exist. |
||||
|
|
||||
|
Vaultwarden only ever applies *pending* migrations — it never reverts one on its own — so putting |
||||
|
the old image back is not enough. Run the script for your backend once and the old version starts |
||||
|
again. |
||||
|
|
||||
|
## What is lost |
||||
|
|
||||
|
The old schema has nowhere to store the nine permissions, so they are dropped: |
||||
|
|
||||
|
| Before the rollback | After | |
||||
|
|---|---| |
||||
|
| Owner / Admin | Owner / Admin with `access_all = TRUE` | |
||||
|
| Custom with **all three** collection permissions | Manager with `access_all = TRUE` | |
||||
|
| Custom with only some collection permissions | Manager with `access_all = FALSE` | |
||||
|
| Custom with `manageUsers` / `manageGroups` / `managePolicies` | Manager — those permissions are gone | |
||||
|
| Custom with `accessEventLogs` / `accessImportExport` / `accessReports` | Manager — those permissions are gone | |
||||
|
| plain User | plain User with `access_all = FALSE` | |
||||
|
|
||||
|
Per-collection assignments (`users_collections`, `collections_groups`) and `groups.access_all` are |
||||
|
untouched. Only `users_organizations` changes. |
||||
|
|
||||
|
Two of those rows do not come back byte-identical to what the database held before the *upgrade*, |
||||
|
because the information no longer exists to reconstruct them: |
||||
|
|
||||
|
- **Owner/Admin always come back with `access_all = TRUE`**, even if the flag was `FALSE` for them |
||||
|
before. The upgrade dropped the column precisely because Owners and Admins reach every collection |
||||
|
through their role, so the original value is unknown afterwards. It grants them nothing they did |
||||
|
not already have as Owner/Admin; the visible difference is that unassigned collections show up in |
||||
|
their personal vault view again. |
||||
|
- **A plain User that carried `access_all` comes back with `access_all = FALSE`.** The upgrade wrote |
||||
|
that member's reach out as explicit per-collection assignments before dropping the bit, and those |
||||
|
rows are left untouched here — so the member keeps access to the collections that existed at |
||||
|
upgrade time, just not automatically to ones created afterwards. |
||||
|
|
||||
|
Edit-any-collection deliberately does **not** become `access_all` on its own: in the old schema that |
||||
|
flag also carried the legacy "manage all collections" authority including deletion, so a member who |
||||
|
only held Edit must not come back with delete rights. |
||||
|
|
||||
|
## How to run it |
||||
|
|
||||
|
Stop every Vaultwarden instance and take a backup first. Then: |
||||
|
|
||||
|
```bash |
||||
|
# SQLite |
||||
|
sqlite3 /path/to/data/db.sqlite3 < tools/custom_role_rollback/sqlite.sql |
||||
|
|
||||
|
# MySQL / MariaDB |
||||
|
mysql -u <user> -p <database> < tools/custom_role_rollback/mysql.sql |
||||
|
|
||||
|
# PostgreSQL |
||||
|
psql -U <user> -d <database> -v ON_ERROR_STOP=1 -f tools/custom_role_rollback/postgresql.sql |
||||
|
``` |
||||
|
|
||||
|
Run it exactly once — a second run fails because the permission columns are already gone. On |
||||
|
MySQL/MariaDB the statements cannot be wrapped in a transaction (DDL commits implicitly there); if |
||||
|
the script is interrupted, restore the backup and start over. SQLite and PostgreSQL apply the whole |
||||
|
script atomically. |
||||
|
|
||||
|
Afterwards start the older Vaultwarden version. Upgrading again later re-applies the seven |
||||
|
migrations from a clean state. |
||||
|
|
||||
|
## Reverting with the Diesel CLI instead |
||||
|
|
||||
|
For development checkouts the down migrations do the same thing step by step. The newest one refuses |
||||
|
by default so an accidental revert cannot silently destroy the permission data; acknowledge it |
||||
|
explicitly first: |
||||
|
|
||||
|
```sql |
||||
|
CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
||||
|
``` |
||||
|
|
||||
|
Then `diesel migration revert` works as usual. The rollback scripts above drop that table again. |
||||
@ -0,0 +1,50 @@ |
|||||
|
-- Roll a MySQL/MariaDB database back to the schema the Vaultwarden version *before* the Custom-role |
||||
|
-- change expects, so that older binary starts again. Read README.md in this directory first -- |
||||
|
-- it lists exactly what is lost and how to run this safely. |
||||
|
-- |
||||
|
-- NOTE: MySQL/MariaDB commit every DDL statement implicitly, so this script cannot be wrapped in a |
||||
|
-- transaction. Take a backup before running it; if it is interrupted, restore and start over. |
||||
|
|
||||
|
ALTER TABLE users_organizations ADD COLUMN access_all BOOLEAN NOT NULL DEFAULT FALSE; |
||||
|
|
||||
|
-- The legacy flag is recomputed with the same mapping the down migrations use: everyone who |
||||
|
-- reached every collection keeps that reach, and a Custom member has to hold all three collection |
||||
|
-- permissions -- Edit-only must not silently turn into the legacy "manage all collections" |
||||
|
-- authority, which in that older schema also carried collection deletion. |
||||
|
UPDATE users_organizations SET access_all = TRUE WHERE atype IN (0, 1); |
||||
|
UPDATE users_organizations |
||||
|
SET access_all = TRUE |
||||
|
WHERE atype = 4 |
||||
|
AND create_new_collections = TRUE |
||||
|
AND edit_any_collection = TRUE |
||||
|
AND delete_any_collection = TRUE; |
||||
|
|
||||
|
-- The old server cannot load type 4; Custom members were stored as Manager back then. |
||||
|
UPDATE users_organizations SET atype = 3 WHERE atype = 4; |
||||
|
|
||||
|
ALTER TABLE users_organizations DROP COLUMN manage_users; |
||||
|
ALTER TABLE users_organizations DROP COLUMN manage_groups; |
||||
|
ALTER TABLE users_organizations DROP COLUMN manage_policies; |
||||
|
ALTER TABLE users_organizations DROP COLUMN create_new_collections; |
||||
|
ALTER TABLE users_organizations DROP COLUMN edit_any_collection; |
||||
|
ALTER TABLE users_organizations DROP COLUMN delete_any_collection; |
||||
|
ALTER TABLE users_organizations DROP COLUMN access_event_logs; |
||||
|
ALTER TABLE users_organizations DROP COLUMN access_import_export; |
||||
|
ALTER TABLE users_organizations DROP COLUMN access_reports; |
||||
|
|
||||
|
-- Bookkeeping tables this feature may have left behind. |
||||
|
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
||||
|
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
||||
|
|
||||
|
-- Finally forget the seven migrations, so the older binary does not see a ledger from the future |
||||
|
-- and a later upgrade applies them again from a clean state. |
||||
|
DELETE FROM __diesel_schema_migrations |
||||
|
WHERE version IN ( |
||||
|
'20260630120000', |
||||
|
'20260715120000', |
||||
|
'20260716120000', |
||||
|
'20260723120000', |
||||
|
'20260724120000', |
||||
|
'20260724130000', |
||||
|
'20260724140000' |
||||
|
); |
||||
@ -0,0 +1,54 @@ |
|||||
|
-- Roll a PostgreSQL database back to the schema the Vaultwarden version *before* the Custom-role |
||||
|
-- change expects, so that older binary starts again. Read README.md in this directory first -- |
||||
|
-- it lists exactly what is lost and how to run this safely. |
||||
|
-- |
||||
|
-- PostgreSQL DDL is transactional, so this whole script either applies or it does not. |
||||
|
|
||||
|
BEGIN; |
||||
|
|
||||
|
ALTER TABLE users_organizations ADD COLUMN access_all BOOLEAN NOT NULL DEFAULT FALSE; |
||||
|
|
||||
|
-- The legacy flag is recomputed with the same mapping the down migrations use: everyone who |
||||
|
-- reached every collection keeps that reach, and a Custom member has to hold all three collection |
||||
|
-- permissions -- Edit-only must not silently turn into the legacy "manage all collections" |
||||
|
-- authority, which in that older schema also carried collection deletion. |
||||
|
UPDATE users_organizations SET access_all = TRUE WHERE atype IN (0, 1); |
||||
|
UPDATE users_organizations |
||||
|
SET access_all = TRUE |
||||
|
WHERE atype = 4 |
||||
|
AND create_new_collections = TRUE |
||||
|
AND edit_any_collection = TRUE |
||||
|
AND delete_any_collection = TRUE; |
||||
|
|
||||
|
-- The old server cannot load type 4; Custom members were stored as Manager back then. |
||||
|
UPDATE users_organizations SET atype = 3 WHERE atype = 4; |
||||
|
|
||||
|
ALTER TABLE users_organizations |
||||
|
DROP COLUMN manage_users, |
||||
|
DROP COLUMN manage_groups, |
||||
|
DROP COLUMN manage_policies, |
||||
|
DROP COLUMN create_new_collections, |
||||
|
DROP COLUMN edit_any_collection, |
||||
|
DROP COLUMN delete_any_collection, |
||||
|
DROP COLUMN access_event_logs, |
||||
|
DROP COLUMN access_import_export, |
||||
|
DROP COLUMN access_reports; |
||||
|
|
||||
|
-- Bookkeeping tables this feature may have left behind. |
||||
|
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
||||
|
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
||||
|
|
||||
|
-- Finally forget the seven migrations, so the older binary does not see a ledger from the future |
||||
|
-- and a later upgrade applies them again from a clean state. |
||||
|
DELETE FROM __diesel_schema_migrations |
||||
|
WHERE version IN ( |
||||
|
'20260630120000', |
||||
|
'20260715120000', |
||||
|
'20260716120000', |
||||
|
'20260723120000', |
||||
|
'20260724120000', |
||||
|
'20260724130000', |
||||
|
'20260724140000' |
||||
|
); |
||||
|
|
||||
|
COMMIT; |
||||
@ -0,0 +1,73 @@ |
|||||
|
-- Roll a SQLite database back to the schema the Vaultwarden version *before* the Custom-role |
||||
|
-- change expects, so that older binary starts again. Read README.md in this directory first -- |
||||
|
-- it lists exactly what is lost and how to run this safely. |
||||
|
-- |
||||
|
-- `ALTER TABLE ... DROP COLUMN` is avoided on purpose: it only exists since SQLite 3.35, and this |
||||
|
-- script has to work on the same older system SQLite the forward migrations support. Rebuilding the |
||||
|
-- table also recreates `access_all` and drops all nine permission columns in one step. |
||||
|
|
||||
|
PRAGMA foreign_keys = OFF; |
||||
|
|
||||
|
BEGIN; |
||||
|
|
||||
|
CREATE TABLE users_organizations_rollback ( |
||||
|
uuid TEXT NOT NULL PRIMARY KEY, |
||||
|
user_uuid TEXT NOT NULL REFERENCES users (uuid), |
||||
|
org_uuid TEXT NOT NULL REFERENCES organizations (uuid), |
||||
|
access_all BOOLEAN NOT NULL DEFAULT 0, |
||||
|
akey TEXT NOT NULL, |
||||
|
status INTEGER NOT NULL, |
||||
|
atype INTEGER NOT NULL, |
||||
|
reset_password_key TEXT, |
||||
|
external_id TEXT, |
||||
|
invited_by_email TEXT DEFAULT NULL, |
||||
|
|
||||
|
UNIQUE (user_uuid, org_uuid) |
||||
|
); |
||||
|
|
||||
|
-- The legacy flag is recomputed with the same mapping the down migrations use: everyone who |
||||
|
-- reached every collection keeps that reach, and a Custom member has to hold all three collection |
||||
|
-- permissions -- Edit-only must not silently turn into the legacy "manage all collections" |
||||
|
-- authority, which in that older schema also carried collection deletion. |
||||
|
INSERT INTO users_organizations_rollback ( |
||||
|
uuid, user_uuid, org_uuid, access_all, akey, status, atype, |
||||
|
reset_password_key, external_id, invited_by_email |
||||
|
) |
||||
|
SELECT |
||||
|
uuid, user_uuid, org_uuid, |
||||
|
CASE |
||||
|
WHEN atype IN (0, 1) THEN 1 |
||||
|
WHEN atype = 4 |
||||
|
AND create_new_collections = 1 |
||||
|
AND edit_any_collection = 1 |
||||
|
AND delete_any_collection = 1 THEN 1 |
||||
|
ELSE 0 |
||||
|
END, |
||||
|
akey, status, |
||||
|
-- The old server cannot load type 4; Custom members were stored as Manager back then. |
||||
|
CASE WHEN atype = 4 THEN 3 ELSE atype END, |
||||
|
reset_password_key, external_id, invited_by_email |
||||
|
FROM users_organizations; |
||||
|
|
||||
|
DROP TABLE users_organizations; |
||||
|
|
||||
|
ALTER TABLE users_organizations_rollback RENAME TO users_organizations; |
||||
|
|
||||
|
-- Bookkeeping tables this feature may have left behind. |
||||
|
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
||||
|
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
||||
|
|
||||
|
-- Finally forget the seven migrations, so the older binary does not see a ledger from the future |
||||
|
-- and a later upgrade applies them again from a clean state. |
||||
|
DELETE FROM __diesel_schema_migrations |
||||
|
WHERE version IN ( |
||||
|
'20260630120000', |
||||
|
'20260715120000', |
||||
|
'20260716120000', |
||||
|
'20260723120000', |
||||
|
'20260724120000', |
||||
|
'20260724130000', |
||||
|
'20260724140000' |
||||
|
); |
||||
|
|
||||
|
COMMIT; |
||||
Loading…
Reference in new issue