Saltar al contenido

Cómo probar Supabase RLS: 30 de 87 permisos no hacían nada

Ver como MarkdownAbrir en ClaudeAbrir en ChatGPTAbrir en Perplexity

La pregunta también se copia. Si el chat se abre vacío, péguela.

En resumen

Pruebe el row level security (RLS, seguridad a nivel de fila) de Supabase tal como lo encuentra un miembro del equipo: inicie sesión con cada rol, ejecute la acción real con un permiso desactivado y luego activado, y compruebe que el resultado cambia. En una aplicación en producción con cinco roles editables, este método encontró que 30 de las 87 casillas de permisos que un administrador podía guardar no cambiaban nada en la base de datos. Una prueba política a política no puede verlo, porque prueba la política que usted escribió, no la casilla de la pantalla.

Soy Denys, ingeniero full-stack y de IA. A principios de este año construí sobre Supabase un sistema de seguimiento de proyectos y horas para una firma de auditoría, donde cada regla de acceso vive en la base de datos. Abajo está el método que uso para demostrar esas reglas, lo que encontró y lo que cuesta ejecutarlo.

¿Por qué las pruebas de políticas no bastan para probar Supabase RLS?

Una prueba de política responde a “¿hace esta política lo que escribí?”. Sus usuarios se hacen otra pregunta: “si desmarco esta casilla para este rol, ¿pueden seguir haciéndolo?”. En una aplicación pequeña las dos preguntas tienen la misma respuesta. En una aplicación con una página de ajustes donde los administradores editan una matriz de permisos, se separan.

La aplicación tenía 41 tablas con RLS activado y 402 sentencias CREATE POLICY repartidas en 255 migraciones. La pantalla de administración mostraba una matriz de áreas (proyectos, tareas, partes de horas, archivos, etc.) frente a acciones (ver, crear, editar, archivar), con una columna por rol. Cada casilla guardada era una promesa al administrador. Nada comprobaba que la base de datos la cumpliera.

Supabase documenta bien las herramientas estándar: las pruebas con pgTAP y los helpers de pruebas le permiten crear usuarios, cambiar a ellos y comprobar qué pueden leer. Úselas. El método de abajo va por encima: lo guía la matriz que ve el administrador, no la lista de políticas.

¿Cómo funciona la prueba de la matriz?

Cada casilla de la matriz se convierte en una sonda. Una sonda inicia sesión como miembro del rol, realiza una acción real y registra el resultado. Lo hace dos veces: una con la casilla desactivada y otra con la casilla activada, todo lo demás igual. Si el resultado cambia, la casilla se aplica. Si no cambia, la casilla no hace nada, por convincente que parezca en pantalla.

  1. Una casilla de la matrizEjemplo: Standard Member · Tareas · Crear
  2. Casilla desactivada: actuar como miembro del rolCrear una tarea por el mismo camino que usa la aplicación. Registrar el resultado.
  3. Casilla activada: repetir la misma acciónMismo usuario, mismos datos. Solo cambió la casilla.
  4. Comparar los dos resultadosEl veredicto depende solo de lo que hizo la base de datos.
  • El resultado cambióSe aplica. Rechazada si está desactivada, permitida si está activada.
  • Igual en ambos casosNo hace nada. La pantalla promete un control que no existe.
  • Pantalla y base de datos no coincidenUn bug. La página muestra un botón que la base de datos rechaza, o al revés.
Figura 1. Cada veredicto sale de un par de resultados reales, nunca de leer el código.

“Resultado” significa aquí lo que haga cualquier consumidor del permiso: una política devuelve filas o rechaza una escritura, una función de base de datos lanza una excepción, un trigger bloquea o una edge function responde 403. Una casilla puede proteger varias de estas cosas, así que las 87 casillas necesitaron 279 comprobaciones por rol. Cada comprobación terminó con uno de estos veredictos:

Veredicto Comprobaciones por rol Qué significa
Efecto 227 El resultado cambió con la casilla
Muerta 30 El resultado fue el mismo con la casilla desactivada y activada
Inalcanzable 19 La acción no es accesible para este rol en absoluto, demostrado por separado
Edge function 1 Lo aplica una edge function, comprobado por HTTP
Solo pantalla 1 Solo cambia lo que muestra la página, cubierto por una prueba de front-end
Pantalla y base de datos coinciden 1 Lo que ofrece la página coincide con lo que permite la base de datos
Fallo 0 Una comprobación cuyo resultado no coincidió con lo esperado

¿Qué encontró la prueba?

La ejecución del 15 de septiembre de 2026 cubrió los cinco roles que un administrador puede editar (Admin, Senior, Standard, Restricted y un rol personalizado clonado). Los resultados fueron idénticos en todos los roles.

87 casillas de permisos que un administrador podía guardar. Según lo que realmente aplicaba cada casilla. Igual para los cinco roles. Aplicadas por la base de datos: 55; Aplicadas fuera: 1 edge function, 1 pantalla: 2; No cambiaban nada: 30.
Figura 2. Las 87 casillas de permisos que un administrador podía guardar, según lo que realmente las aplicaba. El mismo resultado para cada uno de los cinco roles.

Ninguna de las 30 dejaba entrar a nadie. En 29 de ellas, ninguna política, función ni trigger leía nunca la casilla; la última dejó de importar después de reescribir una política. Aun así es un problema real: un administrador que desmarca “Eliminar” para un rol cree que ese rol no puede eliminar. Ocultamos las 30 en lugar de conectarlas, porque cada una habría necesitado una decisión de producto, y una casilla que no hace nada no debería estar en pantalla mientras se toma.

El mismo trabajo sacó a la luz cuatro defectos más:

Qué estaba mal Cómo se vio Corrección
30 casillas no hacían nada Veredicto “muerta”, resultado idéntico desactivada y activada Ocultas en la matriz
Tres funciones de base de datos dejaban pasar una acción cuando una tarea no tenía responsable Una sonda que debía ser rechazada fue permitida Las funciones ahora rechazan cuando falta el valor
Devolver un parte de horas a borrador dependía de la casilla equivocada Pantalla y base de datos no coincidían para un rol Protegido por el permiso de reapertura al que pertenece
Un miembro veía los permisos nuevos solo tras recargar La base de datos había cambiado, la página no Los permisos se refrescan cada 60 segundos y al enfocar la ventana
El valor por defecto de una columna incumplía su propio check constraint, así que fallaban los inserts sin esa columna Lo encontró la revisión de seguridad de la corrección, en el mismo trabajo Valor por defecto cambiado a uno que el constraint admite

El bug del responsable ausente era el que importaba. Las comprobaciones estaban escritas así en PL/pgSQL:

if not (has_permission or assignee_id = auth.uid()) then
  raise exception 'forbidden';
end if;

Cuando assignee_id es null, assignee_id = auth.uid() es null, false or null es null, not null sigue siendo null, y if trata null como false. La excepción nunca salta. En producción ninguna tarea tenía responsable asignado, así que no era un caso límite: afectaba a todas las tareas del alcance. La corrección envuelve la condición en coalesce(…, false) para que un valor ausente rechace, y salió primero, por separado, antes que el resto del lote.

¿Cómo se ejecuta contra una copia de producción?

Una prueba contra un esquema distinto del de producción no demuestra nada sobre producción. El harness se ejecuta en el stack local de Supabase, en una base de datos aparte que es una copia del esquema de producción más las migraciones en revisión. Antes de cada ejecución compara una huella de las dos bases de datos locales: 799 filas que describen tablas, políticas, funciones y grants. Cualquier diferencia detiene la ejecución.

  1. Esquema de producciónCopiado a una sola máquina de confianza. Nunca va a un runner de CI alojado.
  2. Stack local: base de datos de la aplicaciónLa sirven la API y el auth locales, la usan las comprobaciones end-to-end.
  3. Stack local: base de datos aislada de la pruebaMismo esquema, la usa solo la prueba de la matriz.
  4. Comprobación de la huella799 filas por base de datos. Cualquier diferencia detiene la ejecución antes de la primera sonda.
  5. Sondas ejecutadas como cada rolComo miembros reales con sesión iniciada, por los mismos caminos que usa la aplicación.
  6. Sin dejar rastroEl número de filas al final debe ser igual al del principio.
