fix: scope GRANT statements to match actual RLS-permitted access per role
Replace the blanket select/insert/update/delete grant to anon/authenticated/ service_role with privilege-scoped grants: anon/authenticated get select-only on the public-read tables plus insert-only on bookings/inquiries; full CRUD is reserved for authenticated (narrowed to the admin UID by RLS) and service_role. Table grants are now a real second layer of defense instead of mirroring RLS. Updates two pgTAP assertions whose blocking mechanism moved from RLS-filtered-empty to grant-level permission-denied, and adds a new assertion proving anon cannot UPDATE bookings. Claude-Session: https://claude.ai/code/session_01NhxA4npAXAxmCDEJt6kxSH
This commit is contained in:
@@ -3,8 +3,32 @@
|
||||
-- anon/authenticated/service_role, so without these GRANTs the RLS policies
|
||||
-- from the initial schema migration are unreachable (every request fails at
|
||||
-- the privilege check before RLS is even evaluated). This is the documented
|
||||
-- "explicit grants + RLS" pattern Supabase recommends going forward; the
|
||||
-- actual access is still fully governed by each table's RLS policies.
|
||||
-- "explicit grants + RLS" pattern Supabase recommends going forward.
|
||||
--
|
||||
-- Unlike the previous blanket grant, these privileges are scoped to match
|
||||
-- what each role's RLS policies actually permit, so table-level GRANTs are a
|
||||
-- real second layer of defense rather than a no-op rubber stamp on top of
|
||||
-- RLS: anon/authenticated can only select the public-read tables and insert
|
||||
-- into bookings/inquiries; only authenticated (narrowed further by RLS's
|
||||
-- admin-only policies to the single admin UID) gets full read/write on
|
||||
-- everything. service_role still needs an explicit full grant here: table
|
||||
-- privileges and RLS are two independent checks in Postgres — service_role's
|
||||
-- BYPASSRLS role attribute only skips the RLS policy check, it does not
|
||||
-- substitute for the base GRANT privilege check, so without this it cannot
|
||||
-- touch these tables at all (e.g. the test suite's service_role seed step).
|
||||
grant select on
|
||||
public.products,
|
||||
public.services,
|
||||
public.schedule_template,
|
||||
public.schedule_exceptions,
|
||||
public.booked_slots
|
||||
to anon, authenticated;
|
||||
|
||||
grant insert on
|
||||
public.bookings,
|
||||
public.inquiries
|
||||
to anon, authenticated;
|
||||
|
||||
grant select, insert, update, delete on
|
||||
public.products,
|
||||
public.services,
|
||||
@@ -13,7 +37,7 @@ grant select, insert, update, delete on
|
||||
public.bookings,
|
||||
public.booked_slots,
|
||||
public.inquiries
|
||||
to anon, authenticated, service_role;
|
||||
to authenticated, service_role;
|
||||
|
||||
-- Keeps the public, PII-free booked_slots table in sync whenever a booking
|
||||
-- is created. security definer + explicit search_path per Supabase's
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
begin;
|
||||
select plan(11);
|
||||
select plan(12);
|
||||
|
||||
-- Seed reference data as service_role (bypasses RLS)
|
||||
set local role service_role;
|
||||
@@ -38,12 +38,24 @@ select lives_ok(
|
||||
'anon can create a booking'
|
||||
);
|
||||
|
||||
-- Anon cannot read bookings (PII protection)
|
||||
select is_empty(
|
||||
-- Anon cannot read bookings (PII protection). No SELECT privilege is granted
|
||||
-- to anon on this table at all now, so this is blocked at the grant layer
|
||||
-- (permission denied) rather than merely filtered to empty rows by RLS.
|
||||
select throws_ok(
|
||||
$$ select * from public.bookings $$,
|
||||
'42501',
|
||||
null,
|
||||
'anon cannot read bookings'
|
||||
);
|
||||
|
||||
-- Anon cannot update bookings (no UPDATE privilege granted, regardless of RLS)
|
||||
select throws_ok(
|
||||
$$ update public.bookings set contact_notes = 'hacked' $$,
|
||||
'42501',
|
||||
null,
|
||||
'anon cannot update bookings'
|
||||
);
|
||||
|
||||
-- Anyone can create an inquiry
|
||||
select lives_ok(
|
||||
$$ insert into public.inquiries (product_id, contact_name, contact_email, contact_notes)
|
||||
@@ -51,9 +63,12 @@ select lives_ok(
|
||||
'anon can create an inquiry'
|
||||
);
|
||||
|
||||
-- Anon cannot read inquiries (PII protection)
|
||||
select is_empty(
|
||||
-- Anon cannot read inquiries (PII protection). Same as bookings above: no
|
||||
-- SELECT privilege is granted, so this fails at the grant layer.
|
||||
select throws_ok(
|
||||
$$ select * from public.inquiries $$,
|
||||
'42501',
|
||||
null,
|
||||
'anon cannot read inquiries'
|
||||
);
|
||||
|
||||
|
||||
Reference in New Issue
Block a user