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
106 lines
3.3 KiB
PL/PgSQL
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;
|