Become a member!

MCP Firebird 0.6.0: como NÃO dar permissões a uma aplicação Firebird

🌐
Este artigo também está disponível em outros idiomas:
🇬🇧 English  •  🇮🇹 Italiano  •  🇪🇸 Español  •  🇩🇪 Deutsch  •  🇫🇷 Français

Muita aplicação Firebird se conecta como SYSDBA, ou então com um usuário que tem muito mais privilégios do que precisa. A 0.6.0 do MCP Firebird te ajuda a parar com isso: ela te pergunta o que aquela aplicação faz e escreve os GRANT, para você chegar ao privilégio mínimo por etapas e sabendo o que está revogando.

Logo do MCP Firebird, o servidor MCP que permite a assistentes de IA diagnosticar bancos de dados Firebird.

Qual é o problema?

Tem sistemas de gestão Firebird demais em produção, de porte médio ou pequeno, que se conectam como SYSDBA, e os que não fazem isso muitas vezes rodam com um usuário que tem muito mais privilégios do que precisa e do que deveria ter. Quem escreveu assim não fez isso por preguiça. Tente perguntar a quem mantém aquilo há quinze anos quais permissões a aplicação precisa: a resposta honesta é “não sei, depende do que faz aquele módulo que o Marco escreveu em 2014”.

De vez em quando algum “desenvolvedor destemido” tenta arrumar a situação. Só que aí chegam as perguntas, aquelas que te param um instante antes do primeiro REVOKE.

  • E se depois alguma coisa parar de funcionar?
  • E se o cliente ligar às duas da manhã porque não consegue mais fechar uma nota fiscal?
  • E se o job agendado da sexta à noite travar caladinho, porque eu esqueci de dar a ele os privilégios naquela tabela lá?

São perguntas sensatas, e a conta de um erro quem paga é você, que também é quem atende o telefone. Então você fica no SYSDBA, a única configuração da qual, em algum momento, alguém teve certeza.

O problema é o que SYSDBA significa. Não é um usuário com muitas permissões: é um usuário para o qual as permissões não são verificadas, e o mesmo vale para o proprietário de um objeto sobre os próprios objetos. Enquanto a aplicação se conectar assim, os GRANT e os REVOKE que você escrever para ela não mudam nada.

Só que o caminho que eu vejo com mais frequência é outro: criam um usuário só para não usar SYSDBA, e depois dão a ele praticamente todos os privilégios. Num checklist aquela linha passa, e enquanto isso o usuário chega a quase todo lugar aonde chegava antes. Ali pelo menos as permissões são verificadas, então você chega ao privilégio mínimo por etapas. Mas para revogar você precisa saber o que a aplicação usa, e voltamos à pergunta de antes.

A versão 0.6.0 do mcp-firebird serve para responder àquelas três perguntas. Não existe um botão “deixe o banco seguro”: ele te pergunta o que o banco não sabe dizer, escreve o SQL lendo o catálogo, e depois te diz o que aquele usuário alcançaria mesmo assim.

Por que este projeto está agradando

Servidor MCP que se pluga num banco de dados tem vários, e quase todos fazem o mesmo serviço: deixam você executar consultas. O mcp-firebird faz outra coisa, e no Firebird é o único que faz: ele te diz por que o banco está lento e o que você tem que executar para arrumar.

São perguntas com resposta precisa, e a resposta já está dentro do banco. O índice na coluna que você filtra existe, mas está INACTIVE. Aquela varredura completa é o plano certo, porque a tabela devolve 89% das linhas que lê. Você tem que fazer essas perguntas, e tem que ler a resposta sabendo qual motor você tem na frente.

Se você não conhece o projeto, o artigo de apresentação explica o que é e como se instala: um executável Delphi que o seu assistente inicia sozinho, por stdin/stdout, sem serviços nem portas abertas.

