diff --git a/supabase/migrations/20260711140036_677fb7f4-3e2b-4614-ba6b-5fe5b28ff48b.sql b/supabase/migrations/20260711140036_677fb7f4-3e2b-4614-ba6b-5fe5b28ff48b.sql new file mode 100644 index 0000000..8c3038a --- /dev/null +++ b/supabase/migrations/20260711140036_677fb7f4-3e2b-4614-ba6b-5fe5b28ff48b.sql @@ -0,0 +1,29 @@ +-- Restrict direct execution of SECURITY DEFINER function has_role. +-- Switching to SECURITY INVOKER: when called as auth.uid() the user_roles +-- RLS policy already lets a user see (only) their own role rows, so +-- has_role(auth.uid(), 'admin') still returns correctly. +-- We also revoke EXECUTE from anon/public so the function cannot be +-- invoked directly from the Data API. + +CREATE OR REPLACE FUNCTION public.has_role(_user_id uuid, _role app_role) +RETURNS boolean +LANGUAGE sql +STABLE +SECURITY INVOKER +SET search_path = public +AS $$ + SELECT EXISTS ( + SELECT 1 + FROM public.user_roles + WHERE user_id = _user_id + AND role = _role + ) +$$; + +REVOKE EXECUTE ON FUNCTION public.has_role(uuid, app_role) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION public.has_role(uuid, app_role) FROM anon; +-- authenticated retains EXECUTE so RLS policies referencing has_role +-- continue to work; the function itself is now SECURITY INVOKER so it +-- cannot be used to bypass RLS. +GRANT EXECUTE ON FUNCTION public.has_role(uuid, app_role) TO authenticated; +GRANT EXECUTE ON FUNCTION public.has_role(uuid, app_role) TO service_role; \ No newline at end of file