sql >> Base de Datos >  >> RDS >> PostgreSQL

Anonimización de PostgreSQL bajo demanda

Antes, durante y después de la entrada en vigor del RGPD en 2018, ha habido muchas ideas para resolver el problema de eliminar u ocultar datos de usuario, utilizando varias capas de la pila de software, pero también utilizando varios enfoques. (borrado duro, borrado suave, anonimización). La anonimización ha sido una de ellas que se sabe que es popular entre las organizaciones/empresas basadas en PostgreSQL.

En el espíritu del RGPD, vemos cada vez más el requisito de que se intercambien documentos e informes comerciales entre empresas, de modo que las personas que se muestran en esos informes se presenten de forma anónima, es decir, solo se muestra su función/título. , mientras que sus datos personales están ocultos. Lo más probable es que esto se deba al hecho de que las empresas que reciben estos informes no quieren administrar estos datos según los procedimientos/procesos del RGPD, no quieren lidiar con la carga de diseñar nuevos procedimientos/procesos/sistemas para manejarlos. , y solo piden recibir los datos ya anonimizados. Por lo tanto, esta anonimización no solo se aplica a aquellas personas que han expresado su deseo de ser olvidadas, sino a todas las personas mencionadas en el informe, lo cual es bastante diferente de las prácticas comunes de GDPR.

En este artículo, vamos a tratar la anonimización para encontrar una solución a este problema. Comenzaremos presentando una solución permanente, es decir, una solución en la que una persona que solicita ser olvidada debe ocultarse en todas las consultas futuras en el sistema. Luego, sobre la base de esto, presentaremos una forma de lograr "a pedido", es decir, anonimización de corta duración, lo que significa la implementación de un mecanismo de anonimización destinado a estar activo el tiempo suficiente hasta que se generen los informes necesarios en el sistema. En la solución que presento, esto tendrá un efecto global, por lo que esta solución utiliza un enfoque codicioso, que cubre todas las aplicaciones, con una reescritura de código mínima (si la hay) (y proviene de la tendencia de los administradores de bases de datos de PostgreSQL de resolver tales problemas de forma centralizada dejando la aplicación los desarrolladores se ocupan de su verdadera carga de trabajo). Sin embargo, los métodos presentados aquí se pueden modificar fácilmente para aplicarlos en ámbitos limitados/más estrechos.

Anonimización permanente

Aquí presentaremos una forma de lograr la anonimización. Consideremos la siguiente tabla que contiene registros de los empleados de una empresa:

testdb=# create table person(id serial primary key, surname text not null, givenname text not null, midname text, address text not null, email text not null, role text not null, rank text not null);
CREATE TABLE
testdb=# insert into person(surname,givenname,address,email,role,rank) values('Singh','Kumar','2 some street, Mumbai, India','[email protected]','Seafarer','Captain');
INSERT 0 1
testdb=# insert into person(surname,givenname,address,email,role,rank) values('Mantzios','Achilleas','Agiou Titou 10, Iraklio, Crete, Greece','[email protected]','IT','DBA');
INSERT 0 1
testdb=# insert into person(surname,givenname,address,email,role,rank) values('Emanuel','Tsatsadakis','Knossou 300, Iraklio, Crete, Greece','[email protected]','IT','Developer');
INSERT 0 1
testdb=#

Esta tabla es pública, todos pueden consultarla y pertenece al esquema público. Ahora creamos el mecanismo básico para la anonimización que consiste en:

  • un nuevo esquema para contener tablas y vistas relacionadas, llamemos a esto anónimo
  • una tabla que contiene las identificaciones de las personas que quieren ser olvidadas:anonym.person_anonym
  • una vista que proporciona la versión anónima de public.person:anonym.person
  • configuración de search_path, para usar la nueva vista