Em resumo

  • fb_query: executa um SELECT somente leitura e devolve as linhas, com dois tetos em vez de um.
  • fb_audit_security: todas as permissões do banco, mais os três casos que normalmente não deveriam existir; com user_name diz o que um único usuário alcança, e por qual via.
  • fb_suggest_grants e app_user_plan: os privilégios de um usuário de aplicação, decididos numa entrevista e escritos lendo o catálogo.
  • Uma role não é acesso: em qualquer versão uma role é inerte se a conexão não a nomeia; a partir da 4.0 o GRANT DEFAULT deixa isso automático.
  • Dezesseis tools gratuitas, somente leitura, do Firebird 2.5 ao 5.0. github.com/danieleteti/mcp-firebird

As respostas que vêm a seguir são sessões de verdade, executadas enquanto eu escrevia o artigo: as tools no banco de teste do projeto no Firebird 5.0.4, a parte sobre as roles nos quatro motores. Estão aqui traduzidas, porque é assim que você as lê: o servidor responde em inglês para o assistente, que depois te conta na sua língua. Ficam em inglês as mensagens que saem do Firebird.

As linhas de uma consulta

Até a 0.5.0 nenhuma tool devolvia linhas: cada uma executava uma consulta para dizer alguma coisa sobre ela e depois jogava o resultado fora. Só que você abriu aquela conversa porque uma consulta estava lenta. O conselho chega, você cria o índice, o plano muda. E aí? Um plano é o jeito como o motor declara que pretende trabalhar, não o tempo que ele leva.

Resposta do servidor MCPfb_querymax_rows=3
CUSTOMER_ID NAME CITY
0 CUST_0 Rome
4 CUST_4 Rome
8 CUST_8 Rome

3 linhas em 18 ms, leitura interrompida no teto de 3. A consulta tem mais a dar: restrinja com um WHERE, agregue, ou aumente max_rows (até 1000).


O que esta verificação não descarta: quanto custam estas linhas. Elas são o que a consulta devolve agora, em um snapshot somente leitura; nada aqui diz quanto custou responder (isso quem diz é fb_analyze_query), e uma resposta truncada é uma amostra do resultado, não o resultado.

Os tetos são dois. max_rows (padrão 100, máximo 1000) é aplicado sem ler além dele, então um SELECT * em vinte milhões de linhas custa o pacote de linhas em que eu parei, não a tabela. O outro é nos caracteres, uns vinte mil, porque limitar as linhas não limita a largura.

O que não é um SELECT simples é recusado antes do motor, mas o ponto forte é outro. O somente leitura está na conexão,

FConn.TxOptions.ReadOnly := AConfig.ReadOnly;   // Firebird.Connection.pas

e esse valor não vem do .env: está fixo no código, Result.ReadOnly := True. Toda transação que o servidor abre nasce com o TPB read-only do Firebird.

Eu tentei burlar isso. Uma stored procedure selecionável que por dentro faz um INSERT, chamada com um SELECT: uma forma que a verificação do texto deixa passar sem pestanejar. A primeira tentativa, um UPDATE escancarado, nem sai; a segunda chega ao motor, e acaba ali.

Resposta do servidor MCPfb_querysql=UPDATE AUDIT_LOG SET TXT = 'x'

Não executado: isto não é um SELECT simples. Este servidor lê, não escreve, e as transações que ele abre levam o TPB read-only, então o motor recusaria o statement mesmo que esta verificação o deixasse passar.


O que esta verificação não descarta: quanto custam estas linhas. Elas são o que a consulta devolve agora, em um snapshot somente leitura; nada aqui diz quanto custou responder (isso quem diz é fb_analyze_query), e uma resposta truncada é uma amostra do resultado, não o resultado.

Resposta do servidor MCPfb_querysql=SELECT * FROM SP_WRITES
Firebird error (EIBNativeException): [FireDAC][Phys][FB]attempted update during read-only transaction At procedure ‘SP_WRITES’ line: 3, col: 3

