diff --git a/supabase/migrations/20251104152547_6bed441b-9689-4236-8e31-2208a40b6ad5.sql b/supabase/migrations/20251104152547_6bed441b-9689-4236-8e31-2208a40b6ad5.sql new file mode 100644 index 00000000..d91d031b --- /dev/null +++ b/supabase/migrations/20251104152547_6bed441b-9689-4236-8e31-2208a40b6ad5.sql @@ -0,0 +1,159 @@ +-- Fix Security Definer Views Issue +-- Add explicit security_invoker = true to views that are missing it +-- This ensures views execute with the querying user's permissions, not the creator's + +-- Fix moderation_queue_with_entities view +DROP VIEW IF EXISTS public.moderation_queue_with_entities CASCADE; + +CREATE VIEW public.moderation_queue_with_entities +WITH (security_invoker = true) +AS +SELECT + cs.id, + cs.submission_type, + cs.status, + + -- Temporal fields (with backward compatibility alias) + cs.submitted_at AS created_at, + cs.submitted_at, + cs.reviewed_at, + cs.resolved_at, + cs.assigned_at, + cs.escalated_at, + cs.last_modified_at, + cs.first_reviewed_at, + + -- User IDs + cs.user_id AS submitted_by, + cs.reviewer_id AS reviewed_by, + cs.assigned_to, + cs.escalated_by, + cs.last_modified_by, + + -- State flags + cs.escalated, + cs.escalation_reason, + cs.reviewer_notes AS review_notes, + cs.approval_mode, + cs.review_count, + cs.locked_until, + cs.is_test_data, + + -- Submitter profile (aliased for backward compatibility) + sp.id AS submitter_profile_id, + sp.username AS submitter_username, + sp.display_name AS submitter_display_name, + sp.avatar_url AS submitter_avatar_url, + sp.reputation_score AS submitter_reputation, + + -- Reviewer profile + rp.id AS reviewer_profile_id, + rp.username AS reviewer_username, + rp.display_name AS reviewer_display_name, + rp.avatar_url AS reviewer_avatar_url, + + -- Assignee profile + ap.id AS assignee_profile_id, + ap.username AS assignee_username, + ap.display_name AS assignee_display_name, + ap.avatar_url AS assignee_avatar_url, + + -- Submission items (aggregated with new typed FK columns) + ( + SELECT json_agg(si.*) + FROM submission_items si + WHERE si.submission_id = cs.id + ) as submission_items + +FROM content_submissions cs +LEFT JOIN profiles sp ON sp.user_id = cs.user_id +LEFT JOIN profiles rp ON rp.user_id = cs.reviewer_id +LEFT JOIN profiles ap ON ap.user_id = cs.assigned_to; + +COMMENT ON VIEW public.moderation_queue_with_entities IS +'Optimized view for moderation queue with pre-joined entity data. Uses security_invoker to respect querying user permissions and RLS policies.'; + +-- Fix filtered_profiles view +DROP VIEW IF EXISTS public.filtered_profiles CASCADE; + +CREATE VIEW public.filtered_profiles +WITH (security_invoker = true) +AS +SELECT + p.id, + p.user_id, + p.username, + p.display_name, + p.privacy_level, + p.created_at, + p.updated_at, + -- Field-level privacy using security definer functions + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'bio') THEN p.bio + ELSE NULL + END AS bio, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'avatar_url') THEN p.avatar_url + ELSE NULL + END AS avatar_url, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'avatar_image_id') THEN p.avatar_image_id + ELSE NULL + END AS avatar_image_id, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'preferred_pronouns') THEN p.preferred_pronouns + ELSE NULL + END AS preferred_pronouns, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'preferred_pronouns') THEN p.show_pronouns + ELSE false + END AS show_pronouns, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'personal_location') THEN p.personal_location + ELSE NULL + END AS personal_location, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'location_id') THEN p.location_id + ELSE NULL + END AS location_id, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'home_park_id') THEN p.home_park_id + ELSE NULL + END AS home_park_id, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'date_of_birth') THEN p.date_of_birth + ELSE NULL + END AS date_of_birth, + p.timezone, + p.preferred_language, + p.theme_preference, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.ride_count + ELSE 0 + END AS ride_count, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.coaster_count + ELSE 0 + END AS coaster_count, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.park_count + ELSE 0 + END AS park_count, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.review_count + ELSE 0 + END AS review_count, + CASE + WHEN can_view_profile_field(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid), p.user_id, 'ride_count') THEN p.reputation_score + ELSE 0 + END AS reputation_score, + CASE + WHEN auth.uid() = p.user_id OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid)) THEN p.banned + ELSE false + END AS banned +FROM public.profiles p +WHERE (NOT p.banned OR auth.uid() = p.user_id OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid))) + AND (p.privacy_level = 'public' OR auth.uid() = p.user_id OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid))); + +COMMENT ON VIEW public.filtered_profiles IS +'Profile view with field-level privacy controls. Uses security_invoker to respect querying user context and RLS policies. Privacy checks use security definer functions for consistent permission enforcement.'; \ No newline at end of file