Guías de ingeniería
Importaciones en Supabase, RLS y campos controlados por el tenant
Separa los valores del CSV de la autoridad sobre el tenant. Comprende los JWT de usuario, el acceso con el rol de servicio y la limitación de COPY de PostgreSQL cuando se aplica RLS.
Obtén el tenant de la importación a partir de una decisión autenticada y autorizada de la aplicación. Exclúyelo del mapeo de columnas del cliente. Después, haz cumplir en el destino las condiciones de la escritura permitida con el rol que realmente la ejecutará.
Activar RLS es solo una parte de ese diseño. Una solicitud a la API de Supabase con un JWT de usuario, una solicitud del servidor con la autoridad del rol de servicio y una sesión de psql del propietario de la base de datos no tienen los mismos permisos. Que una importación funcione en el panel no demuestra que vaya a funcionar en la sesión de un cliente ni que se haya puesto a prueba el aislamiento entre tenants.
Si solo necesitas los pasos de carga, empieza por importar CSV en Supabase. Esta página trata el límite de autoridad que hay detrás de esas escrituras.
Alcance y supuestos. PostgreSQL 17 y comportamiento de Supabase Auth y Data API. Los ejemplos de políticas son ilustrativos y requieren el modelo de pertenencia y concesión de permisos indicado.
Identifica primero la vía de ejecución
| Vía | Autoridad que debes inspeccionar | Qué demuestra una escritura correcta |
|---|---|---|
| Data API con el JWT de un usuario que ha iniciado sesión | Rol autenticado efectivo, claims del JWT, permisos concedidos y políticas | La solicitud superó las comprobaciones aplicables en ese momento |
| Backend que usa autorización del rol de servicio | Rol con privilegios y autorización propia del backend | No deduzcas que se aplicó RLS; service_role tiene BYPASSRLS |
| Panel / conexión con privilegios a la base de datos | Rol real de la sesión, propiedad y atributos de omisión de RLS | No demuestra el acceso de un usuario ordinario de la aplicación |
| Inicio de sesión en la base de datos con alcance limitado y controlado por el cliente | Permisos concedidos al rol, propiedad de la tabla, RLS aplicable y contexto de la sesión | Solo los permisos y las comprobaciones ejercidos por ese inicio de sesión |
La guía de claves de API de Supabase distingue entre el acceso con claves publicables/anon y el acceso con claves secretas/de rol de servicio. Su guía de RLS también explica un detalle importante del cliente: un cliente inicializado con una clave de servicio puede seguir el contexto de RLS de un usuario que ha iniciado sesión cuando su autorización se sustituye por la sesión de ese usuario. Inspecciona la credencial efectiva de Authorization, además de la clave usada al construir el cliente.
En PostgreSQL, los superusuarios y los roles con BYPASSRLS omiten la seguridad por filas. Los propietarios de las tablas también suelen omitirla, salvo que se aplique FORCE ROW LEVEL SECURITY al propietario. FORCE no restringe a un superusuario ni a un rol con BYPASSRLS. Por eso no basta con probar solo como propietario de la tabla.
Los permisos concedidos y la RLS resuelven problemas distintos. Un rol necesita el privilegio pertinente sobre la tabla o la columna; su política de filas también debe permitir la operación cuando se aplica RLS. Si ninguna política aplicable permite el acceso, una tabla con RLS activada deniega el acceso de forma predeterminada a los roles ordinarios. Consulta las reglas de seguridad por filas de PostgreSQL.
Vincula cada importación a un tenant autorizado
Imagina que un fundador importa contactos para un espacio de trabajo. Su CSV contiene account_id de un CRM anterior. Tu base de datos también tiene un identificador de cuenta. El valor antiguo puede ser útil como referencia externa, pero no demuestra que quien importa tenga permiso para escribir en el espacio de trabajo correspondiente de tu sistema.
La aplicación debe autenticar al operador, comprobar su permiso de importación para el espacio de trabajo seleccionado y vincular ese espacio al trabajo de importación. El mapeo de filas aprobado contendrá entonces solo campos como el correo electrónico y el nombre mostrado. El componente de escritura aporta el contexto del tenant de forma independiente.
subject = authenticate(request)
tenant = authorizeImport(subject, selectedWorkspace)
job = createJob({ tenant, actor: subject.id, contractVersion })
for each submitted row:
reject keys outside the approved source fields
validate source values again on the server
write approved values with tenant = job.tenantLa autenticación por sí sola no es autorización. Un usuario puede haber iniciado sesión sin pertenecer al espacio de trabajo seleccionado, o pertenecer a él sin tener permiso para importar. Comprueba el permiso para esta operación y define cómo afecta su revocación al trabajo en cola. Un trabajo de larga duración no debería obtener silenciosamente una autoridad indefinida de una sesión que era válida ayer.
No uses metadatos de usuario editables como fuente de autoridad sobre el tenant. Si usas claims de JWT, establece quién los emite, quién puede cambiarlos y cómo se gestiona la información de pertenencia desactualizada. Un claim firmado solo resulta tan útil como el proceso de confianza que lo creó.
Usa WITH CHECK para restringir las filas nuevas
La siguiente política ilustra una aplicación donde una tabla de pertenencia de confianza registra quién puede importar en cada tenant. Se supone que existe public.contacts(tenant_id uuid, email text) con ambas columnas NOT NULL. Se supone que public.tenant_memberships(tenant_id uuid, user_id uuid, can_import boolean) está protegida frente a concesiones de permisos creadas por los usuarios y que sus permisos y políticas de SELECT permiten a un usuario leer sus propias pertenencias.
ALTER TABLE public.contacts ENABLE ROW LEVEL SECURITY;
CREATE POLICY contacts_member_import
ON public.contacts
FOR INSERT TO authenticated
WITH CHECK (
EXISTS (
SELECT 1
FROM public.tenant_memberships AS membership
WHERE membership.tenant_id = contacts.tenant_id
AND membership.user_id = (SELECT auth.uid())
AND membership.can_import
)
);Esto es un fragmento de política, no una instalación completa. El rol también necesita privilegios INSERT adecuados. Si la solicitud devuelve filas, revisa los privilegios y las políticas de SELECT necesarios para esa respuesta. Una política INSERT usa WITH CHECK para comprobar la fila propuesta; una política SELECT no la sustituye.
Revisa todas las políticas existentes. Las políticas permisivas suelen combinarse con OR, por lo que añadir esta política junto a una política INSERT permisiva y amplia podría no restringir el acceso. Las políticas restrictivas se combinan de otra forma y necesitan una política permisiva aplicable. Evalúa el conjunto completo de políticas en lugar de tratar una sola expresión aparentemente correcta como si definiera todo el límite de acceso.
Esta comprobación de pertenencia permite un tenant autorizado; no vincula una solicitud al tenant de un trabajo seleccionado previamente. El componente de escritura debe seguir rechazando los campos de tenant controlados por el archivo y usar el valor autorizado del trabajo. Si el modelo de amenazas contempla eludir ese componente, diseña también un límite de trabajo impuesto en el destino. No presentes este fragmento como si ofreciera esa garantía más fuerte.
COPY no es un atajo para importar con la RLS del usuario
PostgreSQL 17 no admite COPY FROM cuando RLS se aplica al rol que ejecuta la operación. La documentación recomienda sentencias INSERT equivalentes. Cargar una tabla temporal de preparación y usar después un INSERT autorizado en el destino puede ser un diseño adecuado, siempre que se revisen la sesión, los valores de las filas, los permisos concedidos y las políticas.
El propietario de una tabla o un rol que omite RLS pueden comportarse de forma distinta porque la seguridad por filas podría no aplicarse a ese rol. Eso no es una solución que conserve la garantía original de las políticas del usuario. Es una vía de migración con privilegios cuyas comprobaciones de tenant y revisión operativa deben establecerse por separado.
Las conexiones SQL directas tampoco llevan automáticamente el contexto JWT de un usuario de Supabase. Una función como auth.uid() depende de que los claims de la solicitud o de la sesión estén disponibles a través de la vía pertinente. No supongas que un inicio de sesión con psql representa al usuario final solo porque se conecta a la misma base de datos.
Para una migración puntual realizada por el comprador, la ejecución con privilegios puede ser una elección deliberada. Pide al ingeniero del comprador que revise la asignación del origen al tenant, las columnas exactas, el destino y las filas resultantes. Mantén ese procedimiento diferenciado de un importador para usuarios finales sujeto a RLS. La documentación de COPY describe la limitación de la base de datos.
Prueba las escrituras prohibidas, además de la permitida
Usa tenants de prueba aislados y filas sintéticas. Ejecuta las pruebas con el mismo rol y la misma vía de solicitud que el importador real, con los mismos permisos concedidos y el conjunto completo de políticas.
- Un usuario con permiso de importación para el tenant A puede crear una fila aprobada en A.
- Se rechaza a un usuario con permiso de importación solo para el tenant A cuando solicita B.
- Para un trabajo ya vinculado a A, se rechaza un tenant B aportado en la entrada aunque el usuario también tenga permiso para B. Esto requiere controles a nivel de trabajo, además de la política de pertenencia.
- Se rechaza a un miembro sin permiso de importación.
- Una sesión sin autenticar o caducada no obtiene una vía alternativa de escritura con privilegios.
- Se rechaza un campo de tenant aportado en la entrada aunque coincida con el tenant correcto.
- Un trabajo en cola sigue la regla documentada después de revocarse la pertenencia.
Después de una operación correcta, lee la fila mediante la vía habitual de la aplicación para A y B. Confirma tanto la visibilidad como la inserción. Evita que los errores revelen identificadores o datos de otro tenant. Si una prueba solo funciona como propietario, has probado la autoridad del propietario, no la vía prevista para el cliente.
También hay que revisar una clave foránea de un contacto a una empresa: un ID de empresa válido globalmente podría pertenecer a otro tenant. Usa una restricción de referencia con alcance de tenant y una estrategia de resolución adecuadas. La guía de claves foráneas desarrolla ese ejemplo.
Relación con ImportFlow
ImportFlow mantiene la autorización de la aplicación separada de los valores de tenant aportados por la hoja de cálculo. Consulta la arquitectura actual y los límites de implementación para conocer el reparto de responsabilidades a nivel de producto.
La aplicación sigue siendo responsable de establecer los permisos del usuario y su autoridad sobre el tenant. ImportFlow trata los valores protegidos de tenant o de sistema como contexto de la aplicación, en lugar de campos de la hoja de cálculo editables por el cliente, y la RLS por sí sola no establece esa autoridad.