Files
buzz/migrations/0065_invite_revocation.sql
Local Builder a3b7b181a8 fix(db): reconcile invitation admission with main
Adapt invitation helpers to the community-admitted transaction API and move the unreleased revocation migration after the current schema changes. Exercise upgrades from the latest pre-revocation schema.

Signed-off-by: Local Builder <1c476cb1127ea270cb900bdc3891060140bdd1173f91498030ee9b4db01c2f20@buzz.block.builderlab.xyz>
2026-10-17 17:20:52 +00:00

76 lines
3.9 KiB
PL/PgSQL

ALTER TABLE relay_invites ADD COLUMN revoked_at TIMESTAMPTZ;
-- Serialize invitation admission with restriction and ownership changes per tenant.
CREATE FUNCTION invite_admission_lock(community UUID) RETURNS VOID
LANGUAGE sql AS $$
SELECT pg_advisory_xact_lock(hashtextextended('invite-admission:' || community::text, 0));
$$;
-- Live revocation is application-owned: supported restriction and owner writers
-- take invite_admission_lock before row locks and revoke in the same commit.
-- Rollout gate: drain all old writer pods before lifting bans or reopening
-- invitation admission, then rerun backfill_invite_revocations() under the
-- schema-destruction maintenance lock to repair bans committed during rollout.
-- Include active bans and recorded prior bans, so lifting a ban before this
-- migration cannot restore an invite issued before that ban. Scope every owner
-- and audit lookup to the invitation's community.
-- Schema migrations hold the exclusive schema-destruction lock. Retain the
-- universal tenant fence too: acquire its shared lock, then read the current
-- generation before applying this narrowly scoped historical security repair.
-- A quiescing tenant may return to active, so skipping it could revive bearers.
CREATE FUNCTION backfill_invite_revocations() RETURNS BIGINT
LANGUAGE plpgsql AS $$
DECLARE
target RECORD;
affected BIGINT;
total BIGINT := 0;
current_generation BIGINT;
prior_community TEXT := current_setting('buzz.deletion_executor_community', true);
prior_generation TEXT := current_setting('buzz.deletion_fence_generation', true);
BEGIN
FOR target IN SELECT id FROM communities
WHERE EXISTS (SELECT 1 FROM relay_invites WHERE community_id = communities.id)
ORDER BY id
LOOP
PERFORM pg_advisory_xact_lock_shared(community_deletion_lock_key(target.id));
SELECT deletion_fence_generation INTO STRICT current_generation
FROM communities WHERE id = target.id;
PERFORM set_config('buzz.deletion_executor_community', target.id::text, true);
PERFORM set_config('buzz.deletion_fence_generation', current_generation::text, true);
UPDATE relay_invites i SET revoked_at = clock_timestamp()
WHERE i.community_id = target.id AND i.revoked_at IS NULL AND EXISTS (
SELECT 1 FROM (
SELECT CASE WHEN i.created_by ~ '^[0-9a-f]{64}$'
THEN decode(i.created_by, 'hex') END AS pubkey
UNION ALL
SELECT agent_owner_pubkey FROM users
WHERE community_id = i.community_id AND encode(pubkey, 'hex') = i.created_by
AND agent_owner_pubkey IS NOT NULL
) principal
WHERE EXISTS (
SELECT 1 FROM community_bans b WHERE b.community_id = i.community_id
AND b.pubkey = principal.pubkey AND b.banned
AND (b.ban_expires_at IS NULL OR b.ban_expires_at > clock_timestamp())
) OR EXISTS (
SELECT 1 FROM moderation_actions a WHERE a.community_id = i.community_id
AND a.target_pubkey = principal.pubkey AND a.action IN ('ban', 'resolve:ban')
AND a.created_at >= i.created_at
) OR EXISTS (
SELECT 1 FROM relay_admin_actions a WHERE a.report_community_id = i.community_id
AND a.enforcement_target_pubkey = principal.pubkey AND a.action = 'ban'
AND (a.step_marker = 'mutation_committed' OR a.state = 'succeeded')
AND a.updated_at >= i.created_at
)
)
;
GET DIAGNOSTICS affected = ROW_COUNT;
total := total + affected;
PERFORM set_config('buzz.deletion_executor_community', COALESCE(prior_community, ''), true);
PERFORM set_config('buzz.deletion_fence_generation', COALESCE(prior_generation, ''), true);
END LOOP;
RETURN total;
END
$$;
SELECT backfill_invite_revocations();