Figura 3. La prueba solo cuenta cuando la base de datos en la que se ejecutó coincide con producción.

Dos detalles hicieron fiable el harness:

  • Romperlo a propósito primero. Antes de fiarme de una ejecución en verde, sustituí una política de insert por otra que lo acepta todo y volví a ejecutar la prueba. Tenía que ponerse en rojo en las casillas que protege esa política. Un harness que sigue en verde con una política rota es decoración.
  • Comprobar también el estado guardado. La primera pasada parte de una base limpia para cada rol. Una segunda pasada parte de la matriz guardada actual de cada rol, porque una casilla puede comportarse distinto junto a las casillas que un administrador marcó de verdad.

¿Cómo mantenerla en verde después del lanzamiento?

Una prueba que se ejecuta una vez queda obsoleta con la siguiente migración. Ahora la prueba se ejecuta antes de cada push que toca las migraciones, el código de auth o la propia matriz.

  1. git pushEl cambio toca migraciones, auth o la matriz.
  2. Hook pre-push: prueba de la matrizSe ejecuta en local contra la base de datos de la prueba. El rojo bloquea el push.
  3. El push llega a mainEl despliegue se ejecuta como siempre.
  4. Ejecución en CI autoalojadoLa misma prueba otra vez, en la máquina que guarda la copia del esquema de producción.
Figura 4. La barrera que bloquea es local, porque la copia del esquema de producción se queda en una sola máquina.

La prueba son 135 tests en 11 archivos, junto a unos 3.100 tests unitarios normales en el mismo proyecto.

¿Qué no funcionó?

  • El CI alojado no podía ejecutarla. La prueba necesita la copia del esquema de producción, y no quería eso en un runner alojado. Por eso la barrera que bloquea es un hook pre-push, y el CI en una máquina autoalojada es una segunda comprobación a la que el despliegue no espera.
  • Los recuentos de filas fijos fallaron. La primera comprobación de “sin rastro” comparaba con números fijos. La siguiente migración legítima añadió una fila de permiso y la puso en rojo. Ahora compara los recuentos del final con los del principio.
  • Omitido no es aprobado. Las comprobaciones end-to-end necesitan el edge runtime local. Cuando está caído, se omiten, y una omisión se lee fácilmente como verde.
  • Es lenta en un portátil cargado. Las pasadas por rol necesitaron timeouts más largos en una máquina con carga.
  • Demuestra lo que describen las sondas. Una casilla para la que nadie escribió una sonda se reporta como un hueco de cobertura, no como segura.

Cómo repetirlo en su aplicación de Supabase

  1. Liste cada permiso que la pantalla permite guardar: rol × área × acción. Esa lista es la especificación, no sus políticas.
  2. Para cada casilla, anote la acción real que debería controlar y cómo se manifiesta un rechazo (sin filas, un error, un 403).
  3. Monte una base de datos que coincida con producción y saque su huella, para que una deriva del esquema detenga la ejecución.
  4. Ejecute cada acción como un miembro con sesión iniciada, casilla desactivada y luego activada, y compare.
  5. Rompa una política a propósito y confirme que la prueba se pone en rojo.
  6. Oculte o conecte cada casilla que no hace nada, y luego convierta la prueba en una barrera antes del push.

Si su aplicación ya tiene RLS pero no está seguro de que haga lo que dice la pantalla de ajustes, el mismo método sirve como auditoría de un código existente, el primer paso de un rescate de proyecto. Escribí sobre el proyecto detrás de estas cifras en el caso del seguimiento de horas para una firma de auditoría.

¿No está seguro de que su RLS coincida con su pantalla de ajustes?

Comparto con gusto cómo lo pruebo yo. Sin discurso comercial.

Escríbame