django hattori

Django Hattori y la LOPD

Desde APSL somos conscientes de que no siempre es fácil aplicar todas las normativas, por eso hace un tiempo y con la llegada de la Ley Orgánica de Protección de Datos (LOPD) nuestro querido amigo mgalofre desarrolló un paquete llamado django hattori. El cual nos permite anonimizar los datos sensibles que hay en una base de datos gestionada por Django de forma muy sencilla.

En este post vamos ha hablar de cómo lo implementamos en uno de los proyectos y de cómo nos ha servido para cumplir con la ya mencionada ley.

Instalación.

Como bien indica la documentación que hay en el repositorio, lo primero de todo es instalar el paquete:

pip install django-hattori

Una vez hecho eso lo metemos en el INSTALLED_APPS:

INSTALLED_APPS = [
    ...
    'hattori',
]

A partir de aquí la configuración es muy simple, más si somos asiduos con el uso de Django y su forma de hacer las cosas.

Configuración.

En nuestro caso lo implentamos en varias aplicaciones de Django, pero como en un principio viene a ser lo mismo vamos a simplificarlo hablando de 2 casos: reservas y usuarios.

Reservas.

Este caso trataba de anonimizar una tablas que contenian datos de clientes de un hotel. Entre todos estos datos, los que nos interesaban eran los siguientes:

  • Nombre de cliente.
  • Apellido de cliente.
  • Teléfono de cliente.
  • DNI o Documento de cliente.
  • Nombre de la tarjeta.
  • Número de la tarjeta.

No todos los campos mencionados són obligatorios.

Así pues y siguiendo la documentación del repositorio creamos nuestra clase dentro de la aplicación en cuestión:

from hattori.base import BaseAnonymizer, faker

from .models import Booking
# Hablaremos más adelante de estos métodos.
from main.utils import gen_random_document, gen_random_phone_number

class BookingAnonymizer(BaseAnonymizer):
    model = Booking

    attributes = [
        ('nombre_cliente', faker.first_name),
        ('apellido_cliente', faker.last_name),
        ('telefono_cliente', gen_random_phone_number),
        ('documentacion_cliente', gen_random_document),

        ('tarjeta_nombre_cliente', faker.name),
        ('tarjeta_numero_cliente', faker.credit_card_number),
    ]

Usuarios.

Si usamos la funcionalidad que ofrece Django para la gestión de usuarios nos encontramos con que también debemos de anonimizar esos datos cuando los trabajemos, por tanto y de la misma manera que antes creamos nuestra clase:

from django.contrib.auth.models import User
from hattori.base import BaseAnonymizer, faker

# Paciencia, en un rato hablamos de esto.
from main.utils import gen_unique_user_email

class UserAnonymizer(BaseAnonymizer):
    model = User

    attributes = [
        ('email', gen_unique_user_email),
        ('first_name', faker.first_name),
        ('last_name', faker.last_name),
    ]

Como podéis ver crear las clases es muy sencillo, simplemente definimos qué campos queremos sobreescribir y con qué valores.

Métodos propios.

Durante todo este proceso hemos usado las funciones que nos ofrece el paquete Faker. Pero nos encontramos con un pequeño dilema, en el caso del usuario al usar la funcionalidad de Django no podíamos simplemente usar Faker porque había una posibilidad de que cuando generara un email nuevo éste ya exisitera. Esto es debido a que Faker tiene un número limitado de emails.

Es por esto y aprovechando que podemos añadir una función propia nos lanzamos a crear nuestras funciones generadoras de emails únicos.

Generando emails únicos random.

Generame un email.

Dentro del fichero utils.py definimos la siguiente función:

No es obligatorio que sea ahí, dependerá de la estructura de cada proyecto.

import random
import string


from hattori.base import faker


def gen_random_email():
    email_domain = faker.free_email_domain()
    email_name = faker.first_name().lower().replace(" ", "")
    num = "".join([random.choice(string.digits) for i in range(4)])
    return "{name}{num}@{domain}".format(name=email_name, domain=email_domain, num=num)

