69 changed files with 3360 additions and 5349 deletions
@ -1,37 +1,127 @@ |
|||||
ALTER TABLE users_organizations ADD COLUMN manage_users BOOLEAN NOT NULL DEFAULT FALSE; |
-- Replace the membership-level `access_all` flag with the persisted Custom role and its nine |
||||
ALTER TABLE users_organizations ADD COLUMN manage_groups BOOLEAN NOT NULL DEFAULT FALSE; |
-- granular permissions. |
||||
ALTER TABLE users_organizations ADD COLUMN manage_policies BOOLEAN NOT NULL DEFAULT FALSE; |
-- |
||||
-- Record which memberships were legacy Managers *before* anything converts them. |
-- Two different columns are called `access_all`, and everything below depends on keeping them apart: |
||||
-- |
-- |
||||
-- This is the only moment at which that is knowable. `atype = 3` means Manager here and Custom |
-- * `users_organizations.access_all` -- the MEMBERSHIP-level bit this migration replaces. Dropped |
||||
-- afterwards -- the conversion below reuses the value -- so once it has run, a genuine legacy |
-- at the end of this file. |
||||
-- Manager and a Custom member created later are byte-identical. Every later step that has to reason |
-- * `groups.access_all` -- the GROUP-level flag, a separate and still-supported feature. Only read |
||||
-- about legacy authority (2026-07-23, 2026-08-09 and tools/custom_role_rollback/) reads this table |
-- here, to decide a legacy Manager's permissions; never written, and it keeps granting group |
||||
-- instead of guessing, which is what stops them from handing legacy privileges to modern members. |
-- members access to every collection afterwards exactly as before. |
||||
-- |
-- |
||||
-- Deliberately not a Diesel model and not in schema.rs: no runtime code reads it. It is |
-- Base `Collection::is_coll_manageable_by_user` accepts either, so a Manager reached every collection |
||||
-- migration/rollback bookkeeping, and it carries no foreign key so that 2026-07-24-120000's table |
-- through either. Only the membership bit is going away, but the capability an owner configured |
||||
-- rebuild does not have to care about it. |
-- through either route is preserved, so both are read below. While this file runs the membership |
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_legacy_manager ( |
-- column still exists and `atype = 3` still unambiguously means "legacy Manager". |
||||
users_organizations_uuid CHAR(36) NOT NULL PRIMARY KEY |
-- |
||||
); |
-- One state cannot be converted and is refused before the first mutation; `src/db/mod.rs` evaluates |
||||
INSERT IGNORE INTO __vw_custom_role_legacy_manager (users_organizations_uuid) |
-- the same condition at startup and prints the recovery text, because Diesel would surface the abort |
||||
SELECT uuid FROM users_organizations WHERE atype = 3; |
-- below as nothing but a driver-level duplicate-key error. |
||||
|
-- |
||||
|
-- A temporary table on purpose: on MySQL/MariaDB it is the only DDL that does not commit implicitly, |
||||
|
-- so a refusal cannot leave a half-applied migration behind. |
||||
|
|
||||
-- Separately, mark that this database's Custom-role history is accounted for -- it was produced by |
-- A plain User carrying membership `access_all`, reachable only on databases written before the web |
||||
-- the migrations that ship today. Nothing else creates this table, which is what lets the startup |
-- vault stopped sending the flag. The bit gave read/write reach over every collection, present and |
||||
-- preflight treat its absence as proof that an earlier revision of this chain ran instead. |
-- future, with no management authority, and the new model has no permission for that: |
||||
-- |
-- `edit_any_collection` would add management authority, dropping the bit would take the reach away. |
||||
-- Deliberately not the record table above: that one holds data an operator has to be able to write |
-- Refuse and let an owner choose. The duplicate key aborts the migration, and is only inserted when |
||||
-- during recovery, so its existence cannot also stand for "the history behind this data was |
-- such a membership exists. |
||||
-- reviewed" -- creating it empty to silence an error would otherwise pass as the audit it asks for. |
CREATE TEMPORARY TABLE __vw_legacy_user_access_all_guard ( |
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_history_verified ( |
blocked INTEGER NOT NULL PRIMARY KEY |
||||
verified 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 TEMPORARY TABLE __vw_legacy_user_access_all_guard; |
||||
|
|
||||
|
-- One ALTER TABLE for all nine columns: MySQL/MariaDB commit every DDL statement implicitly, so nine |
||||
|
-- separate statements would leave nine points at which a crash produces a partially migrated schema. |
||||
|
-- A single ALTER is one such point, and on MySQL 8 it is atomic. |
||||
|
ALTER TABLE users_organizations |
||||
|
ADD COLUMN manage_users BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN manage_groups BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN manage_policies BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN create_new_collections BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN access_event_logs BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN access_import_export BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN access_reports BOOLEAN NOT NULL DEFAULT FALSE; |
||||
|
|
||||
|
-- Owners and Admins are not touched: they carried `access_all` implicitly and the new model gives |
||||
|
-- them every permission by role. A plain User cannot reach this point carrying the bit (the guard |
||||
|
-- above), so only a Manager becomes Custom, keeping the organization-wide collection-management |
||||
|
-- capability it is configured with right now: |
||||
|
-- |
||||
|
-- * membership `access_all` -- the "Manage all collections" checkbox -- covered all three |
||||
|
-- collection permissions, including creating collections; |
||||
|
-- * an organization-local `access_all` group covered editing and deleting every collection, but |
||||
|
-- never creation -- that always required the membership bit; |
||||
|
-- * a Manager with neither keeps all three at FALSE. |
||||
|
-- |
||||
|
-- The second case is a deliberate policy choice. That capability was dynamic: it ended with the |
||||
|
-- group, with the group's own `access_all`, and with the member leaving it. It was never gated on |
||||
|
-- ORG_GROUPS_ENABLED -- `Collection::is_coll_manageable_by_user` reads `groups.access_all` in SQL |
||||
|
-- with no configuration check -- so it applied even where groups were never enabled. Nothing in the |
||||
|
-- new model is bound to a group, so it becomes a membership permission and no longer lapses on its |
||||
|
-- own. The alternative is silently revoking access these members have today, or refusing an ordinary |
||||
|
-- upgrade; the permission is visible in the member's permission list and an owner can clear it. |
||||
|
-- |
||||
|
-- The management (manage_users / manage_groups / manage_policies) and access (event logs / |
||||
|
-- import-export / reports) permissions keep their FALSE default. Nothing they unlock was a Manager |
||||
|
-- capability -- every member mutation, every policy write, the organization export and both |
||||
|
-- event-log routes were gated on Admin/Owner -- so granting one here would be a new privilege. |
||||
|
-- |
||||
|
-- One read is not carried over, and only for members who held the MEMBERSHIP bit: `has_full_access()` |
||||
|
-- read `self.access_all` and the role, never `groups.access_all`, so it gated the full member list |
||||
|
-- (`GET /organizations/<org>/users`) for them and for nobody whose reach came from a group. |
||||
|
-- `manage_users` is not granted to restore it, because it also carries invite, confirm, revoke, |
||||
|
-- restore and delete, which the Manager role never had; such members keep `/users/mini-details`, and |
||||
|
-- an owner can grant `manage_users` deliberately. In the other direction `edit_any_collection` |
||||
|
-- satisfies `has_full_access()`, which opens the organization collection list and |
||||
|
-- `GET /ciphers/organization-details` to the group-derived class -- data they could already reach |
||||
|
-- through the group, so only the route is new. |
||||
|
-- |
||||
|
-- Role conversion and permission values are one statement, so `atype = 3` unambiguously still means |
||||
|
-- Manager everywhere it is read. |
||||
|
-- |
||||
|
-- Status is deliberately not part of the predicate: an invited, accepted or revoked membership is |
||||
|
-- converted like a confirmed one, since none holds authority in that state and the permissions are |
||||
|
-- what it would come back with -- the same thing `access_all` would have done. |
||||
|
-- |
||||
|
-- The group lookup is bound to the membership's own organization: a `groups_users` row pointing at |
||||
|
-- another organization's `access_all` group conveys nothing, exactly as it conveys nothing today. |
||||
|
UPDATE users_organizations |
||||
|
SET create_new_collections = access_all, |
||||
|
edit_any_collection = access_all |
||||
|
OR 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 |
||||
|
), |
||||
|
delete_any_collection = access_all |
||||
|
OR 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 |
||||
|
), |
||||
|
atype = 4 |
||||
|
WHERE atype = 3; |
||||
|
|
||||
|
-- The flag is now fully represented by the role model, so drop the redundant column. This concerns |
||||
|
-- `users_organizations` only; `groups.access_all` stays. |
||||
|
ALTER TABLE users_organizations DROP COLUMN access_all; |
||||
|
|
||||
-- Previously the server stored members created with the Custom role as Manager (3) and |
-- Never inherit a downgrade acknowledgement left behind by an earlier revert. |
||||
-- masqueraded them as Custom (4) in all API responses. Now that Custom is a real, persisted |
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
||||
-- type, convert those members so clients (which no longer know the Manager role) keep |
|
||||
-- seeing exactly what they saw before. access_all is preserved; the new flags stay FALSE, |
|
||||
-- which matches the capabilities these members had. |
|
||||
UPDATE users_organizations SET atype = 4 WHERE atype = 3; |
|
||||
|
|||||
@ -1 +0,0 @@ |
|||||
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
|
||||
@ -1,13 +0,0 @@ |
|||||
-- Record whether 2026-07-16 is about to run in this migration sequence. The durable marker lets a |
|
||||
-- retry distinguish its deterministic group-derived 0/1/1 backfill from older, ambiguous data. |
|
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_same_run_0716 ( |
|
||||
marker INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT IGNORE INTO __vw_custom_role_same_run_0716 (marker) |
|
||||
SELECT 1 |
|
||||
FROM DUAL |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM __diesel_schema_migrations |
|
||||
WHERE version = '20260716120000' |
|
||||
); |
|
||||
@ -1,34 +0,0 @@ |
|||||
-- Lossy revert: this removes the three independent Custom collection permissions, which the legacy |
|
||||
-- role/access_all schema cannot represent -- it only knows all three together. The revert therefore |
|
||||
-- requires the same acknowledgement as 2026-07-24-140000/down.sql -- which only announces the loss, |
|
||||
-- it does not authorize it. Create the marker table while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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 TEMPORARY TABLE`, not `DROP TABLE`: the latter is one more statement that commits |
|
||||
-- implicitly on MySQL/MariaDB, and it would happily drop a permanent table of the same name. |
|
||||
DROP TEMPORARY TABLE __vw_custom_role_downgrade_guard; |
|
||||
|
|
||||
-- The previous schema exposes access_all as the three collection permissions together. Avoid |
|
||||
-- turning Edit-only memberships into Create/Edit/Delete grants when rolling back. |
|
||||
UPDATE users_organizations |
|
||||
SET access_all = create_new_collections AND edit_any_collection AND delete_any_collection |
|
||||
WHERE atype = 4; |
|
||||
|
|
||||
-- One ALTER, not three. Each `ALTER TABLE` commits implicitly on MySQL/MariaDB, so three statements |
|
||||
-- mean two intermediate states that survive a failure while Diesel still considers the migration |
|
||||
-- unapplied; one statement is the closest this backend gets to all-or-nothing. |
|
||||
ALTER TABLE users_organizations |
|
||||
DROP COLUMN create_new_collections, |
|
||||
DROP COLUMN edit_any_collection, |
|
||||
DROP COLUMN delete_any_collection; |
|
||||
@ -1,68 +0,0 @@ |
|||||
-- The legacy-Manager record has to exist before anything below runs: 2026-06-30-120000 writes it, |
|
||||
-- and the group-derived step at the end of this file reads it. Checked *before* the ALTER TABLE so a |
|
||||
-- refusal leaves no half-added column group behind -- every ALTER commits implicitly here, and a |
|
||||
-- partial group is what the startup preflight then has to recover from. |
|
||||
-- |
|
||||
-- `CREATE TEMPORARY TABLE` / `DROP TEMPORARY TABLE` do not commit implicitly, so this whole check is |
|
||||
-- free of durable side effects. |
|
||||
-- |
|
||||
-- Creating the record here instead would manufacture an empty, apparently valid history for exactly |
|
||||
-- the databases that need an operator to look at them; see 2026-07-23-120000 for the full reasoning. |
|
||||
-- This guard exists for a bare migration runner that never consulted the startup 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 FROM DUAL |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() AND table_name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TEMPORARY TABLE __vw_legacy_manager_record_guard; |
|
||||
|
|
||||
ALTER TABLE users_organizations ADD COLUMN create_new_collections BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
|
|
||||
-- Before these permissions were persisted independently, access_all represented the legacy |
|
||||
-- "Manage all collections" checkbox. Preserve that capability for existing Custom members. |
|
||||
-- |
|
||||
-- Driven by the stored value rather than by the membership's shape, so it needs no provenance: a |
|
||||
-- member carrying access_all held exactly this capability, whenever the row was created. |
|
||||
UPDATE users_organizations |
|
||||
SET create_new_collections = access_all, |
|
||||
edit_any_collection = access_all, |
|
||||
delete_any_collection = access_all |
|
||||
WHERE atype = 4; |
|
||||
|
|
||||
-- A legacy Manager also managed every collection when one of their groups had access_all, even if |
|
||||
-- the membership itself did not. Preserve that existing edit/delete capability without granting |
|
||||
-- collection creation, which historically still required membership access_all. |
|
||||
-- |
|
||||
-- Restricted to memberships recorded as legacy Managers, exactly like 2026-07-23-120000 and |
|
||||
-- 2026-08-09-120000. Role and group membership alone are *not* evidence of legacy authority: |
|
||||
-- "Custom, member of an access_all group" is also the shape of every modern Custom member who was |
|
||||
-- simply put into an ordinary access_all group, and granting on that shape hands them |
|
||||
-- organization-wide collection edit and delete -- which, through edit_any_collection, also satisfies |
|
||||
-- has_full_access() and therefore reaches every cipher in the organization. |
|
||||
-- |
|
||||
-- On the normal upgrade path this changes nothing: 2026-06-30-120000 runs first and records every |
|
||||
-- `atype = 3` row, which at this point is every Custom member there is. |
|
||||
UPDATE users_organizations |
|
||||
SET edit_any_collection = TRUE, |
|
||||
delete_any_collection = TRUE |
|
||||
WHERE atype = 4 |
|
||||
AND uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager) |
|
||||
AND EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM groups_users |
|
||||
-- `groups` is a reserved word in MySQL 8 and must be quoted, matching the existing |
|
||||
-- `2022-07-27-110000_add_group_support` migration. (PostgreSQL/SQLite do not reserve it.) |
|
||||
INNER JOIN `groups` ON `groups`.uuid = groups_users.groups_uuid |
|
||||
WHERE groups_users.users_organizations_uuid = users_organizations.uuid |
|
||||
AND `groups`.organizations_uuid = users_organizations.org_uuid |
|
||||
AND `groups`.access_all = TRUE |
|
||||
); |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- This is an idempotent data repair, and it creates no rows: reverting it must not remove permissions |
|
||||
-- or recreate the invalid persisted Manager type. The older-schema migration performs its own safe |
|
||||
-- conversion. |
|
||||
SELECT 1; |
|
||||
@ -1,101 +0,0 @@ |
|||||
-- 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 TEMPORARY 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. Refusing also keeps this file free of DDL, which on MySQL/MariaDB would |
|
||||
-- commit implicitly and break this migration out of its transaction. |
|
||||
-- |
|
||||
-- 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 FROM DUAL |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() AND table_name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TEMPORARY 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 IGNORE INTO __vw_custom_role_legacy_manager (users_organizations_uuid) |
|
||||
SELECT uuid FROM users_organizations WHERE atype = 3; |
|
||||
|
|
||||
-- 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 only the marker row as transactional DML. Keeping the empty bookkeeping table avoids |
|
||||
-- MySQL DDL implicit commits, so the permission repair, marker clear, and Diesel ledger insert |
|
||||
-- either commit together or are all retried. |
|
||||
DELETE FROM __vw_custom_role_same_run_0716 WHERE marker = 1; |
|
||||
@ -1,13 +0,0 @@ |
|||||
-- Recreate the column and repopulate it from the role/permission model that replaced it, restoring |
|
||||
-- the invariant the immediately preceding schema relies on: access_all == access to every collection. |
|
||||
-- That is exactly Owners/Admins, plus Custom members holding `edit_any_collection`. |
|
||||
-- |
|
||||
-- NOTE: this only holds for reverting *this* migration. Reverting further down the chain, |
|
||||
-- 2026-07-16 deliberately recomputes access_all as (create AND edit AND delete) for Custom members, |
|
||||
-- because in that older schema access_all also meant the legacy Manager "Manage all collections" |
|
||||
-- authority -- so a member who only held `edit_any_collection` comes out as a Manager *without* |
|
||||
-- access_all rather than silently gaining collection deletion. That is intentional and fail-closed; |
|
||||
-- the full rollback is blocked by 2026-07-24-140000/down.sql anyway. |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_all BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
UPDATE users_organizations SET access_all = TRUE WHERE atype IN (0, 1); |
|
||||
UPDATE users_organizations SET access_all = TRUE WHERE atype = 4 AND edit_any_collection = TRUE; |
|
||||
@ -1,5 +0,0 @@ |
|||||
-- The membership `access_all` flag was Vaultwarden's pre-permissions patch for "this member can |
|
||||
-- reach every collection". It is now fully represented by the role model: Owners/Admins hold it |
|
||||
-- implicitly, and a Custom member holds it via `edit_any_collection`. Drop the redundant column. |
|
||||
-- This only concerns users_organizations; groups.access_all is a separate, still-supported feature. |
|
||||
ALTER TABLE users_organizations DROP COLUMN access_all; |
|
||||
@ -1,28 +0,0 @@ |
|||||
-- Lossy revert: this removes the three Custom access permissions, which the legacy schema cannot |
|
||||
-- represent at all. The revert therefore |
|
||||
-- requires the same acknowledgement as 2026-07-24-140000/down.sql -- which only announces the loss, |
|
||||
-- it does not authorize it. Create the marker table while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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 TEMPORARY TABLE`, not `DROP TABLE`: the latter is one more statement that commits |
|
||||
-- implicitly on MySQL/MariaDB, and it would happily drop a permanent table of the same name. |
|
||||
DROP TEMPORARY TABLE __vw_custom_role_downgrade_guard; |
|
||||
|
|
||||
-- One ALTER, not three. Each `ALTER TABLE` commits implicitly on MySQL/MariaDB, so three statements |
|
||||
-- mean two intermediate states that survive a failure while Diesel still considers the migration |
|
||||
-- unapplied; one statement is the closest this backend gets to all-or-nothing. |
|
||||
ALTER TABLE users_organizations |
|
||||
DROP COLUMN access_event_logs, |
|
||||
DROP COLUMN access_import_export, |
|
||||
DROP COLUMN access_reports; |
|
||||
@ -1,5 +0,0 @@ |
|||||
-- Three additional Bitwarden Custom-role permissions. They are only meaningful for Custom members |
|
||||
-- (gated on the role in code); Owners/Admins hold every permission implicitly. |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_event_logs BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_import_export BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_reports BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
@ -1,60 +0,0 @@ |
|||||
-- Downgrade guard. Reverting this migration destroys Custom-role permission data that the legacy |
|
||||
-- role/access_all schema cannot represent, so it only runs with an explicit acknowledgement. Create |
|
||||
-- the marker table below while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The acknowledgement stays valid for the rest of the revert chain and is consumed by the oldest |
|
||||
-- lossy migration (2026-06-30-120000), so one decision covers one downgrade -- and a re-upgrade |
|
||||
-- clears it again (2026-07-24-140000/up.sql), so consent is never inherited. |
|
||||
-- |
|
||||
-- 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 ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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 TEMPORARY TABLE`, not `DROP TABLE`: the latter is one more statement that commits |
|
||||
-- implicitly on MySQL/MariaDB, and it would happily drop a permanent table of the same name. |
|
||||
DROP TEMPORARY TABLE __vw_custom_role_downgrade_guard; |
|
||||
|
|
||||
-- Second, MySQL/MariaDB-only guard: this revert chain cannot be resumed here. |
|
||||
-- |
|
||||
-- Every `ALTER TABLE` in it commits on its own, while Diesel deletes the ledger row in a separate |
|
||||
-- statement afterwards. A crash in between leaves the columns gone and the migration still recorded |
|
||||
-- as applied, and re-running it fails forever with `Unknown column` (1091) -- the startup preflight |
|
||||
-- then refuses the database, correctly, and the only way out is the backup. Making it resumable |
|
||||
-- needs conditional DDL, i.e. a stored procedure built before the checks have run; the standalone |
|
||||
-- script in tools/custom_role_rollback/mysql.sql does the whole downgrade in one audited pass |
|
||||
-- instead, and is what operators should use. |
|
||||
-- |
|
||||
-- So this is supported for development checkouts only, and it says so. Acknowledge separately from |
|
||||
-- the data-loss marker above -- that one is about what a downgrade discards, this one is about what |
|
||||
-- an interrupted downgrade cannot repair: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_unresumable_mysql_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The duplicate key aborts the revert. It is only inserted while the acknowledgement is absent. |
|
||||
CREATE TEMPORARY TABLE __vw_mysql_resume_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_mysql_resume_guard (blocked) VALUES (1); |
|
||||
INSERT INTO __vw_mysql_resume_guard (blocked) |
|
||||
SELECT 1 FROM DUAL |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() AND table_name = '__vw_allow_unresumable_mysql_downgrade' |
|
||||
); |
|
||||
DROP TEMPORARY TABLE __vw_mysql_resume_guard; |
|
||||
|
|
||||
-- Nothing else to undo: the acknowledgement deliberately survives this step. It has to still be here |
|
||||
-- when the next revert removes the first permission column, which is what this guard exists to |
|
||||
-- announce -- checking and dropping it in the same step would leave every following lossy revert |
|
||||
-- unguarded. |
|
||||
SELECT 1; |
|
||||
@ -1,13 +0,0 @@ |
|||||
-- Forward migration marker: its down migration intentionally blocks an automatic lossy downgrade |
|
||||
-- before any granular permission column is removed. |
|
||||
-- |
|
||||
-- It also cleans up after 2026-07-15: the same-run bookkeeping table has served its purpose by now |
|
||||
-- (2026-07-23 consumed the marker), so it is not left behind in every database. A single DDL |
|
||||
-- statement is safe even on MySQL, where DDL commits implicitly -- re-running it is a no-op. |
|
||||
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
|
||||
|
|
||||
-- Also clear a downgrade acknowledgement left over from an earlier revert, so consent is |
|
||||
-- never inherited across an upgrade. Both of them: this backend's revert chain needs a second one, |
|
||||
-- acknowledging that it cannot be resumed after a crash between a committed ALTER and the ledger. |
|
||||
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
|
||||
DROP TABLE IF EXISTS __vw_allow_unresumable_mysql_downgrade; |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- Nothing to undo: this migration only re-applies permissions that 2026-07-23-120000 also sets, and |
|
||||
-- the original values are not recoverable. The permission columns themselves are removed further down |
|
||||
-- the chain by 2026-07-16-120000/down.sql, which is guarded. |
|
||||
SELECT 1; |
|
||||
@ -1,111 +0,0 @@ |
|||||
-- Follow-up repair for databases that already recorded 2026-07-23-120000. |
|
||||
-- |
|
||||
-- That migration originally *removed* the direct 0/1/1 collection permissions of a legacy Manager |
|
||||
-- whose authority came from an organization-local `access_all` group, because the runtime derived the |
|
||||
-- authority from the group instead. Deriving it turned out to be unsound -- "Custom, none of the three |
|
||||
-- collection permissions, member of such a group" is also the shape of every newly created flagless |
|
||||
-- Custom member -- so the runtime fallback is gone and 2026-07-23-120000 now materializes the |
|
||||
-- authority into the permission columns. |
|
||||
-- |
|
||||
-- Rewriting that file is not enough on its own: a database whose ledger already carries |
|
||||
-- 20260723120000 never runs it again, and would silently lose the capability. Repeat the |
|
||||
-- materialization here, in its own version, so both paths converge on the same state. |
|
||||
-- |
|
||||
-- Unlike an earlier revision of this file, the repair is driven by the legacy-Manager record written |
|
||||
-- by 2026-06-30-120000 rather than by role and group membership alone. Those two are the same shape, |
|
||||
-- so matching on them blanket-granted organization-wide collection edit and delete to modern Custom |
|
||||
-- members -- turning Create-only into Create+Edit+Delete, Edit-only into Edit+Delete, and a flagless |
|
||||
-- Custom into Edit+Delete, the last of which also implies `has_full_access()`. |
|
||||
-- |
|
||||
-- What this materialization *means* -- a group-bound capability becoming a permanent membership |
|
||||
-- permission -- is confirmed by an owner in 2026-08-10-120000, which runs immediately after it. |
|
||||
-- |
|
||||
-- Idempotent: on a database that ran the rewritten 2026-07-23-120000 every affected row already |
|
||||
-- holds these values. It only reads `groups` / `groups_users` and the record table and writes the two |
|
||||
-- permission columns, so it is also safe after `access_all` has been dropped. |
|
||||
-- |
|
||||
-- Deliberately not `create_new_collections`: collection creation historically required |
|
||||
-- membership-level `access_all`. |
|
||||
-- |
|
||||
-- Every statement here is DML or TEMPORARY-table bookkeeping, so nothing commits implicitly and the |
|
||||
-- repair either lands with the ledger insert or not at all. |
|
||||
|
|
||||
-- The legacy-Manager record has to exist already; see 2026-07-23-120000 for why this refuses rather |
|
||||
-- than creating it. |
|
||||
-- |
|
||||
-- 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 FROM DUAL |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() AND table_name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TEMPORARY TABLE __vw_legacy_manager_record_guard; |
|
||||
|
|
||||
-- Fail closed on a database whose legacy provenance was never recorded. |
|
||||
-- |
|
||||
-- If a Custom member sits in an organization-local `access_all` group but is not on record as a |
|
||||
-- legacy Manager, one of two things is true and this file cannot tell them apart: either the |
|
||||
-- membership really is a converted legacy Manager whose record was never written (a ledger from an |
|
||||
-- earlier revision of this feature branch), or it is an ordinary modern Custom member who must not |
|
||||
-- gain anything. Granting is a silent privilege escalation; skipping silently drops a real |
|
||||
-- capability. |
|
||||
-- |
|
||||
-- `__vw_custom_role_history_verified` settles it: 2026-06-30-120000 creates it, and an operator |
|
||||
-- creates it after auditing an older history, so its presence means the unrecorded memberships below |
|
||||
-- are unrecorded *on purpose*. Its absence means nobody has looked, and this stops. The startup |
|
||||
-- preflight refuses that state before any migration runs; this guard is the backstop for a bare |
|
||||
-- migration runner. `src/db/mod.rs` prints the full recovery, which lists these memberships: |
|
||||
-- |
|
||||
-- SELECT uo.uuid, uo.org_uuid, uo.status, |
|
||||
-- uo.create_new_collections, uo.edit_any_collection, uo.delete_any_collection |
|
||||
-- FROM users_organizations uo |
|
||||
-- INNER JOIN groups_users gu ON gu.users_organizations_uuid = uo.uuid |
|
||||
-- INNER JOIN `groups` g ON g.uuid = gu.groups_uuid AND g.organizations_uuid = uo.org_uuid |
|
||||
-- WHERE uo.atype = 4 AND g.access_all = 1 |
|
||||
-- AND uo.uuid NOT IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager); |
|
||||
-- |
|
||||
-- The marker never grants anything by itself: the update below is always driven by the record table, |
|
||||
-- so an unrecorded membership keeps exactly the permissions it has. |
|
||||
CREATE TEMPORARY TABLE __vw_legacy_group_authority_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_legacy_group_authority_guard (blocked) VALUES (1); |
|
||||
INSERT INTO __vw_legacy_group_authority_guard (blocked) |
|
||||
SELECT 1 |
|
||||
FROM users_organizations AS uo |
|
||||
WHERE uo.atype = 4 |
|
||||
AND uo.uuid NOT 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 = uo.uuid |
|
||||
AND g.organizations_uuid = uo.org_uuid |
|
||||
AND g.access_all = TRUE |
|
||||
) |
|
||||
AND NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() |
|
||||
AND table_name = '__vw_custom_role_history_verified' |
|
||||
) |
|
||||
LIMIT 1; |
|
||||
DROP TEMPORARY TABLE __vw_legacy_group_authority_guard; |
|
||||
|
|
||||
UPDATE users_organizations |
|
||||
SET edit_any_collection = TRUE, |
|
||||
delete_any_collection = TRUE |
|
||||
WHERE atype = 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 |
|
||||
); |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- Nothing to undo: this migration only asks for a decision, it never writes permissions. The |
|
||||
-- acknowledgement it consumes is deliberately not recreated -- a revert is not consent, and the next |
|
||||
-- upgrade has to ask again. |
|
||||
SELECT 1; |
|
||||
@ -1,121 +0,0 @@ |
|||||
-- Make the one semantic change this feature cannot express an owner's decision instead of a default. |
|
||||
-- |
|
||||
-- Before the Custom role, a Manager who reached every collection through an organization-local group |
|
||||
-- with `access_all` held that authority *while* the group relationship lasted. It ended when the |
|
||||
-- group was deleted, when its `accessAll` was switched off, when the member left it, and it was inert |
|
||||
-- whenever `ORG_GROUPS_ENABLED` was false. Nothing in the new model expresses a permission bound to a |
|
||||
-- group like that: `edit_any_collection` and `delete_any_collection` live on the membership. |
|
||||
-- |
|
||||
-- So the earlier migrations in this chain write the authority onto the membership, and the result is |
|
||||
-- deliberately not identical to what it replaces: |
|
||||
-- |
|
||||
-- * it no longer lapses when the last qualifying group disappears, or when `accessAll` is cleared; |
|
||||
-- * it applies even with the groups feature switched off; |
|
||||
-- * `edit_any_collection` additionally satisfies `has_full_access()`, so the member reaches every |
|
||||
-- collection of the organization directly rather than through the group. |
|
||||
-- |
|
||||
-- Materializing it silently would be a migration that grants durable organization-wide collection |
|
||||
-- edit and delete on its own authority. Dropping it silently would take a capability away. Neither is |
|
||||
-- ours to choose, so this migration stops and hands the decision to an owner. It grants nothing and |
|
||||
-- revokes nothing itself. |
|
||||
-- |
|
||||
-- On a database with no Custom membership that both has edit/delete authority and belongs to an |
|
||||
-- organization-local `access_all` group, there is nothing to decide and this is a no-op. |
|
||||
-- |
|
||||
-- Vaultwarden's startup preflight looks ahead for exactly the condition below and refuses with the |
|
||||
-- full text (`RefuseUnconfirmedPermanentCollectionAuthority` in `src/db/mod.rs`), from the legacy |
|
||||
-- schema as well, so an operator normally never reaches the abort here. Diesel reports only the |
|
||||
-- driver error, so on this path the question would arrive as `Duplicate entry '1' for key 'PRIMARY'` |
|
||||
-- and nothing else. Keep the two predicates identical. |
|
||||
-- |
|
||||
-- Review the affected memberships: |
|
||||
-- |
|
||||
-- SELECT uo.uuid, uo.user_uuid, uo.org_uuid, uo.status, |
|
||||
-- uo.create_new_collections, uo.edit_any_collection, uo.delete_any_collection, |
|
||||
-- (uo.uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager)) |
|
||||
-- AS was_legacy_manager |
|
||||
-- FROM users_organizations uo |
|
||||
-- WHERE uo.atype = 4 |
|
||||
-- AND (uo.edit_any_collection = 1 OR uo.delete_any_collection = 1) |
|
||||
-- AND EXISTS ( |
|
||||
-- SELECT 1 FROM groups_users gu |
|
||||
-- INNER JOIN `groups` g ON g.uuid = gu.groups_uuid |
|
||||
-- WHERE gu.users_organizations_uuid = uo.uuid |
|
||||
-- AND g.organizations_uuid = uo.org_uuid |
|
||||
-- AND g.access_all = 1); |
|
||||
-- |
|
||||
-- Reading the result: |
|
||||
-- |
|
||||
-- * `was_legacy_manager = 1` -- a converted Manager. Review it even when |
|
||||
-- `create_new_collections = 1`: that independent permission can be changed after an earlier |
|
||||
-- revision materialized group-derived edit/delete, so its current value cannot prove where those |
|
||||
-- two permissions came from. A membership whose own legacy `access_all` supplied all three may |
|
||||
-- therefore be listed conservatively even though its authority was already permanent. |
|
||||
-- * `was_legacy_manager = 0` -- never a Manager. On a database first upgraded by revision bf54088c |
|
||||
-- they may carry permissions that revision's 2026-08-09-120000 granted in bulk, which nothing can |
|
||||
-- distinguish from a deliberate grant any more -- check them against what you intended. |
|
||||
-- |
|
||||
-- An invited or revoked membership is listed too, and deliberately so. It holds no authority today -- |
|
||||
-- every guard requires a confirmed membership, and `MembershipStatus::from_i32` rejects the revoked |
|
||||
-- value outright -- but the permission is what it would come back with if it is ever restored, and |
|
||||
-- by then the group it came from may be gone. Status is therefore not part of the predicate. |
|
||||
-- |
|
||||
-- Clear whatever you do not want to keep, for example: |
|
||||
-- |
|
||||
-- UPDATE users_organizations |
|
||||
-- SET edit_any_collection = 0, delete_any_collection = 0 |
|
||||
-- WHERE uuid = '<MEMBERSHIP_UUID>'; |
|
||||
-- |
|
||||
-- Then record the decision once, with every Vaultwarden instance stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_ack_permanent_collection_authority (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The acknowledgement is consumed at the end of this file, so one decision covers one upgrade. |
|
||||
-- |
|
||||
-- The legacy-Manager record has to exist already: the chain and supported rollback use it as the |
|
||||
-- immutable role-provenance record. Refuse a damaged history here too; see 2026-07-23-120000 for why |
|
||||
-- this never creates it. |
|
||||
-- |
|
||||
-- `CREATE TEMPORARY TABLE` / `DROP TEMPORARY TABLE` do not commit implicitly, so this check is free |
|
||||
-- of durable side effects. |
|
||||
-- |
|
||||
-- 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 FROM DUAL |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() AND table_name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TEMPORARY TABLE __vw_legacy_manager_record_guard; |
|
||||
|
|
||||
-- The duplicate key aborts the migration. It is only inserted while an unconfirmed membership exists. |
|
||||
CREATE TEMPORARY TABLE __vw_permanent_authority_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_permanent_authority_guard (blocked) VALUES (1); |
|
||||
INSERT INTO __vw_permanent_authority_guard (blocked) |
|
||||
SELECT 1 |
|
||||
FROM users_organizations AS uo |
|
||||
WHERE uo.atype = 4 |
|
||||
AND (uo.edit_any_collection = TRUE OR uo.delete_any_collection = TRUE) |
|
||||
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 = uo.uuid |
|
||||
AND g.organizations_uuid = uo.org_uuid |
|
||||
AND g.access_all = TRUE |
|
||||
) |
|
||||
AND NOT EXISTS ( |
|
||||
SELECT 1 FROM information_schema.tables |
|
||||
WHERE table_schema = DATABASE() |
|
||||
AND table_name = '__vw_ack_permanent_collection_authority' |
|
||||
) |
|
||||
LIMIT 1; |
|
||||
DROP TEMPORARY TABLE __vw_permanent_authority_guard; |
|
||||
|
|
||||
DROP TABLE IF EXISTS __vw_ack_permanent_collection_authority; |
|
||||
@ -1,38 +1,122 @@ |
|||||
ALTER TABLE users_organizations ADD COLUMN manage_users BOOLEAN NOT NULL DEFAULT FALSE; |
-- Replace the membership-level `access_all` flag with the persisted Custom role and its nine |
||||
ALTER TABLE users_organizations ADD COLUMN manage_groups BOOLEAN NOT NULL DEFAULT FALSE; |
-- granular permissions. |
||||
ALTER TABLE users_organizations ADD COLUMN manage_policies BOOLEAN NOT NULL DEFAULT FALSE; |
-- |
||||
-- Record which memberships were legacy Managers *before* anything converts them. |
-- Two different columns are called `access_all`, and everything below depends on keeping them apart: |
||||
-- |
-- |
||||
-- This is the only moment at which that is knowable. `atype = 3` means Manager here and Custom |
-- * `users_organizations.access_all` -- the MEMBERSHIP-level bit this migration replaces. Dropped |
||||
-- afterwards -- the conversion below reuses the value -- so once it has run, a genuine legacy |
-- at the end of this file. |
||||
-- Manager and a Custom member created later are byte-identical. Every later step that has to reason |
-- * `groups.access_all` -- the GROUP-level flag, a separate and still-supported feature. Only read |
||||
-- about legacy authority (2026-07-23, 2026-08-09 and tools/custom_role_rollback/) reads this table |
-- here, to decide a legacy Manager's permissions; never written, and it keeps granting group |
||||
-- instead of guessing, which is what stops them from handing legacy privileges to modern members. |
-- members access to every collection afterwards exactly as before. |
||||
-- |
-- |
||||
-- Deliberately not a Diesel model and not in schema.rs: no runtime code reads it. It is |
-- Base `Collection::is_coll_manageable_by_user` accepts either, so a Manager reached every collection |
||||
-- migration/rollback bookkeeping, and it carries no foreign key so that 2026-07-24-120000's table |
-- through either. Only the membership bit is going away, but the capability an owner configured |
||||
-- rebuild does not have to care about it. |
-- through either route is preserved, so both are read below. While this file runs the membership |
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_legacy_manager ( |
-- column still exists and `atype = 3` still unambiguously means "legacy Manager". |
||||
users_organizations_uuid CHAR(36) NOT NULL PRIMARY KEY |
-- |
||||
); |
-- One state cannot be converted and is refused before the first mutation; `src/db/mod.rs` evaluates |
||||
INSERT INTO __vw_custom_role_legacy_manager (users_organizations_uuid) |
-- the same condition at startup and prints the recovery text, because Diesel would surface the abort |
||||
SELECT uuid FROM users_organizations WHERE atype = 3 |
-- below as nothing but a driver-level duplicate-key error. |
||||
ON CONFLICT DO NOTHING; |
|
||||
|
|
||||
-- Separately, mark that this database's Custom-role history is accounted for -- it was produced by |
-- A plain User carrying membership `access_all`, reachable only on databases written before the web |
||||
-- the migrations that ship today. Nothing else creates this table, which is what lets the startup |
-- vault stopped sending the flag. The bit gave read/write reach over every collection, present and |
||||
-- preflight treat its absence as proof that an earlier revision of this chain ran instead. |
-- future, with no management authority, and the new model has no permission for that: |
||||
-- |
-- `edit_any_collection` would add management authority, dropping the bit would take the reach away. |
||||
-- Deliberately not the record table above: that one holds data an operator has to be able to write |
-- Refuse and let an owner choose. The duplicate key aborts the migration, and is only inserted when |
||||
-- during recovery, so its existence cannot also stand for "the history behind this data was |
-- such a membership exists. |
||||
-- reviewed" -- creating it empty to silence an error would otherwise pass as the audit it asks for. |
CREATE TEMPORARY TABLE __vw_legacy_user_access_all_guard ( |
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_history_verified ( |
blocked INTEGER NOT NULL PRIMARY KEY |
||||
verified 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; |
||||
|
|
||||
|
ALTER TABLE users_organizations |
||||
|
ADD COLUMN manage_users BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN manage_groups BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN manage_policies BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN create_new_collections BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN access_event_logs BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN access_import_export BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
ADD COLUMN access_reports BOOLEAN NOT NULL DEFAULT FALSE; |
||||
|
|
||||
|
-- Owners and Admins are not touched: they carried `access_all` implicitly and the new model gives |
||||
|
-- them every permission by role. A plain User cannot reach this point carrying the bit (the guard |
||||
|
-- above), so only a Manager becomes Custom, keeping the organization-wide collection-management |
||||
|
-- capability it is configured with right now: |
||||
|
-- |
||||
|
-- * membership `access_all` -- the "Manage all collections" checkbox -- covered all three |
||||
|
-- collection permissions, including creating collections; |
||||
|
-- * an organization-local `access_all` group covered editing and deleting every collection, but |
||||
|
-- never creation -- that always required the membership bit; |
||||
|
-- * a Manager with neither keeps all three at FALSE. |
||||
|
-- |
||||
|
-- The second case is a deliberate policy choice. That capability was dynamic: it ended with the |
||||
|
-- group, with the group's own `access_all`, and with the member leaving it. It was never gated on |
||||
|
-- ORG_GROUPS_ENABLED -- `Collection::is_coll_manageable_by_user` reads `groups.access_all` in SQL |
||||
|
-- with no configuration check -- so it applied even where groups were never enabled. Nothing in the |
||||
|
-- new model is bound to a group, so it becomes a membership permission and no longer lapses on its |
||||
|
-- own. The alternative is silently revoking access these members have today, or refusing an ordinary |
||||
|
-- upgrade; the permission is visible in the member's permission list and an owner can clear it. |
||||
|
-- |
||||
|
-- The management (manage_users / manage_groups / manage_policies) and access (event logs / |
||||
|
-- import-export / reports) permissions keep their FALSE default. Nothing they unlock was a Manager |
||||
|
-- capability -- every member mutation, every policy write, the organization export and both |
||||
|
-- event-log routes were gated on Admin/Owner -- so granting one here would be a new privilege. |
||||
|
-- |
||||
|
-- One read is not carried over, and only for members who held the MEMBERSHIP bit: `has_full_access()` |
||||
|
-- read `self.access_all` and the role, never `groups.access_all`, so it gated the full member list |
||||
|
-- (`GET /organizations/<org>/users`) for them and for nobody whose reach came from a group. |
||||
|
-- `manage_users` is not granted to restore it, because it also carries invite, confirm, revoke, |
||||
|
-- restore and delete, which the Manager role never had; such members keep `/users/mini-details`, and |
||||
|
-- an owner can grant `manage_users` deliberately. In the other direction `edit_any_collection` |
||||
|
-- satisfies `has_full_access()`, which opens the organization collection list and |
||||
|
-- `GET /ciphers/organization-details` to the group-derived class -- data they could already reach |
||||
|
-- through the group, so only the route is new. |
||||
|
-- |
||||
|
-- Role conversion and permission values are one statement, so `atype = 3` unambiguously still means |
||||
|
-- Manager everywhere it is read. |
||||
|
-- |
||||
|
-- Status is deliberately not part of the predicate: an invited, accepted or revoked membership is |
||||
|
-- converted like a confirmed one, since none holds authority in that state and the permissions are |
||||
|
-- what it would come back with -- the same thing `access_all` would have done. |
||||
|
-- |
||||
|
-- The group lookup is bound to the membership's own organization: a `groups_users` row pointing at |
||||
|
-- another organization's `access_all` group conveys nothing, exactly as it conveys nothing today. |
||||
|
UPDATE users_organizations |
||||
|
SET create_new_collections = access_all, |
||||
|
edit_any_collection = access_all |
||||
|
OR 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 |
||||
|
), |
||||
|
delete_any_collection = access_all |
||||
|
OR 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 |
||||
|
), |
||||
|
atype = 4 |
||||
|
WHERE atype = 3; |
||||
|
|
||||
|
-- The flag is now fully represented by the role model: Owners/Admins hold it implicitly, a Custom |
||||
|
-- member holds it through `edit_any_collection`. Drop the redundant column. This only concerns |
||||
|
-- users_organizations; `groups.access_all` stays. |
||||
|
ALTER TABLE users_organizations DROP COLUMN access_all; |
||||
|
|
||||
-- Previously the server stored members created with the Custom role as Manager (3) and |
-- Never inherit a downgrade acknowledgement left behind by an earlier revert. |
||||
-- masqueraded them as Custom (4) in all API responses. Now that Custom is a real, persisted |
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
||||
-- type, convert those members so clients (which no longer know the Manager role) keep |
|
||||
-- seeing exactly what they saw before. access_all is preserved; the new flags stay FALSE, |
|
||||
-- which matches the capabilities these members had. |
|
||||
UPDATE users_organizations SET atype = 4 WHERE atype = 3; |
|
||||
|
|||||
@ -1 +0,0 @@ |
|||||
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
|
||||
@ -1,13 +0,0 @@ |
|||||
-- Record whether 2026-07-16 is about to run in this migration sequence. The durable marker lets a |
|
||||
-- retry distinguish its deterministic group-derived 0/1/1 backfill from older, ambiguous data. |
|
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_same_run_0716 ( |
|
||||
marker INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_custom_role_same_run_0716 (marker) |
|
||||
SELECT 1 |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM __diesel_schema_migrations |
|
||||
WHERE version = '20260716120000' |
|
||||
) |
|
||||
ON CONFLICT (marker) DO NOTHING; |
|
||||
@ -1,25 +0,0 @@ |
|||||
-- Lossy revert: this removes the three independent Custom collection permissions, which the legacy |
|
||||
-- role/access_all schema cannot represent -- it only knows all three together. The revert therefore |
|
||||
-- requires the same acknowledgement as 2026-07-24-140000/down.sql -- which only announces the loss, |
|
||||
-- it does not authorize it. Create the marker table while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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; |
|
||||
|
|
||||
-- The previous schema exposes access_all as the three collection permissions together. Avoid |
|
||||
-- turning Edit-only memberships into Create/Edit/Delete grants when rolling back. |
|
||||
UPDATE users_organizations |
|
||||
SET access_all = create_new_collections AND edit_any_collection AND delete_any_collection |
|
||||
WHERE atype = 4; |
|
||||
|
|
||||
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; |
|
||||
@ -1,60 +0,0 @@ |
|||||
-- The legacy-Manager record has to exist before anything below runs: 2026-06-30-120000 writes it, |
|
||||
-- and the group-derived step at the end of this file reads it. Checked *before* the ALTER TABLE statements so |
|
||||
-- the refusal is symmetrical with the other backends -- PostgreSQL DDL is transactional, so nothing |
|
||||
-- would be left behind either way. |
|
||||
-- |
|
||||
-- Creating the record here instead would manufacture an empty, apparently valid history for exactly |
|
||||
-- the databases that need an operator to look at them; see 2026-07-23-120000 for the full reasoning. |
|
||||
-- This guard exists for a bare migration runner that never consulted the startup 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; |
|
||||
|
|
||||
ALTER TABLE users_organizations ADD COLUMN create_new_collections BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
|
|
||||
-- Before these permissions were persisted independently, access_all represented the legacy |
|
||||
-- "Manage all collections" checkbox. Preserve that capability for existing Custom members. |
|
||||
-- |
|
||||
-- Driven by the stored value rather than by the membership's shape, so it needs no provenance: a |
|
||||
-- member carrying access_all held exactly this capability, whenever the row was created. |
|
||||
UPDATE users_organizations |
|
||||
SET create_new_collections = access_all, |
|
||||
edit_any_collection = access_all, |
|
||||
delete_any_collection = access_all |
|
||||
WHERE atype = 4; |
|
||||
|
|
||||
-- A legacy Manager also managed every collection when one of their groups had access_all, even if |
|
||||
-- the membership itself did not. Preserve that existing edit/delete capability without granting |
|
||||
-- collection creation, which historically still required membership access_all. |
|
||||
-- |
|
||||
-- Restricted to memberships recorded as legacy Managers, exactly like 2026-07-23-120000 and |
|
||||
-- 2026-08-09-120000. Role and group membership alone are *not* evidence of legacy authority: |
|
||||
-- "Custom, member of an access_all group" is also the shape of every modern Custom member who was |
|
||||
-- simply put into an ordinary access_all group, and granting on that shape hands them |
|
||||
-- organization-wide collection edit and delete -- which, through edit_any_collection, also satisfies |
|
||||
-- has_full_access() and therefore reaches every cipher in the organization. |
|
||||
-- |
|
||||
-- On the normal upgrade path this changes nothing: 2026-06-30-120000 runs first and records every |
|
||||
-- `atype = 3` row, which at this point is every Custom member there is. |
|
||||
UPDATE users_organizations |
|
||||
SET edit_any_collection = TRUE, |
|
||||
delete_any_collection = TRUE |
|
||||
WHERE atype = 4 |
|
||||
AND uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager) |
|
||||
AND EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM groups_users |
|
||||
INNER JOIN groups ON groups.uuid = groups_users.groups_uuid |
|
||||
WHERE groups_users.users_organizations_uuid = users_organizations.uuid |
|
||||
AND groups.organizations_uuid = users_organizations.org_uuid |
|
||||
AND groups.access_all = TRUE |
|
||||
); |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- This is an idempotent data repair, and it creates no rows: reverting it must not remove permissions |
|
||||
-- or recreate the invalid persisted Manager type. The older-schema migration performs its own safe |
|
||||
-- conversion. |
|
||||
SELECT 1; |
|
||||
@ -1,96 +0,0 @@ |
|||||
-- 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; |
|
||||
@ -1,13 +0,0 @@ |
|||||
-- Recreate the column and repopulate it from the role/permission model that replaced it, restoring |
|
||||
-- the invariant the immediately preceding schema relies on: access_all == access to every collection. |
|
||||
-- That is exactly Owners/Admins, plus Custom members holding `edit_any_collection`. |
|
||||
-- |
|
||||
-- NOTE: this only holds for reverting *this* migration. Reverting further down the chain, |
|
||||
-- 2026-07-16 deliberately recomputes access_all as (create AND edit AND delete) for Custom members, |
|
||||
-- because in that older schema access_all also meant the legacy Manager "Manage all collections" |
|
||||
-- authority -- so a member who only held `edit_any_collection` comes out as a Manager *without* |
|
||||
-- access_all rather than silently gaining collection deletion. That is intentional and fail-closed; |
|
||||
-- the full rollback is blocked by 2026-07-24-140000/down.sql anyway. |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_all BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
UPDATE users_organizations SET access_all = TRUE WHERE atype IN (0, 1); |
|
||||
UPDATE users_organizations SET access_all = TRUE WHERE atype = 4 AND edit_any_collection = TRUE; |
|
||||
@ -1,5 +0,0 @@ |
|||||
-- The membership `access_all` flag was Vaultwarden's pre-permissions patch for "this member can |
|
||||
-- reach every collection". It is now fully represented by the role model: Owners/Admins hold it |
|
||||
-- implicitly, and a Custom member holds it via `edit_any_collection`. Drop the redundant column. |
|
||||
-- This only concerns users_organizations; groups.access_all is a separate, still-supported feature. |
|
||||
ALTER TABLE users_organizations DROP COLUMN access_all; |
|
||||
@ -1,19 +0,0 @@ |
|||||
-- Lossy revert: this removes the three Custom access permissions, which the legacy schema cannot |
|
||||
-- represent at all. The revert therefore |
|
||||
-- requires the same acknowledgement as 2026-07-24-140000/down.sql -- which only announces the loss, |
|
||||
-- it does not authorize it. Create the marker table while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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; |
|
||||
|
|
||||
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; |
|
||||
@ -1,5 +0,0 @@ |
|||||
-- Three additional Bitwarden Custom-role permissions. They are only meaningful for Custom members |
|
||||
-- (gated on the role in code); Owners/Admins hold every permission implicitly. |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_event_logs BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_import_export BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_reports BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
@ -1,27 +0,0 @@ |
|||||
-- Downgrade guard. Reverting this migration destroys Custom-role permission data that the legacy |
|
||||
-- role/access_all schema cannot represent, so it only runs with an explicit acknowledgement. Create |
|
||||
-- the marker table below while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The acknowledgement stays valid for the rest of the revert chain and is consumed by the oldest |
|
||||
-- lossy migration (2026-06-30-120000), so one decision covers one downgrade -- and a re-upgrade |
|
||||
-- clears it again (2026-07-24-140000/up.sql), so consent is never inherited. |
|
||||
-- |
|
||||
-- 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 ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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; |
|
||||
|
|
||||
-- Nothing else to undo: the acknowledgement deliberately survives this step. It has to still be here |
|
||||
-- when the next revert removes the first permission column, which is what this guard exists to |
|
||||
-- announce -- checking and dropping it in the same step would leave every following lossy revert |
|
||||
-- unguarded. |
|
||||
SELECT 1; |
|
||||
@ -1,11 +0,0 @@ |
|||||
-- Forward migration marker: its down migration intentionally blocks an automatic lossy downgrade |
|
||||
-- before any granular permission column is removed. |
|
||||
-- |
|
||||
-- It also cleans up after 2026-07-15: the same-run bookkeeping table has served its purpose by now |
|
||||
-- (2026-07-23 consumed the marker), so it is not left behind in every database. A single DDL |
|
||||
-- statement is safe even on MySQL, where DDL commits implicitly -- re-running it is a no-op. |
|
||||
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
|
||||
|
|
||||
-- Also clear a downgrade acknowledgement left over from an earlier revert, so consent is |
|
||||
-- never inherited across an upgrade. |
|
||||
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- Nothing to undo: this migration only re-applies permissions that 2026-07-23-120000 also sets, and |
|
||||
-- the original values are not recoverable. The permission columns themselves are removed further down |
|
||||
-- the chain by 2026-07-16-120000/down.sql, which is guarded. |
|
||||
SELECT 1; |
|
||||
@ -1,103 +0,0 @@ |
|||||
-- Follow-up repair for databases that already recorded 2026-07-23-120000. |
|
||||
-- |
|
||||
-- That migration originally *removed* the direct 0/1/1 collection permissions of a legacy Manager |
|
||||
-- whose authority came from an organization-local `access_all` group, because the runtime derived the |
|
||||
-- authority from the group instead. Deriving it turned out to be unsound -- "Custom, none of the three |
|
||||
-- collection permissions, member of such a group" is also the shape of every newly created flagless |
|
||||
-- Custom member -- so the runtime fallback is gone and 2026-07-23-120000 now materializes the |
|
||||
-- authority into the permission columns. |
|
||||
-- |
|
||||
-- Rewriting that file is not enough on its own: a database whose ledger already carries |
|
||||
-- 20260723120000 never runs it again, and would silently lose the capability. Repeat the |
|
||||
-- materialization here, in its own version, so both paths converge on the same state. |
|
||||
-- |
|
||||
-- Unlike an earlier revision of this file, the repair is driven by the legacy-Manager record written |
|
||||
-- by 2026-06-30-120000 rather than by role and group membership alone. Those two are the same shape, |
|
||||
-- so matching on them blanket-granted organization-wide collection edit and delete to modern Custom |
|
||||
-- members -- turning Create-only into Create+Edit+Delete, Edit-only into Edit+Delete, and a flagless |
|
||||
-- Custom into Edit+Delete, the last of which also implies `has_full_access()`. |
|
||||
-- |
|
||||
-- What this materialization *means* -- a group-bound capability becoming a permanent membership |
|
||||
-- permission -- is confirmed by an owner in 2026-08-10-120000, which runs immediately after it. |
|
||||
-- |
|
||||
-- Idempotent: on a database that ran the rewritten 2026-07-23-120000 every affected row already |
|
||||
-- holds these values. It only reads `groups` / `groups_users` and the record table and writes the two |
|
||||
-- permission columns, so it is also safe after `access_all` has been dropped. |
|
||||
-- |
|
||||
-- Deliberately not `create_new_collections`: collection creation historically required |
|
||||
-- membership-level `access_all`. |
|
||||
DO $$ |
|
||||
DECLARE |
|
||||
undecidable int := 0; |
|
||||
BEGIN |
|
||||
-- The legacy-Manager record has to exist already; see 2026-07-23-120000 for why this refuses |
|
||||
-- rather than creating it. |
|
||||
IF to_regclass('__vw_custom_role_legacy_manager') IS NULL THEN |
|
||||
RAISE EXCEPTION |
|
||||
'Upgrade refused, nothing was changed: __vw_custom_role_legacy_manager does not exist, ' |
|
||||
'so which memberships were legacy Managers before the upgrade is unknown. Start ' |
|
||||
'Vaultwarden once to get the full recovery instructions, or see ' |
|
||||
'tools/custom_role_rollback/README.md.'; |
|
||||
END IF; |
|
||||
|
|
||||
-- Fail closed on a database whose legacy provenance was never recorded. |
|
||||
-- |
|
||||
-- If a Custom member sits in an organization-local `access_all` group but is not on record as a |
|
||||
-- legacy Manager, one of two things is true and this file cannot tell them apart: either the |
|
||||
-- membership really is a converted legacy Manager whose record was never written (a ledger from |
|
||||
-- an earlier revision of this feature branch), or it is an ordinary modern Custom member who must |
|
||||
-- not gain anything. Granting is a silent privilege escalation; skipping silently drops a real |
|
||||
-- capability. |
|
||||
-- |
|
||||
-- `__vw_custom_role_history_verified` settles it: 2026-06-30-120000 creates it, and an operator |
|
||||
-- creates it after auditing an older history, so its presence means the unrecorded memberships |
|
||||
-- are unrecorded *on purpose*. Its absence means nobody has looked, and this stops. The startup |
|
||||
-- preflight refuses that state before any migration runs; this is the backstop for a bare |
|
||||
-- migration runner. |
|
||||
-- |
|
||||
-- The marker never grants anything by itself: the update below is always driven by the record |
|
||||
-- table, so an unrecorded membership keeps exactly the permissions it has. |
|
||||
IF to_regclass('__vw_custom_role_history_verified') IS NULL THEN |
|
||||
SELECT count(*) INTO undecidable |
|
||||
FROM users_organizations uo |
|
||||
WHERE uo.atype = 4 |
|
||||
AND uo.uuid NOT IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager) |
|
||||
AND EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM groups_users gu |
|
||||
INNER JOIN "groups" g ON g.uuid = gu.groups_uuid |
|
||||
WHERE gu.users_organizations_uuid = uo.uuid |
|
||||
AND g.organizations_uuid = uo.org_uuid |
|
||||
AND g.access_all = TRUE |
|
||||
); |
|
||||
END IF; |
|
||||
|
|
||||
IF undecidable <> 0 THEN |
|
||||
RAISE EXCEPTION |
|
||||
'Upgrade refused, nothing was changed: % Custom membership(s) belong to an access_all ' |
|
||||
'group but are not on record as legacy Managers, and this database''s Custom-role ' |
|
||||
'history has never been audited, so a converted legacy Manager cannot be told from an ' |
|
||||
'ordinary Custom member. Review them with: SELECT uo.uuid, uo.org_uuid, uo.status, ' |
|
||||
'uo.create_new_collections, uo.edit_any_collection, uo.delete_any_collection FROM ' |
|
||||
'users_organizations uo JOIN groups_users gu ON gu.users_organizations_uuid = uo.uuid ' |
|
||||
'JOIN "groups" g ON g.uuid = gu.groups_uuid AND g.organizations_uuid = uo.org_uuid ' |
|
||||
'WHERE uo.atype = 4 AND g.access_all AND uo.uuid NOT IN (SELECT ' |
|
||||
'users_organizations_uuid FROM __vw_custom_role_legacy_manager); Start Vaultwarden once ' |
|
||||
'for the full recovery instructions.', |
|
||||
undecidable; |
|
||||
END IF; |
|
||||
END $$; |
|
||||
|
|
||||
UPDATE users_organizations |
|
||||
SET edit_any_collection = TRUE, |
|
||||
delete_any_collection = TRUE |
|
||||
WHERE atype = 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 |
|
||||
); |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- Nothing to undo: this migration only asks for a decision, it never writes permissions. The |
|
||||
-- acknowledgement it consumes is deliberately not recreated -- a revert is not consent, and the next |
|
||||
-- upgrade has to ask again. |
|
||||
SELECT 1; |
|
||||
@ -1,111 +0,0 @@ |
|||||
-- Make the one semantic change this feature cannot express an owner's decision instead of a default. |
|
||||
-- |
|
||||
-- Before the Custom role, a Manager who reached every collection through an organization-local group |
|
||||
-- with `access_all` held that authority *while* the group relationship lasted. It ended when the |
|
||||
-- group was deleted, when its `accessAll` was switched off, when the member left it, and it was inert |
|
||||
-- whenever `ORG_GROUPS_ENABLED` was false. Nothing in the new model expresses a permission bound to a |
|
||||
-- group like that: `edit_any_collection` and `delete_any_collection` live on the membership. |
|
||||
-- |
|
||||
-- So the earlier migrations in this chain write the authority onto the membership, and the result is |
|
||||
-- deliberately not identical to what it replaces: |
|
||||
-- |
|
||||
-- * it no longer lapses when the last qualifying group disappears, or when `accessAll` is cleared; |
|
||||
-- * it applies even with the groups feature switched off; |
|
||||
-- * `edit_any_collection` additionally satisfies `has_full_access()`, so the member reaches every |
|
||||
-- collection of the organization directly rather than through the group. |
|
||||
-- |
|
||||
-- Materializing it silently would be a migration that grants durable organization-wide collection |
|
||||
-- edit and delete on its own authority. Dropping it silently would take a capability away. Neither is |
|
||||
-- ours to choose, so this migration stops and hands the decision to an owner. It grants nothing and |
|
||||
-- revokes nothing itself. |
|
||||
-- |
|
||||
-- On a database with no Custom membership that both has edit/delete authority and belongs to an |
|
||||
-- organization-local `access_all` group, there is nothing to decide and this is a no-op. |
|
||||
-- |
|
||||
-- Vaultwarden's startup preflight looks ahead for exactly the condition below and refuses with the |
|
||||
-- full text (`RefuseUnconfirmedPermanentCollectionAuthority` in `src/db/mod.rs`), from the legacy |
|
||||
-- schema as well, so an operator normally never reaches the abort here. Diesel reports only the |
|
||||
-- driver error, so on this path the question would arrive as a bare duplicate-key violation on |
|
||||
-- `__vw_permanent_authority_guard` and nothing else. Keep the two predicates identical. |
|
||||
-- |
|
||||
-- Review the affected memberships: |
|
||||
-- |
|
||||
-- SELECT uo.uuid, uo.user_uuid, uo.org_uuid, uo.status, |
|
||||
-- uo.create_new_collections, uo.edit_any_collection, uo.delete_any_collection, |
|
||||
-- (uo.uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager)) |
|
||||
-- AS was_legacy_manager |
|
||||
-- FROM users_organizations uo |
|
||||
-- WHERE uo.atype = 4 |
|
||||
-- AND (uo.edit_any_collection OR uo.delete_any_collection) |
|
||||
-- AND EXISTS ( |
|
||||
-- SELECT 1 FROM groups_users gu |
|
||||
-- INNER JOIN "groups" g ON g.uuid = gu.groups_uuid |
|
||||
-- WHERE gu.users_organizations_uuid = uo.uuid |
|
||||
-- AND g.organizations_uuid = uo.org_uuid |
|
||||
-- AND g.access_all); |
|
||||
-- |
|
||||
-- Reading the result: |
|
||||
-- |
|
||||
-- * `was_legacy_manager = t` -- a converted Manager. Review it even when |
|
||||
-- `create_new_collections = t`: that independent permission can be changed after an earlier |
|
||||
-- revision materialized group-derived edit/delete, so its current value cannot prove where those |
|
||||
-- two permissions came from. A membership whose own legacy `access_all` supplied all three may |
|
||||
-- therefore be listed conservatively even though its authority was already permanent. |
|
||||
-- * `was_legacy_manager = f` -- never a Manager. On a database first upgraded by revision bf54088c |
|
||||
-- they may carry permissions that revision's 2026-08-09-120000 granted in bulk, which nothing can |
|
||||
-- distinguish from a deliberate grant any more -- check them against what you intended. |
|
||||
-- |
|
||||
-- An invited or revoked membership is listed too, and deliberately so. It holds no authority today -- |
|
||||
-- every guard requires a confirmed membership, and `MembershipStatus::from_i32` rejects the revoked |
|
||||
-- value outright -- but the permission is what it would come back with if it is ever restored, and |
|
||||
-- by then the group it came from may be gone. Status is therefore not part of the predicate. |
|
||||
-- |
|
||||
-- Clear whatever you do not want to keep, for example: |
|
||||
-- |
|
||||
-- UPDATE users_organizations |
|
||||
-- SET edit_any_collection = FALSE, delete_any_collection = FALSE |
|
||||
-- WHERE uuid = '<MEMBERSHIP_UUID>'; |
|
||||
-- |
|
||||
-- Then record the decision once, with every Vaultwarden instance stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_ack_permanent_collection_authority (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The acknowledgement is consumed at the end of this file, so one decision covers one upgrade. |
|
||||
-- |
|
||||
-- The legacy-Manager record has to exist already: the chain and supported rollback use it as the |
|
||||
-- immutable role-provenance record. Refuse a damaged history here too; see 2026-07-23-120000 for why |
|
||||
-- this never creates it. |
|
||||
-- |
|
||||
-- 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; |
|
||||
|
|
||||
-- The duplicate key aborts the migration. It is only inserted while an unconfirmed membership exists. |
|
||||
CREATE TEMPORARY TABLE __vw_permanent_authority_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_permanent_authority_guard (blocked) VALUES (1); |
|
||||
INSERT INTO __vw_permanent_authority_guard (blocked) |
|
||||
SELECT 1 |
|
||||
FROM users_organizations AS uo |
|
||||
WHERE uo.atype = 4 |
|
||||
AND (uo.edit_any_collection = TRUE OR uo.delete_any_collection = TRUE) |
|
||||
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 = uo.uuid |
|
||||
AND g.organizations_uuid = uo.org_uuid |
|
||||
AND g.access_all = TRUE |
|
||||
) |
|
||||
AND to_regclass('__vw_ack_permanent_collection_authority') IS NULL |
|
||||
LIMIT 1; |
|
||||
DROP TABLE __vw_permanent_authority_guard; |
|
||||
|
|
||||
DROP TABLE IF EXISTS __vw_ack_permanent_collection_authority; |
|
||||
@ -1,37 +1,156 @@ |
|||||
ALTER TABLE users_organizations ADD COLUMN manage_users BOOLEAN NOT NULL DEFAULT FALSE; |
-- Replace the membership-level `access_all` flag with the persisted Custom role and its nine |
||||
ALTER TABLE users_organizations ADD COLUMN manage_groups BOOLEAN NOT NULL DEFAULT FALSE; |
-- granular permissions. |
||||
ALTER TABLE users_organizations ADD COLUMN manage_policies BOOLEAN NOT NULL DEFAULT FALSE; |
-- |
||||
-- Record which memberships were legacy Managers *before* anything converts them. |
-- Two different columns are called `access_all`, and everything below depends on keeping them apart: |
||||
-- |
-- |
||||
-- This is the only moment at which that is knowable. `atype = 3` means Manager here and Custom |
-- * `users_organizations.access_all` -- the MEMBERSHIP-level bit this migration replaces. Dropped |
||||
-- afterwards -- the conversion below reuses the value -- so once it has run, a genuine legacy |
-- at the end of this file. |
||||
-- Manager and a Custom member created later are byte-identical. Every later step that has to reason |
-- * `groups.access_all` -- the GROUP-level flag, a separate and still-supported feature. Only read |
||||
-- about legacy authority (2026-07-23, 2026-08-09 and tools/custom_role_rollback/) reads this table |
-- here, to decide a legacy Manager's permissions; never written, and it keeps granting group |
||||
-- instead of guessing, which is what stops them from handing legacy privileges to modern members. |
-- members access to every collection afterwards exactly as before. |
||||
-- |
-- |
||||
-- Deliberately not a Diesel model and not in schema.rs: no runtime code reads it. It is |
-- Base `Collection::is_coll_manageable_by_user` accepts either, so a Manager reached every collection |
||||
-- migration/rollback bookkeeping, and it carries no foreign key so that 2026-07-24-120000's table |
-- through either. Only the membership bit is going away, but the capability an owner configured |
||||
-- rebuild does not have to care about it. |
-- through either route is preserved, so both are read below. While this file runs the membership |
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_legacy_manager ( |
-- column still exists and `atype = 3` still unambiguously means "legacy Manager". |
||||
users_organizations_uuid TEXT NOT NULL PRIMARY KEY |
-- |
||||
|
-- One state cannot be converted and is refused before the first mutation; `src/db/mod.rs` evaluates |
||||
|
-- the same condition at startup and prints the recovery text, because Diesel would surface the abort |
||||
|
-- below as nothing but a driver-level duplicate-key error. |
||||
|
|
||||
|
-- A plain User carrying membership `access_all`, reachable only on databases written before the web |
||||
|
-- vault stopped sending the flag. The bit gave read/write reach over every collection, present and |
||||
|
-- future, with no management authority, and the new model has no permission for that: |
||||
|
-- `edit_any_collection` would add management authority, dropping the bit would take the reach away. |
||||
|
-- Refuse and let an owner choose. The duplicate key aborts the migration, and is only inserted when |
||||
|
-- such a membership exists. |
||||
|
CREATE TEMPORARY TABLE __vw_legacy_user_access_all_guard ( |
||||
|
blocked INTEGER NOT NULL PRIMARY KEY |
||||
); |
); |
||||
INSERT OR IGNORE INTO __vw_custom_role_legacy_manager (users_organizations_uuid) |
INSERT INTO __vw_legacy_user_access_all_guard (blocked) VALUES (1); |
||||
SELECT uuid FROM users_organizations WHERE atype = 3; |
INSERT INTO __vw_legacy_user_access_all_guard (blocked) |
||||
|
SELECT 1 |
||||
-- Separately, mark that this database's Custom-role history is accounted for -- it was produced by |
FROM users_organizations |
||||
-- the migrations that ship today. Nothing else creates this table, which is what lets the startup |
WHERE atype = 2 |
||||
-- preflight treat its absence as proof that an earlier revision of this chain ran instead. |
AND access_all = TRUE |
||||
-- |
LIMIT 1; |
||||
-- Deliberately not the record table above: that one holds data an operator has to be able to write |
DROP TABLE __vw_legacy_user_access_all_guard; |
||||
-- during recovery, so its existence cannot also stand for "the history behind this data was |
|
||||
-- reviewed" -- creating it empty to silence an error would otherwise pass as the audit it asks for. |
-- Schema and data change in one table rebuild, which also keeps the conversion unambiguous: |
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_history_verified ( |
-- `atype = 3` still means Manager while the permission values are computed from it. |
||||
verified INTEGER NOT NULL PRIMARY KEY |
-- |
||||
|
-- `ALTER TABLE ... DROP COLUMN` is deliberately not used -- it needs SQLite 3.35.0, while a |
||||
|
-- `sqlite_system` build links whatever the host provides and libsqlite3-sys accepts 3.34.1. The |
||||
|
-- rebuild follows the existing 2022-03-02-210038_update_devices_primary_key pattern; Vaultwarden runs |
||||
|
-- SQLite migrations with `PRAGMA foreign_keys = OFF`, so the drop does not cascade into groups_users. |
||||
|
CREATE TABLE users_organizations_new ( |
||||
|
uuid TEXT NOT NULL PRIMARY KEY, |
||||
|
user_uuid TEXT NOT NULL REFERENCES users (uuid), |
||||
|
org_uuid TEXT NOT NULL REFERENCES organizations (uuid), |
||||
|
|
||||
|
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, |
||||
|
manage_users BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
manage_groups BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
manage_policies BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
create_new_collections BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
access_event_logs BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
access_import_export BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
access_reports BOOLEAN NOT NULL DEFAULT FALSE, |
||||
|
|
||||
|
UNIQUE (user_uuid, org_uuid) |
||||
); |
); |
||||
|
|
||||
-- Previously the server stored members created with the Custom role as Manager (3) and |
-- Owners and Admins are not touched: they carried `access_all` implicitly and the new model gives |
||||
-- masqueraded them as Custom (4) in all API responses. Now that Custom is a real, persisted |
-- them every permission by role. A plain User cannot reach this point carrying the bit (the guard |
||||
-- type, convert those members so clients (which no longer know the Manager role) keep |
-- above), so only a Manager becomes Custom, keeping the organization-wide collection-management |
||||
-- seeing exactly what they saw before. access_all is preserved; the new flags stay FALSE, |
-- capability it is configured with right now: |
||||
-- which matches the capabilities these members had. |
-- |
||||
UPDATE users_organizations SET atype = 4 WHERE atype = 3; |
-- * membership `access_all` -- the "Manage all collections" checkbox -- covered all three |
||||
|
-- collection permissions, including creating collections; |
||||
|
-- * an organization-local `access_all` group covered editing and deleting every collection, but |
||||
|
-- never creation -- that always required the membership bit; |
||||
|
-- * a Manager with neither keeps all three at FALSE. |
||||
|
-- |
||||
|
-- The second case is a deliberate policy choice. That capability was dynamic: it ended with the |
||||
|
-- group, with the group's own `access_all`, and with the member leaving it. It was never gated on |
||||
|
-- ORG_GROUPS_ENABLED -- `Collection::is_coll_manageable_by_user` reads `groups.access_all` in SQL |
||||
|
-- with no configuration check -- so it applied even where groups were never enabled. Nothing in the |
||||
|
-- new model is bound to a group, so it becomes a membership permission and no longer lapses on its |
||||
|
-- own. The alternative is silently revoking access these members have today, or refusing an ordinary |
||||
|
-- upgrade; the permission is visible in the member's permission list and an owner can clear it. |
||||
|
-- |
||||
|
-- The management (manage_users / manage_groups / manage_policies) and access (event logs / |
||||
|
-- import-export / reports) permissions start out FALSE for everyone. Nothing they unlock was a Manager |
||||
|
-- capability -- every member mutation, every policy write, the organization export and both |
||||
|
-- event-log routes were gated on Admin/Owner -- so granting one here would be a new privilege. |
||||
|
-- |
||||
|
-- One read is not carried over, and only for members who held the MEMBERSHIP bit: `has_full_access()` |
||||
|
-- read `self.access_all` and the role, never `groups.access_all`, so it gated the full member list |
||||
|
-- (`GET /organizations/<org>/users`) for them and for nobody whose reach came from a group. |
||||
|
-- `manage_users` is not granted to restore it, because it also carries invite, confirm, revoke, |
||||
|
-- restore and delete, which the Manager role never had; such members keep `/users/mini-details`, and |
||||
|
-- an owner can grant `manage_users` deliberately. In the other direction `edit_any_collection` |
||||
|
-- satisfies `has_full_access()`, which opens the organization collection list and |
||||
|
-- `GET /ciphers/organization-details` to the group-derived class -- data they could already reach |
||||
|
-- through the group, so only the route is new. |
||||
|
-- |
||||
|
-- Status is deliberately not part of the predicate: an invited, accepted or revoked membership is |
||||
|
-- converted like a confirmed one, since none holds authority in that state and the permissions are |
||||
|
-- what it would come back with -- the same thing `access_all` would have done. |
||||
|
-- |
||||
|
-- The group lookup is bound to the membership's own organization: a `groups_users` row pointing at |
||||
|
-- another organization's `access_all` group conveys nothing, exactly as it conveys nothing today. |
||||
|
INSERT INTO users_organizations_new ( |
||||
|
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 |
||||
|
) |
||||
|
SELECT |
||||
|
uo.uuid, uo.user_uuid, uo.org_uuid, uo.akey, uo.status, |
||||
|
CASE WHEN uo.atype = 3 THEN 4 ELSE uo.atype END, |
||||
|
uo.reset_password_key, uo.external_id, uo.invited_by_email, |
||||
|
FALSE, FALSE, FALSE, |
||||
|
CASE WHEN uo.atype = 3 AND uo.access_all = TRUE THEN TRUE ELSE FALSE END, |
||||
|
CASE |
||||
|
WHEN uo.atype = 3 |
||||
|
AND (uo.access_all = TRUE |
||||
|
OR EXISTS ( |
||||
|
SELECT 1 |
||||
|
FROM groups_users AS gu |
||||
|
INNER JOIN "groups" AS g ON g.uuid = gu.groups_uuid |
||||
|
WHERE gu.users_organizations_uuid = uo.uuid |
||||
|
AND g.organizations_uuid = uo.org_uuid |
||||
|
AND g.access_all = TRUE |
||||
|
)) |
||||
|
THEN TRUE ELSE FALSE |
||||
|
END, |
||||
|
CASE |
||||
|
WHEN uo.atype = 3 |
||||
|
AND (uo.access_all = TRUE |
||||
|
OR EXISTS ( |
||||
|
SELECT 1 |
||||
|
FROM groups_users AS gu |
||||
|
INNER JOIN "groups" AS g ON g.uuid = gu.groups_uuid |
||||
|
WHERE gu.users_organizations_uuid = uo.uuid |
||||
|
AND g.organizations_uuid = uo.org_uuid |
||||
|
AND g.access_all = TRUE |
||||
|
)) |
||||
|
THEN TRUE ELSE FALSE |
||||
|
END, |
||||
|
FALSE, FALSE, FALSE |
||||
|
FROM users_organizations AS uo; |
||||
|
|
||||
|
DROP TABLE users_organizations; |
||||
|
|
||||
|
ALTER TABLE users_organizations_new RENAME TO users_organizations; |
||||
|
|
||||
|
-- Never inherit a downgrade acknowledgement left behind by an earlier revert. |
||||
|
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
||||
|
|||||
@ -1 +0,0 @@ |
|||||
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
|
||||
@ -1,12 +0,0 @@ |
|||||
-- Record whether 2026-07-16 is about to run in this migration sequence. The durable marker lets a |
|
||||
-- retry distinguish its deterministic group-derived 0/1/1 backfill from older, ambiguous data. |
|
||||
CREATE TABLE IF NOT EXISTS __vw_custom_role_same_run_0716 ( |
|
||||
marker INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT OR IGNORE INTO __vw_custom_role_same_run_0716 (marker) |
|
||||
SELECT 1 |
|
||||
WHERE NOT EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM __diesel_schema_migrations |
|
||||
WHERE version = '20260716120000' |
|
||||
); |
|
||||
@ -1,28 +0,0 @@ |
|||||
-- Lossy revert: this removes the three independent Custom collection permissions, which the legacy |
|
||||
-- role/access_all schema cannot represent -- it only knows all three together. The revert therefore |
|
||||
-- requires the same acknowledgement as 2026-07-24-140000/down.sql -- which only announces the loss, |
|
||||
-- it does not authorize it. Create the marker table while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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; |
|
||||
|
|
||||
-- The previous schema exposes access_all as the three collection permissions together. Avoid |
|
||||
-- turning Edit-only memberships into Create/Edit/Delete grants when rolling back. |
|
||||
UPDATE users_organizations |
|
||||
SET access_all = create_new_collections AND edit_any_collection AND delete_any_collection |
|
||||
WHERE atype = 4; |
|
||||
|
|
||||
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; |
|
||||
@ -1,63 +0,0 @@ |
|||||
-- The legacy-Manager record has to exist before anything below runs: 2026-06-30-120000 writes it, |
|
||||
-- and the group-derived step at the end of this file reads it. Checked *before* the ALTER TABLE statements so |
|
||||
-- a refusal leaves no half-added column group behind -- on MySQL/MariaDB every ALTER commits on its |
|
||||
-- own, and a partial group is what the startup preflight then has to recover from. |
|
||||
-- |
|
||||
-- Creating the record here instead would manufacture an empty, apparently valid history for exactly |
|
||||
-- the databases that need an operator to look at them; see 2026-07-23-120000 for the full reasoning. |
|
||||
-- This guard exists for a bare migration runner that never consulted the startup 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 NOT EXISTS ( |
|
||||
SELECT 1 FROM sqlite_master |
|
||||
WHERE type = 'table' AND name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TABLE __vw_legacy_manager_record_guard; |
|
||||
|
|
||||
ALTER TABLE users_organizations ADD COLUMN create_new_collections BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
|
|
||||
-- Before these permissions were persisted independently, access_all represented the legacy |
|
||||
-- "Manage all collections" checkbox. Preserve that capability for existing Custom members. |
|
||||
-- |
|
||||
-- Driven by the stored value rather than by the membership's shape, so it needs no provenance: a |
|
||||
-- member carrying access_all held exactly this capability, whenever the row was created. |
|
||||
UPDATE users_organizations |
|
||||
SET create_new_collections = access_all, |
|
||||
edit_any_collection = access_all, |
|
||||
delete_any_collection = access_all |
|
||||
WHERE atype = 4; |
|
||||
|
|
||||
-- A legacy Manager also managed every collection when one of their groups had access_all, even if |
|
||||
-- the membership itself did not. Preserve that existing edit/delete capability without granting |
|
||||
-- collection creation, which historically still required membership access_all. |
|
||||
-- |
|
||||
-- Restricted to memberships recorded as legacy Managers, exactly like 2026-07-23-120000 and |
|
||||
-- 2026-08-09-120000. Role and group membership alone are *not* evidence of legacy authority: |
|
||||
-- "Custom, member of an access_all group" is also the shape of every modern Custom member who was |
|
||||
-- simply put into an ordinary access_all group, and granting on that shape hands them |
|
||||
-- organization-wide collection edit and delete -- which, through edit_any_collection, also satisfies |
|
||||
-- has_full_access() and therefore reaches every cipher in the organization. |
|
||||
-- |
|
||||
-- On the normal upgrade path this changes nothing: 2026-06-30-120000 runs first and records every |
|
||||
-- `atype = 3` row, which at this point is every Custom member there is. |
|
||||
UPDATE users_organizations |
|
||||
SET edit_any_collection = TRUE, |
|
||||
delete_any_collection = TRUE |
|
||||
WHERE atype = 4 |
|
||||
AND uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager) |
|
||||
AND EXISTS ( |
|
||||
SELECT 1 |
|
||||
FROM groups_users |
|
||||
INNER JOIN groups ON groups.uuid = groups_users.groups_uuid |
|
||||
WHERE groups_users.users_organizations_uuid = users_organizations.uuid |
|
||||
AND groups.organizations_uuid = users_organizations.org_uuid |
|
||||
AND groups.access_all = TRUE |
|
||||
); |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- This is an idempotent data repair, and it creates no rows: reverting it must not remove permissions |
|
||||
-- or recreate the invalid persisted Manager type. The older-schema migration performs its own safe |
|
||||
-- conversion. |
|
||||
SELECT 1; |
|
||||
@ -1,98 +0,0 @@ |
|||||
-- 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 NOT EXISTS ( |
|
||||
SELECT 1 FROM sqlite_master |
|
||||
WHERE type = 'table' AND name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
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 where 2026-06-30-120000 already recorded them. |
|
||||
INSERT OR IGNORE INTO __vw_custom_role_legacy_manager (users_organizations_uuid) |
|
||||
SELECT uuid FROM users_organizations WHERE atype = 3; |
|
||||
|
|
||||
-- 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; |
|
||||
@ -1,13 +0,0 @@ |
|||||
-- Recreate the column and repopulate it from the role/permission model that replaced it, restoring |
|
||||
-- the invariant the immediately preceding schema relies on: access_all == access to every collection. |
|
||||
-- That is exactly Owners/Admins, plus Custom members holding `edit_any_collection`. |
|
||||
-- |
|
||||
-- NOTE: this only holds for reverting *this* migration. Reverting further down the chain, |
|
||||
-- 2026-07-16 deliberately recomputes access_all as (create AND edit AND delete) for Custom members, |
|
||||
-- because in that older schema access_all also meant the legacy Manager "Manage all collections" |
|
||||
-- authority -- so a member who only held `edit_any_collection` comes out as a Manager *without* |
|
||||
-- access_all rather than silently gaining collection deletion. That is intentional and fail-closed; |
|
||||
-- the full rollback is blocked by 2026-07-24-140000/down.sql anyway. |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_all BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
UPDATE users_organizations SET access_all = TRUE WHERE atype IN (0, 1); |
|
||||
UPDATE users_organizations SET access_all = TRUE WHERE atype = 4 AND edit_any_collection = TRUE; |
|
||||
@ -1,46 +0,0 @@ |
|||||
-- The membership `access_all` flag was Vaultwarden's pre-permissions patch for "this member can |
|
||||
-- reach every collection". It is now fully represented by the role model: Owners/Admins hold it |
|
||||
-- implicitly, and a Custom member holds it via `edit_any_collection`. Drop the redundant column. |
|
||||
-- This only concerns users_organizations; groups.access_all is a separate, still-supported feature. |
|
||||
-- |
|
||||
-- `ALTER TABLE ... DROP COLUMN` is deliberately NOT used here: it only exists since SQLite 3.35.0, |
|
||||
-- while a `sqlite_system` build links whatever the host provides and libsqlite3-sys accepts 3.34.1 |
|
||||
-- (which is what Debian 11 ships). Forward migrations have to run on every supported build, so use |
|
||||
-- the portable table rebuild instead -- the same pattern as |
|
||||
-- 2022-03-02-210038_update_devices_primary_key. Vaultwarden runs SQLite migrations with |
|
||||
-- `PRAGMA foreign_keys = OFF`, so dropping the old table does not cascade into groups_users. |
|
||||
CREATE TABLE users_organizations_new ( |
|
||||
uuid TEXT NOT NULL PRIMARY KEY, |
|
||||
user_uuid TEXT NOT NULL REFERENCES users (uuid), |
|
||||
org_uuid TEXT NOT NULL REFERENCES organizations (uuid), |
|
||||
|
|
||||
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, |
|
||||
manage_users BOOLEAN NOT NULL DEFAULT FALSE, |
|
||||
manage_groups BOOLEAN NOT NULL DEFAULT FALSE, |
|
||||
manage_policies BOOLEAN NOT NULL DEFAULT FALSE, |
|
||||
create_new_collections BOOLEAN NOT NULL DEFAULT FALSE, |
|
||||
edit_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
|
||||
delete_any_collection BOOLEAN NOT NULL DEFAULT FALSE, |
|
||||
|
|
||||
UNIQUE (user_uuid, org_uuid) |
|
||||
); |
|
||||
|
|
||||
INSERT INTO users_organizations_new ( |
|
||||
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 |
|
||||
) |
|
||||
SELECT |
|
||||
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 |
|
||||
FROM users_organizations; |
|
||||
|
|
||||
DROP TABLE users_organizations; |
|
||||
|
|
||||
ALTER TABLE users_organizations_new RENAME TO users_organizations; |
|
||||
@ -1,22 +0,0 @@ |
|||||
-- Lossy revert: this removes the three Custom access permissions, which the legacy schema cannot |
|
||||
-- represent at all. The revert therefore |
|
||||
-- requires the same acknowledgement as 2026-07-24-140000/down.sql -- which only announces the loss, |
|
||||
-- it does not authorize it. Create the marker table while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
CREATE TEMPORARY TABLE __vw_custom_role_downgrade_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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; |
|
||||
|
|
||||
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; |
|
||||
@ -1,5 +0,0 @@ |
|||||
-- Three additional Bitwarden Custom-role permissions. They are only meaningful for Custom members |
|
||||
-- (gated on the role in code); Owners/Admins hold every permission implicitly. |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_event_logs BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_import_export BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
ALTER TABLE users_organizations ADD COLUMN access_reports BOOLEAN NOT NULL DEFAULT FALSE; |
|
||||
@ -1,29 +0,0 @@ |
|||||
-- Downgrade guard. Reverting this migration destroys Custom-role permission data that the legacy |
|
||||
-- role/access_all schema cannot represent, so it only runs with an explicit acknowledgement. Create |
|
||||
-- the marker table below while every Vaultwarden instance is stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_allow_custom_role_downgrade (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The acknowledgement stays valid for the rest of the revert chain and is consumed by the oldest |
|
||||
-- lossy migration (2026-06-30-120000), so one decision covers one downgrade -- and a re-upgrade |
|
||||
-- clears it again (2026-07-24-140000/up.sql), so consent is never inherited. |
|
||||
-- |
|
||||
-- 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 ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
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; |
|
||||
|
|
||||
-- Nothing else to undo: the acknowledgement deliberately survives this step. It has to still be here |
|
||||
-- when the next revert removes the first permission column, which is what this guard exists to |
|
||||
-- announce -- checking and dropping it in the same step would leave every following lossy revert |
|
||||
-- unguarded. |
|
||||
SELECT 1; |
|
||||
@ -1,11 +0,0 @@ |
|||||
-- Forward migration marker: its down migration intentionally blocks an automatic lossy downgrade |
|
||||
-- before any granular permission column is removed. |
|
||||
-- |
|
||||
-- It also cleans up after 2026-07-15: the same-run bookkeeping table has served its purpose by now |
|
||||
-- (2026-07-23 consumed the marker), so it is not left behind in every database. A single DDL |
|
||||
-- statement is safe even on MySQL, where DDL commits implicitly -- re-running it is a no-op. |
|
||||
DROP TABLE IF EXISTS __vw_custom_role_same_run_0716; |
|
||||
|
|
||||
-- Also clear a downgrade acknowledgement left over from an earlier revert, so consent is |
|
||||
-- never inherited across an upgrade. |
|
||||
DROP TABLE IF EXISTS __vw_allow_custom_role_downgrade; |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- Nothing to undo: this migration only re-applies permissions that 2026-07-23-120000 also sets, and |
|
||||
-- the original values are not recoverable. The permission columns themselves are removed further down |
|
||||
-- the chain by 2026-07-16-120000/down.sql, which is guarded. |
|
||||
SELECT 1; |
|
||||
@ -1,107 +0,0 @@ |
|||||
-- Follow-up repair for databases that already recorded 2026-07-23-120000. |
|
||||
-- |
|
||||
-- That migration originally *removed* the direct 0/1/1 collection permissions of a legacy Manager |
|
||||
-- whose authority came from an organization-local `access_all` group, because the runtime derived the |
|
||||
-- authority from the group instead. Deriving it turned out to be unsound -- "Custom, none of the three |
|
||||
-- collection permissions, member of such a group" is also the shape of every newly created flagless |
|
||||
-- Custom member -- so the runtime fallback is gone and 2026-07-23-120000 now materializes the |
|
||||
-- authority into the permission columns. |
|
||||
-- |
|
||||
-- Rewriting that file is not enough on its own: a database whose ledger already carries |
|
||||
-- 20260723120000 never runs it again, and would silently lose the capability. Repeat the |
|
||||
-- materialization here, in its own version, so both paths converge on the same state. |
|
||||
-- |
|
||||
-- Unlike an earlier revision of this file, the repair is driven by the legacy-Manager record written |
|
||||
-- by 2026-06-30-120000 rather than by role and group membership alone. Those two are the same shape, |
|
||||
-- so matching on them blanket-granted organization-wide collection edit and delete to modern Custom |
|
||||
-- members -- turning Create-only into Create+Edit+Delete, Edit-only into Edit+Delete, and a flagless |
|
||||
-- Custom into Edit+Delete, the last of which also implies `has_full_access()`. |
|
||||
-- |
|
||||
-- What this materialization *means* -- a group-bound capability becoming a permanent membership |
|
||||
-- permission -- is confirmed by an owner in 2026-08-10-120000, which runs immediately after it. |
|
||||
-- |
|
||||
-- Idempotent: on a database that ran the rewritten 2026-07-23-120000 every affected row already |
|
||||
-- holds these values. It only reads `groups` / `groups_users` and the record table and writes the two |
|
||||
-- permission columns, so it is also safe after `access_all` has been dropped. |
|
||||
-- |
|
||||
-- Deliberately not `create_new_collections`: collection creation historically required |
|
||||
-- membership-level `access_all`. |
|
||||
|
|
||||
-- The legacy-Manager record has to exist already; see 2026-07-23-120000 for why this refuses rather |
|
||||
-- than creating it. |
|
||||
-- |
|
||||
-- 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 NOT EXISTS ( |
|
||||
SELECT 1 FROM sqlite_master |
|
||||
WHERE type = 'table' AND name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TABLE __vw_legacy_manager_record_guard; |
|
||||
|
|
||||
-- Fail closed on a database whose legacy provenance was never recorded. |
|
||||
-- |
|
||||
-- If a Custom member sits in an organization-local `access_all` group but is not on record as a |
|
||||
-- legacy Manager, one of two things is true and this file cannot tell them apart: either the |
|
||||
-- membership really is a converted legacy Manager whose record was never written (a ledger from an |
|
||||
-- earlier revision of this feature branch), or it is an ordinary modern Custom member who must not |
|
||||
-- gain anything. Granting is a silent privilege escalation; skipping silently drops a real |
|
||||
-- capability. |
|
||||
-- |
|
||||
-- `__vw_custom_role_history_verified` settles it: 2026-06-30-120000 creates it, and an operator |
|
||||
-- creates it after auditing an older history, so its presence means the unrecorded memberships below |
|
||||
-- are unrecorded *on purpose*. Its absence means nobody has looked, and this stops. The startup |
|
||||
-- preflight refuses that state before any migration runs; this guard is the backstop for a bare |
|
||||
-- migration runner. `src/db/mod.rs` prints the full recovery, which lists these memberships: |
|
||||
-- |
|
||||
-- SELECT uo.uuid, uo.org_uuid, uo.status, |
|
||||
-- uo.create_new_collections, uo.edit_any_collection, uo.delete_any_collection |
|
||||
-- FROM users_organizations uo |
|
||||
-- INNER JOIN groups_users gu ON gu.users_organizations_uuid = uo.uuid |
|
||||
-- INNER JOIN "groups" g ON g.uuid = gu.groups_uuid AND g.organizations_uuid = uo.org_uuid |
|
||||
-- WHERE uo.atype = 4 AND g.access_all = 1 |
|
||||
-- AND uo.uuid NOT IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager); |
|
||||
-- |
|
||||
-- The marker never grants anything by itself: the update below is always driven by the record table, |
|
||||
-- so an unrecorded membership keeps exactly the permissions it has. |
|
||||
CREATE TEMPORARY TABLE __vw_legacy_group_authority_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_legacy_group_authority_guard (blocked) VALUES (1); |
|
||||
INSERT INTO __vw_legacy_group_authority_guard (blocked) |
|
||||
SELECT 1 |
|
||||
FROM users_organizations AS uo |
|
||||
WHERE uo.atype = 4 |
|
||||
AND uo.uuid NOT 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 = uo.uuid |
|
||||
AND g.organizations_uuid = uo.org_uuid |
|
||||
AND g.access_all = TRUE |
|
||||
) |
|
||||
AND NOT EXISTS ( |
|
||||
SELECT 1 FROM sqlite_master |
|
||||
WHERE type = 'table' AND name = '__vw_custom_role_history_verified' |
|
||||
) |
|
||||
LIMIT 1; |
|
||||
DROP TABLE __vw_legacy_group_authority_guard; |
|
||||
|
|
||||
UPDATE users_organizations |
|
||||
SET edit_any_collection = TRUE, |
|
||||
delete_any_collection = TRUE |
|
||||
WHERE atype = 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 |
|
||||
); |
|
||||
@ -1,4 +0,0 @@ |
|||||
-- Nothing to undo: this migration only asks for a decision, it never writes permissions. The |
|
||||
-- acknowledgement it consumes is deliberately not recreated -- a revert is not consent, and the next |
|
||||
-- upgrade has to ask again. |
|
||||
SELECT 1; |
|
||||
@ -1,117 +0,0 @@ |
|||||
-- Make the one semantic change this feature cannot express an owner's decision instead of a default. |
|
||||
-- |
|
||||
-- Before the Custom role, a Manager who reached every collection through an organization-local group |
|
||||
-- with `access_all` held that authority *while* the group relationship lasted. It ended when the |
|
||||
-- group was deleted, when its `accessAll` was switched off, when the member left it, and it was inert |
|
||||
-- whenever `ORG_GROUPS_ENABLED` was false. Nothing in the new model expresses a permission bound to a |
|
||||
-- group like that: `edit_any_collection` and `delete_any_collection` live on the membership. |
|
||||
-- |
|
||||
-- So the earlier migrations in this chain write the authority onto the membership, and the result is |
|
||||
-- deliberately not identical to what it replaces: |
|
||||
-- |
|
||||
-- * it no longer lapses when the last qualifying group disappears, or when `accessAll` is cleared; |
|
||||
-- * it applies even with the groups feature switched off; |
|
||||
-- * `edit_any_collection` additionally satisfies `has_full_access()`, so the member reaches every |
|
||||
-- collection of the organization directly rather than through the group. |
|
||||
-- |
|
||||
-- Materializing it silently would be a migration that grants durable organization-wide collection |
|
||||
-- edit and delete on its own authority. Dropping it silently would take a capability away. Neither is |
|
||||
-- ours to choose, so this migration stops and hands the decision to an owner. It grants nothing and |
|
||||
-- revokes nothing itself. |
|
||||
-- |
|
||||
-- On a database with no Custom membership that both has edit/delete authority and belongs to an |
|
||||
-- organization-local `access_all` group, there is nothing to decide and this is a no-op. |
|
||||
-- |
|
||||
-- Vaultwarden's startup preflight looks ahead for exactly the condition below and refuses with the |
|
||||
-- full text (`RefuseUnconfirmedPermanentCollectionAuthority` in `src/db/mod.rs`), from the legacy |
|
||||
-- schema as well, so an operator normally never reaches the abort here. Diesel reports only the |
|
||||
-- driver error, so on this path the question would arrive as `UNIQUE constraint failed: |
|
||||
-- __vw_permanent_authority_guard.blocked` and nothing else. Keep the two predicates identical. |
|
||||
-- |
|
||||
-- Review the affected memberships: |
|
||||
-- |
|
||||
-- SELECT uo.uuid, uo.user_uuid, uo.org_uuid, uo.status, |
|
||||
-- uo.create_new_collections, uo.edit_any_collection, uo.delete_any_collection, |
|
||||
-- (uo.uuid IN (SELECT users_organizations_uuid FROM __vw_custom_role_legacy_manager)) |
|
||||
-- AS was_legacy_manager |
|
||||
-- FROM users_organizations uo |
|
||||
-- WHERE uo.atype = 4 |
|
||||
-- AND (uo.edit_any_collection = 1 OR uo.delete_any_collection = 1) |
|
||||
-- AND EXISTS ( |
|
||||
-- SELECT 1 FROM groups_users gu |
|
||||
-- INNER JOIN "groups" g ON g.uuid = gu.groups_uuid |
|
||||
-- WHERE gu.users_organizations_uuid = uo.uuid |
|
||||
-- AND g.organizations_uuid = uo.org_uuid |
|
||||
-- AND g.access_all = 1); |
|
||||
-- |
|
||||
-- Reading the result: |
|
||||
-- |
|
||||
-- * `was_legacy_manager = 1` -- a converted Manager. Review it even when |
|
||||
-- `create_new_collections = 1`: that independent permission can be changed after an earlier |
|
||||
-- revision materialized group-derived edit/delete, so its current value cannot prove where those |
|
||||
-- two permissions came from. A membership whose own legacy `access_all` supplied all three may |
|
||||
-- therefore be listed conservatively even though its authority was already permanent. |
|
||||
-- * `was_legacy_manager = 0` -- never a Manager. On a database first upgraded by revision bf54088c |
|
||||
-- they may carry permissions that revision's 2026-08-09-120000 granted in bulk, which nothing can |
|
||||
-- distinguish from a deliberate grant any more -- check them against what you intended. |
|
||||
-- |
|
||||
-- An invited or revoked membership is listed too, and deliberately so. It holds no authority today -- |
|
||||
-- every guard requires a confirmed membership, and `MembershipStatus::from_i32` rejects the revoked |
|
||||
-- value outright -- but the permission is what it would come back with if it is ever restored, and |
|
||||
-- by then the group it came from may be gone. Status is therefore not part of the predicate. |
|
||||
-- |
|
||||
-- Clear whatever you do not want to keep, for example: |
|
||||
-- |
|
||||
-- UPDATE users_organizations |
|
||||
-- SET edit_any_collection = 0, delete_any_collection = 0 |
|
||||
-- WHERE uuid = '<MEMBERSHIP_UUID>'; |
|
||||
-- |
|
||||
-- Then record the decision once, with every Vaultwarden instance stopped: |
|
||||
-- |
|
||||
-- CREATE TABLE __vw_ack_permanent_collection_authority (acknowledged INTEGER NOT NULL PRIMARY KEY); |
|
||||
-- |
|
||||
-- The acknowledgement is consumed at the end of this file, so one decision covers one upgrade. |
|
||||
-- |
|
||||
-- The legacy-Manager record has to exist already: the chain and supported rollback use it as the |
|
||||
-- immutable role-provenance record. Refuse a damaged history here too; see 2026-07-23-120000 for why |
|
||||
-- this never creates it. |
|
||||
-- |
|
||||
-- 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 NOT EXISTS ( |
|
||||
SELECT 1 FROM sqlite_master |
|
||||
WHERE type = 'table' AND name = '__vw_custom_role_legacy_manager' |
|
||||
); |
|
||||
DROP TABLE __vw_legacy_manager_record_guard; |
|
||||
|
|
||||
-- The duplicate key aborts the migration. It is only inserted while an unconfirmed membership exists. |
|
||||
CREATE TEMPORARY TABLE __vw_permanent_authority_guard ( |
|
||||
blocked INTEGER NOT NULL PRIMARY KEY |
|
||||
); |
|
||||
INSERT INTO __vw_permanent_authority_guard (blocked) VALUES (1); |
|
||||
INSERT INTO __vw_permanent_authority_guard (blocked) |
|
||||
SELECT 1 |
|
||||
FROM users_organizations AS uo |
|
||||
WHERE uo.atype = 4 |
|
||||
AND (uo.edit_any_collection = TRUE OR uo.delete_any_collection = TRUE) |
|
||||
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 = uo.uuid |
|
||||
AND g.organizations_uuid = uo.org_uuid |
|
||||
AND g.access_all = TRUE |
|
||||
) |
|
||||
AND NOT EXISTS ( |
|
||||
SELECT 1 FROM sqlite_master |
|
||||
WHERE type = 'table' AND name = '__vw_ack_permanent_collection_authority' |
|
||||
) |
|
||||
LIMIT 1; |
|
||||
DROP TABLE __vw_permanent_authority_guard; |
|
||||
|
|
||||
DROP TABLE IF EXISTS __vw_ack_permanent_collection_authority; |
|
||||
File diff suppressed because it is too large
File diff suppressed because it is too large
Loading…
Reference in new issue