fix(rls): block member self-change of payment/dues columns (D9)
prevent_member_self_role_change covered role/status/space but not the financial columns; RLS members_update lets a member UPDATE their own row, so payment_status/dues_paid_until/last_paid_at/last_payment_at/ stripe_customer_id/payment_note/joined_at were self-settable via PostgREST to forge a dues-good signal. Migration 044 adds them to the blocked set; service-client (auth.uid() NULL) + privileged-on-another- member writes still bypass (Stripe webhook unaffected). Schema + maps.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
-- =============================================================================
|
||||
-- 044: Block member self-change of financial columns
|
||||
-- =============================================================================
|
||||
-- prevent_member_self_role_change protected role/tier/status/approved/
|
||||
-- card-access/onboarding/space but NOT the financial columns. RLS
|
||||
-- members_update lets a member UPDATE their own row, so a member could
|
||||
-- self-set payment_status / dues_paid_until / last_paid_at / etc. via
|
||||
-- PostgREST and forge a "dues good" signal. Add them to the blocked set.
|
||||
-- Legitimate writers are the Stripe webhook / treasurer actions via the
|
||||
-- service client (auth.uid() IS NULL -> early return, unaffected) or a
|
||||
-- privileged user editing ANOTHER member (NEW.user_id <> auth.uid()).
|
||||
--
|
||||
-- Idempotent: CREATE OR REPLACE FUNCTION.
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.prevent_member_self_role_change()
|
||||
RETURNS trigger LANGUAGE plpgsql SECURITY DEFINER SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
v_is_privileged boolean;
|
||||
BEGIN
|
||||
IF auth.uid() IS NULL THEN RETURN NEW; END IF;
|
||||
IF NEW.user_id IS DISTINCT FROM auth.uid() THEN RETURN NEW; END IF;
|
||||
SELECT public.user_has_role_in_space(auth.uid(), NEW.space_id, ARRAY['admin','board','treasurer'])
|
||||
INTO v_is_privileged;
|
||||
IF v_is_privileged THEN RETURN NEW; END IF;
|
||||
IF NEW.role IS DISTINCT FROM OLD.role
|
||||
OR NEW.tier IS DISTINCT FROM OLD.tier
|
||||
OR NEW.tier_id IS DISTINCT FROM OLD.tier_id
|
||||
OR NEW.status IS DISTINCT FROM OLD.status
|
||||
OR NEW.approved IS DISTINCT FROM OLD.approved
|
||||
OR NEW.has_card_access IS DISTINCT FROM OLD.has_card_access
|
||||
OR NEW.onboarding_completed_at IS DISTINCT FROM OLD.onboarding_completed_at
|
||||
OR NEW.space_id IS DISTINCT FROM OLD.space_id
|
||||
OR NEW.payment_status IS DISTINCT FROM OLD.payment_status
|
||||
OR NEW.payment_note IS DISTINCT FROM OLD.payment_note
|
||||
OR NEW.dues_paid_until IS DISTINCT FROM OLD.dues_paid_until
|
||||
OR NEW.last_paid_at IS DISTINCT FROM OLD.last_paid_at
|
||||
OR NEW.last_payment_at IS DISTINCT FROM OLD.last_payment_at
|
||||
OR NEW.stripe_customer_id IS DISTINCT FROM OLD.stripe_customer_id
|
||||
OR NEW.joined_at IS DISTINCT FROM OLD.joined_at THEN
|
||||
RAISE EXCEPTION 'Members cannot change their own role, tier, status, approval, card access, onboarding completion, space, or payment/dues fields.'
|
||||
USING ERRCODE = '42501';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$;
|
||||
|
||||
DROP TRIGGER IF EXISTS trg_prevent_member_self_role_change ON public.space_members;
|
||||
CREATE TRIGGER trg_prevent_member_self_role_change
|
||||
BEFORE UPDATE ON public.space_members
|
||||
FOR EACH ROW EXECUTE FUNCTION public.prevent_member_self_role_change();
|
||||
+16
-5
@@ -1856,9 +1856,13 @@ CREATE TRIGGER trg_seed_default_role_permissions
|
||||
|
||||
|
||||
-- =============================================================================
|
||||
-- 16. Self-change hardening (equivalent to scripts/024_self_change_hardening.sql).
|
||||
-- Adds tier_id and onboarding_completed_at to the columns a non-privileged
|
||||
-- member cannot change on their own space_members row.
|
||||
-- 16. Self-change hardening (equivalent to scripts/024 + scripts/044).
|
||||
-- Columns a non-privileged member cannot change on their own
|
||||
-- space_members row: role, tier(_id), status, approved, has_card_access,
|
||||
-- onboarding_completed_at, space_id, and the payment/dues fields
|
||||
-- (payment_status, payment_note, dues_paid_until, last_paid_at,
|
||||
-- last_payment_at, stripe_customer_id, joined_at). Service-client and
|
||||
-- privileged-on-another-member writes bypass (auth.uid() guards).
|
||||
-- =============================================================================
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.prevent_member_self_role_change()
|
||||
@@ -1879,8 +1883,15 @@ BEGIN
|
||||
OR NEW.approved IS DISTINCT FROM OLD.approved
|
||||
OR NEW.has_card_access IS DISTINCT FROM OLD.has_card_access
|
||||
OR NEW.onboarding_completed_at IS DISTINCT FROM OLD.onboarding_completed_at
|
||||
OR NEW.space_id IS DISTINCT FROM OLD.space_id THEN
|
||||
RAISE EXCEPTION 'Members cannot change their own role, tier, status, approval, card access, onboarding completion, or space.'
|
||||
OR NEW.space_id IS DISTINCT FROM OLD.space_id
|
||||
OR NEW.payment_status IS DISTINCT FROM OLD.payment_status
|
||||
OR NEW.payment_note IS DISTINCT FROM OLD.payment_note
|
||||
OR NEW.dues_paid_until IS DISTINCT FROM OLD.dues_paid_until
|
||||
OR NEW.last_paid_at IS DISTINCT FROM OLD.last_paid_at
|
||||
OR NEW.last_payment_at IS DISTINCT FROM OLD.last_payment_at
|
||||
OR NEW.stripe_customer_id IS DISTINCT FROM OLD.stripe_customer_id
|
||||
OR NEW.joined_at IS DISTINCT FROM OLD.joined_at THEN
|
||||
RAISE EXCEPTION 'Members cannot change their own role, tier, status, approval, card access, onboarding completion, space, or payment/dues fields.'
|
||||
USING ERRCODE = '42501';
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
|
||||
Reference in New Issue
Block a user