LGPD e falsear dados sensíveis no banco de dados de dev e staging - parte 1
Tags: database, programming, go, security
Published: 2022-05-30 16:09:04

## Contextualizando

A [Lei Geral de Proteção de Dados Pessoais (LGPD)]([https://www.gov.br/cidadania/pt-br/acesso-a-informacao/lgpd](https://www.gov.br/cidadania/pt-br/acesso-a-informacao/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](https://dev.mysql.com/doc/refman/8.0/en/mysql.html)) e [mysqldump](https://dev.mysql.com/doc/refman/8.0/en/mysqldump.html)) 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](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`](https://github.com/samuelralmeida/fakedb/blob/main/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:

```bash
mysql -user=root --password=minha_senha -e "CREATE DATABASE temp"
```

Golang oferece um pacote nativo [os/exec](https://pkg.go.dev/os/exec) que permite executar pelo código comandos de terminal. O exemplo abaixo faz o mesmo que o código anterior:

```go
// ...
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
}
```

O arquivo [`mysqldb`](https://github.com/samuelralmeida/fakedb/blob/main/mysqldb/mysqldb.go) 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:

```go
mysqldump -u root -p client > prod_trem.sql
```

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:

```go
mysql -u root -p temp < prod_trem.sql
```

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*:

```go
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
}
```

### *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*](https://pkg.go.dev/sync#WaitGroup).

```go
// 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
}
```

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](https://hub.docker.com/_/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.