A tabela, depois disso, continua com zero linhas. A verificação do texto você contorna com um pouco de imaginação, o TPB não: uma blocklist de palavras-chave não sabe que EXECUTE PROCEDURE pode escrever, nem o que aquela procedure faz.

Quem alcança o quê

fb_audit_security sem parâmetros lista as permissões do banco, uma linha por beneficiário e objeto. Este é o banco de teste do projeto, com algumas linhas tiradas para não repetir duas vezes as mesmas coisas:

Resposta do servidor MCPfb_audit_security

7 permissões em objetos de usuário. Os proprietários não estão listados: o proprietário de um objeto, por definição, tem tudo sobre aquele objeto.

Beneficiário Objeto Privilégios Grant option Concedido por
CHK40 R_CHK40 (role) MEMBRO DE não SYSDBA
PUBLIC CUSTOMERS SELECT não SYSDBA
PUBLIC NOPK_LOG INSERT, UPDATE não SYSDBA
REPORT_ROLE (role) BRANCHES SELECT não SYSDBA
REPORT_USER ORDERS SELECT sim SYSDBA
REPORT_USER REPORT_ROLE (role) MEMBRO DE não SYSDBA
R_CHK40 (role) TBL_ORDERS SELECT não SYSDBA

3 roles definidas pelo usuário.

  • DEAD_ROLE (proprietário SYSDBA), nenhum membro
  • REPORT_ROLE (proprietário SYSDBA), REPORT_USER
  • R_CHK40 (proprietário SYSDBA), CHK40

warning

Achado: PUBLIC tem SELECT em CUSTOMERS. Ou seja, todo usuário que consegue se conectar a este banco, inclusive os criados depois desta permissão.

REVOKE SELECT ON CUSTOMERS FROM PUBLIC;

critical

Achado: PUBLIC tem INSERT, UPDATE em NOPK_LOG. Ou seja, todo usuário que consegue se conectar a este banco, inclusive os criados depois desta permissão, e inclui a alteração dos dados.

REVOKE INSERT, UPDATE ON NOPK_LOG FROM PUBLIC;

PUBLIC não é um grupo no qual alguém se inscreveu: é qualquer um que consiga se conectar, inclusive os usuários criados depois que a permissão foi concedida. Na leitura é warning, na escrita é critical: com a leitura alguém vê dados que não devia ver, com a escrita esse alguém te muda os dados, e você não sabe quem foi. Os outros dois achados são WITH GRANT OPTION, ou seja, quem pode ampliar os acessos sem que um DBA perceba, e as roles sem membros.

O outro lado da pergunta é o que serve com mais frequência. Não “o que eu concedo”, mas “o que o usuário que eu já tenho consegue fazer”: uma resposta espalhada entre os grants diretos, os de PUBLIC e os das roles. Com user_name:

Resposta do servidor MCPfb_audit_securityuser_name=REPORT_USER

O que REPORT_USER alcança.

Objeto Privilégios Por qual via
CUSTOMERS SELECT PUBLIC
NOPK_LOG INSERT, UPDATE PUBLIC
BRANCHES SELECT role REPORT_ROLE (só quando está ativa)
ORDERS SELECT direto

Uma linha marcada (só quando está ativa) passa por uma role, e no Firebird uma role fica inativa se a conexão não a nomeia, ou se ela não foi concedida a este usuário como role DEFAULT (da 4.0 em diante). Qual dos dois casos vale está registrado no security database, ao qual este servidor não se conecta, então daqui não dá para ler.

Membro de: REPORT_ROLE

Esquema das três vias pelas quais um usuário Firebird chega a uma tabela: grant direto, permissão concedida a PUBLIC, role ativa só se a conexão a nomear. SYSDBA e o proprietário do objeto não passam por nenhuma das três.
A resposta para “o que este usuário alcança” está espalhada por três vias, não por uma.

As permissões da aplicação

app_user_plan é o prompt que faz as perguntas. Mas antes ele olha sozinho a versão do motor, os nomes dos objetos e as permissões de PUBLIC, e depois pergunta só o que o catálogo não sabe dizer.

