Files
thrilltrack-explorer/supabase/migrations/20251104152547_6bed441b-9689-4236-8e31-2208a40b6ad5.sql
gpt-engineer-app[bot] abb9761a77 Fix security definer views
2025-11-04 15:26:14 +00:00

159 lines
5.8 KiB
SQL

-- 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.';