Multi-tenant: el error que no se ve

Aislar los datos de cada empresa en una sola base parece resuelto hasta que no lo está. Por qué fijar el search_path no alcanza y dónde mirar de verdad.

Juan Pedro GachetCofundador y director
Publicada
Lectura
2 min
Crónica
N.º 02
Markdown
empresa_aempresa_bempresa_csecurity definerUNA BASE N.º 02Ingeniería
Fig. 02 — Una función que cruza todas las empresas
En esta crónica · 3 partes
  1. La función que se saltea las reglas
  2. Dónde está el control de acceso de verdad
  3. Lo que aprendimos

Cuando un sistema atiende a varias empresas desde una sola base de datos, hay una pregunta que se responde el primer día y no se vuelve a mirar nunca: ¿cómo garantizamos que la empresa A no vea los datos de la empresa B?

En Postgres la respuesta corta es Row Level Security. Se activa RLS en la tabla, se escribe una política que compara la empresa de la fila contra la del usuario logueado, y listo. Es correcto, funciona, y es lo que usamos.

El problema aparece después, cuando el sistema crece y alguien necesita hacer algo que la política no deja hacer.

La función que se saltea las reglas

Supongamos que hay que calcular un total que cruza varias tablas, o correr un proceso nocturno. Escribir eso respetando RLS es incómodo, así que aparece la salida fácil: una función SECURITY DEFINER. Esa función corre con los permisos de quien la creó —normalmente el dueño de la base— y las políticas de RLS no la frenan. Es exactamente para eso que existe.

Hasta acá está bien. El problema es qué se revisa después.

La recomendación que todo el mundo repite sobre SECURITY DEFINER es fijarle el search_path:

CREATE FUNCTION calcular_total(p_empresa uuid)
RETURNS numeric
LANGUAGE sql
SECURITY DEFINER
SET search_path = public   -- esto es lo que todos revisan
AS $$ ... $$;

Eso previene un ataque real: que alguien cree una tabla con el mismo nombre en un esquema que esté antes en el search_path y logre que la función lea la suya. Es una defensa necesaria.

Pero no tiene nada que ver con quién puede llamar a la función.

Dónde está el control de acceso de verdad

Una función SECURITY DEFINER esquiva RLS por diseño. Entonces lo único que decide quién puede ejecutarla es el GRANT EXECUTE. Eso es el control de acceso, no el search_path.

Y ahí está la trampa: en Postgres, cuando creás una función, el permiso de ejecución se le otorga a PUBLIC por defecto. No hay que hacer nada mal para que quede abierta — hay que acordarse de cerrarla.

En un proyecto nuestro encontramos tres funciones SECURITY DEFINER que tenían el search_path correctamente fijado, pasaban todas las revisiones que hacíamos, y estaban ejecutables por el rol anónimo. El chequeo automático daba verde porque miraba lo que todos miran.

La forma de verlo:

SELECT p.proname,
  p.prosecdef AS es_security_definer,
  pg_get_function_identity_arguments(p.oid) AS args,
  array_to_string(p.proacl, ', ') AS permisos
FROM pg_proc p
JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = 'public' AND p.prosecdef
ORDER BY p.proname;

Si en permisos aparece =X/ sin un rol adelante, o el valor es NULL, esa función la puede llamar cualquiera. Cerrarla es explícito:

REVOKE EXECUTE ON FUNCTION calcular_total(uuid) FROM PUBLIC;
GRANT  EXECUTE ON FUNCTION calcular_total(uuid) TO authenticated;

Lo que aprendimos

El invariante que revisábamos estaba incompleto. Verificaba que RLS estuviera activo en todas las tablas y que las funciones SECURITY DEFINER tuvieran search_path fijo. Le faltaba la tercera pregunta, que es la que importa: quién puede ejecutar cada una de esas funciones.

Es un caso de manual de por qué un chequeo automático que da verde puede ser peor que no tener ninguno: genera la confianza sin dar la garantía. Ahora el chequeo mira las tres cosas, y corre en cada deploy.

Si tenés un sistema multi-tenant en Postgres, la consulta de arriba tarda dos segundos. Vale la pena correrla hoy.

Juan Pedro GachetCofundador y director · Mithandir
Empecemos

¿Tenés un problema parecido?

Lo que escribimos sale de sistemas que están corriendo. El tuyo puede ser el próximo.