Esquema do prompt app_user_plan: primeiro ele olha a versão do motor, os nomes dos objetos e as permissões de PUBLIC, depois faz oito perguntas, depois escreve CREATE USER, role e GRANT lidos do catálogo, e por fim verifica o que o usuário alcança fora do plano. Se as respostas não chegam, ele propõe o plano B.
As perguntas vêm depois da leitura do catálogo, não antes: ele pergunta só o que o banco não sabe dizer.

Nos clientes MCP os prompts aparecem como comandos para escolher: você não “executa” o prompt, você seleciona e depois conversa. É mais ou menos assim.

Você: Preciso tirar esta aplicação do SYSDBA.

O assistente: Primeiro eu olho. (chama fb_info, fb_list_tables, fb_audit_security) O motor é 3.0.14, e isso importa, porque abaixo da 4.0 uma role é inerte se a conexão não a nomeia. E enquanto a aplicação se conectar como SYSDBA nenhum GRANT muda o que ela pode fazer. Nos nomes eu vejo dois grupos, 42 tabelas TBL_* e 7 LKP_*: as duas famílias são da aplicação, e em quais delas ela escreve?

Você: Nas duas. Nas TBL_ ela escreve, as LKP_ ela só lê.

O assistente: Mais duas perguntas. Ela cria ou altera objetos enquanto roda, tipo migrations na inicialização? E com o mesmo usuário se conecta mais alguma coisa?

Você: Nada de DDL. Mas de noite roda uma exportação que usa o mesmo usuário.

O assistente: Então a exportação ganha usuário e role só dela, senão os privilégios da aplicação viram a união dos dois trabalhos. Eu gero o plano com fb_suggest_grants, duas chamadas com o mesmo role_name: TBL_* em escrita, LKP_* em leitura.

As perguntas são oito no total, e as que o catálogo já respondeu ele não te faz. Esta conversa é um exemplo, não uma sessão gravada.

Se as respostas não chegam, ele propõe o plano B: um usuário com SELECT, INSERT, UPDATE e DELETE em todas as tabelas de usuário. Não é o privilégio mínimo, mas esse usuário não pode fazer DROP, mudar o schema, ler o security database, criar usuários ou parar o servidor.

O que ele não faz, e não faz de propósito, é te dizer que a aplicação vai continuar funcionando: no prompt está escrito que ele nunca deve afirmar isso, porque não pode saber. É uma regra com casos reais por trás, já que aquele plano B dá DML nas tabelas e não EXECUTE nas stored procedures, então uma aplicação que chama uma delas trava.

Depois é a vez do fb_suggest_grants. “O usuário APP deve acessar só as tabelas tbl_*” no Firebird 5.0 vira isto:

Resposta do servidor MCPfb_suggest_grantsuser_name=APP, object_pattern=tbl_*

acesso de leitura para APP, pela role APP_ROLE.

-- Só se APP ainda não existir (este servidor não enxerga a lista de usuários):
CREATE USER APP PASSWORD 'change-this-before-running';
CREATE ROLE APP_ROLE;
GRANT SELECT ON TBL_ITEMS TO APP_ROLE;
GRANT SELECT ON TBL_ORDERS TO APP_ROLE;
GRANT EXECUTE ON PROCEDURE TBL_SP_COUNT TO APP_ROLE;
GRANT SELECT ON TBL_V_ORDERS TO APP_ROLE;
GRANT DEFAULT APP_ROLE TO USER APP;
  • 2 tabelas, 1 view e 1 procedure correspondem a TBL_%.
  • APP_ROLE é concedida como role DEFAULT (5.0.4), então ela está ativa em toda conexão que APP abre, sem que a aplicação precise pedir.
  • A senha aí em cima é um placeholder, não uma sugestão.

O plano se sustenta?

warning

Achado: APP vai alcançar também CUSTOMERS, que está fora deste plano: PUBLIC tem SELECT naquele objeto, e APP é membro de PUBLIC como qualquer outro usuário.

