Ejercicio 1 — Las bases legales
- La clasificación: email (contrato: la venta), teléfono opcional (consentimiento: solo si el org quiere avisos SMS), historial de compras (contrato + obligación fiscal: la retención), IP del login (interés legítimo: seguridad, la 23), la cookie analítica (consentimiento ANTES de cargarla: el banner), la foto del org (consentimiento + licitud del contenido). La tabla con la base por fila: el dato sin base no se recoge (minimización).
- La evidencia del opt-in:
class MarketingConsent(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE)
version_texto = models.CharField(max_length=20) # "v2027-03"
concedido_en = models.DateTimeField()
revocado_en = models.DateTimeField(null=True)
# el filtro del marketing: consentimiento ACTIVO (concedido y no revocado) — el flag derivado, no el booleano suelto- El flujo de supresión extendida: el perfil y los contactos se BORRAN; el email del ticket/ledger se ANONIMIZA (
u-841@deleted.invalid— la integridad del histórico de facturación preservada) y el audit registra "supresión ejecutada por request X, 3 tablas, 2 anonimizadas". El test: el export posterior sin PII + el ledger cuadrando (la comisión del mes idéntica): la excepción legal del dinero funciona sin romper la facturación.
Ejercicio 2 — El mapa del Art. 30
- Los hallazgos del mapa: el log del 45: identificadores técnicos sin PII (base: interés legítimo, retención 30 días); la caché de perfil (38): PII en Redis → base contrato + purge por usuario (23) + TTL 60 s (el dato que caduca solo: la retención en el caché); la tabla de invitados del evento (el campo libre del nombre de los acompañantes): sin clasificar → clasificada (contrato, retención fin del evento + fiscal).
- El guard del PII: el test que escanea las claves de caché activas y las líneas de log de ejemplo contra los regex de email/teléfono: 0 matches. El hallazgo típico: el detalle del 26 con el email que el usuario escribió mal (el log del error llevaba el payload: corregido con el identificador).
- El doc del mapa con la revisión trimestral y el dueño: el documento del Art. 30 con el enlace a cada schema (48: doc-enlazado-al-código).
Ejercicio 3 — Las evidencias SOC 2
- Las 3 evidencias (scripts del repo):
manage.py reporte_accesos --desde 2027-07 # los escalados de rol del audit (23/21)
manage.py reporte_secretos # las rotaciones del 27 con fecha y actor
manage.py reporte_cambios --sin-review # los deploy sin PR: esperado 0 (41)- La cadena de cambios probada: el push directo a main → bloqueado (protección de branch: el PR + CI del 41); el
kubectl applyfuera del pipeline → el diff del drift del 44 lo caza al plan siguiente; el hotfix de emergencia → PR de emergencia con review posterior documentado (el 47). Los controles de cambio con evidencia: el CI + el IaC. - El gap honesto: el access review trimestral (¿quién tiene acceso que ya no necesita?) no existía → el issue con dueño/fecha (el script del ejercicio 1 como su base). El gap declarado: el control en camino.
Ejercicio 4 — El audit log y la brecha
- Las consultas del auditor con su test:
def test_consulta_de_datos_de_usuario_solo_admin(self):
self.client.force_authenticate(self.soporte) # NO es auditor
r = self.client.get(f"/admin/audit/dato/{self.user.id}")
self.assertEqual(r.status_code, 403) # el 21: el acceso a la auditoría es privilegiado
self.client.force_authenticate(self.auditor)
self.assertEqual(self.client.get(...).status_code, 200) # y la consulta quedó auditada (¡el meta-audit!)- La retención como job: con FakeClock 13 meses: la fila de audit > 12 meses se archiva a frío (el bucket del 42) y el registro del purgado queda en audit. El consentimiento revocado: el dato de marketing purgado en el siguiente ciclo con su fila ("purgado por revocación").
- El runbook de brecha (extracto): T0 detectar (alerta del 46) → T+1h acotar (47: protocolo) → T+2h contener (flag/rollback 41) → T+24h evaluar riesgo (¿alto? → notificar afectados) → T+72h notificar regulador (la plantilla del doc) → postmortem con la acción regulatoria. Los umbrales escritos: "datos sensibles o >100 afectados: afectados siempre; el resto: evaluación del DPO" — la decisión documentada antes del reloj.
Ejercicio 5 — El DPO interno
- El linter del esquema:
def test_toda_tabla_con_pii_clasificada(self):
for model in MODELOS_CON_DATOS_PERSONALES:
assert hasattr(model, "data_classification"), \
f"{model.__name__} sin base legal/retención: el mapa del Art. 30 miente"El data_classification = {"base": "contrato", "retencion": "5y"} en el Meta: el esquema anota su cumplimiento (el mapa del §2 generado del código: la referencia viva del 48).
- El diagrama del DPO interno:
[mapa Art.30 generado del schema] ← el linter de clasificación (1)
[export/supresión API] → audit ← SLA minutos (23)
[jobs de retención 31] → audit ← purgados automáticos (4)
[evidencias SOC 2] → reportes cron (3)
[runbook de brecha] → doc con caducidad (48)- La checklist final (10 ítems): bases legales, mapa generado, consentimiento con evidencia, supresión con excepción del dinero, portabilidad, PII en log/caché guardada, audit consultas del auditor, retención automatizada, runbook de brecha, evidencias SOC 2 (3 de 5: el access review en issue). El estado real: 9 de 10 — el cumplimiento como consecuencia de la ingeniería del curso, con el gap restante documentado.
Resumen del profesor
- El RGPD se implementa en el ciclo del dato: base legal por dato, minimización, retención por tipo, y la supresión que respeta la excepción del dinero (anonimizar, no borrar la facturación).
- SOC 2 audita controles operando: el curso ya produce las evidencias (PRs, rotaciones, drills, postmortems) — el 80% del certificado es ordenar la buena ingeniería.
- El audit log append-only + las consultas del auditor + los jobs de retención + el linter de clasificación = el DPO interno: el cumplimiento como consecuencia observable del sistema.