CAPÍTULO 12: Gobierno de datos - Protección de datos (RGPD)
Marco Legal Europeo
El Reglamento General de Protección de Datos (RGPD) o General Data Protection Regulation (GDPR) entró en vigor el 25 de mayo de 2018, estableciendo el estándar más riguroso del mundo para la protección de datos personales.
Alcance global: Aplica a cualquier organización que procese datos de ciudadanos de la UE, independientemente de su ubicación geográfica.
Contexto histórico:
timeline
title Evolución de la Protección de Datos en Europa
1995 : Directiva 95/46/CE
2012 : Propuesta inicial RGPD
2016 : Aprobación RGPD
2018 : Entrada en vigor (25 mayo)
2018-2020 : Multas millonarias (Google, Amazon, FB)
2023 : ChatGPT bloqueado temporalmente en Italia
12.1. Conceptos fundamentales del RGPD
Definiciones clave:
| Término |
Definición |
Ejemplo |
| Dato Personal |
Información sobre persona física identificada o identificable |
Nombre, email, IP, cookie ID |
| Tratamiento |
Cualquier operación sobre datos |
Recopilación, almacenamiento, análisis, borrado |
| Responsable del Tratamiento |
Entidad que determina finalidad y medios |
Empresa que gestiona BBDD de clientes |
| Encargado del Tratamiento |
Entidad que trata datos por cuenta del responsable |
Proveedor cloud (AWS, Azure) |
| Interesado |
Persona física titular de los datos |
Usuario, cliente, empleado |
| Violación de Seguridad |
Brecha de seguridad que compromete datos |
Fuga de datos, ransomware, acceso no autorizado |
Datos personales: categorías:
| class TiposDatoPersonal:
"""
Clasificación de datos personales según RGPD
"""
# Datos básicos (Art. 4)
DATOS_IDENTIFICATIVOS = [
'nombre', 'apellidos', 'dni', 'pasaporte',
'email', 'teléfono', 'dirección', 'ip'
]
# Categorías especiales (Art. 9) - Sensibles
DATOS_SENSIBLES = [
'origen_racial_etnico',
'opiniones_politicas',
'convicciones_religiosas',
'afiliacion_sindical',
'datos_geneticos',
'datos_biometricos',
'datos_salud',
'vida_sexual',
'orientacion_sexual'
]
# Datos penales (Art. 10)
DATOS_PENALES = [
'infracciones_penales',
'condenas',
'medidas_seguridad'
]
@staticmethod
def clasificar_dato(nombre_campo: str, valor) -> str:
"""
Clasifica un dato según categoría RGPD
"""
nombre_lower = nombre_field.lower()
if any(sensible in nombre_lower for sensible in [
'salud', 'enfermedad', 'medico', 'biometric', 'genetic'
]):
return "SENSIBLE (Art. 9) - Requiere consentimiento explícito"
elif any(penal in nombre_lower for penal in [
'condena', 'delito', 'infraccion'
]):
return "PENAL (Art. 10) - Autoridad competente requerida"
elif any(basico in nombre_lower for basico in [
'nombre', 'email', 'telefono', 'direccion', 'dni'
]):
return "IDENTIFICATIVO (Art. 4) - Protección estándar"
else:
return "NO CLASIFICADO - Revisar manualmente"
# Ejemplo de uso
print(TiposDatoPersonal.clasificar_dato('email_usuario', 'ana@example.com'))
# Output: IDENTIFICATIVO (Art. 4) - Protección estándar
print(TiposDatoPersonal.clasificar_dato('historial_medico', '...'))
# Output: SENSIBLE (Art. 9) - Requiere consentimiento explícito
|
12.2. Principios fundamentales (Art. 5)
Los 7 principios del RGPD:
Principios Vinculantes
El incumplimiento de estos principios puede resultar en multas de hasta 20 millones de € o 4% del volumen de negocio anual global.
1. Licitud, Lealtad y Transparencia
El tratamiento debe ser:
- Lícito: Basado en una de las 6 bases legales (Art. 6)
- Leal: Sin engaño al interesado
- Transparente: Información clara sobre el tratamiento
| class BaseJuridicaTratamiento:
"""
Las 6 bases legales para el tratamiento de datos (Art. 6)
"""
CONSENTIMIENTO = "a) Consentimiento explícito del interesado"
CONTRATO = "b) Necesario para ejecutar un contrato"
OBLIGACION_LEGAL = "c) Cumplir obligación legal"
INTERES_VITAL = "d) Proteger intereses vitales (vida)"
INTERES_PUBLICO = "e) Misión de interés público"
INTERES_LEGITIMO = "f) Interés legítimo del responsable"
@staticmethod
def validar_consentimiento(consentimiento: dict) -> bool:
"""
Valida que el consentimiento cumpla requisitos RGPD
"""
requisitos = {
'informado': False, # Info clara antes de consentir
'especifico': False, # Finalidad concreta
'inequivoco': False, # Acción afirmativa clara
'revocable': False # Fácil retirar consentimiento
}
# Validaciones
if 'texto_informativo' in consentimiento and len(consentimiento['texto_informativo']) > 50:
requisitos['informado'] = True
if 'finalidad' in consentimiento and consentimiento['finalidad']:
requisitos['especifico'] = True
if consentimiento.get('accion_afirmativa') == True: # NOT pre-checked box
requisitos['inequivoco'] = True
if 'mecanismo_revocacion' in consentimiento:
requisitos['revocable'] = True
cumplimiento = all(requisitos.values())
if not cumplimiento:
print("❌ Consentimiento NO VÁLIDO según RGPD:")
for req, cumple in requisitos.items():
print(f" {req}: {'✅' if cumple else '❌'}")
return cumplimiento
# Ejemplo: Validar consentimiento
consentimiento_usuario = {
'texto_informativo': 'Al aceptar, autorizas el uso de tus datos para...',
'finalidad': 'Marketing y análisis estadístico',
'accion_afirmativa': True, # Usuario hizo click activo
'mecanismo_revocacion': 'Enlace en cada email + panel de usuario'
}
BaseJuridicaTratamiento.validar_consentimiento(consentimiento_usuario)
|
2. Limitación de la Finalidad
Los datos solo pueden usarse para los fines comunicados inicialmente.
| class GestorFinalidades:
"""
Controla que los datos solo se usen para finalidades autorizadas
"""
def __init__(self):
self.finalidades_autorizadas = set()
self.tratamientos_realizados = []
def agregar_finalidad(self, finalidad: str, consentimiento: bool):
"""
Registra nueva finalidad autorizada
"""
if consentimiento:
self.finalidades_autorizadas.add(finalidad)
print(f"✅ Finalidad autorizada: {finalidad}")
else:
print(f"❌ Finalidad NO autorizada: {finalidad}")
def validar_tratamiento(self, tratamiento: str, finalidad: str) -> bool:
"""
Valida si un tratamiento está permitido
"""
if finalidad in self.finalidades_autorizadas:
self.tratamientos_realizados.append({
'tratamiento': tratamiento,
'finalidad': finalidad,
'timestamp': pd.Timestamp.now()
})
print(f"✅ Tratamiento permitido: {tratamiento} (finalidad: {finalidad})")
return True
else:
print(f"❌ VIOLACIÓN RGPD: Tratamiento '{tratamiento}' no autorizado para finalidad '{finalidad}'")
return False
# Ejemplo
gestor = GestorFinalidades()
# Usuario consiente marketing pero NO compartir con terceros
gestor.agregar_finalidad('envio_newsletter', consentimiento=True)
gestor.agregar_finalidad('compartir_con_socios', consentimiento=False)
# Intentar realizar tratamientos
gestor.validar_tratamiento('Enviar email promocional', 'envio_newsletter') # ✅ OK
gestor.validar_tratamiento('Vender datos a broker', 'compartir_con_socios') # ❌ VIOLACIÓN
|
3. Minimización de Datos
Solo recoger los datos estrictamente necesarios para la finalidad.
| def minimizar_datos_formulario(datos_completos: dict, finalidad: str) -> dict:
"""
Aplica minimización según finalidad
"""
# Mapeo: finalidad → campos necesarios
campos_necesarios = {
'envio_producto': ['nombre', 'direccion', 'telefono', 'email'],
'newsletter': ['email'],
'facturacion': ['nombre', 'dni', 'direccion', 'email'],
'soporte_tecnico': ['email', 'telefono']
}
if finalidad not in campos_necesarios:
raise ValueError(f"Finalidad '{finalidad}' no reconocida")
campos_permitidos = campos_necesarios[finalidad]
datos_minimizados = {k: v for k, v in datos_completos.items()
if k in campos_permitidos}
campos_eliminados = set(datos_completos.keys()) - set(campos_permitidos)
print(f"📊 Minimización para finalidad '{finalidad}':")
print(f" Datos originales: {len(datos_completos)} campos")
print(f" Datos minimizados: {len(datos_minimizados)} campos")
print(f" Eliminados: {campos_eliminados}")
return datos_minimizados
# Ejemplo
datos_usuario = {
'nombre': 'Ana García',
'dni': '12345678A',
'email': 'ana@example.com',
'telefono': '+34 612345678',
'direccion': 'Calle Mayor 1, Madrid',
'fecha_nacimiento': '1990-05-15', # NO necesario
'estado_civil': 'Soltera', # NO necesario
'hobby': 'Lectura' # NO necesario
}
datos_min = minimizar_datos_formulario(datos_usuario, 'newsletter')
print(f"\n✅ Datos almacenados: {datos_min}")
|
4. Exactitud
Los datos deben ser exactos y actualizados.
| import pandas as pd
from datetime import datetime, timedelta
class ValidadorCalidadDatos:
"""
Valida exactitud de datos personales
"""
@staticmethod
def validar_email(email: str) -> bool:
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
return re.match(pattern, email) is not None
@staticmethod
def validar_dni_espanol(dni: str) -> bool:
"""Valida DNI español con letra correcta"""
if len(dni) != 9:
return False
letras = 'TRWAGMYFPDXBNJZSQVHLCKE'
numero = dni[:8]
letra = dni[8].upper()
try:
return letras[int(numero) % 23] == letra
except:
return False
@staticmethod
def verificar_actualizacion(ultima_actualizacion: pd.Timestamp,
max_dias: int = 365) -> bool:
"""
Verifica si los datos están actualizados
"""
dias_transcurridos = (pd.Timestamp.now() - ultima_actualizacion).days
if dias_transcurridos > max_dias:
print(f"⚠️ Datos desactualizados ({dias_transcurridos} días)")
return False
else:
print(f"✅ Datos actualizados ({dias_transcurridos} días)")
return True
# Ejemplo
validador = ValidadorCalidadDatos()
print("Validación de email:")
print(validador.validar_email('ana@example.com')) # True
print(validador.validar_email('email_invalido')) # False
print("\nValidación de DNI:")
print(validador.validar_dni_espanol('12345678Z')) # True
print(validador.validar_dni_espanol('12345678A')) # False
print("\nVerificación de actualización:")
fecha_antigua = pd.Timestamp.now() - timedelta(days=400)
validador.verificar_actualizacion(fecha_antigua, max_dias=365)
|
5. Limitación del Plazo de Conservación
Los datos solo deben conservarse el tiempo estrictamente necesario.
| class GestorRetencionDatos:
"""
Gestiona plazos de conservación y borrado automático
"""
# Plazos típicos según finalidad
PLAZOS_RETENCION = {
'marketing_sin_compra': 365, # 1 año sin actividad
'cliente_activo': 365 * 5, # 5 años desde última compra
'datos_fiscales': 365 * 6, # 6 años (obligación legal España)
'cv_no_seleccionado': 365 * 2, # 2 años
'logs_acceso': 90 # 3 meses
}
def __init__(self):
self.registros = []
def calcular_fecha_borrado(self, fecha_creacion: pd.Timestamp,
finalidad: str) -> pd.Timestamp:
"""
Calcula cuándo debe borrarse un registro
"""
if finalidad not in self.PLAZOS_RETENCION:
raise ValueError(f"Finalidad '{finalidad}' no tiene plazo definido")
dias_retencion = self.PLAZOS_RETENCION[finalidad]
fecha_borrado = fecha_creacion + timedelta(days=dias_retencion)
return fecha_borrado
def identificar_borrados_pendientes(self, df: pd.DataFrame,
finalidad_col: str = 'finalidad',
fecha_col: str = 'fecha_creacion') -> pd.DataFrame:
"""
Identifica registros que deben borrarse
"""
df['fecha_borrado'] = df.apply(
lambda row: self.calcular_fecha_borrado(row[fecha_col], row[finalidad_col]),
axis=1
)
df['dias_hasta_borrado'] = (df['fecha_borrado'] - pd.Timestamp.now()).dt.days
# Registros a borrar (fecha pasada)
df_borrar = df[df['dias_hasta_borrado'] < 0].copy()
print(f"📊 Análisis de Retención:")
print(f" Total registros: {len(df)}")
print(f" A borrar: {len(df_borrar)} ({len(df_borrar)/len(df)*100:.1f}%)")
return df_borrar
# Ejemplo
gestor_retencion = GestorRetencionDatos()
# Dataset de ejemplo
df_usuarios = pd.DataFrame({
'user_id': ['U001', 'U002', 'U003', 'U004'],
'email': ['user1@example.com', 'user2@example.com',
'user3@example.com', 'user4@example.com'],
'fecha_creacion': [
pd.Timestamp.now() - timedelta(days=400), # Expirado
pd.Timestamp.now() - timedelta(days=300), # Válido
pd.Timestamp.now() - timedelta(days=800), # Expirado
pd.Timestamp.now() - timedelta(days=50) # Válido
],
'finalidad': ['marketing_sin_compra'] * 4
})
df_a_borrar = gestor_retencion.identificar_borrados_pendientes(df_usuarios)
print("\n📋 Registros a borrar:")
print(df_a_borrar[['user_id', 'email', 'dias_hasta_borrado']])
|
6. Integridad y Confidencialidad
Medidas técnicas y organizativas para proteger los datos.
| import hashlib
from cryptography.fernet import Fernet
class SeguridadDatos:
"""
Implementa medidas de seguridad RGPD
"""
def __init__(self):
self.clave_cifrado = Fernet.generate_key()
self.cipher = Fernet(self.clave_cifrado)
def cifrar_dato_sensible(self, dato: str) -> bytes:
"""
Cifrado AES para datos sensibles
"""
return self.cipher.encrypt(dato.encode())
def descifrar_dato(self, dato_cifrado: bytes) -> str:
"""
Descifrado de datos
"""
return self.cipher.decrypt(dato_cifrado).decode()
def hash_identificador(self, identificador: str, salt: str = 'rgpd_salt') -> str:
"""
Hash irreversible para identificadores
"""
return hashlib.pbkdf2_hmac(
'sha256',
identificador.encode(),
salt.encode(),
100000 # Iteraciones
).hex()[:32]
def pseudonimizar_registro(self, registro: dict) -> dict:
"""
Pseudonimiza campos sensibles
"""
campos_sensibles = ['dni', 'email', 'telefono']
registro_pseudo = registro.copy()
for campo in campos_sensibles:
if campo in registro_pseudo:
# Generar pseudónimo consistente
registro_pseudo[campo] = self.hash_identificador(
str(registro_pseudo[campo])
)
return registro_pseudo
# Ejemplo
seguridad = SeguridadDatos()
# Cifrado
dato_original = "Paciente tiene HIV+"
dato_cifrado = seguridad.cifrar_dato_sensible(dato_original)
print(f"Original: {dato_original}")
print(f"Cifrado: {dato_cifrado}")
print(f"Descifrado: {seguridad.descifrar_dato(dato_cifrado)}")
# Pseudonimización
registro = {
'nombre': 'Ana García',
'dni': '12345678Z',
'email': 'ana@example.com',
'ciudad': 'Madrid'
}
registro_pseudo = seguridad.pseudonimizar_registro(registro)
print(f"\nOriginal: {registro}")
print(f"Pseudonimizado: {registro_pseudo}")
|
7. Responsabilidad Proactiva (Accountability)
Demostrar cumplimiento continuamente.
| class RegistroActividadesRGPD:
"""
Registro de actividades de tratamiento (Art. 30)
Obligatorio para organizaciones > 250 empleados
"""
def __init__(self, nombre_organizacion: str):
self.organizacion = nombre_organizacion
self.actividades = []
def registrar_actividad(self, actividad: dict):
"""
Registra una actividad de tratamiento
"""
# Campos obligatorios según Art. 30
campos_requeridos = [
'nombre_tratamiento',
'finalidad',
'categorias_datos',
'destinatarios',
'plazo_supresion',
'medidas_seguridad'
]
for campo in campos_requeridos:
if campo not in actividad:
raise ValueError(f"Campo obligatorio faltante: {campo}")
actividad['fecha_registro'] = pd.Timestamp.now()
actividad['responsable'] = self.organizacion
self.actividades.append(actividad)
print(f"✅ Actividad registrada: {actividad['nombre_tratamiento']}")
def generar_reporte_cumplimiento(self) -> str:
"""
Genera reporte para auditoría
"""
reporte = f"=" * 70 + "\n"
reporte += f"REGISTRO DE ACTIVIDADES DE TRATAMIENTO\n"
reporte += f"Organización: {self.organizacion}\n"
reporte += f"Fecha: {pd.Timestamp.now().strftime('%Y-%m-%d %H:%M:%S')}\n"
reporte += f"=" * 70 + "\n\n"
for i, act in enumerate(self.actividades, 1):
reporte += f"ACTIVIDAD {i}: {act['nombre_tratamiento']}\n"
reporte += f" Finalidad: {act['finalidad']}\n"
reporte += f" Categorías de datos: {', '.join(act['categorias_datos'])}\n"
reporte += f" Destinatarios: {', '.join(act['destinatarios'])}\n"
reporte += f" Plazo de supresión: {act['plazo_supresion']}\n"
reporte += f" Medidas de seguridad: {', '.join(act['medidas_seguridad'])}\n"
reporte += f" Fecha registro: {act['fecha_registro']}\n"
reporte += "\n"
return reporte
# Ejemplo
registro = RegistroActividadesRGPD("Hospital Universitario Madrid")
# Registrar actividad de tratamiento
registro.registrar_actividad({
'nombre_tratamiento': 'Gestión de historiales médicos',
'finalidad': 'Prestación de servicios sanitarios',
'categorias_datos': ['Datos de salud', 'Datos identificativos'],
'destinatarios': ['Personal médico', 'Lab externos autorizados'],
'plazo_supresion': '10 años tras última consulta',
'medidas_seguridad': ['Cifrado AES-256', 'Control de acceso por roles', 'Auditoría continua']
})
print("\n" + registro.generar_reporte_cumplimiento())
|
12.3. Derechos de los interesados
Los 8 derechos RGPD:
| Derecho |
Artículo |
Descripción |
Plazo Respuesta |
| Acceso |
Art. 15 |
Conocer qué datos se tratan |
1 mes |
| Rectificación |
Art. 16 |
Corregir datos inexactos |
1 mes |
| Supresión ("Derecho al olvido") |
Art. 17 |
Borrar datos |
1 mes |
| Limitación |
Art. 18 |
Suspender tratamiento |
1 mes |
| Portabilidad |
Art. 20 |
Recibir datos en formato estructurado |
1 mes |
| Oposición |
Art. 21 |
Oponerse al tratamiento |
Inmediato |
| No decisiones automatizadas |
Art. 22 |
No ser objeto de decisiones solo automatizadas |
- |
| Información |
Art. 13-14 |
Ser informado claramente |
Antes del tratamiento |
Implementación técnica:
| class GestorDerechosRGPD:
"""
Gestiona solicitudes de derechos RGPD
"""
def __init__(self, base_datos: pd.DataFrame):
self.db = base_datos
self.solicitudes = []
def derecho_acceso(self, user_id: str) -> dict:
"""
Art. 15: Devuelve todos los datos del usuario
"""
datos_usuario = self.db[self.db['user_id'] == user_id].to_dict('records')
if not datos_usuario:
return {'error': 'Usuario no encontrado'}
# Crear respuesta completa
respuesta = {
'datos_personales': datos_usuario,
'finalidad': 'Marketing y gestión de clientes',
'destinatarios': ['Personal interno', 'Mailchimp (encargado email)'],
'plazo_conservacion': '5 años desde última compra',
'origen_datos': 'Proporcionados directamente por el usuario',
'decisiones_automatizadas': 'Recomendaciones de productos mediante ML',
'derechos': 'Acceso, Rectificación, Supresión, Limitación, Portabilidad, Oposición'
}
self._registrar_solicitud(user_id, 'ACCESO', 'COMPLETADA')
return respuesta
def derecho_rectificacion(self, user_id: str, datos_corregidos: dict) -> bool:
"""
Art. 16: Corrige datos inexactos
"""
indice = self.db[self.db['user_id'] == user_id].index
if len(indice) == 0:
return False
for campo, nuevo_valor in datos_corregidos.items():
if campo in self.db.columns:
self.db.loc[indice, campo] = nuevo_valor
print(f"✅ Rectificado campo '{campo}' para usuario {user_id}")
self._registrar_solicitud(user_id, 'RECTIFICACION', 'COMPLETADA')
return True
def derecho_supresion(self, user_id: str, motivo: str) -> bool:
"""
Art. 17: Derecho al olvido
"""
# Verificar si hay obligación legal de conservar datos
obligaciones_legales = ['datos_fiscales', 'obligacion_contrato']
if motivo in obligaciones_legales:
print(f"❌ No se puede suprimir: Obligación legal de conservación")
self._registrar_solicitud(user_id, 'SUPRESION', 'DENEGADA', motivo=motivo)
return False
# Eliminar datos
self.db = self.db[self.db['user_id'] != user_id]
print(f"✅ Datos del usuario {user_id} eliminados (derecho al olvido)")
self._registrar_solicitud(user_id, 'SUPRESION', 'COMPLETADA')
return True
def derecho_portabilidad(self, user_id: str, formato: str = 'json') -> str:
"""
Art. 20: Exporta datos en formato estructurado
"""
datos = self.db[self.db['user_id'] == user_id].to_dict('records')
if formato == 'json':
import json
output = json.dumps(datos, indent=2, ensure_ascii=False, default=str)
elif formato == 'csv':
temp_df = pd.DataFrame(datos)
output = temp_df.to_csv(index=False)
elif formato == 'xml':
# Implementación XML simplificada
output = f"<usuario>{datos}</usuario>"
else:
output = str(datos)
self._registrar_solicitud(user_id, 'PORTABILIDAD', 'COMPLETADA', formato=formato)
print(f"📦 Datos exportados en formato {formato}")
return output
def derecho_oposicion(self, user_id: str, finalidad: str) -> bool:
"""
Art. 21: Usuario se opone a tratamiento específico
"""
indice = self.db[self.db['user_id'] == user_id].index
if len(indice) == 0:
return False
# Marcar oposición a finalidad específica
if f'oposicion_{finalidad}' not in self.db.columns:
self.db[f'oposicion_{finalidad}'] = False
self.db.loc[indice, f'oposicion_{finalidad}'] = True
print(f"✅ Usuario {user_id} se opuso a: {finalidad}")
print(f"⚠️ NO enviar comunicaciones de tipo: {finalidad}")
self._registrar_solicitud(user_id, 'OPOSICION', 'COMPLETADA', finalidad=finalidad)
return True
def _registrar_solicitud(self, user_id: str, tipo: str, estado: str, **kwargs):
"""
Registra solicitud en log de auditoría
"""
self.solicitudes.append({
'timestamp': pd.Timestamp.now(),
'user_id': user_id,
'tipo_solicitud': tipo,
'estado': estado,
**kwargs
})
def generar_reporte_solicitudes(self) -> pd.DataFrame:
"""
Reporte de todas las solicitudes
"""
return pd.DataFrame(self.solicitudes)
# Ejemplo completo
df_usuarios = pd.DataFrame({
'user_id': ['U001', 'U002', 'U003'],
'nombre': ['Ana García', 'Luis Pérez', 'María López'],
'email': ['ana@example.com', 'luis@example.com', 'maria@example.com'],
'telefono': ['+34 612345678', '+34 698765432', '+34 611222333'],
'ultima_compra': ['2023-05-15', '2022-11-20', '2024-01-10']
})
gestor = GestorDerechosRGPD(df_usuarios.copy())
print("=" * 70)
print("GESTIÓN DE DERECHOS RGPD")
print("=" * 70 + "\n")
# 1. Derecho de acceso
print("1️⃣ DERECHO DE ACCESO (Art. 15)")
respuesta_acceso = gestor.derecho_acceso('U001')
print(json.dumps(respuesta_acceso, indent=2, ensure_ascii=False, default=str))
# 2. Derecho de rectificación
print("\n2️⃣ DERECHO DE RECTIFICACIÓN (Art. 16)")
gestor.derecho_rectificacion('U001', {'email': 'ana.nueva@example.com'})
# 3. Derecho de portabilidad
print("\n3️⃣ DERECHO DE PORTABILIDAD (Art. 20)")
datos_exportados = gestor.derecho_portabilidad('U001', formato='json')
print(datos_exportados[:200] + "...")
# 4. Derecho de oposición
print("\n4️⃣ DERECHO DE OPOSICIÓN (Art. 21)")
gestor.derecho_oposicion('U002', 'marketing')
# 5. Derecho de supresión
print("\n5️⃣ DERECHO DE SUPRESIÓN (Art. 17)")
gestor.derecho_supresion('U003', motivo='retira_consentimiento')
# Reporte de solicitudes
print("\n📊 REPORTE DE SOLICITUDES RGPD:")
print(gestor.generar_reporte_solicitudes())
|
12.4. Privacy by Design y by Default
Principios de diseño (Art. 25):
Privacy by Design
Incorporar protección de datos desde el diseño inicial del sistema, no como añadido posterior.
| class PrivacyByDesignChecklist:
"""
Checklist de cumplimiento Privacy by Design
"""
PRINCIPIOS = {
'1. Proactivo no reactivo': [
'¿Se identifican riesgos de privacidad antes del diseño?',
'¿Hay evaluación de impacto (DPIA) antes de implementar?'
],
'2. Privacidad por defecto': [
'¿Los ajustes más restrictivos están activos por defecto?',
'¿El usuario debe opt-in (no opt-out) para compartir datos?'
],
'3. Privacidad embebida': [
'¿La privacidad es parte integral del sistema, no añadido?',
'¿Se usa cifrado end-to-end?'
],
'4. Funcionalidad completa': [
'¿Se logra privacidad SIN sacrificar funcionalidad?',
'¿Hay balance sum-positive (win-win)?'
],
'5. Seguridad end-to-end': [
'¿Datos protegidos en todo el ciclo de vida?',
'¿Borrado seguro al finalizar retención?'
],
'6. Visibilidad y transparencia': [
'¿Políticas de privacidad claras y accesibles?',
'¿Auditorías independientes realizadas?'
],
'7. Respeto por la privacidad del usuario': [
'¿Interfaz user-friendly para gestionar privacidad?',
'¿Fácil ejercer derechos RGPD?'
]
}
@staticmethod
def evaluar_proyecto(respuestas: dict) -> dict:
"""
Evalúa cumplimiento de Privacy by Design
Args:
respuestas: dict con princip io → list de bool
Returns:
Score de cumplimiento por principio
"""
resultados = {}
for principio, preguntas in PrivacyByDesignChecklist.PRINCIPIOS.items():
if principio in respuestas:
# Calcular % cumplimiento
cumplimiento = sum(respuestas[principio]) / len(preguntas) * 100
resultados[principio] = cumplimiento
else:
resultados[principio] = 0
cumplimiento_total = sum(resultados.values()) / len(resultados)
print("📊 EVALUACIÓN PRIVACY BY DESIGN")
print("=" * 60)
for principio, score in resultados.items():
emoji = "✅" if score >= 80 else "⚠️" if score >= 50 else "❌"
print(f"{emoji} {principio}: {score:.0f}%")
print(f"\n🎯 CUMPLIMIENTO TOTAL: {cumplimiento_total:.1f}%")
if cumplimiento_total >= 90:
print("✅ Excelente - Cumple Privacy by Design")
elif cumplimiento_total >= 70:
print("⚠️ Aceptable - Requiere mejoras")
else:
print("❌ Insuficiente - Rediseño necesario")
return resultados
# Ejemplo de evaluación
respuestas_proyecto = {
'1. Proactivo no reactivo': [True, True],
'2. Privacidad por defecto': [True, False], # Opt-out en vez de opt-in
'3. Privacidad embebida': [True, True],
'4. Funcionalidad completa': [True, True],
'5. Seguridad end-to-end': [True, False], # No hay borrado seguro
'6. Visibilidad y transparencia': [False, True], # Política poco clara
'7. Respeto por la privacidad del usuario': [True, True]
}
PrivacyByDesignChecklist.evaluar_proyecto(respuestas_proyecto)
|
Privacy by Default:
Por defecto, el sistema debe:
- ✅ Solo procesar datos necesarios
- ✅ Minimizar período de conservación
- ✅ Limitar accesibilidad (need-to-know)
- ✅ No compartir datos sin consentimiento explícito
| class ConfiguracionPrivacyByDefault:
"""
Configuración por defecto RGPD-compliant
"""
DEFAULT_SETTINGS = {
# Recopilación de datos
'recopilar_datos_necesarios_solo': True,
'solicitar_datos_opcionales': False, # Usuario debe activar
# Comunicaciones
'suscripcion_newsletter': False, # Opt-IN requerido
'compartir_con_socios': False,
'publicidad_personalizada': False,
# Privacidad
'perfil_publico': False, # Por defecto privado
'mostrar_email': False,
'permitir_indexacion_buscadores': False,
# Retención
'dias_retencion_logs': 30, # Mínimo necesario
'borrado_automatico_inactivos': 365,
# Cookies
'cookies_analiticas': False, # Solo técnicas por defecto
'cookies_marketing': False,
'cookies_redes_sociales': False
}
@staticmethod
def validar_configuracion_usuario(config_usuario: dict) -> dict:
"""
Valida que configuración de usuario no sea menos restrictiva
que el default RGPD
"""
config_valida = ConfiguracionPrivacyByDefault.DEFAULT_SETTINGS.copy()
for key, valor_usuario in config_usuario.items():
if key in config_valida:
# Solo permitir cambios que AUMENTEN privacidad o
# cambios explícitos del usuario
config_valida[key] = valor_usuario
return config_valida
# Ejemplo
config_usuario_propuesta = {
'suscripcion_newsletter': True, # Usuario consintió
'perfil_publico': True # Usuario quiere perfil público
}
config_final = ConfiguracionPrivacyByDefault.validar_configuracion_usuario(
config_usuario_propuesta
)
print("⚙️ Configuración final (Privacy by Default):")
for key, value in config_final.items():
print(f" {key}: {value}")
|
12.5. Data Protection Impact Assessment (DPIA)
¿Cuándo es obligatoria? (Art. 35):
DPIA Obligatoria En
- Evaluación sistemática y exhaustiva (incluido perfilado)
- Tratamiento a gran escala de categorías especiales de datos (Art. 9)
- Observación sistemática a gran escala de zonas de acceso público
- Nuevas tecnologías con alto riesgo
Ejemplos que requieren DPIA:
- Sistema de reconocimiento facial en espacios públicos
- Base de datos médica centralizada
- Scoring crediticio automatizado
- Monitorización masiva de empleados
Metodología DPIA:
| import pandas as pd
from enum import Enum
class NivelRiesgo(Enum):
BAJO = 1
MEDIO = 2
ALTO = 3
CRITICO = 4
class DPIA:
"""
Data Protection Impact Assessment Framework
"""
def __init__(self, nombre_proyecto: str):
self.proyecto = nombre_proyecto
self.riesgos_identificados = []
self.medidas_mitigacion = []
def evaluar_necesidad_dpia(self, caracteristicas: dict) -> bool:
"""
Determina si se requiere DPIA
"""
criterios_obligatorios = {
'evaluacion_perfilado': False,
'datos_sensibles_gran_escala': False,
'vigilancia_sistematica': False,
'nuevas_tecnologias_alto_riesgo': False,
'datos_menores': False,
'cruce_masivo_datos': False
}
for criterio, value in caracteristicas.items():
if criterio in criterios_obligatorios:
criterios_obligatorios[criterio] = value
# DPIA obligatoria si al menos 2 criterios se cumplen
criterios_cumplidos = sum(criterios_obligatorios.values())
requiere_dpia = criterios_cumplidos >= 2
print(f"📋 EVALUACIÓN DE NECESIDAD DE DPIA: {self.proyecto}")
print(f" Criterios cumplidos: {criterios_cumplidos}/6")
if requiere_dpia:
print(f" ✅ DPIA OBLIGATORIA")
else:
print(f" ℹ️ DPIA recomendada pero no obligatoria")
return requiere_dpia
def identificar_riesgo(self, descripcion: str, probabilidad: NivelRiesgo,
impacto: NivelRiesgo, afectados: int):
"""
Registra un riesgo identificado
"""
# Calcular riesgo total
riesgo_total = probabilidad.value * impacto.value
if riesgo_total >= 9:
nivel_riesgo = "CRÍTICO"
elif riesgo_total >= 6:
nivel_riesgo = "ALTO"
elif riesgo_total >= 3:
nivel_riesgo = "MEDIO"
else:
nivel_riesgo = "BAJO"
self.riesgos_identificados.append({
'descripcion': descripcion,
'probabilidad': probabilidad.name,
'impacto': impacto.name,
'personas_afectadas': afectados,
'nivel_riesgo': nivel_riesgo,
'score': riesgo_total
})
print(f"⚠️ Riesgo identificado [{nivel_riesgo}]: {descripcion}")
def proponer_mitigacion(self, riesgo_index: int, medida: str,
riesgo_residual: NivelRiesgo):
"""
Propone medida de mitigación para un riesgo
"""
if riesgo_index >= len(self.riesgos_identificados):
print("❌ Índice de riesgo inválido")
return
riesgo = self.riesgos_identificados[riesgo_index]
self.medidas_mitigacion.append({
'riesgo': riesgo['descripcion'],
'medida': medida,
'riesgo_residual': riesgo_residual.name,
'estado': 'PROPUESTA'
})
print(f"✅ Medida de mitigación propuesta: {medida}")
def generar_reporte_dpia(self) -> dict:
"""
Genera reporte completo de DPIA
"""
reporte = {
'proyecto': self.proyecto,
'fecha_evaluacion': pd.Timestamp.now().isoformat(),
'resumen_riesgos': {
'total_riesgos': len(self.riesgos_identificados),
'criticos': sum(1 for r in self.riesgos_identificados if r['nivel_riesgo'] == 'CRÍTICO'),
'altos': sum(1 for r in self.riesgos_identificados if r['nivel_riesgo'] == 'ALTO'),
'medios': sum(1 for r in self.riesgos_identificados if r['nivel_riesgo'] == 'MEDIO'),
'bajos': sum(1 for r in self.riesgos_identificados if r['nivel_riesgo'] == 'BAJO')
},
'riesgos_detallados': self.riesgos_identificados,
'medidas_mitigacion': self.medidas_mitigacion
}
return reporte
def imprimir_reporte(self):
"""
Imprime reporte legible
"""
reporte = self.generar_reporte_dpia()
print("\n" + "=" * 70)
print(f"REPORTE DPIA: {self.proyecto}")
print("=" * 70)
print(f"Fecha: {reporte['fecha_evaluacion']}")
print(f"\n📊 RESUMEN:")
print(f" Total riesgos: {reporte['resumen_riesgos']['total_riesgos']}")
print(f" - Críticos: {reporte['resumen_riesgos']['criticos']}")
print(f" - Altos: {reporte['resumen_riesgos']['altos']}")
print(f" - Medios: {reporte['resumen_riesgos']['medios']}")
print(f" - Bajos: {reporte['resumen_riesgos']['bajos']}")
print(f"\n⚠️ RIESGOS IDENTIFICADOS:")
for i, riesgo in enumerate(self.riesgos_identificados, 1):
print(f"\n {i}. [{riesgo['nivel_riesgo']}] {riesgo['descripcion']}")
print(f" Probabilidad: {riesgo['probabilidad']} | Impacto: {riesgo['impacto']}")
print(f" Personas afectadas: {riesgo['personas_afectadas']:,}")
print(f"\n✅ MEDIDAS DE MITIGACIÓN:")
for i, medida in enumerate(self.medidas_mitigacion, 1):
print(f"\n {i}. {medida['medida']}")
print(f" Riesgo objetivo: {medida['riesgo']}")
print(f" Riesgo residual: {medida['riesgo_residual']}")
# EJEMPLO COMPLETO: DPIA para Sistema de Reconocimiento Facial
dpia = DPIA("Sistema de Control de Acceso con Reconocimiento Facial")
# 1. Evaluar necesidad
caracteristicas_proyecto = {
'evaluacion_perfilado': False,
'datos_sensibles_gran_escala': True, # Datos biométricos
'vigilancia_sistematica': True, # Cámaras en edificio
'nuevas_tecnologias_alto_riesgo': True
}
dpia.evaluar_necesidad_dpia(caracteristicas_proyecto)
print("\n" + "="*70)
print("FASE 1: IDENTIFICACIÓN DE RIESGOS")
print("="*70)
# 2. Identificar riesgos
dpia.identificar_riesgo(
descripcion="Fuga de base de datos biométrica → Re-identificación perpetua",
probabilidad=NivelRiesgo.MEDIO,
impacto=NivelRiesgo.CRITICO,
afectados=5000
)
dpia.identificar_riesgo(
descripcion="Falsos positivos/negativos → Denegación acceso legítimo",
probabilidad=NivelRiesgo.ALTO,
impacto=NivelRiesgo.MEDIO,
afectados=100
)
dpia.identificar_riesgo(
descripcion="Uso indebido para vigilancia no autorizada",
probabilidad=NivelRiesgo.BAJO,
impacto=NivelRiesgo.ALTO,
afectados=5000
)
print("\n" + "="*70)
print("FASE 2: MEDIDAS DE MITIGACIÓN")
print("="*70)
# 3. Proponer mitigaciones
dpia.proponer_mitigacion(
riesgo_index=0,
medida="Cifrado AES-256 de DB biométrica + Almacenamiento solo de templates (no imágenes originales) + Penetration Testing trimestral",
riesgo_residual=NivelRiesgo.BAJO
)
dpia.proponer_mitigacion(
riesgo_index=1,
medida="Threshold ajustable + Sistema de override manual + Registro de eventos fallidos",
riesgo_residual=NivelRiesgo.BAJO
)
dpia.proponer_mitigacion(
riesgo_index=2,
medida="Control de acceso estricto al sistema + Auditoría de logs mensual + Política de uso acceptable",
riesgo_residual=NivelRiesgo.BAJO
)
# 4. Generar reporte
dpia.imprimir_reporte()
|
12.6. Sanciones y enforcement
Niveles de multas:
Régimen Sancionador
Nivel 1: Hasta 10M€ o 2% volumen negocio anual (el mayor)
- Incumplir medidas técnicas y organizativas (Art. 32)
- No notificar brechas de seguridad (Art. 33-34)
- No realizar DPIA cuando es obligatoria (Art. 35)
Nivel 2: Hasta 20M€ o 4% volumen negocio anual (el mayor)
- Violar principios fundamentales (Art. 5)
- Tratamiento ilícito de datos (Art. 6)
- Violar derechos de los interesados (Art. 12-22)
- Transferencias internacionales ilegales (Art. 44)
Casos reales de sanciones:
| Empresa |
Año |
Multa |
Motivo |
| Amazon |
2021 |
€746M |
Publicidad sin consentimiento válido |
| Google (Ireland) |
2022 |
€90M |
Cookies sin consentimiento explícito |
| Meta (Facebook) |
2023 |
€1,200M |
Transferencias ilegales a USA |
| British Airways |
2020 |
€22M |
Fuga de datos de 400k clientes |
| Marriott |
2020 |
€20.4M |
Brecha afectó 339M registros |
| H&M |
2020 |
€35M |
Vigilancia excesiva de empleados |
| Google (France) |
2019 |
€50M |
Falta de transparencia en Android |
12.7. Transferencias internacionales (Capítulo V)
Reglas para transfers fuera de la UE:
Restricción General
Datos personales NO pueden transferirse fuera del Espacio Económico Europeo (EEE) salvo:
- País con decisión de adecuación de la Comisión Europea
- Garantías adecuadas (cláusulas contractuales tipo)
- Normas corporativas vinculantes (BCR)
- Excepción específica (Art. 49)
| class TransferenciasInternacionales:
"""
Valida transferencias de datos fuera de la UE
"""
# Países con decisión de adecuación (actualizado 2024)
PAISES_ADECUADOS = [
'Andorra', 'Argentina', 'Canadá', 'Islas Feroe', 'Guernsey',
'Israel', 'Isla de Man', 'Japón', 'Jersey', 'Nueva Zelanda',
'Corea del Sur', 'Suiza', 'Reino Unido', 'Uruguay'
]
# USA invalidado tras sentencia Schrems II (2020)
PAISES_NO_PERMITIDOS = ['USA (sin garantías adicionales)', 'China', 'Rusia']
@staticmethod
def validar_transferencia(pais_destino: str, garantias: list = None) -> dict:
"""
Valida si una transferencia internacional es válida
"""
resultado = {
'pais': pais_destino,
'permitido': False,
'base_legal': None,
'acciones_requeridas': []
}
if pais_destino in TransferenciasInternacionales.PAISES_ADECUADOS:
resultado['permitido'] = True
resultado['base_legal'] = 'Decisión de adecuación (Art. 45)'
print(f"✅ Transferencia a {pais_destino} PERMITIDA (decisión de adecuación)")
elif garantias:
if 'Cláusulas Contractuales Tipo (SCC)' in garantias:
resultado['permitido'] = True
resultado['base_legal'] = 'Cláusulas Contractuales Tipo (Art. 46)'
resultado['acciones_requeridas'].append('Firmar SCC actualizadas (2021)')
print(f"✅ Transferencia a {pais_destino} PERMITIDA con SCC")
elif 'Normas Corporativas Vinculantes (BCR)' in garantias:
resultado['permitido'] = True
resultado['base_legal'] = 'BCR aprobadas por autoridad (Art. 47)'
print(f"✅ Transferencia a {pais_destino} PERMITIDA con BCR")
else:
resultado['permitido'] = False
resultado['acciones_requeridas'].append('Implementar garantías adecuadas')
resultado['acciones_requeridas'].append('Considerar data localization en EU')
print(f"❌ Transferencia a {pais_destino} NO PERMITIDA sin garantías")
return resultado
# Ejemplo: Validar uso de AWS USA
print("🌍 VALIDACIÓN DE TRANSFERENCIAS INTERNACIONALES\n")
# Caso 1: Transferencia a USA con SCC
resultado_usa = TransferenciasInternacionales.validar_transferencia(
'USA',
garantias=['Cláusulas Contractuales Tipo (SCC)']
)
# Caso 2: Transferencia a Suiza (país adecuado)
resultado_suiza = TransferenciasInternacionales.validar_transferencia('Suiza')
# Caso 3: Transferencia a China sin garantías
resultado_china = TransferenciasInternacionales.validar_transferencia('China')
|
12.8. Herramientas de cumplimiento RGPD
Software de gestión de consentimientos (CMP):
| Herramienta |
Descripción |
Precio |
| OneTrust |
Plataforma enterprise completa |
$$$$ |
| TrustArc |
Solución integral con consultores |
$$$$ |
| Cookiebot |
Especializado en cookies |
$$ |
| Osano |
Gestión de consentimientos automatizada |
$$$ |
| Usercentrics |
CMP europea popular |
$$ |
Data Discovery & Classification:
| Herramienta |
Descripción |
| BigID |
Descubrimiento automático de PII en enterprise |
| Collibra |
Data cataloging + governance |
| Varonis |
Monitorización de acceso a datos sensibles |
| Microsoft Purview |
Clasificación automática (Azure/M365) |
Implementación práctica en Python:
| # Ejemplo: Detector automático de PII
import re
class PIIDetector:
"""
Detecta automáticamente datos personales en texto/datasets
"""
PATTERNS = {
'email': r'\b[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Z|a-z]{2,}\b',
'telefono_ES': r'\+34\s?\d{3}\s?\d{3}\s?\d{3}',
'dni_ES': r'\b\d{8}[A-Z]\b',
'tarjeta_credito': r'\b\d{4}[\s-]?\d{4}[\s-]?\d{4}[\s-]?\d{4}\b',
'iban': r'\b[A-Z]{2}\d{2}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\s?\d{4}\b',
'fecha_nacimiento': r'\b\d{2}/\d{2}/\d{4}\b'
}
@staticmethod
def detectar_pii(texto: str) -> dict:
"""
Escanea texto en busca de PII
"""
found_pii = {}
for tipo, pattern in PIIDetector.PATTERNS.items():
matches = re.findall(pattern, texto)
if matches:
found_pii[tipo] = matches
return found_pii
@staticmethod
def escanear_dataframe(df: pd.DataFrame) -> pd.DataFrame:
"""
Escanea DataFrame completo en busca de PII
"""
resultados = []
for col in df.columns:
# Convertir columna a string para análisis
texto_columna = ' '.join(df[col].astype(str).values)
pii_encontrada = PIIDetector.detectar_pii(texto_columna)
if pii_encontrada:
for tipo_pii, ejemplos in pii_encontrada.items():
resultados.append({
'columna': col,
'tipo_pii': tipo_pii,
'ocurrencias': len(ejemplos),
'ejemplo': ejemplos[0] if ejemplos else None
})
df_resultados = pd.DataFrame(resultados)
print(f"🔍 ESCANEO DE PII COMPLETADO")
print(f" Columnas escaneadas: {len(df.columns)}")
print(f" Tipos de PII encontrados: {len(df_resultados)}")
return df_resultados
# Ejemplo de uso
texto_muestra = """
Contacto: ana.garcia@example.com
Teléfono: +34 612 345 678
DNI: 12345678Z
Tarjeta: 4532 0151 1283 0366
Nacimiento: 15/05/1990
"""
print("🔍 DETECTOR DE PII\n")
pii_detectada = PIIDetector.detectar_pii(texto_muestra)
for tipo, valores in pii_detectada.items():
print(f" {tipo}: {valores}")
|
12.9. Best practices y recomendaciones
Checklist de Cumplimiento RGPD
Antes del Proyecto
Durante Desarrollo
En Producción
Documentación
Referencias y recursos
Textos legales:
Autoridades de control:
Guías y Toolkit:
Formación:
Conclusión
El RGPD no es solo una obligación legal, es una oportunidad para construir confianza con los usuarios. Las organizaciones que priorizan la privacidad desde el diseño no solo evitan sanciones, sino que obtienen ventaja competitiva al diferenciarse por el respeto a los datos personales.
Principio clave: "Privacidad como valor diferencial, no como barrera regulatoria."