MCP Firebird 0.6.0 : comment NE PAS donner les droits à une application Firebird
🇬🇧 English • 🇮🇹 Italiano • 🇪🇸 Español • 🇩🇪 Deutsch • 🇧🇷 Português
Beaucoup d'applications Firebird se connectent en SYSDBA, ou en tout cas avec un compte qui a bien plus de droits qu'il ne lui en faut. La 0.6.0 de MCP Firebird t'aide à en sortir : elle te demande ce que fait cette application et elle t'écrit les GRANT, comme ça tu vas vers le moindre privilège par étapes, en sachant ce que tu révoques.
Quel est le problème ?
Trop de logiciels de gestion Firebird en production, de taille moyenne ou petite, se connectent en SYSDBA, et ceux qui ne le font pas tournent très souvent avec un compte qui a bien plus de droits que nécessaire, et bien plus qu’il ne devrait en avoir. Ceux qui l’ont écrite comme ça ne l’ont pas fait par paresse. Demande à celui qui maintient l’application depuis quinze ans de quels droits elle a besoin : la réponse honnête, c’est « je ne sais pas, ça dépend de ce que fait le module que Marco a écrit en 2014 ».
De temps en temps, un « développeur intrépide » essaie d’assainir la situation. Puis arrivent les questions, celles qui t’arrêtent juste avant le premier REVOKE.
- Et si après ça quelque chose ne marche plus ?
- Et si le client appelle à deux heures du matin parce qu’il n’arrive plus à clôturer un bon de livraison ?
- Et si le job planifié du vendredi soir se plante en silence, parce que j’ai oublié de lui donner les droits sur cette table-là ?
Ce sont des questions sensées, et l’erreur, c’est toi qui la paies, toi qui réponds aussi au téléphone. Donc tu restes sur SYSDBA, la seule configuration dont quelqu’un, à un moment, a été sûr.
L’ennui, c’est ce que veut dire SYSDBA. Ce n’est pas un utilisateur avec beaucoup de droits : c’est un utilisateur dont les droits ne sont pas contrôlés, et c’est pareil pour le propriétaire d’un objet sur ses propres objets. Tant que l’application se connecte comme ça, les GRANT et les REVOKE que tu écris pour elle ne changent rien.
Le plus souvent, pourtant, je vois autre chose : on crée un compte exprès pour ne pas utiliser SYSDBA, et ensuite on lui donne pratiquement tous les droits. Dans une checklist, cette ligne passe, et pendant ce temps le compte arrive presque partout où il arrivait avant. Au moins là, les droits sont contrôlés, donc au moindre privilège on y arrive par étapes. Mais pour révoquer, il faut savoir ce qui sert, et on retombe sur la question d’avant.
La version 0.6.0 de mcp-firebird sert à répondre à ces trois questions. Il n’y a pas de bouton « mets la base en sécurité » : l’outil te demande les choses que la base ne sait pas lui dire, il écrit le SQL en lisant le catalogue, et ensuite il te dit ce que ce compte atteindrait quand même.
Pourquoi ce projet plaît
Des serveurs MCP qui se branchent sur une base de données, il y en a pas mal, et ils font presque tous le même métier : ils te font exécuter des requêtes. mcp-firebird fait autre chose, et sur Firebird il est le seul à le faire : il te dit pourquoi la base est lente et ce que tu dois exécuter pour y remédier.
Ce sont des questions avec une réponse précise, et la réponse est déjà dans la base. L’index sur la colonne que tu filtres existe, mais il est INACTIVE. Ce parcours complet de table est le bon plan, parce que la table renvoie 89 % des lignes qu’elle lit. Ces réponses, il faut les demander, et il faut les lire en sachant quel moteur tu as en face.
Si tu ne le connais pas, l’article de présentation explique ce que c’est et comment on l’installe : un exécutable Delphi que ton assistant démarre tout seul, sur stdin/stdout, sans services ni ports ouverts.
En bref
fb_query: exécute uneSELECTen lecture seule et renvoie les lignes, avec deux plafonds au lieu d’un.fb_audit_security: tous les droits de la base, plus les trois cas qui d’habitude ne devraient pas être là ; avecuser_nameil dit ce qu’un seul compte atteint, et par quelle voie.fb_suggest_grantsetapp_user_plan: les droits d’un compte applicatif, décidés par un entretien et écrits en lisant le catalogue.- Un rôle n’est pas un accès : sur toutes les versions un rôle est inerte si la connexion ne le nomme pas ; à partir de la 4.0
GRANT DEFAULTle rend automatique.- Seize outils gratuits, en lecture seule, de Firebird 2.5 à 5.0. github.com/danieleteti/mcp-firebird
Les réponses qui suivent sont de vraies sessions, exécutées pendant que j’écrivais l’article : les outils sur la base de test du projet, sous Firebird 5.0.4, et la partie sur les rôles sur les quatre moteurs. Je les mets ici traduites, parce que c’est comme ça que tu les lis : le serveur répond en anglais à l’assistant, qui te les raconte ensuite dans ta langue. Les messages qui sortent de Firebird restent en anglais.
Les lignes d’une requête
Jusqu’à la 0.5.0, aucun outil ne renvoyait de lignes : chacun exécutait une requête pour dire quelque chose sur elle et jetait ensuite le résultat. Mais toi, cette conversation, tu l’as ouverte parce qu’une requête était lente. Le conseil arrive, tu crées l’index, le plan change. Et après ? Un plan, c’est la façon dont le moteur déclare vouloir travailler, pas le temps qu’il y met.
Il y a deux plafonds. max_rows (100 par défaut, 1000 au maximum) est appliqué en ne lisant pas au-delà, comme ça un SELECT * sur vingt millions de lignes coûte le paquet de lignes où je l’ai arrêté, pas la table entière. L’autre porte sur les caractères, environ vingt mille, parce que limiter les lignes ne limite pas la largeur.
Ce qui n’est pas une SELECT simple est refusé avant le moteur, mais le point fort est ailleurs. La lecture seule est sur la connexion,
FConn.TxOptions.ReadOnly := AConfig.ReadOnly; // Firebird.Connection.pas
et cette valeur ne vient pas du .env : elle est fixée dans le code, Result.ReadOnly := True. Chaque transaction que le serveur ouvre naît avec le TPB read-only de Firebird.
J’ai essayé de le contourner. Une procédure stockée sélectionnable qui fait une INSERT en interne, appelée avec une SELECT : une forme que le contrôle sur le texte laisse passer sans broncher. La première tentative, un UPDATE en clair, s’arrête avant de partir ; la seconde arrive au moteur, et c’est là qu’elle finit.
La table, après, a encore zéro ligne. Le contrôle sur le texte, tu le contournes avec un peu d’imagination, le TPB non : une blocklist de mots-clés ne sait pas que EXECUTE PROCEDURE peut écrire, ni ce que fait cette procédure.
Qui atteint quoi
fb_audit_security sans paramètres liste les droits de la base, une ligne par bénéficiaire et par objet. Voici la base de test du projet, avec quelques lignes enlevées pour ne pas répéter deux fois les mêmes choses :
PUBLIC n’est pas un groupe auquel quelqu’un s’est inscrit : c’est quiconque arrive à se connecter, y compris les comptes créés après que le droit a été concédé. En lecture c’est warning, en écriture c’est critical : avec la lecture quelqu’un voit des données qu’il ne devait pas voir, avec l’écriture il te les change, et tu ne sais pas qui c’était. Les deux autres constats sont WITH GRANT OPTION, c’est-à-dire qui peut élargir les accès sans qu’un DBA s’en aperçoive, et les rôles sans membres.
L’autre face de la question sert plus souvent. Pas « qu’est-ce que je concède », mais « qu’est-ce qu’il arrive à faire, le compte que j’ai déjà » : une réponse éparpillée entre les grants directs, ceux à PUBLIC et ceux aux rôles. Avec user_name :
Les droits de l’application
app_user_plan est le prompt qui pose les questions. Mais avant, il regarde tout seul la version du moteur, les noms des objets et les droits de PUBLIC, et il ne demande ensuite que ce que le catalogue ne peut pas lui dire.
Dans les clients MCP, un prompt apparaît comme une commande à choisir : tu ne l’« exécutes » pas, tu le sélectionnes et ensuite tu parles. Ça se passe plus ou moins comme ça.
Toi : Je dois sortir cette application de
SYSDBA.L’assistant : Je regarde d’abord. (appelle
fb_info,fb_list_tables,fb_audit_security) Le moteur est en 3.0.14, et ça compte, parce qu’en dessous de la 4.0 un rôle est inerte si la connexion ne le nomme pas. Et tant que l’application se connecte enSYSDBA, aucunGRANTne change ce qu’elle peut faire. Dans les noms je vois deux groupes, 42 tablesTBL_*et 7LKP_*: les deux sont à l’application, et sur lesquels est-ce qu’elle écrit ?Toi : Les deux. Sur les
TBL_elle écrit, lesLKP_elle les lit et c’est tout.L’assistant : Encore deux questions. Est-ce qu’elle crée ou modifie des objets pendant qu’elle tourne, du genre migrations au démarrage ? Et est-ce que quelque chose d’autre se connecte avec le même compte ?
Toi : Pas de DDL. Mais la nuit il y a un export qui tourne avec le même compte.
L’assistant : Alors l’export prend son propre compte et son propre rôle, sinon les droits de l’application deviennent l’union des deux traitements. Je te génère le plan avec
fb_suggest_grants, deux appels avec le mêmerole_name:TBL_*en écriture,LKP_*en lecture.
Il y a huit questions en tout, et celles auxquelles le catalogue a déjà répondu, il ne te les pose pas. Cette conversation est un exemple, pas une session enregistrée.
Si les réponses n’arrivent pas, il propose le repli : un compte avec SELECT, INSERT, UPDATE et DELETE sur toutes les tables utilisateur. Ce n’est pas le moindre privilège, mais ce compte ne peut pas faire DROP, changer le schéma, lire la security database, créer des comptes ou arrêter le serveur.
Ce qu’il ne fait pas, et c’est délibéré, c’est te dire que l’application continuera à fonctionner : dans le prompt il est écrit qu’il ne doit jamais l’affirmer, parce qu’il ne peut pas le savoir. La règle a de vrais cas derrière elle, vu que ce repli donne le DML sur les tables et pas l’EXECUTE sur les procédures stockées : une application qui en appelle une s’arrête.
Ensuite c’est au tour de fb_suggest_grants. « Le compte APP doit accéder seulement aux tables tbl_* » sur Firebird 5.0 devient ceci :
La dernière partie, « Le plan tient ? », existe parce que « ce compte doit voir seulement ses tables » se trompe presque toujours sur les droits qui étaient déjà là, pas sur ceux que tu donnes.
Et il y a un détail dans les lignes générées. tbl_* est un préfixe : comme pattern LIKE, TBL_% prendrait aussi TBLX_OTHER, parce qu’en SQL le tiret bas est un caractère joker. Il est donc échappé, et dans la base de test il y a une table TBLX_OTHER mise là exprès pour que le test échoue si un jour quelqu’un « simplifie » cette ligne. Dans le même esprit, la procédure reçoit EXECUTE et la vue SELECT parce que le type est lu dans le catalogue, et les identifiants sont quotés seulement là où Firebird l’exige.
Un rôle n’est pas un accès
Sous Firebird, un rôle est inactif tant que la connexion ne le nomme pas. C’est un comportement documenté, et quiconque a travaillé avec les rôles le sait. Je le mets ici quand même, parce que c’est le point où un plan de GRANT écrit dans les règles de l’art ne produit rien et personne ne s’en aperçoit.
La bonne pratique dit de mettre les droits sur le rôle et le rôle sur le compte. Écrit comme ça, ce plan est inerte sur les quatre versions, et je les ai toutes les quatre essayées. Firebird 3.0.14, plan exécuté ligne par ligne sans une erreur, puis la connexion telle qu’un logiciel de gestion l’ouvre.
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>
La même requête exactement, en nommant le rôle à la connexion :
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>
Le compte perd seulement ce que le rôle lui apportait : les droits directs et ceux de PUBLIC restent où ils sont. Sur une vraie base, l’application lit donc quand même quelques tables, elle ne se plante pas au démarrage, et le problème ressort plus tard, sur l’écran que personne n’ouvre en janvier.
Nommer un rôle n’est pas une façon de se l’attribuer : si le compte n’en est pas membre, Firebird l’ignore et CURRENT_ROLE répond NONE.
À partir de la 4.0, il y a un moyen de le rendre automatique, GRANT DEFAULT, et sur 4.0.7 et 5.0.4 ça marche : avec le seul GRANT APP_ROLE TO <utilisateur> la SELECT est rejetée, après GRANT DEFAULT elle passe. Avec une bizarrerie qui vaut la peine d’être connue : même avec le rôle DEFAULT actif, CURRENT_ROLE continue à répondre NONE. C’est une des raisons pour lesquelles, depuis une connexion SQL, tu ne distingues pas un DEFAULT d’un rôle normal.
Sur 3.0 cette syntaxe n’existe carrément pas, et le parser se plante exactement ici :
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
Donc la forme du plan suit le moteur, et le paramètre grant_to force l’une ou l’autre.
Domaines, CHECK et vues
fb_generate_documentation affiche maintenant le domaine sur lequel repose une colonne, les contraintes CHECK et, pour une vue, la SELECT qui la définit : le seul endroit où le coût d’une vue devient visible, parce que derrière un nom se cachent trois jointures et un tri.
Par où je commence, si l’application tourne déjà ?
Par aucun REVOKE. La première étape ne touche à rien, donc elle ne peut rien casser.
1. Regarde, et c’est tout. fb_audit_security sans paramètres te dit ce que PUBLIC a en main, ce qui vaudra aussi pour le nouveau compte. Ensuite le même outil avec user_name égal au compte actuel.
2. Restaure une copie. C’est là qu’on essaie le plan, pas en production.
3. Fais l’entretien sur la copie. Choisis app_user_plan parmi les prompts du client et réponds. Il y a presque toujours plus d’un groupe d’objets : fb_suggest_grants doit être appelé une fois par groupe avec le même role_name, comme ça le rôle se crée une seule fois et les droits s’additionnent.
4. Exécute le plan sur la copie et fais tourner l’application depuis là. C’est la seule étape qui te dit si quelque chose casse, et ce sont toujours les mêmes choses qui cassent : les lectures des tables MON$, les utilitaires lancés par l’application, les objets créés au démarrage, les procédures stockées en dehors du pattern. Chacune est une ligne à ajouter au plan, pas une raison de revenir en arrière.
5. Vérifie que le rôle est actif. En dessous de la 4.0, connecte-toi à la copie sans nommer le rôle, comme le fait l’application. Si la lecture marche, le plan tient. Si tu reçois no permission, tu as deux solutions : ajouter le nom du rôle à la chaîne de connexion, ou régénérer le plan avec grant_to=user et mettre les droits directement sur le compte.
6. PUBLIC est une décision à part. Ces REVOKE enlèvent le droit à quiconque se connecte, y compris les programmes que tu as oubliés. Pas le même soir où tu déplaces l’application.
7. En production tu changes une ligne. Le nouveau compte et le rôle se créent à côté de l’ancien, qui reste où il est et continue à fonctionner. Tu ne changes que la chaîne de connexion, et c’est pour ça que le rollback consiste à la remettre comme elle était, sans toucher un droit. SYSDBA ne disparaît pas pour autant : il sert encore pour les sauvegardes et l’administration, il cesse seulement d’être le compte du logiciel de gestion.
Comment on l’essaie
Télécharge MCPFirebird-0.6.0-win64.zip depuis la release sur GitHub, copie .env.example en .env et pointe firebird.client_lib sur la fbclient.dll de ton installation : dans le zip tu ne la trouves pas, exprès, parce que toi seul sais à quel serveur tu parles. Ensuite enregistre l’exe comme serveur MCP stdio dans ton agent.
Avant la publication, la suite tourne sur les quatre moteurs : 122 tests core plus 95 de conformité au protocole. Un de ces tests exécute le plan de GRANT généré, crée le compte, se connecte sans rôle, lit ce que le plan a concédé et se fait refuser ce que le plan n’a pas concédé. Sans ça, je saurais seulement que le SQL est plausible.
Le changelog complet liste toute la 0.6.0.
Comments
comments powered by Disqus