# F1.9 — Repontar o BI, e o isolamento que não estava ligado

Feita em 27/08/2026, no ambiente local.

> **ESTA FRENTE FOI ENCERRADA EM 09/09/2026, e por remoção do alvo.** O Power BI
> saiu do sistema por decisão do dono — "Remover Power BI, não será mais
> utilizado" —, e com ele saíram o papel `bvip_bi`, a coluna
> `unidades.iframe_bi` e a tela do painel. Ver **D9** e **D85**.
>
> **O texto abaixo fica como está**, porque é o registro do que foi medido e
> provado na época. Mas os comandos dele **não valem mais**: `SENHA_BI` não é
> lida por roteiro nenhum, e `scripts/conferir-papeis.sh` hoje faz seis provas,
> não dezesseis. O que sobreviveu desta frente é o achado que ela revelou sem
> querer — o **D48**, o isolamento por inquilino que não estava em vigor —, e
> esse continua valendo.

O portão desta frente é curto: **BI lendo do PostgreSQL, com credencial própria
e sem permissão de escrita.**

Ao construí-lo apareceu algo maior, que não estava no plano e muda o significado
da F1.2 e da F1.3: **as 75 políticas de isolamento por inquilino não estavam em
vigor**, porque a aplicação conectava como superusuário.

---

## O achado: superusuário ignora Row-Level Security

Medido em 27/08/2026, sessão nova, sem declarar inquilino:

| Quem consulta | Resultado |
|---|---|
| `franquiabv` — o dono, **superusuário** | **61 unidades**. O RLS nem é avaliado. |
| um papel comum | **`ERROR: unrecognized configuration parameter "app.inquilino_id"`** |

`FORCE ROW LEVEL SECURITY` faz a política valer para o **dono da tabela**. Não
faz — nem poderia — valer para superusuário: superusuário está acima de qualquer
política, por definição.

Como toda a F1.8 rodou com esse usuário, o isolamento existia no papel e não na
prática. **Isto também corrige uma afirmação que eu havia escrito** em
`f1-8-migracao-dados.md`: eu disse que sem `app.inquilino_id` o RLS devolveria
"zero linhas, sem erro". As duas metades estavam erradas — devolve *tudo* para o
superusuário, e *erro* para o papel comum.

O erro, aliás, é a boa notícia: uma configuração faltando **derruba a consulta**
em vez de devolver uma tela vazia que parece falta de dado.

---

## Os dois papéis

```bash
SENHA_APP='...' SENHA_BI='...' sh scripts/criar-papeis.sh
SENHA_APP='...' SENHA_BI='...' sh scripts/conferir-papeis.sh
```

O modelo é `postgres/papeis.sql.modelo`, versionado e **com marcadores no lugar
das senhas**. A extensão `.modelo` não é enfeite: em 25/08/2026 o
`scripts/anonimizar.sql` foi rodado direto e gravou o marcador como hash de
senha em todos os usuários. Renomear foi o conserto que faz o erro falhar alto.

**`bvip_app`** — lê e escreve, e **não é superusuário**. É essa única diferença
que faz o RLS existir.

**`bvip_bi`** — somente leitura, com três cintos além da falta de permissão:

| | Por quê |
|---|---|
| `SET app.inquilino_id = '1'` **no papel** | o Power BI não tem onde emitir um `SET` antes de cada consulta. Pedir isso a quem monta relatório seria pedir que lembrasse de uma regra de segurança. |
| `default_transaction_read_only = on` | falha mais cedo e com mensagem mais clara, inclusive em caminhos que não passam por permissão de tabela |
| `statement_timeout = '10min'` | numa réplica, consulta longa atrasa a replicação e pode ser cancelada por conflito, do jeito mais confuso possível |

E `REVOKE CREATE ON SCHEMA public FROM PUBLIC` — porque `PUBLIC` é todo mundo,
inclusive papel criado depois, e é assim que um papel só-leitura acaba
conseguindo gravar em algum canto.

---

## As provas

Nenhuma delas lê a definição do papel. "Somente leitura" não se demonstra
lendo o `GRANT`: demonstra-se **tentando escrever** e exigindo recusa.

### No primário — `scripts/conferir-papeis.sh`

```
== Nenhum dos dois é superusuário ==
   bvip_app não é superusuário nem tem BYPASSRLS              OK
   bvip_bi  não é superusuário nem tem BYPASSRLS              OK

== O BI LÊ ==
   SELECT em unidades devolve 61 linhas                       OK
   inquilino já declarado na conexão (sem SET nenhum)         OK

== O BI NÃO ESCREVE ==
   INSERT / UPDATE / DELETE / TRUNCATE / CREATE recusados     OK

== O BI não enxerga outro inquilino ==
   a linha do inquilino 2 existe (1, vista pelo dono)         OK
   o BI NÃO vê a linha do inquilino 2                         OK
   o BI vê as do inquilino 1 (61)                             OK

== O RLS vale para a aplicação ==
   bvip_app sem inquilino declarado FALHA ALTO                OK
   o dono superusuário VÊ (é a razão de não usá-lo)           OK
```

