Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions supabase/migrations/20260516000000_configure_storage_rls_policies.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
-- ================================================================
-- Configure Storage RLS for 360ghar-storage Bucket
--
-- Flutter uploads directly to Supabase Storage using:
-- bucket: 360ghar-storage
-- paths:
-- users/{uid}/listings/{filename} — listing photos & video tours
-- users/{uid}/profile/{filename} — profile photos
-- users/{uid}/chats/{filename} — chat photos
--
-- After uploading, Flutter calls createSignedUrl (requires SELECT).
-- Backend uses service role key which bypasses RLS entirely.
--
-- ⚠️ Run this via the Supabase Dashboard SQL Editor
-- (SQL migrations lack owner permissions on storage.objects).
Comment on lines +14 to +15
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | 🏗️ Heavy lift

Don't ship this as a manual-only migration.

Documenting an out-of-band SQL Editor step here makes the change non-reproducible and easy to miss in later environments. Please rework this so the RLS policies are applied by the normal migration path instead of requiring a separate manual run after merge.

As per coding guidelines, supabase/migrations/**/*: All migrations must be placed in supabase/migrations/ and applied via supabase db push

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@supabase/migrations/20260516000000_configure_storage_rls_policies.sql` around
lines 14 - 15, Remove the manual-only note and convert the out-of-band step into
a proper SQL migration under supabase/migrations/ (so it runs via supabase db
push); add statements to enable RLS and create/replace the necessary policies
for the storage.objects table (e.g., ALTER TABLE storage.objects ENABLE ROW
LEVEL SECURITY; CREATE OR REPLACE POLICY "<policy_name>" ON storage.objects FOR
SELECT USING (<condition>) WITH CHECK (<condition>); and any required GRANTs),
ensuring the migration file name follows the existing timestamped pattern and
contains the full DDL/DDL POLICY changes so no manual Dashboard SQL Editor step
is required.

-- ================================================================

-- ── Ensure RLS is enabled ──────────────────────────────────────
ALTER TABLE storage.objects ENABLE ROW LEVEL SECURITY;

-- ================================================================
-- 1. BROAD OWN-FOLDER POLICIES (covers listings, profile, chats)
-- Pattern: users/{auth.uid()}/*
-- ================================================================

-- ── INSERT: authenticated users can upload into their own folder ──
DROP POLICY IF EXISTS "users_insert_own" ON storage.objects;
CREATE POLICY "users_insert_own"
ON storage.objects FOR INSERT TO authenticated
WITH CHECK (
bucket_id = '360ghar-storage'
AND auth.uid() IS NOT NULL
AND name ~ ('^users/' || auth.uid()::text || '/[^/]+/[^/]+$')
);

-- ── SELECT: authenticated users can read their own files ──
DROP POLICY IF EXISTS "users_select_own" ON storage.objects;
CREATE POLICY "users_select_own"
ON storage.objects FOR SELECT TO authenticated
USING (
bucket_id = '360ghar-storage'
AND auth.uid() IS NOT NULL
AND name ~ ('^users/' || auth.uid()::text || '/[^/]+/[^/]+$')
);

-- ── UPDATE: authenticated users can update their own files ──
DROP POLICY IF EXISTS "users_update_own" ON storage.objects;
Comment on lines +40 to +47
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Missing cross-user SELECT for chat photos

The users_select_own policy only grants SELECT to the file owner (auth.uid() matches). When a chat recipient tries to display a received photo at users/{sender_uid}/chats/{filename}, they get an RLS denial because their UID doesn't match. An authenticated_read_chats policy (mirroring authenticated_read_listings) is needed unless all chat photo reads are exclusively mediated by backend-generated signed URLs (service role bypasses RLS). The PR description and existing SELECT comment say Flutter calls createSignedUrl itself, which also requires SELECT on the object.

CREATE POLICY "users_update_own"
ON storage.objects FOR UPDATE TO authenticated
USING (
bucket_id = '360ghar-storage'
AND auth.uid() IS NOT NULL
AND name ~ ('^users/' || auth.uid()::text || '/[^/]+/[^/]+$')
)
WITH CHECK (
bucket_id = '360ghar-storage'
AND auth.uid() IS NOT NULL
AND name ~ ('^users/' || auth.uid()::text || '/[^/]+/[^/]+$')
);

-- ── DELETE: authenticated users can delete their own files ──
DROP POLICY IF EXISTS "users_delete_own" ON storage.objects;
CREATE POLICY "users_delete_own"
ON storage.objects FOR DELETE TO authenticated
USING (
bucket_id = '360ghar-storage'
AND auth.uid() IS NOT NULL
Comment on lines +66 to +67
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 security 10-year signed URL expiry is functionally a permanent public link

The comment "signed URLs (10-year expiry) as a fallback" describes a URL that effectively never expires. If one of these URLs leaks via logs, browser history, a forwarded message, or a bug that exposes the URL in an API response, the file remains accessible indefinitely to anyone with the URL, defeating the private bucket and the RLS policies. Consider using much shorter expiries (hours or days) with a backend endpoint to regenerate signed URLs on demand.

AND name ~ ('^users/' || auth.uid()::text || '/[^/]+/[^/]+$')
);

-- ================================================================
-- 2. CROSS-USER READ: authenticated users can read listing photos
-- (so other users see listing images when browsing).
-- Uses signed URLs (10-year expiry) as a fallback, but this
-- policy allows direct reads too.
-- ================================================================

DROP POLICY IF EXISTS "authenticated_read_listings" ON storage.objects;
CREATE POLICY "authenticated_read_listings"
ON storage.objects FOR SELECT TO authenticated
USING (
bucket_id = '360ghar-storage'
AND name ~ '^users/[^/]+/listings/[^/]+$'
);

-- ================================================================
-- 3. PUBLIC READ: agent avatars (already documented in bucket config)
-- ================================================================

DROP POLICY IF EXISTS "public_read_agent_avatars" ON storage.objects;
CREATE POLICY "public_read_agent_avatars"
ON storage.objects FOR SELECT TO anon, authenticated
USING (
bucket_id = '360ghar-storage'
AND name ~ '^agents/[0-9]+/avatars/[^/]+$'
);

-- ================================================================
-- 4. AUTHENTICATED READ: tour content
-- ================================================================

DROP POLICY IF EXISTS "authenticated_read_tours" ON storage.objects;
CREATE POLICY "authenticated_read_tours"
ON storage.objects FOR SELECT TO authenticated
USING (
bucket_id = '360ghar-storage'
AND name ~ '^users/[^/]+/tours/[^/]+$'
);

-- ================================================================
-- Cleanup: drop the old narrow policies if they existed
-- ================================================================

DROP POLICY IF EXISTS "flatmates_listing_photos_insert_own" ON storage.objects;
DROP POLICY IF EXISTS "flatmates_listing_photos_select_own" ON storage.objects;
Loading