testdb=# create schema anonym;
CREATE SCHEMA
testdb=# create table anonym.person_anonym(id INT NOT NULL REFERENCES public.person(id));
CREATE TABLE
CREATE OR REPLACE VIEW anonym.person AS
SELECT p.id,
    CASE
        WHEN pa.id IS NULL THEN p.givenname
        ELSE '****'::character varying
    END AS givenname,
    CASE
        WHEN pa.id IS NULL THEN p.midname
        ELSE '****'::character varying
    END AS midname,
    CASE
        WHEN pa.id IS NULL THEN p.surname
        ELSE '****'::character varying
    END AS surname,
    CASE
        WHEN pa.id IS NULL THEN p.address
        ELSE '****'::text
    END AS address,
    CASE
        WHEN pa.id IS NULL THEN p.email
        ELSE '****'::character varying
    END AS email,
    role,
    rank
  FROM person p
LEFT JOIN anonym.person_anonym pa ON p.id = pa.id
;

Establezcamos search_path en nuestra aplicación:

set search_path = anonym,"$user", public;

Advertencia :es esencial que search_path esté configurado correctamente en la definición de la fuente de datos en la aplicación. Se alienta al lector a explorar formas más avanzadas de manejar la ruta de búsqueda, p. con el uso de una función que puede manejar una lógica más compleja y dinámica. Por ejemplo, puede especificar un conjunto de usuarios de entrada de datos (o rol) y dejar que sigan usando la tabla public.person durante el intervalo de anonimización (para que sigan viendo datos normales), mientras define un conjunto de usuarios administrativos/de informes (o rol) para quien se aplicará la lógica de anonimización.

Ahora consultemos nuestra relación persona:

testdb=# select * from person;
-[ RECORD 1 ]-------------------------------------
id    | 2
givenname | Achilleas
midname   |
surname   | Mantzios
address   | Agiou Titou 10, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | DBA
-[ RECORD 2 ]-------------------------------------
id    | 1
givenname | Kumar
midname   |
surname   | Singh
address   | 2 some street, Mumbai, India
email | [email protected]
role  | Seafarer
rank  | Captain
-[ RECORD 3 ]-------------------------------------
id    | 3
givenname | Tsatsadakis
midname   |
surname   | Emanuel
address   | Knossou 300, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | Developer

testdb=#

Ahora, supongamos que el Sr. Singh deja la empresa y expresa explícitamente su derecho al olvido mediante una declaración escrita. La aplicación hace esto insertando su identificación en el conjunto de identificaciones "para ser olvidadas":

testdb=# insert into anonym.person_anonym (id) VALUES(1);
INSERT 0 1

Repitamos ahora la consulta exacta que ejecutamos antes:

testdb=# select * from person;
-[ RECORD 1 ]-------------------------------------
id    | 1
givenname | ****
midname   | ****
surname   | ****
address   | ****
email | ****
role  | Seafarer
rank  | Captain
-[ RECORD 2 ]-------------------------------------
id    | 2
givenname | Achilleas
midname   |
surname   | Mantzios
address   | Agiou Titou 10, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | DBA
-[ RECORD 3 ]-------------------------------------
id    | 3
givenname | Tsatsadakis
midname   |
surname   | Emanuel
address   | Knossou 300, Iraklio, Crete, Greece
email | [email protected]
role  | IT
rank  | Developer

testdb=#

Podemos ver que los detalles del Sr. Singh no son accesibles desde la aplicación.

Anonimización global temporal

La idea principal

  • El usuario marca el inicio del intervalo de anonimización (un breve período de tiempo).
  • Durante este intervalo, solo se permiten selecciones para la tabla llamada persona.
  • Todos los accesos (selecciones) se anonimizan para todos los registros en la tabla de personas, independientemente de cualquier configuración previa de anonimización.
  • El usuario marca el final del intervalo de anonimización.

Bloques de construcción

  • Confirmación en dos fases (también conocidas como transacciones preparadas).
  • Bloqueo explícito de tablas.
  • La configuración de anonimización que hicimos anteriormente en la sección "Anonimización permanente".

Implementación

Una aplicación de administración especial (por ejemplo, llamada:markStartOfAnynimizationPeriod) realiza 

