# Sauvegardes anonymes

> Transcription française complète du chapitre « Sauvegardes anonymes » du manuel DEV42, révision 26.09, pages PDF 521 à 532.

---

Index LLMS : [llms.txt](/fr/llms.txt)

---

<!-- Source: https://public.dalibo.com/exports/formation/manuels/formations/dev42/dev42.handout.html, révision 26.09. -->

# Sauvegardes anonymes {#dev42-ch14}

Dans beaucoup de situations, le besoin est simplement d’exporter les données anonymisées pour les importer dans une autre base de données, pour mener des tests ou produire des statistiques.

postgresql_anonymizer permet de générer des sauvegardes logiques anonymisées avec `pg_dump`.

> Les outils `pg_dump_anon.sh` et `pg_dump_anon` des versions 0.x ou 1.x de postgresql_anonymizer sont dépréciés. Modifiez vos scripts pour utiliser `pg_dump`.

## L’histoire {#dev42-ch14-1}

- Paul a un site web qui dispose d’une section commentaires où les utilisateurs peuvent partager leurs points de vue.

- Paul a engagé un prestataire pour développer le nouveau design de son site web.

- Le prestataire lui demande un export de la base de données.

- Paul veut « nettoyer » le dump et y retirer toute information personnelle qui pourrait figurer dans la section commentaire.

## Comment ça marche ? {#dev42-ch14-2}

![Illustration du manuel DEV42](/images/dev42/html/ch14-9fcf3183eabf.png)

## Objectifs {#dev42-ch14-3}

- Extraire les données anonymisées de la base de données
- Écrire une fonction de masquage personnalisée pour gérer une colonne de type JSON

## Table « website_comment » {#dev42-ch14-4}

``` sql
\c boutique paul

DROP TABLE IF EXISTS website_comment CASCADE ;

CREATE TABLE website_comment (
  id SERIAL PRIMARY KEY,
  message jsonb
) ;
```

### Quelques données {#dev42-ch14-4-1}

``` bash
curl -kLs https://dali.bo/website_comment -o /tmp/website_comment.tsv
head /tmp/website_comment.tsv
```

``` json
    1   {"meta": {"name": "Lee Perry", "ip_addr": "40.87.29.113"}, "content": "Hello Nasty!"}
    2   {"meta": {"name": "", "email": "biz@bizmarkie.com"}, "content": "Great Shop"}
    3   {"meta": {"name": "Jimmy"}, "content": "Hi! This is me, Jimmy James "}
```

``` sql
\c boutique paul
\copy website_comment from '/tmp/website_comment.tsv'
```

``` sql
SELECT jsonb_pretty(message)
FROM website_comment
ORDER BY id ASC
LIMIT 1 ;
```

``` default
           jsonb_pretty
-----------------------------------
 {                                +
     "meta": {                    +
         "name": "Lee Perry",     +
         "ip_addr": "40.87.29.113"+
     },                           +
     "content": "Hello Nasty!"    +
 }
```

## Activer l’extension {#dev42-ch14-5}

``` sql
CREATE EXTENSION IF NOT EXISTS anon;
SELECT anon.init();
```

## Masquer une colonne de type JSON {#dev42-ch14-6}

Généralement, les données non structurées sont difficiles à masquer…

``` sql
SELECT message->'meta'->'name' AS name,
       message->'content' AS content
FROM website_comment
ORDER BY id ASC ;
```

La colonne `comment` contient beaucoup d’informations personnelles. Le fait de ne pas utiliser un schéma standard pour les commentaires rend ici la tâche plus complexe.

Comme on peut le voir, les visiteurs du site peuvent écrire toutes sortes d’informations dans la section « commentaire ». La meilleure option serait donc de supprimer entièrement la clé JSON car il est impossible d’y exclure les données sensibles.

Il est possible de nettoyer la colonne `comment` en supprimant la clé `content` :

``` sql
SELECT (message - ARRAY['content']) AS website_comment
FROM website_comment
WHERE id=1 ;
```

``` default
                      website_comment
------------------------------------------------------------
 {"meta": {"name": "Lee Perry", "ip_addr": "40.87.29.113"}}
(1 ligne)
```

