From 09c7903a9c73e3dc854df0fb46b8f5ff489784d0 Mon Sep 17 00:00:00 2001 From: Rodribm10 Date: Tue, 14 Apr 2026 22:24:15 -0300 Subject: [PATCH] fix: permissoes + RLS pro admin conseguir escrever nas tabelas de catalogo Antes: 'permission denied for table marcas' ao tentar criar/editar qualquer entidade. Authenticated so tinha SELECT. Agora: - Grants de INSERT/UPDATE/DELETE pra authenticated nas 9 tabelas - RLS enabled em todas - Helper function is_tenant_member(bigint) via security definer - Policies members_write_* permitem escrita apenas se user estiver no tenant_members do tenant_id da row - public_read_* mantem SELECT livre pro anon da pagina publica - Reservas/reserva_extras continuam service_role-only (backend Chatwoot) Aplicada via MCP. Testada com curl + JWT do admin (INSERT marcas = 201). --- .../20260414000005_admin_write_policies.sql | 137 ++++++++++++++++++ 1 file changed, 137 insertions(+) create mode 100644 supabase/migrations/20260414000005_admin_write_policies.sql diff --git a/supabase/migrations/20260414000005_admin_write_policies.sql b/supabase/migrations/20260414000005_admin_write_policies.sql new file mode 100644 index 0000000..145a1d0 --- /dev/null +++ b/supabase/migrations/20260414000005_admin_write_policies.sql @@ -0,0 +1,137 @@ +-- Admin write permissions + tenant-scoped RLS +-- Já aplicada via MCP. +-- +-- Antes desta migration, o authenticated (admin logado) só tinha SELECT nas +-- tabelas de catalogo. Qualquer CREATE/UPDATE/DELETE via Supabase JS retornava +-- "permission denied for table X". Agora: +-- 1. authenticated ganha INSERT/UPDATE/DELETE nas tabelas de catalogo +-- 2. RLS + policy filtra por tenant_members: so escreve se o user for +-- membro do tenant dono da row +-- 3. Reservas e reserva_extras continuam service_role-only (escritas pelo +-- backend Chatwoot controller) + +-- Helper: verifica se o usuario logado eh membro de um tenant +create or replace function reserva_hotel.is_tenant_member(check_tenant_id bigint) +returns boolean +language sql +security definer +stable +as $$ + select exists ( + select 1 from reserva_hotel.tenant_members tm + where tm.user_id = auth.uid() + and tm.tenant_id = check_tenant_id + ); +$$; + +grant execute on function reserva_hotel.is_tenant_member(bigint) to authenticated, anon; + +-- Pattern aplicado em cada tabela de catalogo: +-- alter table enable row level security +-- grant insert/update/delete to authenticated +-- policy public_read: select true +-- policy members_write: for all to authenticated using/with check is_tenant_member(tenant_id) + +-- MARCAS +alter table reserva_hotel.marcas enable row level security; +grant insert, update, delete on reserva_hotel.marcas to authenticated; +drop policy if exists "public_read_marcas" on reserva_hotel.marcas; +create policy "public_read_marcas" on reserva_hotel.marcas for select using (true); +drop policy if exists "members_write_marcas" on reserva_hotel.marcas; +create policy "members_write_marcas" on reserva_hotel.marcas + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- UNIDADES +alter table reserva_hotel.unidades enable row level security; +grant insert, update, delete on reserva_hotel.unidades to authenticated; +drop policy if exists "public_read_unidades" on reserva_hotel.unidades; +create policy "public_read_unidades" on reserva_hotel.unidades for select using (true); +drop policy if exists "members_write_unidades" on reserva_hotel.unidades; +create policy "members_write_unidades" on reserva_hotel.unidades + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- SUITES +alter table reserva_hotel.suites enable row level security; +grant insert, update, delete on reserva_hotel.suites to authenticated; +drop policy if exists "public_read_suites" on reserva_hotel.suites; +create policy "public_read_suites" on reserva_hotel.suites for select using (true); +drop policy if exists "members_write_suites" on reserva_hotel.suites; +create policy "members_write_suites" on reserva_hotel.suites + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- SUITES_UNIDADES (junction — sem tenant_id direto, resolve via unidade) +alter table reserva_hotel.suites_unidades enable row level security; +grant insert, update, delete on reserva_hotel.suites_unidades to authenticated; +drop policy if exists "public_read_suites_unidades" on reserva_hotel.suites_unidades; +create policy "public_read_suites_unidades" on reserva_hotel.suites_unidades for select using (true); +drop policy if exists "members_write_suites_unidades" on reserva_hotel.suites_unidades; +create policy "members_write_suites_unidades" on reserva_hotel.suites_unidades + for all to authenticated + using (exists ( + select 1 from reserva_hotel.unidades u + where u.id = suites_unidades.id_unidade + and reserva_hotel.is_tenant_member(u.tenant_id) + )) + with check (exists ( + select 1 from reserva_hotel.unidades u + where u.id = suites_unidades.id_unidade + and reserva_hotel.is_tenant_member(u.tenant_id) + )); + +-- CONTAS_PAGAMENTO +alter table reserva_hotel.contas_pagamento enable row level security; +grant insert, update, delete on reserva_hotel.contas_pagamento to authenticated; +drop policy if exists "public_read_contas_pagamento" on reserva_hotel.contas_pagamento; +create policy "public_read_contas_pagamento" on reserva_hotel.contas_pagamento for select using (true); +drop policy if exists "members_write_contas_pagamento" on reserva_hotel.contas_pagamento; +create policy "members_write_contas_pagamento" on reserva_hotel.contas_pagamento + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- PRECOS +alter table reserva_hotel.precos enable row level security; +grant insert, update, delete on reserva_hotel.precos to authenticated; +drop policy if exists "public_read_precos" on reserva_hotel.precos; +create policy "public_read_precos" on reserva_hotel.precos for select using (true); +drop policy if exists "members_write_precos" on reserva_hotel.precos; +create policy "members_write_precos" on reserva_hotel.precos + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- FOTOS_CATEGORIA +alter table reserva_hotel.fotos_categoria enable row level security; +grant insert, update, delete on reserva_hotel.fotos_categoria to authenticated; +drop policy if exists "public_read_fotos" on reserva_hotel.fotos_categoria; +create policy "public_read_fotos" on reserva_hotel.fotos_categoria for select using (true); +drop policy if exists "members_write_fotos" on reserva_hotel.fotos_categoria; +create policy "members_write_fotos" on reserva_hotel.fotos_categoria + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- EXTRAS +alter table reserva_hotel.extras enable row level security; +grant insert, update, delete on reserva_hotel.extras to authenticated; +drop policy if exists "public_read_extras" on reserva_hotel.extras; +create policy "public_read_extras" on reserva_hotel.extras for select using (true); +drop policy if exists "members_write_extras" on reserva_hotel.extras; +create policy "members_write_extras" on reserva_hotel.extras + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id)); + +-- APP_CONFIG (public SELECT ja existia via migration anterior) +grant insert, update, delete on reserva_hotel.app_config to authenticated; +drop policy if exists "members_write_app_config" on reserva_hotel.app_config; +create policy "members_write_app_config" on reserva_hotel.app_config + for all to authenticated + using (reserva_hotel.is_tenant_member(tenant_id)) + with check (reserva_hotel.is_tenant_member(tenant_id));