Primeramente, usamos faker para obtener un dominio de email y un nombre aleatorio.

email_domain = faker.free_email_domain()
email_name = faker.first_name().lower().replace(" ", "")

Luego de esto generamos un número de 4 cifras totalmente aleatorias:

num = "".join([random.choice(string.digits) for i in range(4)])

Una vez tenemos estas dos partes las unimos y la enviamos en el return.

return "{name}{num}@{domain}".format(name=email_name, domain=email_domain, num=num)

De esta forma hemos conseguido solventar la primera parte, el limitado número de emails que tiene Faker. Pero sin embargo seguíamos teniendo un problema. Aun con los número generados aleatoriamente, había una posibilidad de que el email se repitiera por lo que obtamos por crear una nueva función que solventara esto.

Que sea único, por favor.

Así pues tenemos la siguiente función:

from django.contrib.auth.models import User
from hattori.base import faker


generated_emails = []

def gen_unique_user_email():
    email = gen_random_email()
    already_exists = User.objects.filter(email=email).exists()
    # Revisamos la lista de los emails ya generados para no repetirlos y
    # así evitar que de fallo cuando Django vaya a guardar los registros.
    listed = email in generated_emails
    if already_exists or listed:
        return gen_unique_user_email()
    else:
        generated_emails.append(email)
        return email

Esta función genera un email nuevo usando la función anterior pero en vez de devolverlo sin más comprobamos si ya existe dentro de la base de datos de Usuarios (User) y lo almacenamos en una variable.

email = gen_random_email() # genera
already_exists = User.objects.filter(email=email).exists() # comprobamos

Después de eso comprobamos si ya está dentro de la lista de generated_emails y lo guardamos de nuevo en una variable. Esta lista es importante porque evitará que creemos emails repetidos antes de guardarlos.

listed = email in generated_emails

Más info sobre como guarda los datos Django aquí.

Una vez tenemos ambas variables already_exists y listed podemos hacer la magia.

Si es un email totalmente nuevo, que no existe en la base de datos actual y tampoco está listada para ser guardada entonces guardamos este nuevo email en la lista y después lo devolvemos en el return.

Pero, si ya existe o está dentro de la lista a guardar entonces nos aprovechamos de la recursividad para generar otro email y volver a hacer todas las comprobaciones hasta que tengamos uno nuevo. Es como el bitcoin de los emails, hasta que no se descubre uno nuevo, no vale.

if already_exists or listed:
        return gen_unique_user_email()
    else:
        generated_emails.append(email)
        return email

Anonimizame.

Una vez tengamos todo listo, desde el terminal ejecutamos el siguiente comando ./manage.py anonymize_db y este se encargará de hacer todo el proceso. Una vez finalizado simplemente queda seguir trabajando de forma habitual.

Da igual la cantidad.

Pese a que a nosotros esto nos ha servido hay que tener en cuenta una cosa. En la función de generar un email suelto usamos 4 cifras generadas de forma aleatoria, un dominio generado aleatoriamente y nombre también aleatorio. Esto puede generar una cantidad muy alta de emails, ya sólo con las 4 cifras tenemos hasta 10.000 posibles números.

Pero, si se da el caso de tener una cantidad enorme (en serio, enorme) de registros de usuarios, siempre podemos retocar la función para que use 5 cifras en vez de 4 con lo que pasamos a 100.000 posibles resultados sólo con los números.

Aunque también puedes hacerlo de la forma que más cómoda te sea, no es obligatorio usar los números, dominios y nombres como lo hemos hecho nosotros.

Arigatō (Gracias)

Gracias de nuevo a nuestro compañero mgalofre por este estupendo paquete y a ti por llegar hasta aquí. Si sientes curiosidad por Hattori no dudes en ir al repositorio y urgar, mejorar o trastear con Hattori.

Espero que tengas un magnífico día.

Hasta pronto, Ksoler.

blog comments powered by Disqus