DEV Community 👩‍💻👨‍💻

LGPD e falsear dados sensíveis no banco de dados de dev e staging - parte 1

Contextualizando

A Lei Geral de Proteção de Dados Pessoais (LGPD) visa regulamentar direitos e deveres no tratamento de dados pessoais. Participando de conversas sobre o tema, o time que trabalho chegou a conclusão que é mais seguro e inteligente que os bancos de dados de desenvolvimento e staging não tivessem dados pessoais reais dos clientes por alguns motivos:

a) como os desenvolvedores trabalham de forma remota o acesso a esses bancos é mais aberto.

b) uma vez que não é necessário aos ambientes de desenvolvimento e staging que existam dados reais, é mais seguro e eficiente não tê-los do que criar estratégias e recursos para protegê-los.

O desafio

Nosso banco de produção possui dois databases importantes que possuem dados sensíveis para serem falseados e disponibilizados em desenvolvimento e staging. O desafio foi gerar um programa que automatize esse processo de gerar um banco com dados falsos a partir do de produção; ainda é necessário que seja possível escolher qual database de origem e qual ambiente de destino.

Um rascunho seria:

  1. Fazer a cópia do banco de produção
  2. Inserir essa cópia em um banco temporário
  3. Falsear os dados sensíveis (parte 2)
  4. Fazer uma nova cópia com os dados falsos
  5. Inserir no bancos de desenvolvimento e/ou staging
  6. Excluir arquivos intermediários gerados (parte 2)

Disclaimer

Por motivos de segurança da informação os databases, tabelas e colunas serão inventados. Além disso, os exemplos usam tabelas menores e mais simples, uma vez que o objetivo é mostrar e explicar o código desenvolvido.

Parte 1

O programa foi feito em Golang e o chamei de fakedb. Essa primeira parte aborda como o fakedb usa os recursos dos clientes CLI mysql) e mysqldump) para resolver os itens 1, 2, 4 e 5 do rascunho apresentado anteriormente.

O código completo pode ser acessado em https://github.com/samuelralmeida/fakedb

A estrutura do banco de produção

O código SQL para gerar o banco de produção com dados para nosso código se encontra no arquivo populate.sql

Go exec, mysql e mysqldump

mysql e mysqldump são clientes CLI para manipular bancos de forma direta, por exemplo, o código abaixo cria o database temp diretamente pelo terminal:

mysql -user=root --password=minha_senha -e "CREATE DATABASE temp"
Enter fullscreen mode Exit fullscreen mode

Golang oferece um pacote nativo os/exec que permite executar pelo código comandos de terminal. O exemplo abaixo faz o mesmo que o código anterior:

// ...
import "os/exec"

func dropDatabase(dbName) error {
    cmd := exec.Command("mysql", "--user=root", "--password=minha_senha", "-e", "CREATE DATABASE temp")
    err := cmd.Run()
    return err
}
Enter fullscreen mode Exit fullscreen mode

O arquivo mysqldb contem as funções que usamos para deletar o database se existir (DropDatabase), criar um database (CreateDatabase), exportar dados (DumpDatabase), importar dados (RestoreData).

Função de exportar dados recebe o nome do arquivo sql que vai ser gerado. No terminal seria similar a:

mysqldump -u root -p client > prod_trem.sql
Enter fullscreen mode Exit fullscreen mode

Já a função para importar os dados recebe o nome do arquivo que possui os dados para serem inseridos, no terminal teriamos algo como:

mysql -u root -p temp < prod_trem.sql
Enter fullscreen mode Exit fullscreen mode

Essa particularidade do arquivo ser um input para o cliente mysql faz com que precisamos abrir o arquivo em buffer e controlar o stdin no Go para usá-lo junto com o mysql:

func RestoreData(filename string, args credentials.ConnectionParams) error {
    bytes, err := ioutil.ReadFile(fmt.Sprintf("./%s", filename))
    if err != nil {
        return fmt.Errorf("restoreData: error to read file: %w", err)
    }

    dropArgs := []string{
        fmt.Sprintf("--host=%s", args.Host),
        fmt.Sprintf("--port=%s", args.Port),
        fmt.Sprintf("--user=%s", args.User),
        fmt.Sprintf("--password=%s", args.Password),
        args.Database,
    }

    cmd := exec.Command("mysql", dropArgs...)

    stdin, err := cmd.StdinPipe()
    if err != nil {
        return fmt.Errorf("restoreData: error to get stdin pipe: %w", err)
    }

    go func() {
        defer stdin.Close()
        stdin.Write(bytes)
    }()

    _, err = cmd.CombinedOutput()
    if err != nil {
        return fmt.Errorf("restoreData: error to run command: %w", err)
    }

    return nil
}
Enter fullscreen mode Exit fullscreen mode

Goroutines para restaurar dados

As etapas mais demoradas são: a) fazer o dump do banco, que por ser de um único banco não tem como fazer em paralelo; e b) restaurar os dados em um novo database, o que podemos melhorar a performance paralelizando essa etapa uma vez que a restauração ocorre para bancos de dev e staging.

O código abaixo mostra como abrimos uma goroutine para cada banco alvo, controlamos os possíveis erros de forma independente e aguardamos que finalizem usando sync.WaitGroup.

// goroutine para alvos
func restoreTargets(filename string, targets []credentials.ConnectionParams) error {
    var (
        wg   sync.WaitGroup
        errs = []string{}
    )

    for _, target := range targets {
        t := target

        wg.Add(1)
        go func(credential credentials.ConnectionParams) {
            defer wg.Done()

            var err error

            log.Printf("drop database if exixts - %s", credential.Database)
            err = mysqldb.DropDatabase(credential)
            if err != nil {
                err = fmt.Errorf("target - %s: %w", credential.Database, err)
                errs = append(errs, err.Error())
                return
            }

            log.Printf("create database - %s", credential.Database)
            err = mysqldb.CreateDatabase(credential)
            if err != nil {
                err = fmt.Errorf("target - %s: %w", credential.Database, err)
                errs = append(errs, err.Error())
                return
            }

            log.Printf("restore database - %s", credential.Database)
            err = mysqldb.RestoreData(filename, credential)
            if err != nil {
                err = fmt.Errorf("target - %s: %w", credential.Database, err)
                errs = append(errs, err.Error())
                return
            }

            log.Printf("DONE: restore database - %s", credential.Database)

        }(t)
    }

    wg.Wait()

    if len(errs) > 0 {
        return errors.New(strings.Join(errs, "&&"))
    }
    return nil
}
Enter fullscreen mode Exit fullscreen mode

Empregamos esse mesmo raciocínio para executar o script em paralelo para bancos diferentes, uma goroutine para o banco trem e outra para o banco coisa, como pode ser visto no arquivo main.go. Nos nossos testes, o uso de goroutines nos fez economizar 30% do tempo gasto sem elas.

Acessando bancos por variáveis de ambiente

Para não deixar como hardcode dados de acesso aos bancos, usamos variáveis de ambiente que são carregadas dentro do container docker que roda todo o script. As variáveis ficam em um arquivo .env, e no arquivo example.env tem um esqueleto dessas variáveis.

A imagem docker criada para executar o script se baseia em uma imagem do Docker do Mysql, que já vem com os clientes mysql e mysldump instalados. Além disso, usando a variável de ambiente MYSQL_ROOT_PASSWORD, já inicia um banco local dentro do container o usado como banco intermediário para receber os dados de produção, falsear e exportar. Por isso a mesma senha dele é usada na variável de ambiente INTERMEDIATE_PASS.

A vantagem de usar esse banco local do container como intermediário, é que finalizando o script e removendo o container, todos os dados intermediários que o script gerou são apagados também.

Continua…

Essa primeira parte abordou mais a manipulação dos bancos, a segunda parte vai ser a respeito da lógica para gerar dados falsos para o banco. O artigo ficou um pouco grande e denso devido aos detalhes do assunto e do código, mas espero que tenha ajudado você. Até breve.

Top comments (0)

🌙 Dark Mode?!

 
Turn it on in Settings