-- QA-047: swap_requests authorization bypass. -- -- Live REST probes with a non-privileged it_staff token showed: -- * SELECT is not scoped: any authenticated user reads EVERY swap (60/60), -- including 48 they are neither requester nor recipient of. -- * UPDATE is not scoped: the requester could PATCH their OWN swap straight -- to status='accepted' (forging the recipient's acceptance), and a total -- non-participant could PATCH ANY swap's status/recipient. This bypasses the -- identity checks in respond_shift_swap() entirely (same class as QA-015). -- -- The base swap_requests policies live in a migration that predates this repo -- ("already exists in many deployments"), so their names aren't known here; -- the DO block drops whatever SELECT/INSERT/UPDATE/DELETE policies exist and -- this migration then defines the complete, hardened set. Safe because: -- * respond_shift_swap() (accept/reject/escalate) is SECURITY DEFINER -- (20260322170000+), so it mutates rows regardless of these policies. -- * request_shift_swap() inserts with requester_id = auth.uid(). -- * The only direct table UPDATE the app makes is reassignSwap(), an -- admin/dispatcher action (workforce_provider.dart:reassignSwap). ALTER TABLE public.swap_requests ENABLE ROW LEVEL SECURITY; DO $$ DECLARE pol record; BEGIN FOR pol IN SELECT policyname FROM pg_policies WHERE schemaname = 'public' AND tablename = 'swap_requests' LOOP EXECUTE format('DROP POLICY IF EXISTS %I ON public.swap_requests', pol.policyname); END LOOP; END $$; -- SELECT: only the two participants and admins/dispatchers. The provider still -- filters client-side, so tightening here only removes rows a user shouldn't -- have seen in the first place. CREATE POLICY "swap_requests: select" ON public.swap_requests FOR SELECT TO authenticated USING ( requester_id = auth.uid() OR recipient_id = auth.uid() OR EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher')) ); -- INSERT: a user may only open a swap as themselves (request_shift_swap pins -- requester_id to auth.uid()). CREATE POLICY "swap_requests: insert" ON public.swap_requests FOR INSERT TO authenticated WITH CHECK ( requester_id = auth.uid() ); -- UPDATE: only admins/dispatchers may write the row directly (reassignSwap). -- Participants accept/reject/escalate through respond_shift_swap(), which is -- SECURITY DEFINER and therefore not bound by this policy. This is what closes -- the bypass: a requester/recipient can no longer forge status directly. CREATE POLICY "swap_requests: update" ON public.swap_requests FOR UPDATE TO authenticated USING ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher')) ) WITH CHECK ( EXISTS (SELECT 1 FROM public.profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher')) ); -- No DELETE policy: direct deletes stay blocked (unchanged behaviour).