testdb=# BEGIN ;
BEGIN
testdb=# LOCK public.person IN SHARE MODE ;
LOCK TABLE
testdb=# PREPARE TRANSACTION 'personlock';
PREPARE TRANSACTION
testdb=#

Lo que hace lo anterior es adquirir un bloqueo en la tabla en modo COMPARTIR para que las INSERCIONES, ACTUALIZACIONES, ELIMINACIONES estén bloqueadas. Además, al iniciar una transacción de compromiso de dos fases (también conocida como transacción preparada, en otros contextos conocida como transacciones distribuidas o transacciones XA de arquitectura extendida), liberamos la transacción de la conexión de la sesión que marca el inicio del período de anonimización, mientras permitimos que se realicen otras sesiones posteriores. consciente de su existencia. La transacción preparada es una transacción persistente que permanece viva después de la desconexión de la conexión/sesión que la inició (a través de PREPARE TRANSACTION). Tenga en cuenta que la instrucción "PREPARAR TRANSACCIÓN" desasocia la transacción de la sesión actual. La transacción preparada se puede recoger en una sesión posterior y se puede revertir o confirmar. El uso de este tipo de transacciones XA permite que un sistema trate de manera confiable con muchas fuentes de datos XA diferentes y realice una lógica transaccional en esas fuentes de datos (posiblemente heterogéneas). Sin embargo, las razones por las que lo usamos en este caso específico:

  • para permitir que la sesión del cliente emisor finalice la sesión y desconecte/libere su conexión (dejar o, peor aún, "persistir" en una conexión es una muy mala idea, una conexión debe liberarse tan pronto como funcione las consultas que necesita hacer)
  • para que las sesiones/conexiones posteriores puedan consultar la existencia de esta transacción preparada
  • para hacer que la sesión final sea capaz de realizar esta transacción preparada (mediante el uso de su nombre), marcando así:
    • la liberación del bloqueo del MODO COMPARTIR
    • el final del período de anonimización

Para verificar que la transacción está activa y asociada con el bloqueo SHARE en nuestra tabla de personas, hacemos lo siguiente:

testdb=# select px.*,l0.* from pg_prepared_xacts px , pg_locks l0 where px.gid='personlock' AND l0.virtualtransaction='-1/'||px.transaction AND l0.relation='public.person'::regclass AND l0.mode='ShareLock';
-[ RECORD 1 ]------+----------------------------
transaction    | 725
gid            | personlock
prepared       | 2020-05-23 15:34:47.2155+03
owner          | postgres
database       | testdb
locktype       | relation
database       | 16384
relation       | 32829
page           |
tuple          |
virtualxid     |
transactionid  |
classid        |
objid          |
objsubid       |
virtualtransaction | -1/725
pid            |
mode           | ShareLock
granted        | t
fastpath       | f

testdb=#

Lo que hace la consulta anterior es garantizar que el bloqueo de persona de la transacción preparada nombrada esté activo y que, de hecho, el bloqueo asociado en la persona de la tabla en poder de esta transacción virtual esté en el modo previsto:COMPARTIR.

Así que ahora podemos ajustar la vista:

CREATE OR REPLACE VIEW anonym.person AS
WITH perlockqry AS (
    SELECT 1
      FROM pg_prepared_xacts px,
        pg_locks l0
      WHERE px.gid = 'personlock'::text AND l0.virtualtransaction = ('-1/'::text || px.transaction) AND l0.relation = 'public.person'::regclass::oid AND l0.mode = 'ShareLock'::text
    )
SELECT p.id,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.givenname::character varying
        ELSE '****'::character varying
    END AS givenname,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.midname::character varying
        ELSE '****'::character varying
    END AS midname,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.surname::character varying
        ELSE '****'::character varying
    END AS surname,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.address
        ELSE '****'::text
    END AS address,
    CASE
        WHEN pa.id IS NULL AND NOT (EXISTS ( SELECT 1
          FROM perlockqry)) THEN p.email::character varying
        ELSE '****'::character varying
    END AS email,
