Primer commit - Sprint 1
This commit is contained in:
148
supabase/migrations/20260531000003_rls_policies.sql
Normal file
148
supabase/migrations/20260531000003_rls_policies.sql
Normal 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
|
||||
)
|
||||
);
|
||||
Reference in New Issue
Block a user