Become a member!

MCP Firebird 0.6.0 : comment NE PAS donner les droits à une application Firebird

🌐
Cet article est aussi disponible dans d'autres langues :
🇬🇧 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.

Logo de MCP Firebird, le serveur MCP qui permet aux assistants IA de diagnostiquer des bases de données Firebird.

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 une SELECT en 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à ; avec user_name il dit ce qu’un seul compte atteint, et par quelle voie.
  • fb_suggest_grants et app_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 DEFAULT le 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.

Réponse du serveur MCPfb_querymax_rows=3
CUSTOMER_ID NAME CITY
0 CUST_0 Rome
4 CUST_4 Rome
8 CUST_8 Rome

3 lignes en 18 ms, coupées au plafond de 3. La requête a encore des lignes à donner : restreins-la avec une WHERE, agrège-la, ou augmente max_rows (jusqu’à 1000).


Ce que ce contrôle n’exclut pas : combien coûtent ces lignes. Ce sont celles que la requête renvoie maintenant, dans un snapshot en lecture seule ; rien ici ne dit ce que la réponse a coûté (c’est fb_analyze_query qui le dit), et une réponse tronquée est un échantillon du résultat, pas le résultat.

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.

Réponse du serveur MCPfb_querysql=UPDATE AUDIT_LOG SET TXT = 'x'

Non exécuté : ce n’est pas une SELECT simple. Ce serveur lit, il n’écrit pas, et les transactions qu’il ouvre portent le TPB read-only, donc le moteur refuserait l’instruction même si ce contrôle la laissait passer.


Ce que ce contrôle n’exclut pas : combien coûtent ces lignes. Ce sont celles que la requête renvoie maintenant, dans un snapshot en lecture seule ; rien ici ne dit ce que la réponse a coûté (c’est fb_analyze_query qui le dit), et une réponse tronquée est un échantillon du résultat, pas le résultat.

Réponse du serveur 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

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 :

Réponse du serveur MCPfb_audit_security

7 droits sur des objets utilisateur. Les propriétaires ne sont pas listés : le propriétaire d’un objet, par définition, a tout sur cet objet.

Bénéficiaire Objet Privilèges Grant option Concédé par
CHK40 R_CHK40 (rôle) MEMBRE DE non SYSDBA
PUBLIC CUSTOMERS SELECT non SYSDBA
PUBLIC NOPK_LOG INSERT, UPDATE non SYSDBA
REPORT_ROLE (rôle) BRANCHES SELECT non SYSDBA
REPORT_USER ORDERS SELECT oui SYSDBA
REPORT_USER REPORT_ROLE (rôle) MEMBRE DE non SYSDBA
R_CHK40 (rôle) TBL_ORDERS SELECT non SYSDBA

3 rôles définis par l’utilisateur.

  • DEAD_ROLE (propriétaire SYSDBA), aucun membre
  • REPORT_ROLE (propriétaire SYSDBA), REPORT_USER
  • R_CHK40 (propriétaire SYSDBA), CHK40

warning

Constat : PUBLIC a SELECT sur CUSTOMERS. C’est-à-dire tout compte qui arrive à se connecter à cette base, y compris ceux créés après ce droit.

REVOKE SELECT ON CUSTOMERS FROM PUBLIC;

critical

Constat : PUBLIC a INSERT, UPDATE sur NOPK_LOG. C’est-à-dire tout compte qui arrive à se connecter à cette base, y compris ceux créés après ce droit, et ce droit comprend la modification des données.

REVOKE INSERT, UPDATE ON NOPK_LOG FROM PUBLIC;

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 :

Réponse du serveur MCPfb_audit_securityuser_name=REPORT_USER

Ce que REPORT_USER atteint.

Objet Privilèges Par quelle voie
CUSTOMERS SELECT PUBLIC
NOPK_LOG INSERT, UPDATE PUBLIC
BRANCHES SELECT rôle REPORT_ROLE (seulement quand il est actif)
ORDERS SELECT direct