Duas dessas merecem nota.

**A linha do inquilino 2 é uma cópia de uma unidade real**, com id e inquilino
trocados, montada com `%ROWTYPE`. A primeira versão montava a linha à mão e
esbarrou na primeira das 60 colunas `NOT NULL` — e o `INSERT` falhava em
silêncio, deixando a prova sem o dado que ela precisava observar. **O roteiro
acusou isso como "prova inválida" em vez de passar**, que era exatamente o
comportamento desejado: sem um segundo inquilino com dado, "não vejo nada de
outro inquilino" seria verdade por não haver o que ver.

**A última linha é a que justifica o arquivo inteiro.** O dono superusuário *vê*
o inquilino 2. É a demonstração direta de por que a aplicação não pode continuar
usando essa credencial.

### Na réplica — `conferir-bi-replica.sh` (removido em 09/09/2026)

O roteiro saiu junto com o Power BI (**D9**); o que ele provou na época fica
registrado aqui. Rodava contra o laboratório de alta disponibilidade da F1.7:

```
   no1 está em recuperação (é standby)                        OK
   o SUPERUSUÁRIO não escreve na réplica                      OK
   o papel do BI lê o que o líder gravou                      OK
   e não escreve (duas barreiras: permissão e standby)        OK
```

**São dois mecanismos independentes**, e é a independência que interessa: se um
dia alguém conceder um `GRANT` a mais por engano, a réplica continua recusando.

Uma consequência prática apareceu ao escrever isso: **papéis são objetos do
agrupamento**, moram em `pg_authid` e viajam pela replicação física. Não há como
criá-los na réplica — ela é somente leitura. **A credencial do BI nasce no
primário, mesmo sendo usada só na réplica.**

---

## O que mudou na aplicação

`config/autoload/local.postgres.php.dist` passa a conectar como **`bvip_app`**,
não como o dono do banco.

E os **67 testes de caracterização passam assim** — papel comum, RLS em vigor,
as mesmas 128 asserções, em 2 min 17 s. O isolamento por inquilino deixou de ser
declaração e passou a ser comportamento, sem que nada no sistema mudasse de
resultado.

`ISBase\Doctrine\DeclararInquilino`, que até aqui era inócuo, passa a ser
indispensável.

### A ordem importa

Recarregar `postgres/schema.sql` faz `DROP SCHEMA bvip CASCADE`, e isso **leva
junto todas as permissões**. A sequência é:

```
1. postgres/schema.sql
2. scripts/criar-papeis.sh      <-- depois do schema, sempre
3. scripts/migrar-dados.sh
```

Inverter 1 e 2 deixa os papéis sem permissão nenhuma, e o sintoma é a aplicação
subindo e falhando na primeira consulta.

---

## O que esta frente NÃO entrega

**O Power BI não foi repontado.** O BI é embutido por unidade
(`unidades.iframe_bi`) e os relatórios vivem no serviço da Microsoft. Trocar a
fonte de dados exige, do lado de lá: abrir o modelo no Power BI Desktop, trocar
o conector de MySQL para PostgreSQL, instalar o provedor **Npgsql** no gateway,
republicar e reconfigurar as credenciais. Nada disso se faz daqui, e nada disso
está feito.

**O inventário do D9 continua aberto.** Não se sabe quais consultas o Power BI
executa hoje, nem com que usuário. `bvip_bi` recebe `SELECT` em **todas** as
tabelas do schema — o mínimo seguro seria dar acesso só ao que ele lê, e isso
depende de um inventário que exige acesso ao ambiente de produção, hoje
congelado por decisão de 23/08.

Vale dizer com todas as letras: **`SELECT` em tudo não é privilégio mínimo.** É
o menor privilégio *possível sem o inventário*. A diferença importa porque
`clientes` tem 242.587 linhas com dado pessoal, e um painel de faturamento não
precisa de nenhuma delas.

**Produção continua com o usuário superusuário.** A troca para `bvip_app` está
provada aqui e precisa acontecer lá no corte — ver **D48**.

---

## Situação do portão

| | |
|---|---|
| Credencial própria para o BI | **sim** — `bvip_bi` |
| Sem permissão de escrita | **provado**, cinco tentativas recusadas |
| Escopo por inquilino | **provado**, com contraprova |
| Sobre réplica, não sobre o primário | **provado** no laboratório |
| Power BI apontado para o PostgreSQL | **não** — depende do lado da Microsoft |
| Inventário das consultas do BI (D9) | **não** — depende de produção |

**Portão parcialmente fechado.** O lado do banco está pronto e provado; o lado
da ferramenta não foi tocado, e é honesto dizer que essa metade não é de
engenharia de banco.
