Primer commit - Sprint 1

This commit is contained in:
markosbenitez
2026-05-31 04:33:07 -03:00
commit ef9e4bf998
22 changed files with 2515 additions and 0 deletions

View File

@@ -0,0 +1,148 @@
-- ============================================================
-- 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
)
);