Become a member!

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.

Logo di MCP Firebird, il server MCP che permette agli assistenti AI di diagnosticare database Firebird.

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 una SELECT in 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; con user_name dice cosa raggiunge una singola utenza, e per quale via.
  • fb_suggest_grants e app_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 DEFAULT lo 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.

Risposta del server MCPfb_querymax_rows=3
CUSTOMER_ID NAME CITY
0 CUST_0 Rome
4 CUST_4 Rome
8 CUST_8 Rome

3 righe in 18 ms, fermato al tetto di 3. La query ha altro da dare: restringila con una WHERE, aggregala, oppure alza max_rows (fino a 1000).


Cosa questo controllo non esclude: quanto costano queste righe. Sono quello che la query restituisce adesso, attraverso uno snapshot in sola lettura; niente qui dice quanto sia costato rispondere (lo dice fb_analyze_query), e una risposta troncata è un campione del risultato, non il risultato.

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.

Risposta del server MCPfb_querysql=UPDATE AUDIT_LOG SET TXT = 'x'

Non eseguito: questa non è una SELECT semplice. Questo server legge, non scrive, e le transazioni che apre portano il TPB read-only, quindi il motore rifiuterebbe lo statement anche se questo controllo lo lasciasse passare.


Cosa questo controllo non esclude: quanto costano queste righe. Sono quello che la query restituisce adesso, attraverso uno snapshot in sola lettura; niente qui dice quanto sia costato rispondere (lo dice fb_analyze_query), e una risposta troncata è un campione del risultato, non il risultato.

Risposta del server 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 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:

Risposta del server MCPfb_audit_security

7 permessi su oggetti utente. I proprietari non sono elencati: il proprietario di un oggetto, per definizione, ha tutto su quell’oggetto.

Beneficiario Oggetto Privilegi Grant option Concesso da
CHK40 R_CHK40 (ruolo) MEMBRO DI no SYSDBA
PUBLIC CUSTOMERS SELECT no SYSDBA
PUBLIC NOPK_LOG INSERT, UPDATE no SYSDBA
REPORT_ROLE (ruolo) BRANCHES SELECT no SYSDBA
REPORT_USER ORDERS SELECT SYSDBA
REPORT_USER REPORT_ROLE (ruolo) MEMBRO DI no SYSDBA
R_CHK40 (ruolo) TBL_ORDERS SELECT no SYSDBA

3 ruoli definiti dall’utente.

  • DEAD_ROLE (proprietario SYSDBA), nessun membro
  • REPORT_ROLE (proprietario SYSDBA), REPORT_USER
  • R_CHK40 (proprietario SYSDBA), CHK40

warning

Rilievo: PUBLIC ha SELECT su CUSTOMERS. Cioè ogni utenza che riesce a collegarsi a questo database, comprese quelle create dopo questo permesso.

REVOKE SELECT ON CUSTOMERS FROM PUBLIC;

critical

Rilievo: PUBLIC ha INSERT, UPDATE su NOPK_LOG. Cioè ogni utenza che riesce a collegarsi a questo database, comprese quelle create dopo questo permesso, e comprende la modifica dei dati.

REVOKE INSERT, UPDATE ON NOPK_LOG FROM PUBLIC;

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:

Risposta del server MCPfb_audit_securityuser_name=REPORT_USER

Cosa raggiunge REPORT_USER.

Oggetto Privilegi Per quale via
CUSTOMERS SELECT PUBLIC
NOPK_LOG INSERT, UPDATE PUBLIC
BRANCHES SELECT ruolo REPORT_ROLE (solo quando è attivo)
ORDERS SELECT diretto

Una riga marcata (solo quando è attivo) passa da un ruolo, e in Firebird un ruolo è inattivo se la connessione non lo nomina, oppure se non è stato concesso a questa utenza come ruolo DEFAULT (dalla 4.0). Quale dei due casi valga è registrato nel security database, a cui questo server non si collega, quindi da qui non si può leggere.

Membro di: REPORT_ROLE

Schema delle tre vie con cui un'utenza Firebird arriva a una tabella: grant diretto, permesso concesso a PUBLIC, ruolo attivo solo se la connessione lo nomina. SYSDBA e il proprietario dell'oggetto non passano da nessuna delle tre.
La risposta a “cosa raggiunge questa utenza” sta sparsa su tre vie, non su una.

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.

Schema del prompt app_user_plan: prima guarda versione del motore, nomi degli oggetti e permessi di PUBLIC, poi fa otto domande, poi scrive CREATE USER, ruolo e GRANT letti dal catalogo, infine verifica cosa l'utenza raggiunge fuori dal piano. Se le risposte non arrivano propone il ripiego.
Le domande arrivano dopo la lettura del catalogo, non prima: chiede solo quello che il database non gli sa 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 come SYSDBA nessun GRANT cambia quello che può fare. Nei nomi vedo due gruppi, 42 tabelle TBL_* e 7 LKP_*: sono tutte e due dell’applicazione, e su quali scrive?

Tu — Tutte e due. Sulle TBL_ scrive, le LKP_ 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 stesso role_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:

Risposta del server MCPfb_suggest_grantsuser_name=APP, object_pattern=tbl_*

accesso in lettura per APP, attraverso il ruolo APP_ROLE.

-- Solo se APP non esiste ancora (questo server non vede l'elenco delle utenze):
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 tabelle, 1 vista e 1 procedura corrispondono a TBL_%.
  • APP_ROLE è concesso come ruolo DEFAULT (5.0.4), quindi è attivo su ogni connessione che APP apre, senza che l’applicazione debba chiederlo.
  • La password qui sopra è un segnaposto, non un suggerimento.

Il piano regge?

warning

Rilievo: APP raggiungerà anche CUSTOMERS, che sta fuori da questo piano: PUBLIC ha SELECT su quell’oggetto, e APP è membro di PUBLIC come ogni altra utenza.

REVOKE SELECT ON CUSTOMERS FROM PUBLIC;

critical

Rilievo: APP raggiungerà anche NOPK_LOG, che sta fuori da questo piano: PUBLIC ha INSERT, UPDATE su quell’oggetto, e APP è membro di PUBLIC come ogni altra utenza.

REVOKE INSERT, UPDATE ON NOPK_LOG FROM PUBLIC;

Cosa questo controllo non esclude: se questi statement gireranno, e se l’utenza esiste. Due cose questo piano non le vincola affatto: SYSDBA e il proprietario di un oggetto non sono soggetti ai suoi privilegi, quindi un’applicazione ancora collegata come uno dei due non è toccata da nessuno degli statement qui sopra.

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.

Confronto fra i piani di GRANT generati su Firebird 2.5/3.0 e su 4.0/5.0: sotto la 4.0 il ruolo resta inerte se la connessione non lo nomina e l'applicazione riceve “no permission for SELECT access”, da 4.0 in su GRANT DEFAULT rende il ruolo attivo su ogni connessione.
Il piano che il tool genera non è lo stesso sui due motori, ed è questo il punto.
Su ogni versione di Firebird, i privilegi messi su un ruolo non arrivano all'applicazione finché la connessione non nomina il ruolo. Dalla 4.0 puoi evitarlo con GRANT DEFAULT, sotto no.

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