La seguridad a nivel de fila es un mecanismo de base de datos que filtra qué filas puede ver o modificar cada usuario, aplicado por políticas en cada consulta. El modelo mental es una cláusula WHERE invisible y obligatoria: la condición que de otro modo tendrías que recordar en cada consulta se vuelve una propiedad de la tabla misma — aplicada por el motor, en todo camino de acceso, incluidos los que el código de tu aplicación nunca ve.
Puntos clave
| Pregunta | Respuesta |
|---|---|
| Qué es | Control de acceso por fila, aplicado por el propio motor de la base de datos |
| El modelo mental | Una cláusula WHERE que no se puede olvidar |
| El caso de uso estrella | Aislamiento multi-tenant y datos por usuario, en la capa que las brechas no pueden saltarse |
| La letra chica | Roles de bypass, denegar por defecto, contexto de pooling — los detalles traicioneros son operativos |
| La falla que previene | Un filtro olvidado en el código de la aplicación = brecha entre tenants |
RLS en diez líneas de SQL
CREATE TABLE invoices (
tenant_id uuid NOT NULL,
total numeric(10,2)
);
ALTER TABLE invoices ENABLE ROW LEVEL SECURITY; -- desde aquí: denegar por defecto
CREATE POLICY tenant_isolation ON invoices
USING (tenant_id = current_setting('app.tenant')::uuid) -- lo que puedes leer
WITH CHECK (tenant_id = current_setting('app.tenant')::uuid); -- lo que puedes escribir
-- Cada consulta ahora se comporta como si terminara con WHERE tenant_id = <el tuyo>.
-- Una consulta que olvida su filtro devuelve nada — no todo.
La misma garantía existe en bases de datos de documentos como control de acceso en la fila misma — cada objeto lleva su ACL, y la plataforma la aplica en cada solicitud:
// JavaScript / Node.js — Back4app JS SDK
// Row-level security as data: this row is readable by its owner, period
const note = new Parse.Object('Note');
note.set('text', 'Q3 salary planning');
note.setACL(new Parse.ACL(Parse.User.current())); // owner-only, on the row itself
await note.save();
// Later, any query by any user — the row exists only for its owner
const notes = await new Parse.Query('Note').find(); // others never see it // Flutter / Dart — Back4app Flutter SDK
// Row-level security as data: this row is readable by its owner, period
final user = await ParseUser.currentUser() as ParseUser;
final acl = ParseACL(owner: user); // owner-only, on the row itself
final note = ParseObject('Note')
..set('text', 'Q3 salary planning')
..setACL(acl);
await note.save(); // other users' queries never return this row // iOS / Swift — Back4app Swift SDK
// Row-level security as data: this row is readable by its owner, period
var note = Note()
note.text = "Q3 salary planning"
note.ACL = try ParseACL.defaultACL() // owner-only, on the row itself
note.save { result in
if case .success = result {
print("saved — other users' queries never return this row")
}
} // Android / Kotlin — Back4app Android SDK
// Row-level security as data: this row is readable by its owner, period
val note = ParseObject("Note").apply {
put("text", "Q3 salary planning")
acl = ParseACL(ParseUser.getCurrentUser()) // owner-only, on the row itself
}
note.saveInBackground { e ->
if (e == null) Log.d("Notes", "saved — invisible to every other user")
} Qué hace el motor con una consulta
USING vs. WITH CHECK, permisivas vs. restrictivas
| Concepto | Gobierna | Regla práctica |
|---|---|---|
USING | Lo que existe para ti: lecturas y filas elegibles para update/delete | ”¿Puedo verlo?” |
WITH CHECK | Lo que puedes escribir: inserts y valores post-update | ”¿Puedo crearlo así?” |
| Políticas permisivas (por defecto) | Combinadas con OR — cualquier coincidencia concede | Conceder acceso |
| Políticas restrictivas | Sumadas con AND — todas deben pasar | Imponer límites obligatorios |
Dos reglas de composición que vale la pena memorizar de la referencia de PostgreSQL: omitir WITH CHECK reutiliza USING para las escrituras, y al menos una política permisiva debe pasar antes de que las restrictivas siquiera se consulten — solo restrictivas significa que nadie entra.
Dónde está soportado RLS
| Familia de motor | Mecanismo |
|---|---|
| PostgreSQL (9.5+) y derivados | CREATE POLICY — la implementación de referencia |
| SQL Server (2016+) | Políticas de seguridad sobre funciones de predicado inline (filter + block) |
| SQL distribuido (CockroachDB, YugabyteDB) | Políticas compatibles con PostgreSQL |
| Data warehouses en la nube | Políticas de acceso por fila, en el dialecto de cada proveedor |
| Bases de datos de documentos | No basadas en políticas — ACLs por objeto aplicadas por la capa de plataforma |
La última fila es la que le interesa a este glosario: en las bases de documentos la frontera a nivel de fila típicamente es dato en la propia fila (ACLs) en lugar de un predicado en el motor — misma garantía, mecanismo distinto, detallado en el artículo de ingeniería de Back4app.
Los detalles traicioneros, por fin en un solo lugar
- Sorpresas del denegar por defecto. Habilitar RLS sin ninguna política bloquea a todos excepto al dueño — la mitad de las historias de “RLS tumbó producción” son esto.
- La matriz de bypass. Los superusuarios, los roles con
BYPASSRLSy los dueños de tabla se saltan las políticas — defineFORCE ROW LEVEL SECURITYsi el dueño es también el usuario de la aplicación, y audita quién tiene qué. - Las views se evalúan como su dueño por defecto — una view sobre una tabla con RLS puede saltárselo en silencio, a menos que se cree con derechos del invocador (
security_invokeren PostgreSQL). - Las constraints filtran existencia. Una violación de unicidad o de clave foránea puede revelar que una fila invisible existe — el canal encubierto documentado; trata la unicidad sobre valores protegidos en consecuencia.
- Los errores pueden filtrar valores. Expresiones diseñadas para fallar con datos específicos (una división por cero cuando un valor oculto coincide) exfiltran a través de mensajes de error — la clase de side-channel que describe la documentación de SQL Server.
- Los dumps y los pools tienen modos. Las herramientas de respaldo pueden necesitar el row security apagado para exportar todo; los poolers en modo transacción necesitan contexto con alcance de transacción (
SET LOCAL), o los tenants se mezclan entre solicitudes.
Rendimiento: las políticas son código en el hot path
Tres reglas cubren la mayor parte de la experiencia de campo: indexa cada columna que una política referencia (el predicado corre antes que los filtros de tu propia consulta — sin índice, es un scan); mantén los predicados libres de joins, empujando los lookups a funciones o verificaciones de rol; y haz que las funciones por solicitud se evalúen una vez por consulta, no una vez por fila, envolviéndolas en una subconsulta escalar. Bien medido, RLS cuesta un solo dígito porcentual; mal medido, es la lentitud misteriosa en cada tabla que aseguraste.
Casos de uso comunes
- SaaS multi-tenant. El caso canónico — aislamiento de tenants aplicado por debajo de la aplicación, donde un filtro olvidado no puede convertirse en brecha.
- Datos por usuario. Mensajes, documentos, historiales médicos: los usuarios ven sus filas, punto.
- Fronteras departamentales y regionales. Ventas ve su región; los auditores ven todo, en solo lectura — capas de políticas en acción.
- Regímenes de compliance. Control de acceso demostrable en la capa de datos, que es donde les gusta a los auditores.
- Acceso analítico compartido. Los analistas consultan réplicas de producción directamente, viendo solo lo que su rol permite — seguro porque la base de datos lo aplica, no el dashboard.
¿Deberías aplicar a nivel de fila? Matriz de decisión
| Aplica con RLS/ACLs cuando… | El filtrado en la aplicación puede bastar cuando… |
|---|---|
| Múltiples tenants o usuarios comparten tablas | La base de datos tiene exactamente un llamador confiable |
| Algo además de la app toca la base de datos | Sin herramientas de BI, sin SQL de admin, sin un segundo servicio |
| Un filtro olvidado significa brecha, no bug | El acotamiento es conveniencia, no seguridad |
| El compliance quiere prueba en la capa de datos | Los datos no son sensibles |
| Quieres la frontera probada una vez, centralmente | Disfrutas auditar cada consulta para siempre |
La síntesis honesta de las dos columnas: mantén el acotamiento en la aplicación por legibilidad — y aplícalo en la base de datos de todos modos. La defensa en profundidad es todo el punto.
Limitaciones y trade-offs
- Las políticas son invisibles por diseño — lo que convierte depurar el “¿dónde quedaron mis filas?” en una habilidad genuina; registra el contexto de sesión primero.
- La autorización compleja desborda los predicados. Las reglas que necesitan estado de workflow o lógica entre entidades pertenecen a las capas de autorización de la aplicación, con RLS como respaldo.
- La superficie operativa es real. La matriz de bypass, los modos de dump y el contexto de pooling son conocimiento operativo continuo, no configuración de una sola vez.
- La evaluación por fila es un impuesto — pequeño cuando está bien diseñado, ilimitado cuando no.
- Asegura filas, no columnas. Ocultar campos es seguridad a nivel de columna; combina ambas para un efecto a nivel de celda.
Seguridad a nivel de fila en Back4app
Back4app es una plataforma open-source de Backend as a Service (BaaS) que combina base de datos gestionada, APIs REST y GraphQL generadas automáticamente, autenticación, almacenamiento de archivos y funciones serverless con Cloud Code. Su modelo a nivel de fila es el mecanismo de ACL-como-dato de las pestañas de arriba: cada objeto lleva su lista de acceso, los roles expresan tenants y equipos, y la plataforma aplica ambos en cada solicitud — SDKs, REST, GraphQL y el dashboard incluidos, con los permisos a nivel de clase como la capa restrictiva por encima. Es la garantía de la cláusula WHERE obligatoria, entregada sobre una base de datos de documentos, con la lista de detalles traicioneros de arriba absorbida por la plataforma en lugar de asignada a ti.
Preguntas frecuentes
¿Qué es la seguridad a nivel de fila en términos simples?
Una cláusula WHERE invisible y obligatoria. Adjuntas políticas a una tabla y la base de datos aplica sus condiciones a cada consulta automáticamente — cada usuario ve y modifica solo las filas que la política permite, sin importar qué consulta escriba o qué herramienta use. El filtro vive en la base de datos, así que ningún camino de código de la aplicación puede olvidarlo.
¿Cómo funciona la seguridad a nivel de fila?
Habilitas RLS en una tabla y creas políticas con predicados booleanos — típicamente comparando la columna de dueño o de tenant de la fila con el usuario actual o una variable de sesión. Al ejecutar la consulta, el motor evalúa el predicado por fila antes de tus propias condiciones, filtrando lecturas y bloqueando escrituras no permitidas. Con RLS habilitado y ninguna política, el comportamiento por defecto es denegar todo.
¿Cuál es la diferencia entre USING y WITH CHECK?
La dirección. USING filtra lo que existe para ti: filas visibles al SELECT y elegibles para UPDATE o DELETE. WITH CHECK valida lo que escribes: filas siendo insertadas y los valores nuevos tras un update. Si omites WITH CHECK, el predicado de USING aplica a ambos — pero las tablas donde los usuarios leen amplio y escriben estrecho necesitan los dos, configurados distinto.
¿Quién puede saltarse la seguridad a nivel de fila?
En PostgreSQL: los superusuarios, los roles con BYPASSRLS y — el que todos olvidan — el dueño de la tabla, a menos que definas FORCE ROW LEVEL SECURITY. Auditar esa matriz de bypass es parte de desplegar RLS; una política perfecta no protege nada si la aplicación se conecta como el dueño de la tabla.
¿La seguridad a nivel de fila afecta el rendimiento?
Puede — el predicado de la política se evalúa contra las filas candidatas en cada consulta. Las mitigaciones son consistentes en la experiencia de producción: indexa las columnas que tus políticas referencian, mantén los predicados libres de joins (usa funciones o roles de lookup) y haz que las funciones por solicitud se evalúen una vez por consulta, no una vez por fila. Una política sobre una columna sin índice es un table scan con credencial de seguridad.
¿Cuál es la diferencia entre RLS y el filtrado en la aplicación?
Dónde vive la frontera. El filtrado en la aplicación acota consultas en el código — flexible, pero duplicado en cada camino y evadido por cualquier cosa que hable con la base de datos directamente. RLS aplica en el motor, cubriendo todo camino de acceso, incluidas las herramientas de admin y otros servicios. El consenso es defensa en profundidad: acota en la aplicación por claridad, aplica en la base de datos por seguridad.
¿Cómo funciona RLS con connection pooling?
Con cuidado. Las aplicaciones con pool se conectan como un único usuario de base de datos, así que las políticas dependen de una variable de sesión por solicitud en lugar de la identidad de la conexión — definida al tomar la conexión, leída por la política, reseteada al devolverla. Con poolers en modo transacción, la variable debe tener alcance de transacción, o el contexto de un tenant se filtra a la siguiente solicitud en la misma conexión. Esta interacción es el bug de RLS más común en producción.
¿Cómo se prueban las políticas de seguridad a nivel de fila?
Suplanta cada rol y ejecuta las cuatro operaciones — select, insert, update, delete — verificando tanto lo que aparece como lo que se rechaza. Agrega los dos casos olvidados: el contexto sin definir (ninguna variable de tenant debería significar cero filas, no todas) y la matriz de bypass (dueño, superusuario, caminos de replicación). RLS se gana la confianza con pruebas adversariales, no con una política bien redactada.