mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 06:51:12 -05:00
Fix profile data exposure
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
-- 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.';
|
||||
Reference in New Issue
Block a user