Une ligne marquée (seulement quand il est actif) passe par un rôle, et sous Firebird un rôle est inactif si la connexion ne le nomme pas, ou s’il n’a pas été concédé à ce compte comme rôle DEFAULT (à partir de la 4.0). Lequel des deux, c’est enregistré dans la security database, à laquelle ce serveur ne se connecte pas : d’ici, impossible de le lire.

Membre de : REPORT_ROLE

Schéma des trois voies par lesquelles un compte Firebird arrive à une table : grant direct, droit concédé à PUBLIC, rôle actif seulement si la connexion le nomme. SYSDBA et le propriétaire de l'objet ne passent par aucune des trois.
La réponse à « qu'est-ce que ce compte atteint » est éparpillée sur trois voies, pas sur une.

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.

Schéma du prompt app_user_plan : il regarde d'abord la version du moteur, les noms des objets et les droits de PUBLIC, puis il pose huit questions, puis il écrit CREATE USER, le rôle et les GRANT lus dans le catalogue, et enfin il vérifie ce que le compte atteint en dehors du plan. Si les réponses n'arrivent pas, il propose le repli.
Les questions arrivent après la lecture du catalogue, pas avant : il demande seulement ce que la base ne sait 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 en SYSDBA, aucun GRANT ne change ce qu’elle peut faire. Dans les noms je vois deux groupes, 42 tables TBL_* et 7 LKP_* : les deux sont à l’application, et sur lesquels est-ce qu’elle écrit ?

Toi : Les deux. Sur les TBL_ elle écrit, les LKP_ 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ême role_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 :

Réponse du serveur MCPfb_suggest_grantsuser_name=APP, object_pattern=tbl_*

accès en lecture pour APP, via le rôle APP_ROLE.

-- Seulement si APP n'existe pas encore (ce serveur ne voit pas la liste des comptes) :
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 tables, 1 vue et 1 procédure correspondent à TBL_%.
  • APP_ROLE est concédé comme rôle DEFAULT (5.0.4), donc il est actif sur chaque connexion qu’APP ouvre, sans que l’application ait à le demander.
  • Le mot de passe ci-dessus est une valeur à remplacer, pas une suggestion.

Le plan tient ?

warning

Constat : APP atteindra aussi CUSTOMERS, qui est en dehors de ce plan : PUBLIC a SELECT sur cet objet, et APP est membre de PUBLIC comme tout autre compte.

REVOKE SELECT ON CUSTOMERS FROM PUBLIC;

critical

Constat : APP atteindra aussi NOPK_LOG, qui est en dehors de ce plan : PUBLIC a INSERT, UPDATE sur cet objet, et APP est membre de PUBLIC comme tout autre compte.

REVOKE INSERT, UPDATE ON NOPK_LOG FROM PUBLIC;

Ce que ce contrôle n’exclut pas : si ces instructions seront exécutées, et si le compte existe. Il y a deux choses que ce plan ne contraint pas du tout : SYSDBA et le propriétaire d’un objet ne sont pas soumis à ses privilèges, donc une application encore connectée comme l’un des deux n’est touchée par aucune des instructions ci-dessus.

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.

Comparaison des plans de GRANT générés sur Firebird 2.5/3.0 et sur 4.0/5.0 : en dessous de la 4.0 le rôle reste inerte si la connexion ne le nomme pas et l'application reçoit « no permission for SELECT access », à partir de la 4.0 GRANT DEFAULT rend le rôle actif sur chaque connexion.
Le plan que l'outil génère n'est pas le même sur les deux moteurs, et c'est bien là le point.
Sur toutes les versions de Firebird, les droits mis sur un rôle n'arrivent pas à l'application tant que la connexion ne nomme pas le rôle. À partir de la 4.0 tu peux l'éviter avec GRANT DEFAULT, en dessous non.

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