REVOKE SELECT ON CUSTOMERS FROM PUBLIC;

critical

Achado: APP vai alcançar também NOPK_LOG, que está fora deste plano: PUBLIC tem INSERT, UPDATE naquele objeto, e APP é membro de PUBLIC como qualquer outro usuário.

REVOKE INSERT, UPDATE ON NOPK_LOG FROM PUBLIC;

O que esta verificação não descarta: se estes statements vão rodar, e se o usuário existe. Duas coisas este plano não limita de jeito nenhum: SYSDBA e o proprietário de um objeto não estão sujeitos aos privilégios dele, então uma aplicação ainda conectada como um dos dois não é tocada por nenhum dos statements aí em cima.

A última parte, “O plano se sustenta?”, existe porque “este usuário deve enxergar só as tabelas dele” erra quase sempre nas permissões que já estavam lá, não nas que você dá.

E tem um detalhe nas linhas geradas. tbl_* é um prefixo: como pattern LIKE, TBL_% pegaria também TBLX_OTHER, porque em SQL o underscore é um curinga. O underscore então é escapado, e no banco de teste existe uma tabela TBLX_OTHER colocada ali de propósito para que o teste falhe se um dia alguém “simplificar” aquela linha. No mesmo espírito, a procedure recebe EXECUTE e a view SELECT porque a tool lê o tipo no catálogo, e os identificadores só levam aspas onde o Firebird exige.

Uma role não é acesso

Que no Firebird uma role fique inativa enquanto a conexão não a nomeia é comportamento documentado, e quem já trabalhou com roles sabe disso. Repito aqui mesmo assim, porque é o ponto em que um plano de GRANT escrito com todo o capricho não produz nada e ninguém percebe.

A boa prática manda colocar os privilégios na role e a role no usuário. Escrito assim, aquele plano é inerte nas quatro versões, e eu testei as quatro. Firebird 3.0.14, plano executado linha por linha sem um erro, e depois a conexão do jeito que um sistema de gestão abre.

C:\DEV>isql -u APP -p change-this-before-running localhost/3053:C:\DEV\ROLEDEMO.FDB
Database: localhost/3053:C:\DEV\ROLEDEMO.FDB, User: APP
SQL> SELECT * FROM TBL_ORDERS;
Statement failed, SQLSTATE = 28000
no permission for SELECT access to TABLE TBL_ORDERS
SQL>

A mesmíssima consulta, nomeando a role na conexão:

C:\DEV>isql -u APP -p change-this-before-running -role APP_ROLE localhost/3053:C:\DEV\ROLEDEMO.FDB
Database: localhost/3053:C:\DEV\ROLEDEMO.FDB, User: APP, Role: APP_ROLE
SQL> SELECT * FROM TBL_ORDERS;

          ID DESCR
============ ==================================================
           1 first order

SQL>

O usuário perde só o que vinha da role: as permissões diretas e as de PUBLIC continuam onde estão. Num banco de verdade a aplicação lê algumas tabelas mesmo assim, não trava na inicialização, e o problema aparece depois, naquela tela que ninguém abre em janeiro.

Nomear uma role não é um jeito de se apossar dela: se o usuário não é membro, o Firebird ignora o pedido e CURRENT_ROLE responde NONE.

Da 4.0 em diante existe um jeito de deixar isso automático, o GRANT DEFAULT, e no 4.0.7 e no 5.0.4 ele funciona: só com GRANT APP_ROLE TO <usuário> o SELECT é recusado, depois do GRANT DEFAULT passa. Com uma esquisitice que vale a pena saber: mesmo com a role DEFAULT ativa, CURRENT_ROLE continua respondendo NONE. É um dos motivos pelos quais, de dentro de uma conexão SQL, você não distingue uma DEFAULT de uma role normal.

No 3.0 aquela sintaxe simplesmente não existe, e o parser trava exatamente aqui:

