Skip to content
Back to projects

Nakamoto

Mensajería interna con copiloto de IA, donde la garantía de permisos no vive en el código de aplicación sino en la base de datos.

This case study hasn't been translated yet — the text below is in Spanish. Read it on the Spanish site

Outcome

Cero

mensajes visibles para quien no debe verlos

3

barreras independientes protegen esa regla

Siempre

el asistente cita su fuente o admite que no sabe

Funciona

aunque no haya servicio de IA disponible

Constraints

  • Ningún usuario puede leer, buscar ni consultar por copiloto contenido de un canal donde no es miembro. La regla se valida en frontend, backend y base de datos, y solo la última no se puede rodear escribiendo mal el código de arriba.
  • El copiloto debe citar los mensajes en los que se apoya y negar explícitamente cuando no tiene permisos o contexto. Un modelo que improvisa cuando no sabe es peor que uno que calla.
  • El sistema arranca sin clave de OpenAI. Sin ella el copiloto responde con un proveedor de respaldo determinista, porque una demo que no levanta sin API ajena no es una demo.

El problema

Una plataforma de mensajería interna con canales públicos, privados y directos, y encima un copiloto que responde preguntas sobre lo conversado. El requisito difícil no es el copiloto: es que el copiloto no puede convertirse en la puerta trasera que deja leer canales ajenos.

Un buscador de texto completo y un pipeline de RAG tienen el mismo defecto de origen: son consultas que barren todo el corpus. Si el filtro de permisos se aplica después de recuperar, ya se leyó de más. Y si se aplica en el código de la aplicación, basta un endpoint nuevo escrito con prisa para abrir el hueco.

Lo que decidí, y por qué

La última línea es PostgreSQL, no el backend. Row Level Security evalúa membresía en cada fila devuelta, sin importar qué ruta o qué consulta llegó hasta ahí. El backend se conecta con un rol sin BYPASSRLS y declara el actor al inicio de cada transacción. A partir de ese punto, aunque el SQL esté mal escrito o falte una validación en el caso de uso, la política sigue aplicando.

La razón de invertir el esfuerzo principal ahí no es que las otras capas sobren. Es que es la única donde un error de programación se traduce en «no devuelve nada» en vez de «devuelve de más». Un endpoint nuevo nace protegido incluso si quien lo escribió olvidó pensar en permisos.

Defensa en profundidad, con razones distintas por capa. El frontend no muestra lo que la sesión no debería ver, pero corre en el navegador del usuario y cualquiera puede llamar a la API desde las herramientas de desarrollador. El backend valida el JWT y resuelve el userId solo desde el token, pero es código como cualquier otro: el agujero que deja un filtro olvidado no se ve en revisión, porque el código que falta no aparece en el diff. Cada capa falla de una forma distinta, y esa es exactamente la idea.

El copiloto cita, o niega. Responde únicamente con información que el usuario autenticado tiene permitido ver, y cuando no tiene contexto o permisos lo dice en vez de rellenar. La verificación más simple de todo el sistema es un SELECT * desde psql con el rol de la aplicación: sin actor declarado, devuelve cero filas.

Qué me llevo

Que la pregunta útil sobre una capa de seguridad no es «¿valida?», sino «¿de qué manera falla cuando alguien se equivoca?». Tres capas que fallan igual son una capa repetida tres veces.