Files
thrilltrack-explorer/supabase/migrations/20251009120430_d39a795e-c84d-4311-a192-14d074645e7c.sql
gpt-engineer-app[bot] 036baf9f6d Fix profile data exposure
2025-10-09 12:05:08 +00:00

126 lines
5.1 KiB
SQL

-- Revert the previous policy and implement proper security model
-- The filtered_profiles view should be the ONLY way to view other users' profiles
-- Remove the policy that allows direct profile viewing
DROP POLICY IF EXISTS "Authenticated users can view public profiles" ON public.profiles;
-- Change filtered_profiles view to use SECURITY DEFINER
-- This allows it to bypass RLS and implement its own access control
DROP VIEW IF EXISTS public.filtered_profiles;
CREATE VIEW public.filtered_profiles
WITH (security_invoker = off, security_barrier = true)
AS
SELECT
p.id,
p.user_id,
p.username,
p.display_name,
p.privacy_level,
p.created_at,
p.updated_at,
-- Conditionally show bio based on privacy
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,
-- Conditionally show avatar based on privacy
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,
-- Conditionally show pronouns based on privacy
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,
-- Conditionally show location based on privacy
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,
-- Conditionally show home park based on privacy
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,
-- Conditionally show date of birth based on privacy
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,
-- Always show safe fields
p.timezone,
p.preferred_language,
p.theme_preference,
-- Conditionally show activity stats based on privacy
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,
-- Only show banned status to owner and moderators
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
-- Hide banned profiles unless viewer is owner or moderator
((NOT p.banned) OR (auth.uid() = p.user_id) OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid)))
AND
-- Only show public profiles OR owner's own profile OR moderator viewing
((p.privacy_level = 'public') OR (auth.uid() = p.user_id) OR is_moderator(COALESCE(auth.uid(), '00000000-0000-0000-0000-000000000000'::uuid)));
-- Grant access to the filtered_profiles view
GRANT SELECT ON public.filtered_profiles TO authenticated, anon;
-- Add explanatory comment
COMMENT ON VIEW public.filtered_profiles IS
'Privacy-safe profile view that enforces field-level access control.
Uses security_invoker=off (SECURITY DEFINER) to bypass RLS and implement custom filtering.
Sensitive fields are conditionally shown based on user privacy settings.
Users can only see public non-banned profiles, their own profile, or all profiles if they are moderators.
All client code should query this view instead of the profiles table directly.';