Files
braidsbymira/supabase/tests/database/rls.test.sql
belviskhoremk aacb9dca12 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
2026-08-08 13:08:55 +00:00

106 lines
3.3 KiB
PL/PgSQL

begin;
select plan(12);
-- Seed reference data as service_role (bypasses RLS)
set local role service_role;
insert into public.services (id, name, duration_minutes, price_from, description, image)
values ('knotless', 'Knotless Braids', 240, 150, 'Tension-free braids.', 'styles/knotless.jpg');
insert into public.products (id, name, category, price, image, description, in_stock)
values ('edge-control', 'Silk Edge Control', 'hair-care', 12, 'products/edge-control.jpg', 'Hold with shine.', true);
reset role;
-- Anyone can read services
set local role anon;
select isnt_empty(
$$ select * from public.services $$,
'anon can read services'
);
-- Anyone can read products
select isnt_empty(
$$ select * from public.products $$,
'anon can read products'
);
-- Anon cannot write to services
select throws_ok(
$$ insert into public.services (id, name, duration_minutes, price_from, description, image)
values ('hack', 'Hack', 1, 1, 'x', 'x') $$,
'42501',
null,
'anon cannot insert into services'
);
-- Anyone can create a booking
select lives_ok(
$$ insert into public.bookings (service_id, date, time, contact_name, contact_phone, contact_email, contact_notes)
values ('knotless', '2026-09-15', '10:00 AM', 'Jane Doe', '555-0100', 'jane@example.com', '') $$,
'anon can create a booking'
);
-- 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)
values ('edge-control', 'Jane Doe', 'jane@example.com', 'Is this in stock?') $$,
'anon can create an inquiry'
);
-- 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'
);
-- Anyone can read booked_slots (no PII there)
select isnt_empty(
$$ select * from public.booked_slots $$,
'anon can read booked_slots (populated by the trigger from the booking above)'
);
-- Anon cannot write booked_slots directly
select throws_ok(
$$ insert into public.booked_slots (date, time) values ('2026-09-16', '1:00 PM') $$,
'42501',
null,
'anon cannot insert into booked_slots directly'
);
-- The admin (matching UID) can read bookings
set local role authenticated;
set local request.jwt.claim.sub = '00000000-0000-0000-0000-000000000001';
select isnt_empty(
$$ select * from public.bookings $$,
'admin can read bookings'
);
-- The admin can write to services
select lives_ok(
$$ insert into public.services (id, name, duration_minutes, price_from, description, image)
values ('cornrows', 'Cornrows', 120, 90, 'Classic style.', 'styles/cornrows.jpg') $$,
'admin can insert into services'
);
select * from finish();
rollback;