-- QA-046: pass_slips self-approval bypass. -- -- The original "pass_slips_update" policy (20260306090200_pass_slips.sql) is: -- FOR UPDATE USING (user_id = auth.uid() -- OR profile.role IN ('admin','dispatcher')) -- with NO WITH CHECK. Postgres then reuses the USING expression as the -- WITH CHECK, so the row OWNER can PATCH their own pass slip to any values -- -- including `status = 'approved'`, `approved_by`, `approved_at`. The approve/ -- reject buttons are only gated in the UI, so any signed-in user could -- self-approve their own pass slip through the REST API (the same class of -- flaw as QA-015 on it_service_requests). -- -- Intended behaviour (see pass_slip_provider.dart): -- * admin/dispatcher may approve/reject (and otherwise manage) any slip -- * the owner may only mark their OWN already-approved slip 'completed' -- (they return from the excusal); they must never set 'approved'/'rejected'. DROP POLICY IF EXISTS "pass_slips_update" ON pass_slips; CREATE POLICY "pass_slips_update" ON pass_slips FOR UPDATE TO authenticated USING ( -- Approvers can act on any slip. EXISTS ( SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher') ) -- The owner may only touch their own slip once it is approved (to complete it). OR (user_id = auth.uid() AND status = 'approved') ) WITH CHECK ( EXISTS ( SELECT 1 FROM profiles p WHERE p.id = auth.uid() AND p.role IN ('admin', 'dispatcher') ) -- The owner's only allowed transition is approved -> completed. This blocks -- self-approval: a pending slip's owner matches neither branch here, and the -- USING clause above already refuses to expose a non-approved slip to them. OR (user_id = auth.uid() AND status = 'completed') );