-- Repair the legacy role/permission state while membership `access_all` still exists. -- -- A plain User carrying the historical membership-level `access_all` bit is deliberately not -- converted: that state grants dynamic reach over every collection *without* management authority, -- and the new model has no equivalent. It is refused instead -- and refused *here*, not only in Rust: -- Vaultwarden's startup preflight already stops such a database before any migration runs and prints -- the two explicit choices (`RefuseLegacyUserAccessAll` in `src/db/mod.rs`), but a migration run -- outside that wrapper -- `diesel migration run`, a bare `MigrationHarness`, any other SQL runner -- -- would not consult it, and 2026-07-24-120000 removes the only source of that reach a few -- statements later. Repeating the check before this file's first mutation is what makes the silent -- loss impossible rather than unlikely. -- -- The duplicate key aborts the migration. It is only inserted when such a membership exists. CREATE TEMPORARY TABLE __vw_legacy_user_access_all_guard ( blocked INTEGER NOT NULL PRIMARY KEY ); INSERT INTO __vw_legacy_user_access_all_guard (blocked) VALUES (1); INSERT INTO __vw_legacy_user_access_all_guard (blocked) SELECT 1 FROM users_organizations WHERE atype = 2 AND access_all = TRUE LIMIT 1; DROP TABLE __vw_legacy_user_access_all_guard; -- The legacy-Manager record has to exist already: 2026-06-30-120000 writes it, and the startup -- preflight refuses a database whose ledger carries that version without it. Creating it here would -- manufacture an empty, apparently valid history for precisely the databases that need an operator -- to look at them, so refuse instead -- this guard exists for a bare migration runner that never -- consulted the preflight. -- -- The duplicate key aborts the migration. It is only inserted while the record table is absent. CREATE TEMPORARY TABLE __vw_legacy_manager_record_guard ( blocked INTEGER NOT NULL PRIMARY KEY ); INSERT INTO __vw_legacy_manager_record_guard (blocked) VALUES (1); INSERT INTO __vw_legacy_manager_record_guard (blocked) SELECT 1 WHERE to_regclass('__vw_custom_role_legacy_manager') IS NULL; DROP TABLE __vw_legacy_manager_record_guard; -- A database that reaches this file with memberships still at `atype = 3` never ran the rewritten -- 2026-06-30-120000 -- for instance because a runner applied the files out of order. Those rows are -- unambiguously legacy Managers *right now*, so record them before the conversion at the end of this -- file makes them indistinguishable from modern Custom members. Idempotent, and a no-op on the -- normal path. INSERT INTO __vw_custom_role_legacy_manager (users_organizations_uuid) SELECT uuid FROM users_organizations WHERE atype = 3 ON CONFLICT DO NOTHING; -- Step 1: a legacy Manager who managed every collection through an organization-local group with -- `access_all` keeps that authority, materialized into the permission columns it now lives in. -- -- Restricted to memberships recorded as legacy Managers. Matching on role and group membership -- alone -- which an earlier revision did -- also matches every *modern* flagless Custom member who -- happens to sit in an ordinary `access_all` group, because the two states are the same shape, and -- would hand them organization-wide collection edit and delete. -- -- Earlier revisions derived this authority live from the group at request time instead, which was -- unsound for exactly that reason. Materializing it makes it visible to an owner in the member's -- permission list and revocable by clearing a checkbox. It is deliberately a one-time snapshot: the -- permission no longer lapses when the source group does. See tools/custom_role_rollback/README.md. -- -- Deliberately not `create_new_collections`: creating collections historically required -- membership-level `access_all`, and it is an independent permission now. UPDATE users_organizations SET edit_any_collection = TRUE, delete_any_collection = TRUE WHERE atype IN (3, 4) AND uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager) AND EXISTS ( SELECT 1 FROM groups_users AS gu INNER JOIN "groups" AS g ON g.uuid = gu.groups_uuid WHERE gu.users_organizations_uuid = users_organizations.uuid AND g.organizations_uuid = users_organizations.org_uuid AND g.access_all = TRUE ); -- Step 2: membership `access_all` on a legacy Manager represented all three collection capabilities. -- Set only TRUE values so this repair never removes independently configured permissions, and again -- only for recorded legacy Managers -- an intermediate revision of this feature branch could leave a -- modern Custom member carrying the old column as well. UPDATE users_organizations SET create_new_collections = TRUE, edit_any_collection = TRUE, delete_any_collection = TRUE WHERE atype IN (3, 4) AND uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager) AND access_all = TRUE; -- Convert only after the legacy bit has been copied. UPDATE users_organizations SET atype = 4 WHERE atype = 3; -- Clear the same-run marker only after every permission update succeeds. DELETE FROM __vw_custom_role_same_run_0716 WHERE marker = 1;