149 lines
5.3 KiB
PL/PgSQL
149 lines
5.3 KiB
PL/PgSQL
-- ============================================================
|
|
-- 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
|
|
)
|
|
);
|