You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
209 lines
9.3 KiB
209 lines
9.3 KiB
-- 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 needs SQLite 3.35, and this script has to
|
|
-- work on the same older system SQLite the forward migrations support. The rebuild recreates
|
|
-- `access_all` and drops all nine permission columns in one step.
|
|
|
|
-- Stop at the first error. Without this the sqlite3 shell keeps going after a failed statement,
|
|
-- and a second run -- where the SELECT below can no longer see the permission columns -- would
|
|
-- still reach DROP TABLE and commit an empty users_organizations. `.bail on` is a shell command;
|
|
-- a runner that is not the sqlite3 CLI has to abort on the first error and roll back by itself.
|
|
.bail on
|
|
|
|
PRAGMA foreign_keys = OFF;
|
|
|
|
BEGIN;
|
|
|
|
-- Refuse to start at all unless the database is in the exact state this script converts *from*. A
|
|
-- repeat run, or a half-finished upgrade, would otherwise only fail somewhere in the middle. Each
|
|
-- check is read-only, and the name of the failing CHECK constraint *is* the error message.
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_membership_access_all_still_exists_so_this_database_was_not_upgraded_or_was_already_rolled_back
|
|
CHECK (ok = 1)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition (ok)
|
|
SELECT CASE
|
|
WHEN NOT EXISTS (SELECT 1 FROM pragma_table_xinfo('users_organizations') WHERE name = 'access_all')
|
|
THEN 1
|
|
ELSE 0
|
|
END;
|
|
DROP TABLE __vw_rollback_precondition;
|
|
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition_columns (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_all_nine_custom_role_permission_columns_must_exist_restore_the_pre_upgrade_backup
|
|
CHECK (ok = 9)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition_columns (ok)
|
|
SELECT COUNT(*)
|
|
FROM pragma_table_xinfo('users_organizations')
|
|
WHERE name IN (
|
|
'manage_users', 'manage_groups', 'manage_policies',
|
|
'create_new_collections', 'edit_any_collection', 'delete_any_collection',
|
|
'access_event_logs', 'access_import_export', 'access_reports'
|
|
);
|
|
DROP TABLE __vw_rollback_precondition_columns;
|
|
|
|
-- The rebuild below copies a fixed column list, so anything this script does not know about would be
|
|
-- silently dropped together with its data. Require the table to hold *exactly* the eighteen columns
|
|
-- the Custom-role upgrade leaves behind -- not merely to contain them. A newer migration that added a
|
|
-- column, or a local modification, therefore refuses here instead of being destroyed at COMMIT.
|
|
--
|
|
-- `table_xinfo`, not `table_info`: the latter omits generated columns entirely, so a STORED or
|
|
-- VIRTUAL column would pass the count unseen and then be lost in the rebuild.
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition_exact_columns (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_users_organizations_has_unexpected_columns_this_script_is_older_than_the_database
|
|
CHECK (ok = 1)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition_exact_columns (ok)
|
|
SELECT CASE WHEN total = 18 AND known = 18 THEN 1 ELSE 0 END
|
|
FROM (
|
|
SELECT
|
|
COUNT(*) AS total,
|
|
SUM(CASE WHEN name IN (
|
|
'uuid', 'user_uuid', 'org_uuid', 'akey', 'status', 'atype',
|
|
'reset_password_key', 'external_id', 'invited_by_email',
|
|
'manage_users', 'manage_groups', 'manage_policies',
|
|
'create_new_collections', 'edit_any_collection', 'delete_any_collection',
|
|
'access_event_logs', 'access_import_export', 'access_reports'
|
|
) THEN 1 ELSE 0 END) AS known
|
|
FROM pragma_table_xinfo('users_organizations')
|
|
);
|
|
DROP TABLE __vw_rollback_precondition_exact_columns;
|
|
|
|
-- Same reasoning for everything else attached to the table: `DROP TABLE` takes its indexes and
|
|
-- triggers with it, and the rebuild recreates only the PRIMARY KEY and the UNIQUE pair.
|
|
--
|
|
-- Counting `index_list` rather than `sqlite_master` on purpose. An index that SQLite created for a
|
|
-- UNIQUE constraint has no SQL text, so `sqlite_master.sql IS NOT NULL` cannot see it -- an extra
|
|
-- `UNIQUE(external_id)` would pass unnoticed and be gone afterwards. `index_list` reports every
|
|
-- index, so the upgraded table's own two are the exact expected count.
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition_objects (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_users_organizations_has_extra_indexes_constraints_or_triggers_the_rebuild_would_destroy
|
|
CHECK (ok = 1)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition_objects (ok)
|
|
SELECT CASE WHEN indexes = 2 AND triggers = 0 THEN 1 ELSE 0 END
|
|
FROM (
|
|
SELECT
|
|
(SELECT COUNT(*) FROM pragma_index_list('users_organizations')) AS indexes,
|
|
(SELECT COUNT(*) FROM sqlite_master
|
|
WHERE tbl_name = 'users_organizations' AND type = 'trigger') AS triggers
|
|
);
|
|
DROP TABLE __vw_rollback_precondition_objects;
|
|
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition_ledger (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_the_custom_role_migration_must_be_recorded_schema_and_ledger_disagree
|
|
CHECK (ok = 1)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition_ledger (ok)
|
|
SELECT COUNT(*)
|
|
FROM __diesel_schema_migrations
|
|
WHERE version IN (
|
|
'20260630120000'
|
|
);
|
|
DROP TABLE __vw_rollback_precondition_ledger;
|
|
|
|
-- A migration newer than the Custom-role one has run, so this script cannot know what it changed
|
|
-- or whether the rebuild below would undo it. Removing only that one version would also leave the
|
|
-- ledger claiming a migration whose schema objects are gone.
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition_future_ledger (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_migrations_newer_than_the_custom_role_change_are_recorded_use_a_newer_rollback_script
|
|
CHECK (ok = 0)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition_future_ledger (ok)
|
|
SELECT COUNT(*) FROM __diesel_schema_migrations WHERE version > '20260630120000';
|
|
DROP TABLE __vw_rollback_precondition_future_ledger;
|
|
|
|
-- Which memberships come back as Manager has to be decided *for this rollback*. See README.md; an
|
|
-- empty list is a valid answer and maps every Custom member to plain User.
|
|
CREATE TEMPORARY TABLE __vw_rollback_precondition_allowlist (
|
|
ok INTEGER NOT NULL CONSTRAINT
|
|
refused_create_vw_rollback_manager_allowlist_first_see_readme_role_mapping
|
|
CHECK (ok = 1)
|
|
);
|
|
INSERT INTO __vw_rollback_precondition_allowlist (ok)
|
|
SELECT COUNT(*)
|
|
FROM sqlite_master
|
|
WHERE type = 'table' AND name = '__vw_rollback_manager_allowlist';
|
|
DROP TABLE __vw_rollback_precondition_allowlist;
|
|
|
|
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)
|
|
);
|
|
|
|
-- Roles and the legacy flag are recomputed together, because in the old schema they are not
|
|
-- independent.
|
|
--
|
|
-- Only a membership on the allowlist comes back as Manager; everything else becomes a plain User and
|
|
-- keeps every grant `users_collections` and `collections_groups` carry. The legacy role is not a
|
|
-- subset of what a Custom member holds, so handing it out on less than a current, deliberate decision
|
|
-- would *grant* authority during a downgrade -- and historical provenance is not that decision. See
|
|
-- README.md.
|
|
--
|
|
-- `access_all` follows the mapping the down migrations use: a Custom member has to hold all three
|
|
-- collection permissions, because in the old schema the bit also carried collection deletion. A
|
|
-- member mapped to plain User never keeps it: `User + access_all` is the one state the upgrade
|
|
-- refuses, so leaving it set would strand this database.
|
|
INSERT INTO users_organizations_rollback (
|
|
uuid, user_uuid, org_uuid, access_all, akey, status, atype,
|
|
reset_password_key, external_id, invited_by_email
|
|
)
|
|
SELECT
|
|
uo.uuid, uo.user_uuid, uo.org_uuid,
|
|
CASE
|
|
WHEN uo.atype IN (0, 1) THEN 1
|
|
WHEN uo.atype = 4
|
|
AND uo.uuid IN (SELECT users_organizations_uuid FROM __vw_rollback_manager_allowlist)
|
|
AND uo.create_new_collections = 1
|
|
AND uo.edit_any_collection = 1
|
|
AND uo.delete_any_collection = 1 THEN 1
|
|
ELSE 0
|
|
END,
|
|
uo.akey, uo.status,
|
|
-- The old server cannot load type 4.
|
|
CASE
|
|
WHEN uo.atype = 4
|
|
AND uo.uuid IN (SELECT users_organizations_uuid FROM __vw_rollback_manager_allowlist)
|
|
THEN 3
|
|
WHEN uo.atype = 4 THEN 2
|
|
ELSE uo.atype
|
|
END,
|
|
uo.reset_password_key, uo.external_id, uo.invited_by_email
|
|
FROM users_organizations AS uo;
|
|
|
|
DROP TABLE users_organizations;
|
|
|
|
ALTER TABLE users_organizations_rollback RENAME TO users_organizations;
|
|
|
|
-- The two decisions this rollback required. A later re-upgrade needs neither: it reads the restored
|
|
-- `atype = 3` rows directly and converts them deterministically.
|
|
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade;
|
|
DROP TABLE IF EXISTS __vw_rollback_manager_allowlist;
|
|
|
|
-- Finally forget the migration, so the older binary does not see a ledger from the future and a
|
|
-- later upgrade applies it again from a clean state.
|
|
DELETE FROM __diesel_schema_migrations
|
|
WHERE version IN (
|
|
'20260630120000'
|
|
);
|
|
|
|
COMMIT;
|
|
|