-- ============================================================ -- Migration 003: RLS helper functions + policies -- -- Roles de la plataforma (via JWT custom claim "app_role"): -- 'org_admin' → empresa cliente: accede solo a agregados de su org, nunca filas crudas -- 'analyst' → análisis interno (InQ): accede vía service_role que bypasa RLS -- 'respondent' → rellenador de encuesta: puede insertar, no leer -- (no claim) → anon: puede leer encuestas live y consentimientos para rellenar el form -- -- CRÍTICO: ninguna policy devuelve filas crudas de responses/answers a org_admin -- El acceso de org_admin es SOLO vía la función agg_segment (migration 005) -- ============================================================ -- ============================================================ -- Funciones helper para leer JWT claims -- ============================================================ CREATE OR REPLACE FUNCTION public.current_app_role() RETURNS text LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT COALESCE(auth.jwt() ->> 'app_role', '') $$; CREATE OR REPLACE FUNCTION public.current_org_id() RETURNS uuid LANGUAGE sql STABLE SECURITY DEFINER SET search_path = public AS $$ SELECT NULLIF(auth.jwt() ->> 'org_id', '')::uuid $$; -- ============================================================ -- organizations -- org_admin ve solo su propia org; analyst usa service_role -- ============================================================ CREATE POLICY "org_select_own" ON public.organizations FOR SELECT TO authenticated USING ( (public.current_app_role() = 'org_admin' AND id = public.current_org_id()) OR public.current_app_role() = 'analyst' ); -- ============================================================ -- consent_versions -- Cualquiera (incluido anon) puede leer el texto de consentimiento -- para mostrarlo al respondente antes de que envíe. -- INSERT: solo via service_role (sin policy de INSERT para auth/anon) -- UPDATE/DELETE: bloqueados por trigger en migration 004 -- ============================================================ CREATE POLICY "consent_versions_select_all" ON public.consent_versions FOR SELECT USING (true); -- ============================================================ -- surveys -- anon: solo encuestas live (para rellenar el formulario) -- org_admin: todas sus encuestas en cualquier estado -- analyst: service_role (bypasa RLS) -- ============================================================ CREATE POLICY "surveys_select_live_anon" ON public.surveys FOR SELECT TO anon USING (status = 'live'); CREATE POLICY "surveys_select_auth" ON public.surveys FOR SELECT TO authenticated USING ( public.current_app_role() = 'analyst' OR ( public.current_app_role() = 'org_admin' AND org_id = public.current_org_id() ) ); -- ============================================================ -- questions -- anon: preguntas de encuestas live (necesario para mostrar el form) -- org_admin: preguntas de sus encuestas -- ============================================================ CREATE POLICY "questions_select_live" ON public.questions FOR SELECT TO anon USING ( EXISTS ( SELECT 1 FROM public.surveys s WHERE s.id = survey_id AND s.status = 'live' ) ); CREATE POLICY "questions_select_auth" ON public.questions FOR SELECT TO authenticated USING ( public.current_app_role() = 'analyst' OR ( public.current_app_role() = 'org_admin' AND EXISTS ( SELECT 1 FROM public.surveys s WHERE s.id = survey_id AND s.org_id = public.current_org_id() ) ) ); -- ============================================================ -- consent_records -- anon/authenticated pueden INSERT al enviar el formulario -- SELECT: solo service_role (analyst interno) -- UPDATE/DELETE: bloqueados por trigger en migration 004 -- ============================================================ CREATE POLICY "consent_records_insert" ON public.consent_records FOR INSERT TO anon, authenticated WITH CHECK (true); -- ============================================================ -- responses -- CRÍTICO: NO hay policy SELECT para org_admin ni anon. -- Resultado: org_admin recibe 0 filas en SELECT directo → solo puede -- acceder mediante agg_segment, que aplica k_threshold. -- INSERT: permitido a anon/authenticated solo para encuestas live. -- ============================================================ CREATE POLICY "responses_insert" ON public.responses FOR INSERT TO anon, authenticated WITH CHECK ( EXISTS ( SELECT 1 FROM public.surveys s WHERE s.id = survey_id AND s.status = 'live' ) ); -- ============================================================ -- answers -- Mismo patrón que responses: INSERT permitido, 0 SELECT para org_admin. -- El texto libre (type='text_long') no se expone en agg_segment (regla #4). -- ============================================================ CREATE POLICY "answers_insert" ON public.answers FOR INSERT TO anon, authenticated WITH CHECK ( EXISTS ( SELECT 1 FROM public.responses r WHERE r.id = response_id ) );