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 RLS
2
3-- 1. Activar RLS en la tabla
4ALTER TABLE pacientes ENABLE ROW LEVEL SECURITY;
5
6-- 2. Forzar RLS incluso para el owner de la tabla
7ALTER TABLE pacientes FORCE ROW LEVEL SECURITY;
8
9-- 3. Definir politica: medicos solo ven a SUS pacientes
10CREATE POLICY medico_ve_sus_pacientes ON pacientes
11 FOR SELECT
12 TO rol_medico
13 USING (
14 id IN (
15 SELECT paciente_id FROM consultas
16 WHERE medico_id = current_setting('app.current_user_id')
17 )
18 );
19
20-- 4. Definir politica: juniors NO ven la tabla pacientes directamente
21-- (simplemente no creamos politica para rol_junior = 0 filas visibles)
22
23-- 5. Definir politica: DPO ve todo (para auditorías)
24CREATE POLICY dpo_ve_todo ON pacientes
25 FOR ALL
26 TO rol_dpo
27 USING (true); -- Sin restriccion
28
29-- Resultado:
30-- Dr. Martin (rol_medico) hace: SELECT * FROM pacientes
31-- -> Solo ve a los pacientes que ha atendido
32-- Javier (rol_junior) hace: SELECT * FROM pacientes
33-- -> Ve 0 filas (sin politica = acceso denegado)
34-- Rosa (rol_dpo) hace: SELECT * FROM pacientes
35-- -> 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 MediTrack
2-- Paso 1: Crear los roles de PostgreSQL
3
4CREATE 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;
11
12-- Paso 2: Permisos base (GRANT) -- lo que PUEDE hacer tecnicamente
13GRANT 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;
20
21-- Paso 3: Activar RLS en TODAS las tablas sensibles
22ALTER 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;
32
33-- Paso 4: Politicas para tabla PACIENTES
34CREATE POLICY pac_medico ON pacientes FOR SELECT TO rol_medico
35 USING (id IN (
36 SELECT DISTINCT paciente_id FROM consultas
37 WHERE medico_id = current_setting('app.current_user_id')
38 ));
39
40CREATE POLICY pac_dpo ON pacientes FOR SELECT TO rol_dpo
41 USING (true);
42
43CREATE POLICY pac_senior_de ON pacientes FOR SELECT TO rol_senior_de
44 USING (true); -- Acceso completo pero con audit obligatorio
45
46-- Paso 5: Politicas para tabla HISTORIALES
47CREATE POLICY hist_medico ON historiales FOR ALL TO rol_medico
48 USING (medico_id = current_setting('app.current_user_id')
49 OR paciente_id IN (
50 SELECT DISTINCT paciente_id FROM consultas
51 WHERE medico_id = current_setting('app.current_user_id')
52 ));
53
54CREATE POLICY hist_dpo ON historiales FOR SELECT TO rol_dpo
55 USING (true);
56
57CREATE POLICY hist_senior_de ON historiales FOR SELECT TO rol_senior_de
58 USING (true);
59
60-- Paso 6: Politicas para tabla CONSULTAS
61CREATE POLICY cons_medico ON consultas FOR ALL TO rol_medico
62 USING (medico_id = current_setting('app.current_user_id'));
63
64CREATE POLICY cons_enfermero ON consultas FOR SELECT TO rol_enfermero
65 USING (
66 fecha_hora::date = CURRENT_DATE -- Solo consultas de hoy
67 AND tipo = 'video' -- Solo videoconsultas (su turno)
68 );
69
70CREATE POLICY cons_analista ON consultas FOR SELECT TO rol_analista
71 USING (true); -- Ve todas las consultas PERO con masking en paciente_id
72
73CREATE POLICY cons_junior_de ON consultas FOR SELECT TO rol_junior_de
74 USING (true); -- Para ETL pero con masking
75
76-- Paso 7: Politicas para tabla WEARABLES
77CREATE POLICY wear_analista ON wearables FOR SELECT TO rol_analista
78 USING (true); -- Datos de telemetria sin restriccion de filas
79
80CREATE POLICY wear_junior_de ON wearables FOR SELECT TO rol_junior_de
81 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 desarrollo
2-- Transforman PII en valores ficticios pero con formato válido
3
4CREATE OR REPLACE FUNCTION mask_nombre(nombre TEXT) RETURNS TEXT AS $$
5BEGIN
6 -- 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;
21
22CREATE OR REPLACE FUNCTION mask_email(email TEXT) RETURNS TEXT AS $$
23BEGIN
24 RETURN 'user_' || ABS(hashtext(email)) || '@ejemplo.test';
25END;
26$$ LANGUAGE plpgsql IMMUTABLE;
27
28CREATE OR REPLACE FUNCTION mask_telefono(tel TEXT) RETURNS TEXT AS $$
29BEGIN
30 RETURN '600' || LPAD(ABS(hashtext(tel))::text, 6, '0');
31END;
32$$ LANGUAGE plpgsql IMMUTABLE;
33
34CREATE OR REPLACE FUNCTION mask_dni(dni TEXT) RETURNS TEXT AS $$
35BEGIN
36 RETURN LPAD(ABS(hashtext(dni))::text, 8, '0') || 'X';
37END;
38$$ LANGUAGE plpgsql IMMUTABLE;
39
40-- Vista para desarrollo: misma estructura, datos enmascarados
41CREATE OR REPLACE VIEW pacientes_dev AS
42SELECT
43 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ías
48 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 estado
57FROM 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

[01]

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.

Cargando editor...
[02]

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.

Cargando editor...
[03]

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.

Cargando editor...

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...