Ici, `ARRAY['content']` est un tableau (`ARRAY`) contenant une seule valeur, `content`. L’opérateur `-` est utilisé pour supprimer toutes les occurrences de `content` du tableau message.

### Fonctions de masquage personnalisées {#dev42-ch14-6-1}

``` sql
\c boutique paul

CREATE SCHEMA IF NOT EXISTS my_masks;

SECURITY LABEL FOR anon ON SCHEMA my_masks IS 'TRUSTED';
```

``` sql
CREATE OR REPLACE FUNCTION my_masks.remove_content(j jsonb)
RETURNS jsonb
AS $func$
  SELECT j - ARRAY['content']
$func$
LANGUAGE sql ;
```

- Super-utilisateurs seulement !

Créer en premier lieu un schéma dédié, ici **my_masks**, et le déclarer en `TRUSTED` (« de confiance »). Cela signifie que l’extension `anon` va considérer les fonctions de ce schéma comme des fonctions de masquage valides.

> Seul un super-utilisateur devrait être capable d’ajouter des fonctions dans ce schéma !

Cette fonction de masquage se contente de supprimer du JSON le champ avec le message :

``` sql
CREATE OR REPLACE FUNCTION my_masks.remove_content(j jsonb)
RETURNS jsonb
AS $func$
  SELECT j - ARRAY['content']
$func$
LANGUAGE sql ;
```

Exécuter la fonction :

``` sql
SELECT my_masks.remove_content(message)
FROM website_comment ;
```

``` default
 {"meta": {"name": "Lee Perry", "ip_addr": "40.87.29.113"}}
 {"meta": {"name": "", "email": "biz@bizmarkie.com"}}
 {"meta": {"name": "Jimmy"}}
```

La fonction va pouvoir ensuite être utilisée dans une règle de masquage.

### Utilisation de la fonction de masquage personnalisée {#dev42-ch14-6-2}

``` sql
SECURITY LABEL FOR anon ON COLUMN website_comment.message
IS 'MASKED WITH FUNCTION my_masks.remove_content(message)';
```

### Sauvegarde anonymisée (1/2) {#dev42-ch14-6-3}

Créer un rôle dédié pour les exports anonymisés :

``` sql
CREATE ROLE anon_dumper LOGIN ;

ALTER ROLE anon_dumper
SET anon.transparent_dynamic_masking TO TRUE;

SECURITY LABEL FOR anon ON ROLE anon_dumper IS 'MASKED';

GRANT pg_read_all_data TO anon_dumper;
```

Saisir le mot du passe de l’utilisateur `anon_dumper` :

``` sql
\password anon_dumper
```

En tant qu’utilisateur système **postgres**, compléter le fichier **.pgpass** :

``` bash
echo "*:*:boutique:anon_dumper:<mot_de_passe>" >- ~/.pgpass
```

**Remplacer par le mot de passe au rôle `anon_dumper`**

### Sauvegarde anonymisée (2/2) {#dev42-ch14-6-4}

Depuis l’utilisateur système `postgres` :

``` bash
pg_dump -U anon_dumper boutique --table=website_comment > /tmp/anon_boutique.sql
```

On utilise l’outil habituel de sauvegarde `pg_dump` mais avec un rôle masqué.

Il est possible d’obtenir les données au format `plain` ou binaire.

## Exercices {#dev42-ch14-7}

Pour rappel, sauf précision contraire, toutes les commandes sont à exécuter en tant que **paul** (le propriétaire).

### E301 - Exporter les données anonymisées dans une nouvelle base de données {#dev42-ch14-7-1}

- Créer une base de données nommée **boutique_anon**.
- Charger l’extension `anon` dans la base de données **boutique_anon**, et
- y insérer les données anonymisées provenant de la base de données **boutique**.

### E302 - Pseudonymiser les métadonnées du commentaire {#dev42-ch14-7-2}

Pierre compte extraire des informations générales depuis les métadonnées. Par exemple, il souhaite calculer le nombre de visiteurs uniques sur la base des adresses IP des visiteurs, mais une adresse IP est un **identifiant indirect**.

