lección 6
Día 4 -- Row-level security y data masking post-incidente
Después del incidente de Javier, implementas Row-Level Security en PostgreSQL para que cada rol solo vea los datos que necesita. Además, configuras dynamic data masking para entornos de desarrollo y staging.
⏱ 45 min
### Jueves, 8:30 AM -- La urgencia de Rosa
Datasets de esta lección (en tu carpeta datos/): datos/pacientes.csv y datos/diagnosticos.csv (lo que cada rol puede o no ver), datos/profesionales.csv (columnas id, especialidad_id, especialidad_nombre, tipo — define qué médico ve a qué pacientes en las políticas RLS), datos/citas.csv y datos/lecturas_wearable.csv. Para el ejercicio de data masking trabajarás sobre las columnas PII de datos/pacientes.csv (nombre, email, telefono, dni, direccion).
Rosa te espera en la puerta de la oficina cuando llegas el jueves. Literalmente en la puerta, con un café en la mano y cara de no haber dormido bien. "He estado pensando toda la noche en lo de ayer. El audit trail funciona -- detectamos el incidente en 2 minutos. Pero detectar NO es prevenir. Necesito que HOY sea técnicamente IMPOSIBLE que lo de Javier se repita. No me vale confiar en que la gente no haga cosas -- necesito que el SISTEMA les impida hacerlas."
Laura llega justo detrás: "Row-Level Security. PostgreSQL lo soporta nativamente desde la versión 9.5. Básicamente defines políticas que filtran automáticamente las filas visibles según el rol del usuario. Un médico solo ve a SUS pacientes. Un analista ve datos agregados sin PII. Un junior de datos ve solo las tablas que tiene asignadas. Y un DPO ve todo pero con audit trail completo."
### Cómo funciona Row-Level Security en PostgreSQL
RLS funciona así: defines una POLITICA (policy) en una tabla que es una condición WHERE invisible. Cuando un usuario hace SELECT * FROM pacientes, PostgreSQL automáticamente añade un filtro extra según el rol del usuario. El usuario NO VE ese filtro -- simplemente le devuelve menos filas. Es transparente e inalterable desde el lado del cliente.
1-- Ejemplo simplificado de como funciona RLS23-- 1. Activar RLS en la tabla4ALTER TABLE pacientes ENABLE ROW LEVEL SECURITY;56-- 2. Forzar RLS incluso para el owner de la tabla7ALTER TABLE pacientes FORCE ROW LEVEL SECURITY;89-- 3. Definir politica: medicos solo ven a SUS pacientes10CREATE POLICY medico_ve_sus_pacientes ON pacientes11 FOR SELECT12 TO rol_medico13 USING (14 id IN (15 SELECT paciente_id FROM consultas16 WHERE medico_id = current_setting('app.current_user_id')17 )18 );1920-- 4. Definir politica: juniors NO ven la tabla pacientes directamente21-- (simplemente no creamos politica para rol_junior = 0 filas visibles)2223-- 5. Definir politica: DPO ve todo (para auditorías)24CREATE POLICY dpo_ve_todo ON pacientes25 FOR ALL26 TO rol_dpo27 USING (true); -- Sin restriccion2829-- Resultado:30-- Dr. Martin (rol_medico) hace: SELECT * FROM pacientes31-- -> Solo ve a los pacientes que ha atendido32-- Javier (rol_junior) hace: SELECT * FROM pacientes33-- -> Ve 0 filas (sin politica = acceso denegado)34-- Rosa (rol_dpo) hace: SELECT * FROM pacientes35-- -> Ve todo (para cumplir su función legal)
RLS: el filtro invisible que impide accesos no autorizados
Consejo de senior: FORCE ROW LEVEL SECURITY es crítico. Sin el FORCE, el owner de la tabla (normalmente el usuario de la aplicación) bypasea todas las políticas. Con FORCE, incluso el owner está sujeto a las políticas. Solo el superuser de PostgreSQL puede ignorar RLS -- y ese usuario no se usa desde la aplicación.
### Diseño de roles y políticas para MediTrack
Laura te ayuda a definir la matriz de acceso. Cada rol tiene un conjunto específico de permisos por tabla:
- rol_medico: SELECT en pacientes (solo los suyos), historiales (solo los suyos), prescripciones (las que él prescribió). INSERT/UPDATE en historiales y prescripciones de sus pacientes.
- rol_enfermero: SELECT en pacientes (los de su departamento), consultas (las de su turno). No ve historiales completos ni prescripciones.
- rol_analista: SELECT en consultas, wearables -- pero sin paciente_id visible (masking). No ve pacientes ni historiales directamente.
- rol_investigador: SELECT en vistas anonimizadas (zona gold). No accede a tablas base.
- rol_junior_de: SELECT en wearables y consultas para tareas de ETL asignadas. No ve pacientes ni historiales.
- rol_senior_de: SELECT en todas las tablas para mantenimiento. Con audit obligatorio y justificación.
- rol_dpo: SELECT en todo (función legal). Solo lectura. Con audit completo.
### Implementación completa de RLS
Laura te sienta a su lado y programáis juntos la implementación. "Lo hacemos tabla por tabla, probamos con cada rol y verificamos que las políticas funcionan antes de activar en producción. Si una política está mal y bloquea a un médico de ver a su paciente en una urgencia... eso es peor que no tener RLS."
1-- Implementacion RLS completa para MediTrack2-- Paso 1: Crear los roles de PostgreSQL34CREATE ROLE rol_medico;5CREATE ROLE rol_enfermero;6CREATE ROLE rol_analista;7CREATE ROLE rol_investigador;8CREATE ROLE rol_junior_de;9CREATE ROLE rol_senior_de;10CREATE ROLE rol_dpo;1112-- Paso 2: Permisos base (GRANT) -- lo que PUEDE hacer tecnicamente13GRANT SELECT ON pacientes TO rol_medico, rol_dpo, rol_senior_de;14GRANT SELECT, INSERT, UPDATE ON historiales TO rol_medico;15GRANT SELECT ON historiales TO rol_dpo, rol_senior_de;16GRANT SELECT, INSERT, UPDATE ON prescripciones TO rol_medico;17GRANT SELECT ON prescripciones TO rol_dpo, rol_senior_de;18GRANT SELECT ON consultas TO rol_medico, rol_enfermero, rol_analista, rol_junior_de;19GRANT SELECT ON wearables TO rol_analista, rol_junior_de, rol_senior_de;2021-- Paso 3: Activar RLS en TODAS las tablas sensibles22ALTER TABLE pacientes ENABLE ROW LEVEL SECURITY;23ALTER TABLE pacientes FORCE ROW LEVEL SECURITY;24ALTER TABLE historiales ENABLE ROW LEVEL SECURITY;25ALTER TABLE historiales FORCE ROW LEVEL SECURITY;26ALTER TABLE prescripciones ENABLE ROW LEVEL SECURITY;27ALTER TABLE prescripciones FORCE ROW LEVEL SECURITY;28ALTER TABLE consultas ENABLE ROW LEVEL SECURITY;29ALTER TABLE consultas FORCE ROW LEVEL SECURITY;30ALTER TABLE wearables ENABLE ROW LEVEL SECURITY;31ALTER TABLE wearables FORCE ROW LEVEL SECURITY;3233-- Paso 4: Politicas para tabla PACIENTES34CREATE POLICY pac_medico ON pacientes FOR SELECT TO rol_medico35 USING (id IN (36 SELECT DISTINCT paciente_id FROM consultas37 WHERE medico_id = current_setting('app.current_user_id')38 ));3940CREATE POLICY pac_dpo ON pacientes FOR SELECT TO rol_dpo41 USING (true);4243CREATE POLICY pac_senior_de ON pacientes FOR SELECT TO rol_senior_de44 USING (true); -- Acceso completo pero con audit obligatorio4546-- Paso 5: Politicas para tabla HISTORIALES47CREATE POLICY hist_medico ON historiales FOR ALL TO rol_medico48 USING (medico_id = current_setting('app.current_user_id')49 OR paciente_id IN (50 SELECT DISTINCT paciente_id FROM consultas51 WHERE medico_id = current_setting('app.current_user_id')52 ));5354CREATE POLICY hist_dpo ON historiales FOR SELECT TO rol_dpo55 USING (true);5657CREATE POLICY hist_senior_de ON historiales FOR SELECT TO rol_senior_de58 USING (true);5960-- Paso 6: Politicas para tabla CONSULTAS61CREATE POLICY cons_medico ON consultas FOR ALL TO rol_medico62 USING (medico_id = current_setting('app.current_user_id'));6364CREATE POLICY cons_enfermero ON consultas FOR SELECT TO rol_enfermero65 USING (66 fecha_hora::date = CURRENT_DATE -- Solo consultas de hoy67 AND tipo = 'video' -- Solo videoconsultas (su turno)68 );6970CREATE POLICY cons_analista ON consultas FOR SELECT TO rol_analista71 USING (true); -- Ve todas las consultas PERO con masking en paciente_id7273CREATE POLICY cons_junior_de ON consultas FOR SELECT TO rol_junior_de74 USING (true); -- Para ETL pero con masking7576-- Paso 7: Politicas para tabla WEARABLES77CREATE POLICY wear_analista ON wearables FOR SELECT TO rol_analista78 USING (true); -- Datos de telemetria sin restriccion de filas7980CREATE POLICY wear_junior_de ON wearables FOR SELECT TO rol_junior_de81 USING (true); -- Necesario para pipelines de ETL asignados
RLS completo: cada rol solo ve lo que necesita, por diseño
### Dynamic Data Masking para entornos no-producción
Laura añade un requisito más: "En desarrollo y staging necesitamos datos realistas para probar pipelines, pero NO pueden ser datos reales de pacientes. Necesitamos dynamic data masking: funciones que transforman los datos PII en valores ficticios pero con el mismo formato y distribución. Así los developers pueden trabajar sin ver datos reales."
1-- Funciones de data masking para entornos de desarrollo2-- Transforman PII en valores ficticios pero con formato válido34CREATE OR REPLACE FUNCTION mask_nombre(nombre TEXT) RETURNS TEXT AS $$5BEGIN6 -- Reemplaza por nombre ficticio basado en hash (determinista)7 RETURN CASE MOD(ABS(hashtext(nombre)), 10)8 WHEN 0 THEN 'Maria Garcia'9 WHEN 1 THEN 'Antonio Lopez'10 WHEN 2 THEN 'Carmen Martinez'11 WHEN 3 THEN 'Jose Rodriguez'12 WHEN 4 THEN 'Ana Fernandez'13 WHEN 5 THEN 'Francisco Gonzalez'14 WHEN 6 THEN 'Isabel Sanchez'15 WHEN 7 THEN 'Manuel Perez'16 WHEN 8 THEN 'Laura Diaz'17 WHEN 9 THEN 'David Moreno'18 END;19END;20$$ LANGUAGE plpgsql IMMUTABLE;2122CREATE OR REPLACE FUNCTION mask_email(email TEXT) RETURNS TEXT AS $$23BEGIN24 RETURN 'user_' || ABS(hashtext(email)) || '@ejemplo.test';25END;26$$ LANGUAGE plpgsql IMMUTABLE;2728CREATE OR REPLACE FUNCTION mask_telefono(tel TEXT) RETURNS TEXT AS $$29BEGIN30 RETURN '600' || LPAD(ABS(hashtext(tel))::text, 6, '0');31END;32$$ LANGUAGE plpgsql IMMUTABLE;3334CREATE OR REPLACE FUNCTION mask_dni(dni TEXT) RETURNS TEXT AS $$35BEGIN36 RETURN LPAD(ABS(hashtext(dni))::text, 8, '0') || 'X';37END;38$$ LANGUAGE plpgsql IMMUTABLE;3940-- Vista para desarrollo: misma estructura, datos enmascarados41CREATE OR REPLACE VIEW pacientes_dev AS42SELECT43 id,44 mask_nombre(nombre) AS nombre,45 mask_nombre(apellidos) AS apellidos,46 mask_dni(dni) AS dni,47 fecha_nacimiento + (RANDOM() * 30 - 15)::int AS fecha_nacimiento, -- +/- 15 días48 sexo,49 mask_email(email) AS email,50 mask_telefono(telefono) AS telefono,51 'Calle Ficticia ' || (RANDOM() * 100)::int AS dirección,52 codigo_postal, -- Se mantiene (útil para testing geografico)53 provincia,54 grupo_sanguineo,55 fecha_registro,56 estado57FROM pacientes;
Masking determinista: el mismo input siempre genera el mismo output ficticio
El data masking debe ser DETERMINISTA (mismo input = mismo output ficticio) para que las relaciones entre tablas se mantengan en desarrollo. Si mask_nombre("Carlos") siempre devuelve "Antonio Lopez", los JOINs siguen funcionando en staging. Si fuera aleatorio, los datos no serian coherentes entre tablas.
## ejercicios
Implementar politicas RLS completas
Escribe el DDL completo para implementar Row-Level Security en MediTrack: crear roles, activar RLS en todas las tablas, definir politicas por rol, y crear una función de test que verifique que cada rol ve solo lo que debe ver.
Pipeline de data masking para entornos de desarrollo
Implementa un pipeline Python que genera un dump enmascarado de la base de datos para entornos de desarrollo. Debe: 1) copiar la estructura, 2) aplicar masking determinista a todos los campos PII, 3) mantener relaciones entre tablas, 4) validar que no queda PII real en el output.
Tests de verificacion de politicas RLS
Escribe un script de tests que verifica que CADA rol ve exactamente lo que debe ver y NADA más. Incluye tests positivos (el medico ve a sus pacientes) y negativos (el junior NO ve historiales). Estos tests se ejecutan antes de cada deploy.
Regístrate para guardar tu progreso.
## comentarios
Reporta erratas, ayuda a otros o comparte tu opinión. Sé constructivo.
Inicia sesión para comentar y responder.
cargando comentarios...