p.role,
p.rank
  FROM public.person p
LEFT JOIN person_anonym pa ON p.id = pa.id

Ahora, con la nueva definición, si el usuario ha comenzado a preparar la transacción de bloqueo personal, se devolverá la siguiente selección:

testdb=# select * from person;
id | givenname | midname | surname | address | email |   role   |   rank   
----+-----------+---------+---------+---------+-------+----------+-----------
  1 | ****  | **** | **** | **** | ****  | Seafarer | Captain
  2 | ****  | **** | **** | **** | ****  | IT   | DBA
  3 | ****  | **** | **** | **** | ****  | IT   | Developer
(3 rows)

testdb=#

lo que significa anonimización global incondicional.

Cualquier aplicación que intente usar datos de la persona de la tabla obtendrá un "****" anónimo en lugar de datos reales reales. Ahora supongamos que el administrador de esta aplicación decide que el período de anonimización debe finalizar, por lo que su aplicación ahora emite:

COMMIT PREPARED 'personlock';

Ahora cualquier selección posterior devolverá:

testdb=# select * from person;
id |  givenname  | midname | surname  |            address             |         email         |   role   |   rank   
----+-------------+---------+----------+----------------------------------------+-------------------------------+----------+-----------
  1 | ****    | **** | **** | ****                               | ****                      | Seafarer | Captain
  2 | Achilleas   |     | Mantzios | Agiou Titou 10, Iraklio, Crete, Greece | [email protected]   | IT   | DBA
  3 | Tsatsadakis |     | Emanuel  | Knossou 300, Iraklio, Crete, Greece | [email protected] | IT   | Developer
(3 rows)

testdb=#

¡Advertencia! :El bloqueo evita las escrituras simultáneas, pero no evita la escritura eventual cuando se haya liberado el bloqueo. Por lo tanto, existe un peligro potencial para actualizar aplicaciones, leer '****' de la base de datos, un usuario descuidado, presionar actualizar y luego, después de un período de espera, se libera el bloqueo COMPARTIDO y la actualización tiene éxito escribiendo '*** *' en lugar de donde deberían estar los datos normales correctos. Los usuarios, por supuesto, pueden ayudar aquí al no presionar los botones a ciegas, pero aquí se podrían agregar algunas protecciones adicionales. La actualización de aplicaciones podría generar un:

set lock_timeout TO 1;

al comienzo de la transacción de actualización. De esta manera, cualquier espera/bloqueo de más de 1 ms generará una excepción. Lo que debería proteger contra la gran mayoría de los casos. Otra forma sería una restricción de verificación en cualquiera de los campos confidenciales para verificar el valor '****'.

¡ALARMA! :es imperativo que la transacción preparada se complete finalmente. Ya sea por el usuario que lo inició (u otro usuario), o incluso por un script cron que verifica las transacciones olvidadas cada, digamos, 30 minutos. Olvidarse de finalizar esta transacción provocará resultados catastróficos, ya que impide que VACUUM se ejecute y, por supuesto, el bloqueo seguirá estando allí, evitando que se escriba en la base de datos. Si no se siente lo suficientemente cómodo con su sistema, si no comprende completamente todos los aspectos y todos los efectos secundarios del uso de una transacción preparada/distribuida con un bloqueo, si no cuenta con un monitoreo adecuado, especialmente con respecto a MVCC métricas, entonces simplemente no siga este enfoque. En este caso, podría tener una tabla especial que contenga parámetros para fines administrativos donde podría usar dos valores de columna especiales, uno para el funcionamiento normal y otro para la anonimización forzada global, o podría experimentar con bloqueos de avisos compartidos a nivel de aplicación de PostgreSQL:

  • https://www.postgresql.org/docs/10/explicit-locking.html#ADVISORY-LOCKS
  • https://www.postgresql.org/docs/10/functions-admin.html#FUNCTIONS-ADVISORY-LOCKS