MCP Firebird 0.6.0: come NON dare i permessi a un'applicazione Firebird
Molte applicazioni Firebird si collegano come SYSDBA, o comunque con un'utenza che ha molti più privilegi di quelli che le servono. La 0.6.0 di MCP Firebird ti aiuta a smettere: ti chiede cosa fa quell'applicazione e ti scrive i GRANT, così arrivi al minimo privilegio per gradi e sapendo cosa stai revocando.
Qual è il problema?
Troppi gestionali Firebird in produzione, di dimensione media o piccola, si collegano come SYSDBA, e quelli che non lo fanno girano molto spesso con un’utenza che ha molti più privilegi di quelli che le servono e che dovrebbe avere. Chi l’ha scritta così non l’ha fatto per pigrizia. Prova a chiedere a chi lo mantiene da quindici anni quali permessi servano all’applicazione: la risposta onesta è “non lo so, dipende da cosa fa quel modulo che ha scritto Marco nel 2014”.
Ogni tanto qualche “sviluppatore impavido” prova a sanare la situazione. Poi però arrivano le domande, quelle che ti fermano un attimo prima del primo REVOKE.
- E se poi qualcosa smette di funzionare?
- E se il cliente chiama alle due di notte perché non riesce più a chiudere una bolla?
- E se il job schedulato del venerdì sera si pianta zitto zitto, perché mi sono scordato di dargli i privilegi su quella tabella lì?
Sono domande sensate, e il conto di uno sbaglio lo paghi tu, che sei anche quello che risponde al telefono. Quindi resti su SYSDBA, l’unica configurazione di cui a un certo punto qualcuno è stato sicuro.
Il guaio è cosa vuol dire SYSDBA. Non è un utente con tanti permessi: è un utente per cui i permessi non vengono controllati, e lo stesso vale per il proprietario di un oggetto sui propri oggetti. Finché l’applicazione si collega così, i GRANT e le REVOKE che scrivi per lei non cambiano niente.
La strada che vedo più spesso però è un’altra: si crea un’utenza apposta per non usare SYSDBA, e poi le si danno praticamente tutti i privilegi. In una checklist quella riga passa, e intanto l’utenza arriva quasi dappertutto dove arrivava prima. Almeno lì i permessi vengono controllati, quindi al minimo privilegio ci si arriva per gradi. Ma per revocare bisogna sapere cosa serve, e siamo di nuovo alla domanda di prima.
La versione 0.6.0 di mcp-firebird serve a rispondere a quelle tre domande. Non c’è un bottone “metti in sicurezza il database”: ti chiede le cose che il database non gli sa dire, scrive l’SQL leggendo il catalogo, e poi ti dice cosa quell’utenza raggiungerebbe lo stesso.
Perché questo progetto sta piacendo
Di server MCP che si attaccano a un database ce ne sono parecchi, e fanno quasi tutti lo stesso mestiere: ti fanno eseguire query. mcp-firebird fa un’altra cosa, e su Firebird è l’unico a farla: ti dice perché il database è lento e cosa devi eseguire per sistemarlo.
Sono domande con una risposta precisa, e la risposta sta già dentro il database. L’indice sulla colonna che filtri c’è, ma è INACTIVE. Quella scansione completa è il piano giusto, perché la tabella restituisce l'89% delle righe che legge. Vanno chiesti, e vanno letti sapendo quale motore hai davanti.
Se non lo conosci, l’articolo di presentazione spiega cos’è e come si installa: un eseguibile Delphi che il tuo assistente avvia da solo, su stdin/stdout, senza servizi né porte aperte.
In breve
fb_query: esegue unaSELECTin sola lettura e restituisce le righe, con due tetti invece di uno.fb_audit_security: tutti i permessi del database, più i tre casi che di solito non ci dovrebbero essere; conuser_namedice cosa raggiunge una singola utenza, e per quale via.fb_suggest_grantseapp_user_plan: i privilegi di un’utenza applicativa, decisi con un’intervista e scritti leggendo il catalogo.- Un ruolo non è accesso: su ogni versione un ruolo è inerte se la connessione non lo nomina; dalla 4.0
GRANT DEFAULTlo rende automatico.- Sedici tool gratuiti, in sola lettura, da Firebird 2.5 a 5.0. github.com/danieleteti/mcp-firebird
Le risposte che seguono sono sessioni vere, eseguite mentre scrivevo l’articolo: i tool sul database di test del progetto su Firebird 5.0.4, la parte sui ruoli su tutti e quattro i motori. Le riporto tradotte, perché è così che le leggi: il server risponde in inglese all’assistente, che poi te le racconta nella tua lingua. Restano in inglese i messaggi che escono da Firebird.
Le righe di una query
Fino alla 0.5.0 nessun tool restituiva righe: ognuno eseguiva una query per dire qualcosa su di essa e poi buttava via il risultato. Tu però quella conversazione l’hai aperta perché una query era lenta. Il consiglio arriva, crei l’indice, il piano cambia. E poi? Un piano è il modo in cui il motore dichiara di voler lavorare, non il tempo che ci mette.
I tetti sono due. max_rows (default 100, massimo 1000) viene applicato non leggendo oltre, così una SELECT * su venti milioni di righe costa il pacchetto di righe a cui l’ho fermata, non la tabella. L’altro è sui caratteri, circa ventimila, perché limitare le righe non limita la larghezza.
Quello che non è una SELECT semplice viene rifiutato prima del motore, ma il punto forte è un altro. La sola lettura sta sulla connessione,
FConn.TxOptions.ReadOnly := AConfig.ReadOnly; // Firebird.Connection.pas
e quel valore non arriva dal .env: è fissato nel codice, Result.ReadOnly := True. Ogni transazione che il server apre nasce con il TPB read-only di Firebird.
Ho provato ad aggirarlo. Una stored procedure selezionabile che dentro fa una INSERT, chiamata con una SELECT: una forma che il controllo sul testo lascia passare senza battere ciglio. Il primo tentativo, un UPDATE in chiaro, si ferma prima di partire; il secondo arriva al motore, e lì finisce.
La tabella, dopo, ha ancora zero righe. Il controllo sul testo lo aggiri con un po’ di fantasia, il TPB no: una blocklist di parole chiave non sa che EXECUTE PROCEDURE può scrivere, né cosa fa quella procedura.
Chi arriva a cosa
fb_audit_security senza parametri elenca i permessi del database, una riga per beneficiario e oggetto. Questo è il database di test del progetto, con qualche riga tolta per non ripetere due volte le stesse cose:
PUBLIC non è un gruppo a cui qualcuno si è iscritto: è chiunque riesca a collegarsi, comprese le utenze create dopo che il permesso è stato concesso. In lettura è warning, in scrittura è critical: con la lettura qualcuno vede dei dati che non doveva vedere, con la scrittura te li cambia, e non sai chi è stato. Gli altri due rilievi sono WITH GRANT OPTION, cioè chi può allargare gli accessi senza che un DBA se ne accorga, e i ruoli senza membri.
L’altra faccia della domanda serve più spesso. Non “cosa concedo”, ma “cosa riesce a fare l’utenza che ho già”: una risposta sparsa fra i grant diretti, quelli a PUBLIC e quelli ai ruoli. Con user_name:
I permessi dell’applicazione
app_user_plan è il prompt che fa le domande. Prima però guarda da sé versione del motore, nomi degli oggetti e permessi di PUBLIC, e poi chiede soltanto quello che il catalogo non gli può dire.
Nei client MCP i prompt compaiono come comandi da scegliere: non lo “esegui”, lo selezioni e poi parli. Va più o meno così.
Tu — Devo togliere questa applicazione da
SYSDBA.L’assistente — Prima guardo. (chiama
fb_info,fb_list_tables,fb_audit_security) Il motore è 3.0.14, e conta, perché sotto la 4.0 un ruolo è inerte se la connessione non lo nomina. E finché l’applicazione si collega comeSYSDBAnessunGRANTcambia quello che può fare. Nei nomi vedo due gruppi, 42 tabelleTBL_*e 7LKP_*: sono tutte e due dell’applicazione, e su quali scrive?Tu — Tutte e due. Sulle
TBL_scrive, leLKP_le legge e basta.L’assistente — Altre due. Crea o modifica oggetti mentre gira, tipo migrazioni all’avvio? E con la stessa utenza si collega qualcos’altro?
Tu — Niente DDL. Però la notte gira un export che usa la stessa utenza.
L’assistente — Allora l’export si prende la sua utenza e il suo ruolo, altrimenti i privilegi dell’applicazione diventano l’unione dei due lavori. Ti genero il piano con
fb_suggest_grants, due chiamate con lo stessorole_name:TBL_*in scrittura,LKP_*in lettura.
Le domande in tutto sono otto, e quelle a cui il catalogo ha già risposto non te le fa. Questa conversazione è un esempio, non una sessione registrata.
Se le risposte non arrivano propone il ripiego: un’utenza con SELECT, INSERT, UPDATE e DELETE su tutte le tabelle utente. Non è il minimo privilegio, però non può fare DROP, cambiare lo schema, leggere il security database, creare utenze o fermare il server.
Quello che non fa, e non lo fa apposta, è dirti che l’applicazione continuerà a funzionare: nel prompt c’è scritto che non deve mai affermarlo, perché non può saperlo. È una regola con dei casi veri sotto, visto che quel ripiego dà DML sulle tabelle e non EXECUTE sulle stored procedure, quindi un’applicazione che ne chiama una si ferma.
Poi tocca a fb_suggest_grants. “L’utenza APP deve accedere solo alle tabelle tbl_*” su Firebird 5.0 diventa questo:
L’ultima parte, “Il piano regge?”, esiste perché “questa utenza deve vedere solo le sue tabelle” si sbaglia quasi sempre sui permessi che c’erano già, non su quelli che dai tu.
E c’è un dettaglio nelle righe generate. tbl_* è un prefisso: come pattern LIKE, TBL_% prenderebbe anche TBLX_OTHER, perché in SQL l’underscore è un carattere jolly. L’underscore quindi viene protetto, e nel database di test c’è una tabella TBLX_OTHER messa lì apposta perché il test fallisca se un domani qualcuno “semplifica” quella riga. Nello stesso spirito, la procedura riceve EXECUTE e la vista SELECT perché il tipo lo legge dal catalogo, e gli identificatori sono quotati solo dove Firebird lo richiede.
Un ruolo non è accesso
Che in Firebird un ruolo sia inattivo finché la connessione non lo nomina è comportamento documentato, e chi ha lavorato con i ruoli lo sa. Lo metto qui lo stesso, perché è il punto in cui un piano di GRANT scritto a regola d’arte non produce niente e nessuno se ne accorge.
La buona pratica dice di mettere i privilegi sul ruolo e il ruolo sull’utenza. Scritto così quel piano è inerte su tutte e quattro le versioni, e le ho provate tutte e quattro. Firebird 3.0.14, piano eseguito riga per riga senza un errore, e poi la connessione come la apre un gestionale.
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 stessa identica query, nominando il ruolo alla connessione:
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>
L’utenza perde soltanto quello che le arrivava dal ruolo: i permessi diretti e quelli di PUBLIC restano dove sono. Su un database vero quindi l’applicazione qualche tabella la legge lo stesso, non si pianta all’avvio, e il problema salta fuori dopo, sulla schermata che nessuno apre a gennaio.
Nominare un ruolo non è un modo per prenderselo: se l’utenza non ne è membro Firebird lo ignora e CURRENT_ROLE risponde NONE.
Dalla 4.0 arriva un modo per renderlo automatico, GRANT DEFAULT, e su 4.0.7 e 5.0.4 funziona: con il solo GRANT APP_ROLE TO <utente> la SELECT viene respinta, dopo GRANT DEFAULT passa. Con una stranezza che vale la pena sapere: anche con il ruolo DEFAULT attivo, CURRENT_ROLE continua a rispondere NONE. È uno dei motivi per cui da una connessione SQL non distingui un DEFAULT da un ruolo normale.
Su 3.0 quella sintassi non esiste proprio, e il parser si pianta esattamente qui:
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
Quindi la forma del piano segue il motore, e il parametro grant_to forza l’una o l’altra.
Domini, CHECK e viste
fb_generate_documentation ora stampa il dominio su cui poggia una colonna, i vincoli CHECK e, per una vista, la SELECT che la definisce: l’unico posto in cui il costo di una vista diventa visibile, perché dietro un nome si nascondono tre join e un sort.
Da dove comincio, se l’applicazione gira già?
Da nessun REVOKE. Il primo passo non tocca niente, quindi non può rompere niente.
1. Guarda, e basta. fb_audit_security senza parametri ti dice cosa ha in mano PUBLIC, che varrà anche per l’utenza nuova. Poi lo stesso tool con user_name uguale all’utenza di adesso.
2. Ripristina una copia. Il piano si prova lì, non in produzione.
3. Fai l’intervista sulla copia. Scegli app_user_plan fra i prompt del client e rispondi. I gruppi di oggetti sono quasi sempre più di uno: fb_suggest_grants va chiamato una volta per gruppo con lo stesso role_name, così il ruolo si crea una volta sola e i privilegi si sommano.
4. Esegui il piano sulla copia e fai girare l’applicazione da lì. È l’unico passo che ti dice se qualcosa si rompe, e si rompono sempre le stesse cose: le letture delle tabelle MON$, le utility lanciate dall’applicazione, gli oggetti creati all’avvio, le stored procedure fuori dal pattern. Ognuna è una riga da aggiungere al piano, non un motivo per tornare indietro.
5. Controlla che il ruolo sia attivo. Sotto la 4.0, collegati alla copia senza nominare il ruolo, come fa l’applicazione. Se la lettura funziona, il piano regge. Se ricevi no permission, le strade sono due: aggiungere il nome del ruolo alla connection string, oppure rigenerare il piano con grant_to=user e mettere i privilegi direttamente sull’utenza.
6. PUBLIC è una decisione a parte. Quelle REVOKE tolgono il permesso a chiunque si colleghi, compresi i programmi di cui ti sei dimenticato. Non nella stessa serata in cui sposti l’applicazione.
7. In produzione cambi una riga. L’utenza nuova e il ruolo si creano accanto a quella vecchia, che resta dov’è e continua a funzionare. Sposti solo la connection string, ed è per questo che il rollback è rimetterla com’era, senza toccare un privilegio. SYSDBA intanto non sparisce: serve ancora per backup e amministrazione, smette solo di essere l’utenza del gestionale.
Come si prova
Scarica MCPFirebird-0.6.0-win64.zip dalla release su GitHub, copia .env.example in .env e punta firebird.client_lib alla fbclient.dll della tua installazione: nello zip non la trovi, di proposito, perché solo tu sai con quale server stai parlando. Poi registra l’exe come server MCP stdio nel tuo agente.
Prima della release la suite gira su tutti e quattro i motori: 122 test core più 95 di conformità al protocollo. Uno di quei test esegue il piano di GRANT generato, crea l’utenza, si collega senza ruolo, legge quello che il piano ha concesso e viene respinto su quello che non ha concesso. Senza, saprei soltanto che l’SQL è plausibile.
Il changelog completo elenca tutta la 0.6.0.
Comments
comments powered by Disqus