MCP Firebird 0.6.0: wie du einer Firebird-Anwendung NICHT die Rechte gibst
🇬🇧 English • 🇮🇹 Italiano • 🇪🇸 Español • 🇫🇷 Français • 🇧🇷 Português
Viele Firebird-Anwendungen verbinden sich als SYSDBA, oder jedenfalls mit einem Benutzer, der viel mehr Privilegien hat, als er braucht. Die 0.6.0 von MCP Firebird hilft dir, damit aufzuhören: sie fragt dich, was diese Anwendung tut, und schreibt dir die GRANTs. So kommst du schrittweise zu minimalen Rechten, und du weißt jedes Mal, was du gerade entziehst.
Wo liegt das Problem?
Zu viele Firebird-Branchenanwendungen in Produktion, kleine wie mittlere, verbinden sich als SYSDBA. Und die, die es nicht tun, laufen sehr oft mit einem Benutzer, der viel mehr Privilegien hat, als er braucht und haben sollte. Wer das so geschrieben hat, war nicht faul. Frag mal den, der die Anwendung seit fünfzehn Jahren pflegt, welche Rechte sie eigentlich braucht: die ehrliche Antwort ist „weiß ich nicht, kommt drauf an, was das Modul macht, das Marco 2014 geschrieben hat“.
Ab und zu versucht ein „furchtloser Entwickler“, die Sache in Ordnung zu bringen. Dann aber kommen die Fragen, die dich kurz vor dem ersten REVOKE aufhalten.
- Und wenn danach irgendwas nicht mehr funktioniert?
- Und wenn der Kunde um zwei Uhr nachts anruft, weil er keinen Lieferschein mehr abschließen kann?
- Und wenn der Job, der freitagabends läuft, still und leise stehen bleibt, weil ich vergessen habe, ihm die Privilegien auf genau diese eine Tabelle zu geben?
Das sind vernünftige Fragen. Die Rechnung für einen Fehler zahlst du, und du bist auch der, der ans Telefon geht. Also bleibst du bei SYSDBA, der einzigen Konfiguration, bei der sich irgendwann mal jemand sicher war.
Der Haken ist, was SYSDBA bedeutet. Es ist kein Benutzer mit vielen Rechten: es ist ein Benutzer, bei dem die Rechte gar nicht erst geprüft werden. Für den Eigentümer eines Objekts gilt dasselbe, jedenfalls bei seinen eigenen Objekten. Solange sich die Anwendung so verbindet, ändern die GRANT und REVOKE, die du für sie schreibst, überhaupt nichts.
Der Weg, den ich am häufigsten sehe, ist aber ein anderer: man legt eigens einen Benutzer an, um SYSDBA zu vermeiden, und gibt ihm dann praktisch alle Privilegien. In einer Checkliste geht diese Zeile durch, und der Benutzer kommt trotzdem fast überall dorthin, wo er vorher schon hinkam. Wenigstens werden die Rechte dort geprüft, also kommst du schrittweise zu minimalen Rechten. Aber um etwas zu entziehen, musst du wissen, was die Anwendung wirklich braucht, und damit sind wir wieder bei der Frage von vorhin.
Die Version 0.6.0 von mcp-firebird beantwortet genau diese drei Fragen. Es gibt keinen Knopf „Datenbank absichern“: sie fragt dich nach dem, was die Datenbank ihr nicht verrät, schreibt das SQL aus dem Katalog und sagt dir dann, was dieser Benutzer trotzdem noch erreicht.
Warum das Projekt ankommt
MCP-Server, die sich an eine Datenbank hängen, gibt es einige, und fast alle machen denselben Job: sie lassen dich Abfragen ausführen. mcp-firebird macht etwas anderes, und auf Firebird macht das sonst keiner: es sagt dir, warum die Datenbank langsam ist und was du ausführen musst, um das zu beheben.
Das sind Fragen mit einer genauen Antwort, und die Antwort steckt schon in der Datenbank. Der Index auf der Spalte, nach der du filterst, ist da, aber er ist INACTIVE. Dieser vollständige Scan ist der richtige Plan, weil die Tabelle 89% der Zeilen zurückgibt, die sie liest. Diese Fragen musst du stellen, und die Antworten liest du nur richtig, wenn du weißt, welche Engine du vor dir hast.
Wenn du es noch nicht kennst: der Vorstellungsartikel erklärt, was es ist und wie du es installierst. Eine Delphi-Exe, die dein Assistent selbst startet, über stdin/stdout, ohne Dienste und ohne offene Ports.
Kurz gefasst
fb_query: führt einSELECTrein lesend aus und gibt die Zeilen zurück, mit zwei Obergrenzen statt einer.fb_audit_security: alle Rechte der Datenbank, plus die drei Fälle, die es normalerweise nicht geben sollte; mituser_namesagt es, was ein einzelner Benutzer erreicht und auf welchem Weg.fb_suggest_grantsundapp_user_plan: die Privilegien eines Anwendungsbenutzers, festgelegt per Interview und geschrieben aus dem Katalog.- Eine Rolle ist kein Zugriff: auf jeder Version ist eine Rolle wirkungslos, wenn die Verbindung sie nicht nennt; ab 4.0 macht
GRANT DEFAULTdas automatisch.- Sechzehn Tools kostenlos, rein lesend, von Firebird 2.5 bis 5.0. github.com/danieleteti/mcp-firebird
Die Antworten, die jetzt kommen, sind echte Sessions, ausgeführt, während ich den Artikel geschrieben habe: die Tools auf der Testdatenbank des Projekts unter Firebird 5.0.4, der Teil über die Rollen auf allen vier Engines. Ich gebe sie übersetzt wieder, weil du sie genau so liest: der Server antwortet dem Assistenten auf Englisch, und der erzählt sie dir dann in deiner Sprache. Auf Englisch bleiben die Meldungen, die aus Firebird herauskommen.
Die Zeilen einer Abfrage
Bis zur 0.5.0 gab kein Tool Zeilen zurück: jedes führte eine Abfrage aus, um etwas über sie zu sagen, und warf das Ergebnis dann weg. Du hast diese Unterhaltung aber angefangen, weil eine Abfrage langsam war. Der Rat kommt, du legst den Index an, der Plan ändert sich. Und dann? Ein Plan ist die Ansage, wie die Engine arbeiten will, nicht die Zeit, die sie dafür braucht.
Es sind zwei Obergrenzen. max_rows (Default 100, Maximum 1000) wird umgesetzt, indem gar nicht weiter gelesen wird. Ein SELECT * auf zwanzig Millionen Zeilen kostet also nur das Paket Zeilen, bei dem ich es gestoppt habe, nicht die Tabelle. Die zweite zählt Zeichen, rund zwanzigtausend, weil eine Grenze für die Zeilen die Breite nicht begrenzt.
Was kein einfaches SELECT ist, wird schon vor der Engine abgelehnt. Die eigentliche Garantie ist aber eine andere: das Nur-Lesen sitzt auf der Verbindung,
FConn.TxOptions.ReadOnly := AConfig.ReadOnly; // Firebird.Connection.pas
und dieser Wert kommt nicht aus der .env: er ist im Code fixiert, Result.ReadOnly := True. Jede Transaktion, die der Server öffnet, startet mit dem Read-only-TPB von Firebird.
Ich habe versucht, das zu umgehen. Eine selektierbare Stored Procedure, die intern ein INSERT macht, aufgerufen per SELECT: so etwas winkt eine Textprüfung durch, ohne mit der Wimper zu zucken. Der erste Versuch, ein UPDATE im Klartext, wird gestoppt, bevor er losläuft. Der zweite kommt bei der Engine an, und dort ist Schluss.
Die Tabelle hat danach immer noch null Zeilen. Die Textprüfung umgehst du mit ein bisschen Fantasie, das TPB nicht: eine Blocklist von Schlüsselwörtern weiß nicht, dass EXECUTE PROCEDURE schreiben kann, und auch nicht, was diese Prozedur tut.
Wer erreicht was
fb_audit_security ohne Parameter listet die Rechte der Datenbank auf, eine Zeile pro Empfänger und Objekt. Das hier ist die Testdatenbank des Projekts, aus der ich ein paar Zeilen entfernt habe, um nicht zweimal dasselbe zu zeigen:
PUBLIC ist keine Gruppe, in die sich jemand eingetragen hat: es ist jeder, der sich verbinden kann, einschließlich der Benutzer, die angelegt wurden, nachdem das Recht erteilt wurde. Beim Lesen ist es warning, beim Schreiben critical: beim Lesen sieht jemand Daten, die er nicht sehen sollte, beim Schreiben ändert er sie dir, und du weißt nicht, wer es war. Die beiden anderen Befunde sind WITH GRANT OPTION, also wer die Zugriffe erweitern kann, ohne dass ein DBA es merkt, und die Rollen ohne Mitglieder.
Die andere Seite der Frage brauchst du öfter. Nicht „was erteile ich“, sondern „was kann der Benutzer, den ich schon habe“. Die Antwort liegt über drei Stellen verstreut: die direkten Grants, die an PUBLIC und die an die Rollen. Mit user_name:
Die Rechte der Anwendung
app_user_plan ist der Prompt, der die Fragen stellt. Vorher schaut er aber selbst nach: Version der Engine, Namen der Objekte und Rechte von PUBLIC, und dann fragt er nur das, was der Katalog ihm nicht sagen kann.
In den MCP-Clients taucht ein Prompt als auswählbarer Befehl auf: du „führst ihn nicht aus“, du wählst ihn aus und redest dann. Das läuft ungefähr so.
Du: Ich muss diese Anwendung von
SYSDBAwegbekommen.Der Assistent: Erst schaue ich nach. (ruft
fb_info,fb_list_tables,fb_audit_securityauf) Die Engine ist 3.0.14, und das macht einen Unterschied, weil unter 4.0 eine Rolle wirkungslos ist, wenn die Verbindung sie nicht nennt. Und solange sich die Anwendung alsSYSDBAverbindet, ändert keinGRANTetwas an dem, was sie darf. In den Namen sehe ich zwei Gruppen, 42 TabellenTBL_*und 7LKP_*: gehören beide zur Anwendung, und auf welche schreibt sie?Du: Beide. Auf die
TBL_schreibt sie, dieLKP_liest sie nur.Der Assistent: Noch zwei. Legt sie Objekte an oder ändert sie welche, während sie läuft, etwa Migrationen beim Start? Und verbindet sich sonst noch etwas mit demselben Benutzer?
Du: Kein DDL. Nachts läuft aber ein Export, der denselben Benutzer verwendet.
Der Assistent: Dann bekommt der Export seinen eigenen Benutzer und seine eigene Rolle, sonst sind die Privilegien der Anwendung am Ende die Summe aus beiden Aufgaben. Ich erzeuge dir den Plan mit
fb_suggest_grants, zwei Aufrufe mit demselbenrole_name:TBL_*schreibend,LKP_*lesend.
Insgesamt sind es acht Fragen, und die, auf die der Katalog schon geantwortet hat, stellt er dir nicht. Diese Unterhaltung ist ein Beispiel, keine aufgezeichnete Session.
Bleiben die Antworten aus, schlägt er den Rückfallplan vor: ein Benutzer mit SELECT, INSERT, UPDATE und DELETE auf allen Benutzertabellen. Das sind keine minimalen Rechte, er kann aber kein DROP machen, das Schema nicht ändern, die Security-Datenbank nicht lesen, keine Benutzer anlegen und den Server nicht stoppen.
Was er nicht macht, und zwar mit Absicht: dir sagen, dass die Anwendung weiter funktionieren wird. Im Prompt steht, dass er das nie behaupten darf, weil er es nicht wissen kann. Unter der Regel liegen echte Fälle: dieser Rückfallplan gibt DML auf den Tabellen und nicht EXECUTE auf den Stored Procedures. Eine Anwendung, die eine davon aufruft, bleibt also stehen.
Dann ist fb_suggest_grants dran. „Der Benutzer APP darf nur auf die Tabellen tbl_* zugreifen“ wird auf Firebird 5.0 zu diesem hier:
Den letzten Teil, „Hält der Plan?“, gibt es, weil sich „dieser Benutzer darf nur seine eigenen Tabellen sehen“ fast immer bei den Rechten irrt, die schon da waren, nicht bei denen, die du vergibst.
Und in den erzeugten Zeilen steckt ein Detail. tbl_* ist ein Präfix: als LIKE-Muster würde TBL_% auch TBLX_OTHER erwischen, weil der Unterstrich in SQL eine Wildcard ist. Der Unterstrich wird also geschützt, und in der Testdatenbank liegt eine Tabelle TBLX_OTHER, die eigens dafür da ist, dass der Test fehlschlägt, wenn irgendwann jemand diese Zeile „vereinfacht“. Genauso bekommt die Prozedur EXECUTE und der View SELECT: den Typ liest das Tool aus dem Katalog. Und die Bezeichner stehen nur dort in Anführungszeichen, wo Firebird es verlangt.
Eine Rolle ist kein Zugriff
Dass in Firebird eine Rolle inaktiv ist, solange die Verbindung sie nicht nennt, ist dokumentiertes Verhalten, und wer mit Rollen gearbeitet hat, weiß das. Ich schreibe es trotzdem hierhin, weil das der Punkt ist, an dem ein lehrbuchmäßig geschriebener GRANT-Plan nichts bewirkt und es niemand merkt.
Best Practice sagt: Privilegien auf die Rolle, Rolle auf den Benutzer. So geschrieben ist dieser Plan auf allen vier Versionen wirkungslos, und ich habe alle vier ausprobiert. Firebird 3.0.14, Plan Zeile für Zeile ohne einen Fehler ausgeführt, und dann die Verbindung so, wie eine Branchenanwendung sie öffnet.
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>
Genau dieselbe Abfrage, diesmal mit der Rolle in der Verbindung:
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>
Der Benutzer verliert nur das, was über die Rolle kam: die direkten Rechte und die von PUBLIC bleiben, wo sie sind. Auf einer echten Datenbank liest die Anwendung also die eine oder andere Tabelle trotzdem, sie bleibt beim Start nicht stehen, und das Problem taucht später auf, auf der Maske, die im Januar niemand öffnet.
Eine Rolle zu nennen heißt nicht, sie zu bekommen: ist der Benutzer kein Mitglied, ignoriert Firebird sie, und CURRENT_ROLE antwortet NONE.
Ab 4.0 gibt es dafür GRANT DEFAULT, und auf 4.0.7 und 5.0.4 funktioniert es: mit nur GRANT APP_ROLE TO <Benutzer> wird das SELECT abgelehnt, nach GRANT DEFAULT geht es durch. Eine Merkwürdigkeit gibt es dabei, die du kennen solltest: auch bei aktiver DEFAULT-Rolle antwortet CURRENT_ROLE weiterhin NONE. Das ist einer der Gründe, warum du aus einer SQL-Verbindung heraus eine DEFAULT-Rolle nicht von einer normalen Rolle unterscheidest.
Auf 3.0 gibt es diese Syntax überhaupt nicht, und der Parser bleibt genau hier stehen:
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
Die Form des Plans richtet sich also nach der Engine, und der Parameter grant_to erzwingt die eine oder die andere.
Domains, CHECK und Views
fb_generate_documentation gibt jetzt die Domain aus, auf der eine Spalte aufsetzt, die CHECK-Constraints und, bei einem View, das SELECT, das ihn definiert: die einzige Stelle, an der die Kosten eines Views sichtbar werden, weil sich hinter einem Namen drei Joins und ein Sort verstecken.
Womit fange ich an, wenn die Anwendung schon läuft?
Mit keinem REVOKE. Der erste Schritt fasst nichts an, also kann er auch nichts kaputt machen.
1. Schau nach, sonst nichts. fb_audit_security ohne Parameter sagt dir, was PUBLIC in der Hand hat, und das gilt auch für den neuen Benutzer. Dann dasselbe Tool mit user_name auf den Benutzer, der heute im Einsatz ist.
2. Stell eine Kopie wieder her. Den Plan probierst du dort aus, nicht in Produktion.
3. Mach das Interview auf der Kopie. Wähle app_user_plan aus den Prompts deines Clients und antworte. Es gibt fast immer mehr als eine Objektgruppe: ruf fb_suggest_grants einmal pro Gruppe auf, mit demselben role_name. So wird die Rolle nur einmal angelegt, und die Privilegien summieren sich.
4. Führ den Plan auf der Kopie aus und lass die Anwendung von dort laufen. Das ist der einzige Schritt, der dir sagt, ob etwas kaputtgeht, und es gehen immer dieselben Dinge kaputt: die Lesezugriffe auf die MON$-Tabellen, die von der Anwendung gestarteten Utilities, die beim Start angelegten Objekte, die Stored Procedures außerhalb des Musters. Jeder dieser Fälle ist eine Zeile mehr im Plan, kein Grund umzukehren.
5. Prüf nach, ob die Rolle aktiv ist. Unter 4.0: verbinde dich mit der Kopie, ohne die Rolle zu nennen, so wie es die Anwendung macht. Funktioniert das Lesen, hält der Plan. Bekommst du no permission, gibt es zwei Wege: den Namen der Rolle in den Connection String aufnehmen, oder den Plan mit grant_to=user neu erzeugen und die Privilegien direkt auf den Benutzer legen.
6. PUBLIC ist eine eigene Entscheidung. Diese REVOKE nehmen das Recht jedem weg, der sich verbindet, auch den Programmen, die du vergessen hast. Nicht am selben Abend, an dem du die Anwendung umstellst.
7. In Produktion änderst du eine Zeile. Der neue Benutzer und die Rolle werden neben dem alten Benutzer angelegt, der bleibt, wo er ist, und funktioniert weiter. Du änderst nur den Connection String. Das Rollback ist deshalb, ihn wieder so hinzuschreiben wie vorher, ohne ein einziges Privileg anzufassen. SYSDBA verschwindet dabei nicht: du brauchst ihn weiter für Backup und Administration, er hört nur auf, der Benutzer der Anwendung zu sein.
Wie du es ausprobierst
Lade MCPFirebird-0.6.0-win64.zip aus dem Release auf GitHub, kopiere .env.example nach .env und richte firebird.client_lib auf die fbclient.dll deiner Installation: im Zip findest du sie nicht, mit Absicht, weil nur du weißt, mit welchem Server du redest. Dann registrierst du die Exe als stdio-MCP-Server in deinem Agenten.
Vor dem Release läuft die Suite auf allen vier Engines: 122 Core-Tests plus 95 zur Protokollkonformität. Einer dieser Tests führt den erzeugten GRANT-Plan aus, legt den Benutzer an, verbindet sich ohne Rolle, liest das, was der Plan erteilt hat, und wird bei dem abgewiesen, was er nicht erteilt hat. Ohne den wüsste ich nur, dass das SQL plausibel ist.
Das vollständige Changelog listet die ganze 0.6.0 auf.
Comments
comments powered by Disqus