Paul doit donc anonymiser cette colonne tout en conservant la possibilité de faire le lien entre les enregistrements provenant de la même adresse.

- Remplacer la fonction `remove_content` par la fonction `clean_comment` (ci-dessous), qui :
  - supprime la clé JSON `content` ;
  - remplace la valeur dans la colonne `name` par un faux nom ;
  - remplace l’adresse IP dans la colonne `ip_address` par sa somme de contrôle `md5` ;
  - met à `NULL` la clé `email`.

``` sql
\c boutique paul

CREATE OR REPLACE FUNCTION my_masks.clean_comment(message jsonb)
RETURNS jsonb
VOLATILE
LANGUAGE SQL
AS $func$
SELECT
  jsonb_set(
    message,
    ARRAY['meta'],
    jsonb_build_object(
        'name',anon.fake_last_name(),
        'ip_address', md5((message->'meta'->'ip_addr')::TEXT),
        'email', NULL
    )
  ) - ARRAY['content'];
$func$;
```

## Solutions {#dev42-ch14-8}

### S301 {#dev42-ch14-8-1}

- Créer une base de données nommée **boutique_anon**.
- Charger l’extension `anon` dans la base de données **boutique_anon**, et
- y insérer les données anonymisées provenant de la base de données **boutique**.

``` bash
dropdb -U paul --if-exists boutique_anon
createdb boutique_anon -U paul --owner paul
```

``` bash
psql -U paul -d boutique_anon -c 'SELECT COUNT(*) FROM company'
```

``` default
     count
    -------
         3
```

Chargement de l’extension :

``` sql
ALTER DATABASE boutique_anon
SET session_preload_libraries = 'anon';
\c boutique_anon
CREATE EXTENSION IF NOT EXISTS anon ;
SELECT anon.init() ;
```

Insertion des données anonymisées dans `boutique_anon` :

``` bash
pg_dump -U anon_dumper -d boutique | psql --quiet -d boutique_anon
```

### S302 {#dev42-ch14-8-2}

- Remplacer la fonction `remove_content` par la fonction `clean_comment` (ci-dessous), qui :
  - supprime la clé JSON `content` ;
  - remplace la valeur dans la colonne `name` par un faux nom ;
  - remplace l’adresse IP dans la colonne `ip_address` par sa somme de contrôle `md5` ;
  - met à `NULL` la clé `email`.

``` sql
\c boutique_anon paul
CREATE SCHEMA IF NOT EXISTS my_masks ;

CREATE OR REPLACE FUNCTION my_masks.clean_comment(message jsonb)
RETURNS jsonb
VOLATILE
LANGUAGE SQL
AS $func$
SELECT
  jsonb_set(
    message,
    ARRAY['meta'],
    jsonb_build_object(
        'name', anon.fake_last_name(),
        'ip_address', md5((message->'meta'->'ip_addr')::TEXT),
        'email', NULL
    )
  ) - ARRAY['content'];
$func$;
```

Suite à l’utilisation de la fonction personnalisée `clean_comment`, les données n’ont plus rien à voir :

``` sql
SELECT my_masks.clean_comment(message)
FROM website_comment;
```

``` default
                                         clean_comment
-----------------------------------------------------------------------------------------------
 {"meta": {"name": "Heller", "email": null, "ip_address": "1d8cbcdef988d55982af1536922ddcd1"}}
 {"meta": {"name": "Christiansen", "email": null, "ip_address": null}}
 {"meta": {"name": "Frami", "email": null, "ip_address": null}}
(3 lignes)
```

On applique le masquage comme à l’habitude :

``` sql
SECURITY LABEL FOR anon ON COLUMN website_comment.message
IS 'MASKED WITH FUNCTION my_masks.clean_comment(message)';
```
## Navigation

Pour revenir au chapitre précédent, voir [le chapitre précédent](/fr/dev42/13-masquage-dynamique/#dev42-ch13).
Pour poursuivre la lecture, voir [le chapitre suivant](/fr/dev42/15-vues-de-masquage/#dev42-ch15).
