You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
117 lines
6.1 KiB
117 lines
6.1 KiB
-- 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;
|
|
|