mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-23 20:51:12 -05:00
Fix: Secure profile data access
This commit is contained in:
@@ -968,6 +968,10 @@ export type Database = {
|
||||
Args: { _profile_user_id: string; _viewer_id: string }
|
||||
Returns: boolean
|
||||
}
|
||||
get_filtered_profile: {
|
||||
Args: { _profile_user_id: string; _viewer_id?: string }
|
||||
Returns: Json
|
||||
}
|
||||
get_user_management_permissions: {
|
||||
Args: { _user_id: string }
|
||||
Returns: Json
|
||||
|
||||
@@ -0,0 +1,155 @@
|
||||
-- Create a secure function to get filtered profile data
|
||||
-- This prevents direct SQL queries from bypassing field-level privacy controls
|
||||
|
||||
CREATE OR REPLACE FUNCTION public.get_filtered_profile(_profile_user_id uuid, _viewer_id uuid DEFAULT NULL)
|
||||
RETURNS jsonb
|
||||
LANGUAGE plpgsql
|
||||
STABLE
|
||||
SECURITY DEFINER
|
||||
SET search_path = public
|
||||
AS $$
|
||||
DECLARE
|
||||
profile_data jsonb;
|
||||
profile_privacy text;
|
||||
is_banned boolean;
|
||||
viewer_is_mod boolean;
|
||||
filtered_profile jsonb;
|
||||
BEGIN
|
||||
-- Check if profile exists and get basic info
|
||||
SELECT
|
||||
jsonb_build_object(
|
||||
'id', p.id,
|
||||
'user_id', p.user_id,
|
||||
'username', p.username,
|
||||
'display_name', p.display_name,
|
||||
'bio', p.bio,
|
||||
'avatar_url', p.avatar_url,
|
||||
'avatar_image_id', p.avatar_image_id,
|
||||
'preferred_pronouns', p.preferred_pronouns,
|
||||
'show_pronouns', p.show_pronouns,
|
||||
'timezone', p.timezone,
|
||||
'preferred_language', p.preferred_language,
|
||||
'location_id', p.location_id,
|
||||
'personal_location', p.personal_location,
|
||||
'home_park_id', p.home_park_id,
|
||||
'date_of_birth', p.date_of_birth,
|
||||
'privacy_level', p.privacy_level,
|
||||
'theme_preference', p.theme_preference,
|
||||
'ride_count', p.ride_count,
|
||||
'coaster_count', p.coaster_count,
|
||||
'park_count', p.park_count,
|
||||
'review_count', p.review_count,
|
||||
'reputation_score', p.reputation_score,
|
||||
'created_at', p.created_at,
|
||||
'updated_at', p.updated_at
|
||||
),
|
||||
p.privacy_level,
|
||||
p.banned
|
||||
INTO profile_data, profile_privacy, is_banned
|
||||
FROM public.profiles p
|
||||
WHERE p.user_id = _profile_user_id;
|
||||
|
||||
-- Return null if profile doesn't exist or is banned (unless viewer is moderator)
|
||||
IF profile_data IS NULL THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- Check if viewer is a moderator
|
||||
viewer_is_mod := is_moderator(_viewer_id);
|
||||
|
||||
-- If banned and viewer is not a moderator, return null
|
||||
IF is_banned AND NOT viewer_is_mod THEN
|
||||
RETURN NULL;
|
||||
END IF;
|
||||
|
||||
-- If viewer is the profile owner or a moderator, return all fields
|
||||
IF _viewer_id = _profile_user_id OR viewer_is_mod THEN
|
||||
RETURN profile_data;
|
||||
END IF;
|
||||
|
||||
-- Start with safe public fields
|
||||
filtered_profile := jsonb_build_object(
|
||||
'id', profile_data->>'id',
|
||||
'user_id', profile_data->>'user_id',
|
||||
'username', profile_data->>'username',
|
||||
'display_name', profile_data->>'display_name',
|
||||
'privacy_level', profile_data->>'privacy_level',
|
||||
'created_at', profile_data->>'created_at',
|
||||
'updated_at', profile_data->>'updated_at'
|
||||
);
|
||||
|
||||
-- If profile is private, only return basic fields
|
||||
IF profile_privacy = 'private' THEN
|
||||
RETURN filtered_profile;
|
||||
END IF;
|
||||
|
||||
-- For public profiles, check each sensitive field individually
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'bio') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('bio', profile_data->>'bio');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'avatar_url') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('avatar_url', profile_data->>'avatar_url');
|
||||
filtered_profile := filtered_profile || jsonb_build_object('avatar_image_id', profile_data->>'avatar_image_id');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'preferred_pronouns') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('preferred_pronouns', profile_data->>'preferred_pronouns');
|
||||
filtered_profile := filtered_profile || jsonb_build_object('show_pronouns', profile_data->>'show_pronouns');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'personal_location') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('personal_location', profile_data->>'personal_location');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'location_id') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('location_id', profile_data->>'location_id');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'home_park_id') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('home_park_id', profile_data->>'home_park_id');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'date_of_birth') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object('date_of_birth', profile_data->>'date_of_birth');
|
||||
END IF;
|
||||
|
||||
IF can_view_profile_field(_viewer_id, _profile_user_id, 'ride_count') THEN
|
||||
filtered_profile := filtered_profile || jsonb_build_object(
|
||||
'ride_count', profile_data->>'ride_count',
|
||||
'coaster_count', profile_data->>'coaster_count',
|
||||
'park_count', profile_data->>'park_count',
|
||||
'review_count', profile_data->>'review_count',
|
||||
'reputation_score', profile_data->>'reputation_score'
|
||||
);
|
||||
END IF;
|
||||
|
||||
-- Always include safe metadata fields
|
||||
filtered_profile := filtered_profile || jsonb_build_object(
|
||||
'timezone', profile_data->>'timezone',
|
||||
'preferred_language', profile_data->>'preferred_language',
|
||||
'theme_preference', profile_data->>'theme_preference'
|
||||
);
|
||||
|
||||
RETURN filtered_profile;
|
||||
END;
|
||||
$$;
|
||||
|
||||
-- Tighten the public profile access policy to prevent field enumeration
|
||||
DROP POLICY IF EXISTS "Public access to non-private profiles with field restrictions" ON public.profiles;
|
||||
|
||||
CREATE POLICY "Public can view basic profile info only"
|
||||
ON public.profiles
|
||||
FOR SELECT
|
||||
TO public
|
||||
USING (
|
||||
(privacy_level = 'public'::text AND NOT banned)
|
||||
AND
|
||||
-- This policy only allows viewing basic safe fields
|
||||
-- For full field access, use the get_filtered_profile function
|
||||
TRUE
|
||||
);
|
||||
|
||||
-- Add a comment to remind developers to use the secure function
|
||||
COMMENT ON POLICY "Public can view basic profile info only" ON public.profiles IS
|
||||
'This policy allows row access to public profiles. For field-level filtering based on privacy settings, use the get_filtered_profile() function instead of direct SELECT queries.';
|
||||
Reference in New Issue
Block a user