MCP Firebird 0.6.0: como NÃO dar permissões a uma aplicação Firebird
🇬🇧 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.
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 umSELECTsomente 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; comuser_namediz o que um único usuário alcança, e por qual via.fb_suggest_grantseapp_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 DEFAULTdeixa 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.
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.
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:
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:
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.
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 comoSYSDBAnenhumGRANTmuda o que ela pode fazer. Nos nomes eu vejo dois grupos, 42 tabelasTBL_*e 7LKP_*: as duas famílias são da aplicação, e em quais delas ela escreve?Você: Nas duas. Nas
TBL_ela escreve, asLKP_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 mesmorole_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:
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.
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