MCP Firebird 0.6.0: cómo NO dar los permisos a una aplicación Firebird
🇬🇧 English • 🇮🇹 Italiano • 🇩🇪 Deutsch • 🇫🇷 Français • 🇧🇷 Português
Muchas aplicaciones Firebird se conectan como SYSDBA, o en todo caso con una cuenta que tiene bastantes más privilegios de los que necesita. La 0.6.0 de MCP Firebird te ayuda a dejarlo: te pregunta qué hace esa aplicación y te escribe los GRANT, así llegas al mínimo privilegio por pasos y sabiendo qué estás revocando.
¿Cuál es el problema?
Demasiadas aplicaciones de gestión Firebird en producción, de tamaño medio o pequeño, se conectan como SYSDBA, y las que no lo hacen funcionan muy a menudo con una cuenta que tiene bastantes más privilegios de los que necesita y de los que debería tener. Quien la escribió así no lo hizo por pereza. Prueba a preguntarle a quien la mantiene desde hace quince años qué permisos necesita la aplicación: la respuesta honesta es “no lo sé, depende de qué haga ese módulo que escribió Marco en 2014”.
De vez en cuando algún “desarrollador intrépido” intenta arreglar la situación. Pero después llegan las preguntas, las que te frenan justo antes del primer REVOKE.
- ¿Y si luego algo deja de funcionar?
- ¿Y si el cliente llama a las dos de la mañana porque ya no consigue cerrar un albarán?
- ¿Y si el job programado del viernes por la noche se queda colgado sin decir nada, porque me olvidé de darle privilegios sobre esa tabla?
Son preguntas sensatas, y el error lo pagas tú, que además eres el que responde al teléfono. Así que te quedas en SYSDBA, la única configuración de la que en algún momento alguien estuvo seguro.
El lío está en lo que significa SYSDBA. No es un usuario con muchos permisos: es un usuario para el que los permisos no se comprueban, y lo mismo vale para el propietario de un objeto sobre sus propios objetos. Mientras la aplicación se conecte así, los GRANT y los REVOKE que escribas para ella no cambian nada.
El camino que veo más a menudo es otro: se crea una cuenta a propósito para no usar SYSDBA, y luego se le dan prácticamente todos los privilegios. En una checklist esa línea pasa, y mientras tanto la cuenta sigue llegando casi a todos los sitios a los que llegaba. Al menos ahí los permisos sí se comprueban, así que al mínimo privilegio se llega por pasos. Pero para revocar hay que saber qué hace falta, y volvemos a la pregunta de antes.
La versión 0.6.0 de mcp-firebird sirve para responder a esas tres preguntas. No hay un botón “asegura la base de datos”: te pregunta las cosas que la base de datos no le sabe decir, escribe el SQL leyendo el catálogo, y después te dice qué alcanzaría esa cuenta de todas formas.
Por qué gusta este proyecto
Servidores MCP que se enganchan a una base de datos hay unos cuantos, y casi todos hacen lo mismo: te dejan ejecutar consultas. mcp-firebird hace otra cosa, y en Firebird es el único que la hace: te dice por qué la base de datos va lenta y qué tienes que ejecutar para arreglarlo.
Son preguntas con una respuesta precisa, y la respuesta ya está dentro de la base de datos. El índice en la columna por la que filtras existe, pero está INACTIVE. Ese escaneo completo es el plan correcto, porque la tabla devuelve el 89% de las filas que lee. Hay que preguntarlas, y hay que leer las respuestas sabiendo qué motor tienes delante.
Si no lo conoces, el artículo de presentación explica qué es y cómo se instala: un ejecutable Delphi que tu asistente arranca solo, por stdin/stdout, sin servicios ni puertos abiertos.
En resumen
fb_query: ejecuta unaSELECTde solo lectura y devuelve las filas, con dos topes en vez de uno.fb_audit_security: todos los permisos de la base de datos, más los tres casos que normalmente no deberían estar ahí; conuser_namedice qué alcanza una sola cuenta, y por qué vía.fb_suggest_grantsyapp_user_plan: los privilegios de una cuenta de aplicación, decididos con una entrevista y escritos leyendo el catálogo.- Un rol no es acceso: en todas las versiones un rol es inerte si la conexión no lo nombra; desde la 4.0
GRANT DEFAULTlo hace automático.- Dieciséis herramientas gratuitas, de solo lectura, de Firebird 2.5 a 5.0. github.com/danieleteti/mcp-firebird
Las respuestas que siguen son sesiones reales, ejecutadas mientras escribía el artículo: las herramientas contra la base de datos de test del proyecto en Firebird 5.0.4, la parte de los roles en los cuatro motores. Las pongo traducidas, porque es así como las lees: el servidor le responde en inglés al asistente, que después te las cuenta en tu idioma. Se quedan en inglés los mensajes que salen de Firebird.
Las filas de una consulta
Hasta la 0.5.0 ninguna herramienta devolvía filas: cada una ejecutaba una consulta para decir algo sobre ella y después tiraba el resultado. Pero tú esa conversación la abriste porque una consulta iba lenta. Llega el consejo, creas el índice, el plan cambia. ¿Y luego? Un plan es la forma en que el motor declara cómo quiere trabajar, no el tiempo que tarda.
Los topes son dos. max_rows (por defecto 100, máximo 1000) se aplica no leyendo más allá, así que una SELECT * sobre veinte millones de filas cuesta el paquete de filas en el que la he parado, no la tabla. El otro es sobre los caracteres, unos veinte mil, porque limitar las filas no limita el ancho.
Lo que no es una SELECT simple se rechaza antes del motor, pero lo importante es otra cosa. La solo lectura está en la conexión,
FConn.TxOptions.ReadOnly := AConfig.ReadOnly; // Firebird.Connection.pas
y ese valor no viene del .env: está fijado en el código, Result.ReadOnly := True. Cada transacción que abre el servidor nace con el TPB read-only de Firebird.
He intentado saltármelo. Una stored procedure seleccionable que por dentro hace un INSERT, llamada con una SELECT: una forma que la comprobación del texto deja pasar sin pestañear. El primer intento, un UPDATE explícito, se para antes de arrancar; el segundo llega al motor, y ahí se acaba.
Después, la tabla sigue teniendo cero filas. La comprobación del texto la esquivas con un poco de imaginación, el TPB no: una lista de palabras prohibidas no sabe que EXECUTE PROCEDURE puede escribir, ni qué hace ese procedimiento.
Quién alcanza qué
fb_audit_security sin parámetros lista los permisos de la base de datos, una fila por beneficiario y objeto. Esta es la base de datos de test del proyecto, con alguna fila quitada para no repetir lo mismo dos veces:
PUBLIC no es un grupo al que alguien se haya apuntado: es cualquiera que consiga conectarse, incluidas las cuentas creadas después de que el permiso se concediera. En lectura es warning, en escritura es critical: con la lectura alguien ve datos que no tenía que ver, con la escritura te los cambia, y no sabes quién fue. Los otros dos hallazgos son WITH GRANT OPTION, es decir, quién puede ampliar los accesos sin que un DBA se entere, y los roles sin miembros.
La otra cara de la pregunta hace falta más a menudo. No “qué concedo”, sino “qué consigue hacer la cuenta que ya tengo”: una respuesta repartida entre los grants directos, los de PUBLIC y los de los roles. Con user_name:
Los permisos de la aplicación
app_user_plan es el prompt que hace las preguntas. Pero antes mira por sí mismo la versión del motor, los nombres de los objetos y los permisos de PUBLIC, y después pregunta solo lo que el catálogo no le puede decir.
En los clientes MCP los prompts aparecen como comandos que eliges: no lo “ejecutas”, lo seleccionas y después hablas. Va más o menos así.
Tú - Tengo que sacar esta aplicación de
SYSDBA.El asistente - Primero miro. (llama a
fb_info,fb_list_tables,fb_audit_security) El motor es 3.0.14, y eso importa, porque por debajo de la 4.0 un rol es inerte si la conexión no lo nombra. Y mientras la aplicación se conecte comoSYSDBAningúnGRANTcambia lo que puede hacer. En los nombres veo dos grupos, 42 tablasTBL_*y 7LKP_*: ¿los dos grupos son de la aplicación? ¿Y sobre cuáles escribe?Tú - Las dos. Sobre las
TBL_escribe, lasLKP_solo las lee.El asistente - Otras dos. ¿Crea o modifica objetos mientras está en marcha, tipo migraciones al arrancar? ¿Y con la misma cuenta se conecta algo más?
Tú - Nada de DDL. Pero por la noche se lanza un export que usa la misma cuenta.
El asistente - Entonces el export se lleva su propia cuenta y su propio rol; si no, los privilegios de la aplicación acaban siendo la unión de los dos trabajos. Te genero el plan con
fb_suggest_grants, dos llamadas con el mismorole_name:TBL_*en escritura,LKP_*en lectura.
Las preguntas en total son ocho, y las que el catálogo ya ha respondido no te las hace. Esta conversación es un ejemplo, no una sesión grabada.
Si las respuestas no llegan, propone el plan de reserva: una cuenta con SELECT, INSERT, UPDATE y DELETE sobre todas las tablas de usuario. No es el mínimo privilegio, pero no puede hacer DROP, cambiar el esquema, leer la security database, crear cuentas ni parar el servidor.
Lo que no hace, y no lo hace a propósito, es decirte que la aplicación va a seguir funcionando: en el prompt está escrito que no debe afirmarlo nunca, porque no puede saberlo. Es una regla con casos reales detrás: ese plan de reserva da DML sobre las tablas y no EXECUTE sobre las stored procedures, así que una aplicación que llame a una de ellas se queda parada.
Después le toca a fb_suggest_grants. “La cuenta APP tiene que acceder solo a las tablas tbl_*” en Firebird 5.0 se convierte en esto:
La última parte, “¿El plan aguanta?”, existe porque “esta cuenta tiene que ver solo sus tablas” se equivoca casi siempre en los permisos que ya estaban, no en los que das tú.
Y hay un detalle en las líneas generadas. tbl_* es un prefijo: como patrón LIKE, TBL_% incluiría también TBLX_OTHER, porque en SQL el guion bajo es un comodín. Por eso se escapa, y en la base de datos de test hay una tabla TBLX_OTHER puesta ahí a propósito para que el test falle si un día alguien “simplifica” esa línea. Con el mismo espíritu, el procedimiento recibe EXECUTE y la vista SELECT porque el tipo lo lee del catálogo, y los identificadores se entrecomillan solo donde Firebird lo exige.
Un rol no es acceso
Que en Firebird un rol esté inactivo mientras la conexión no lo nombra es comportamiento documentado, y quien ha trabajado con roles lo sabe. Lo pongo aquí igualmente, porque es el punto en el que un plan de GRANT escrito como es debido no produce nada y nadie se entera.
La buena práctica dice que hay que poner los privilegios sobre el rol y el rol sobre la cuenta. Escrito así ese plan es inerte en las cuatro versiones, y las he probado las cuatro. Firebird 3.0.14, plan ejecutado línea por línea sin un error, y después la conexión tal como la abre una aplicación de gestión.
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 misma consulta exacta, nombrando el rol en la conexión:
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>
La cuenta pierde solo lo que le llegaba del rol: los permisos directos y los de PUBLIC se quedan donde estaban. En una base de datos real la aplicación lee alguna tabla igualmente, no se cuelga al arrancar, y el problema sale después, en la pantalla que nadie abre en enero.
Nombrar un rol no basta para tenerlo: si la cuenta no es miembro, Firebird lo ignora y CURRENT_ROLE responde NONE.
Desde la 4.0 hay una forma de hacerlo automático, GRANT DEFAULT, y en 4.0.7 y 5.0.4 funciona: con solo GRANT APP_ROLE TO <usuario> la SELECT se rechaza, después de GRANT DEFAULT pasa. Con una rareza que conviene saber: incluso con el rol DEFAULT activo, CURRENT_ROLE sigue respondiendo NONE. Es uno de los motivos por los que desde una conexión SQL no distingues un DEFAULT de un rol normal.
En 3.0 esa sintaxis no existe en absoluto, y el parser se atasca exactamente aquí:
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
Así que la forma del plan sigue al motor, y el parámetro grant_to fuerza una u otra.
Dominios, CHECK y vistas
fb_generate_documentation ahora imprime el dominio sobre el que se apoya una columna, las restricciones CHECK y, para una vista, la SELECT que la define: el único sitio en el que el coste de una vista se vuelve visible, porque detrás de un nombre se esconden tres joins y un sort.
¿Por dónde empiezo, si la aplicación ya está en marcha?
Por ningún REVOKE. El primer paso no toca nada, así que no puede romper nada.
1. Mira, y ya está. fb_audit_security sin parámetros te dice qué tiene en la mano PUBLIC, que valdrá también para la cuenta nueva. Después la misma herramienta con user_name igual a la cuenta de ahora.
2. Restaura una copia. El plan se prueba ahí, no en producción.
3. Haz la entrevista sobre la copia. Elige app_user_plan entre los prompts del cliente y responde. Los grupos de objetos casi siempre son más de uno: fb_suggest_grants hay que llamarlo una vez por grupo con el mismo role_name, así el rol se crea una sola vez y los privilegios se suman.
4. Ejecuta el plan sobre la copia y arranca la aplicación contra ella. Es el único paso que te dice si algo se rompe, y se rompen siempre las mismas cosas: las lecturas de las tablas MON$, las utilidades que lanza la aplicación, los objetos creados al arrancar, las stored procedures fuera del patrón. Cada una es una línea que añadir al plan, no un motivo para echarse atrás.
5. Comprueba que el rol esté activo. Por debajo de la 4.0, conéctate a la copia sin nombrar el rol, como hace la aplicación. Si la lectura funciona, el plan aguanta. Si recibes no permission, tienes dos opciones: añadir el nombre del rol a la connection string, o regenerar el plan con grant_to=user y poner los privilegios directamente sobre la cuenta.
6. PUBLIC es una decisión aparte. Esos REVOKE le quitan el permiso a cualquiera que se conecte, incluidos los programas de los que te has olvidado. No en la misma tarde en la que mueves la aplicación.
7. En producción cambias una línea. La cuenta nueva y el rol se crean al lado de la vieja, que se queda donde está y sigue funcionando. Mueves solo la connection string, y por eso el rollback es dejarla como estaba, sin tocar un privilegio. SYSDBA mientras tanto no desaparece: sigue haciendo falta para backups y administración, solo deja de ser la cuenta de la aplicación de gestión.
Cómo se prueba
Descarga MCPFirebird-0.6.0-win64.zip desde la release en GitHub, copia .env.example en .env y apunta firebird.client_lib a la fbclient.dll de tu instalación: en el zip no la encuentras, a propósito, porque solo tú sabes con qué servidor estás hablando. Después registra el exe como servidor MCP stdio en tu agente.
Antes de la release la suite se ejecuta en los cuatro motores: 122 tests core más 95 de conformidad al protocolo. Uno de esos tests ejecuta el plan de GRANT generado, crea la cuenta, se conecta sin rol, lee lo que el plan ha concedido y recibe un rechazo en lo que no. Sin eso, sabría solamente que el SQL es plausible.
El changelog completo lista toda la 0.6.0.
Comments
comments powered by Disqus