SQL> GRANT DEFAULT APP_ROLE TO USER APP;
Statement failed, SQLSTATE = 42000
Dynamic SQL Error
-SQL error code = -104
-Token unknown - line 1, column 7
-DEFAULT

Então a forma do plano segue o motor, e o parâmetro grant_to força uma ou outra.

Comparação entre os planos de GRANT gerados no Firebird 2.5/3.0 e no 4.0/5.0: abaixo da 4.0 a role continua inerte se a conexão não a nomeia e a aplicação recebe “no permission for SELECT access”, da 4.0 em diante o GRANT DEFAULT deixa a role ativa em toda conexão.
O plano que a tool gera não é o mesmo nos dois motores, e é esse o ponto.
Em qualquer versão do Firebird, os privilégios colocados numa role não chegam à aplicação enquanto a conexão não nomear a role. Da 4.0 em diante você evita isso com GRANT DEFAULT, abaixo dela não.

Domínios, CHECK e views

fb_generate_documentation agora imprime o domínio em que uma coluna se baseia, as constraints CHECK e, para uma view, o SELECT que a define: o único lugar em que o custo de uma view fica visível, porque atrás de um nome se escondem três joins e um sort.

Por onde começo, se a aplicação já roda?

Por nenhum REVOKE. O primeiro passo não toca em nada, então não pode quebrar nada.

1. Olhe, e só. fb_audit_security sem parâmetros te diz o que PUBLIC tem na mão, que vai valer também para o usuário novo. Depois a mesma tool com user_name igual ao usuário de agora.

2. Restaure uma cópia. O plano se testa lá, não em produção.

3. Faça a entrevista na cópia. Escolha app_user_plan entre os prompts do seu cliente MCP e responda. Os grupos de objetos quase sempre são mais de um: fb_suggest_grants tem que ser chamado uma vez por grupo com o mesmo role_name, assim a role é criada uma vez só e os privilégios se somam.

4. Execute o plano na cópia e rode a aplicação a partir dela. É o único passo que te diz se alguma coisa quebra, e quebram sempre as mesmas coisas: as leituras das tabelas MON$, os utilitários disparados pela aplicação, os objetos criados na inicialização, as stored procedures fora do pattern. Cada uma é uma linha para acrescentar ao plano, não um motivo para voltar atrás.

5. Verifique se a role está ativa. Abaixo da 4.0, conecte-se à cópia sem nomear a role, do jeito que a aplicação faz. Se a leitura funcionar, o plano se sustenta. Se você receber no permission, os caminhos são dois: acrescentar o nome da role à connection string, ou gerar o plano de novo com grant_to=user e colocar os privilégios direto no usuário.

6. PUBLIC é uma decisão à parte. Aqueles REVOKE tiram a permissão de qualquer um que se conecte, inclusive dos programas que você já esqueceu. Não na mesma noite em que você move a aplicação.

7. Em produção você muda uma linha. O usuário novo e a role são criados ao lado do usuário velho, que fica onde está e continua funcionando. Você move só a connection string, e é por isso que o rollback é colocá-la de volta como era, sem tocar em um privilégio. O SYSDBA enquanto isso não some: ele ainda serve para backup e administração, só deixa de ser o usuário do sistema de gestão.

Como testar

Baixe o MCPFirebird-0.6.0-win64.zip da release no GitHub, copie o .env.example para .env e aponte firebird.client_lib para a fbclient.dll da sua instalação: no zip você não a encontra, de propósito, porque só você sabe com qual servidor está falando. Depois registre o exe como servidor MCP stdio no seu agente.

Antes da release a suíte roda nos quatro motores: 122 testes core mais 95 de conformidade ao protocolo. Um desses testes executa o plano de GRANT gerado, cria o usuário, se conecta sem role, lê o que o plano concedeu e é recusado no que ele não concedeu. Sem isso, eu saberia apenas que o SQL é plausível.

O changelog completo lista toda a 0.6.0.

Comments

comments powered by Disqus