-- ============================================================================= -- Migration: 20260418000005_saas_rls_emergency_fix -- Corrige A#30 (P0) — 7 tabelas SaaS estavam com RLS desabilitado + grants -- totais pra anon/authenticated/service_role. Qualquer usuário anônimo -- podia alterar/deletar dados críticos (tenant_features, plan_prices, -- subscription_intents_personal/tenant, plan_public, ...). -- -- Estratégia: -- 1. Habilitar RLS em todas as 7 tabelas -- 2. REVOKE ALL de anon (nunca deveria ter tido) -- 3. REVOKE ALL de authenticated (controle passa a ser via policy) -- 4. Policies explícitas por caso de uso -- ============================================================================= -- ───────────────────────────────────────────────────────────────────────── -- 1. REVOKE grants inseguros -- ----------------------------------------------------------------------------- REVOKE ALL ON public.tenant_features FROM anon, authenticated; REVOKE ALL ON public.plan_prices FROM anon, authenticated; REVOKE ALL ON public.plan_public FROM anon, authenticated; REVOKE ALL ON public.plan_public_bullets FROM anon, authenticated; REVOKE ALL ON public.subscription_intents_personal FROM anon, authenticated; REVOKE ALL ON public.subscription_intents_tenant FROM anon, authenticated; REVOKE ALL ON public.tenant_feature_exceptions_log FROM anon, authenticated; -- Concede o mínimo necessário (controlado por RLS abaixo) GRANT SELECT, INSERT, UPDATE, DELETE ON public.tenant_features TO authenticated; GRANT SELECT, INSERT, UPDATE, DELETE ON public.plan_prices TO authenticated; GRANT SELECT ON public.plan_public TO anon, authenticated; GRANT INSERT, UPDATE, DELETE ON public.plan_public TO authenticated; GRANT SELECT ON public.plan_public_bullets TO anon, authenticated; GRANT INSERT, UPDATE, DELETE ON public.plan_public_bullets TO authenticated; GRANT SELECT, INSERT, UPDATE, DELETE ON public.subscription_intents_personal TO authenticated; GRANT SELECT, INSERT, UPDATE, DELETE ON public.subscription_intents_tenant TO authenticated; GRANT SELECT ON public.tenant_feature_exceptions_log TO authenticated; -- ───────────────────────────────────────────────────────────────────────── -- 2. HABILITAR RLS em todas -- ----------------------------------------------------------------------------- ALTER TABLE public.tenant_features ENABLE ROW LEVEL SECURITY; ALTER TABLE public.plan_prices ENABLE ROW LEVEL SECURITY; ALTER TABLE public.plan_public ENABLE ROW LEVEL SECURITY; ALTER TABLE public.plan_public_bullets ENABLE ROW LEVEL SECURITY; ALTER TABLE public.subscription_intents_personal ENABLE ROW LEVEL SECURITY; ALTER TABLE public.subscription_intents_tenant ENABLE ROW LEVEL SECURITY; ALTER TABLE public.tenant_feature_exceptions_log ENABLE ROW LEVEL SECURITY; -- ───────────────────────────────────────────────────────────────────────── -- 3. POLICIES — tenant_features -- ----------------------------------------------------------------------------- -- SELECT: membros do tenant leem as features do próprio tenant. Saas admin lê tudo. DROP POLICY IF EXISTS tenant_features_select ON public.tenant_features; CREATE POLICY tenant_features_select ON public.tenant_features FOR SELECT TO authenticated USING ( public.is_saas_admin() OR tenant_id IN (SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active') ); -- WRITE: apenas tenant_admin do próprio tenant OU saas_admin. DROP POLICY IF EXISTS tenant_features_write ON public.tenant_features; CREATE POLICY tenant_features_write ON public.tenant_features FOR ALL TO authenticated USING ( public.is_saas_admin() OR tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' AND tm.role IN ('tenant_admin','admin') ) ) WITH CHECK ( public.is_saas_admin() OR tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' AND tm.role IN ('tenant_admin','admin') ) ); -- ───────────────────────────────────────────────────────────────────────── -- 4. POLICIES — plan_prices (SaaS admin only pra escrita; authenticated lê) -- ----------------------------------------------------------------------------- DROP POLICY IF EXISTS plan_prices_read ON public.plan_prices; CREATE POLICY plan_prices_read ON public.plan_prices FOR SELECT TO authenticated USING (true); -- preços são públicos pra usuários logados DROP POLICY IF EXISTS plan_prices_write ON public.plan_prices; CREATE POLICY plan_prices_write ON public.plan_prices FOR ALL TO authenticated USING (public.is_saas_admin()) WITH CHECK (public.is_saas_admin()); -- ───────────────────────────────────────────────────────────────────────── -- 5. POLICIES — plan_public + plan_public_bullets (anon pode ler — landing page) -- ----------------------------------------------------------------------------- DROP POLICY IF EXISTS plan_public_read_anon ON public.plan_public; CREATE POLICY plan_public_read_anon ON public.plan_public FOR SELECT TO anon, authenticated USING (true); DROP POLICY IF EXISTS plan_public_write ON public.plan_public; CREATE POLICY plan_public_write ON public.plan_public FOR ALL TO authenticated USING (public.is_saas_admin()) WITH CHECK (public.is_saas_admin()); DROP POLICY IF EXISTS plan_public_bullets_read_anon ON public.plan_public_bullets; CREATE POLICY plan_public_bullets_read_anon ON public.plan_public_bullets FOR SELECT TO anon, authenticated USING (true); DROP POLICY IF EXISTS plan_public_bullets_write ON public.plan_public_bullets; CREATE POLICY plan_public_bullets_write ON public.plan_public_bullets FOR ALL TO authenticated USING (public.is_saas_admin()) WITH CHECK (public.is_saas_admin()); -- ───────────────────────────────────────────────────────────────────────── -- 6. POLICIES — subscription_intents_personal + _tenant -- ----------------------------------------------------------------------------- -- Dono vê o próprio intent; saas admin vê tudo; owner cria/atualiza seus próprios. DROP POLICY IF EXISTS subscription_intents_personal_owner ON public.subscription_intents_personal; CREATE POLICY subscription_intents_personal_owner ON public.subscription_intents_personal FOR ALL TO authenticated USING (user_id = auth.uid() OR public.is_saas_admin()) WITH CHECK (user_id = auth.uid() OR public.is_saas_admin()); DROP POLICY IF EXISTS subscription_intents_tenant_member ON public.subscription_intents_tenant; CREATE POLICY subscription_intents_tenant_member ON public.subscription_intents_tenant FOR ALL TO authenticated USING ( public.is_saas_admin() OR tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' AND tm.role IN ('tenant_admin','admin') ) ) WITH CHECK ( public.is_saas_admin() OR tenant_id IN ( SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active' AND tm.role IN ('tenant_admin','admin') ) ); -- ───────────────────────────────────────────────────────────────────────── -- 7. POLICY — tenant_feature_exceptions_log (somente leitura) -- ----------------------------------------------------------------------------- -- Log de auditoria. Inserts vêm de triggers/funções server-side (SECURITY DEFINER). DROP POLICY IF EXISTS tenant_feature_exceptions_log_read ON public.tenant_feature_exceptions_log; CREATE POLICY tenant_feature_exceptions_log_read ON public.tenant_feature_exceptions_log FOR SELECT TO authenticated USING ( public.is_saas_admin() OR tenant_id IN (SELECT tm.tenant_id FROM public.tenant_members tm WHERE tm.user_id = auth.uid() AND tm.status = 'active') ); COMMENT ON TABLE public.tenant_features IS 'Controle de features por tenant. RLS: member do tenant lê; tenant_admin ou saas_admin escreve. Antes da migration 20260418000005 estava com RLS off + GRANT ALL pra anon (A#30).';