Ya sabemos qué es Row-Level Security (RLS) y cómo se configura. Sabemos que se basa en funciones, en contexto y en algo muy parecido al sentido común. Pero el verdadero reto no es activarlo: es aplicarlo bien según el escenario real que tienes delante.
Aquí no vamos a hablar de casos teóricos ni de pruebas de laboratorio. Vamos a entrar en los tres escenarios más comunes que nos encontramos en proyectos reales, de esos con datos de verdad, usuarios impacientes y arquitecturas heredadas.
RLS interno en la organización: seguridad basada en Active Directory
Este es el caso más limpio y agradecido: una empresa con usuarios autenticados en Active Directory, accediendo a la base de datos con sus propias credenciales de Windows (sí, esto existe y funciona cuando se hace bien). Aquí el RLS puede aprovechar directamente la identidad del usuario para filtrar datos.
El patrón habitual consiste en mapear usuarios o grupos de AD con permisos o ámbitos de acceso, normalmente usando el ORIGINAL_LOGIN() o SUSER_SNAME() para identificar al usuario, y compararlo contra una tabla de mapeo como:
CREATE TABLE Seguridad.Usuarios (
Login NVARCHAR(256) PRIMARY KEY,
CentroId INT,
EsJefe BIT
);
La función RLS consulta esta tabla y determina qué datos puede ver el usuario actual. Algo así:
CREATE FUNCTION Seguridad.fn_FiltradoCentro(@CentroId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS Permitir
WHERE EXISTS (
SELECT 1
FROM Seguridad.Usuarios U
WHERE U.Login = ORIGINAL_LOGIN()
AND ( U.CentroId = @CentroId)
OR EsJefe = 1)
);
Este modelo funciona bien si los usuarios acceden directamente con sus identidades, y si hay una buena sincronización entre AD y los datos internos. Pero cuidado: este patrón no es válido si la aplicación usa un login compartido, porque entonces ORIGINAL_LOGIN() siempre devuelve lo mismo.
También requiere un mantenimiento cuidadoso de la tabla de mapeo. Si te olvidas de añadir un nuevo usuario, no verá nada. Y eso está bien. Lo preocupante sería que viera de más.
Este patrón es ideal para:
- Reporting interno
- Aplicaciones corporativas en intranets
- Escenarios con SSRS o Power BI con Kerberos bien configurado (sí, existen)
RLS en aplicaciones con un único login SQL y múltiples usuarios
Este es, con diferencia, el escenario más habitual. Una aplicación que se conecta a la base de datos con un solo login SQL Server (por ejemplo, AppUser) y desde ahí da servicio a miles de usuarios distintos, cada uno con sus propios permisos y ámbito de acceso.
Desde el punto de vista de SQL Server, todos los accesos vienen del mismo login. Por tanto, el nombre del usuario no sirve para nada. Aquí RLS solo tiene sentido si usamos SESSION_CONTEXT() para establecer el contexto del usuario en cada sesión.
- El patrón correcto es:
- La aplicación identifica al usuario (login web, token, sesión, lo que sea).
- Determina su TenantId, su ámbito de acceso, si es admin, etc.
- Al abrir la conexión (o justo después), ejecuta
EXEC sp_set_session_context N’TENANT_ID’, 17;
EXEC sp_set_session_context N’ES_ADMINISTRADOR’, 0; - La función RLS consulta ese contexto y decide si mostrar o no cada fila.
Funciona. Es limpio. Es seguro. Pero solo si te aseguras de que cada conexión establece su contexto correctamente. Y eso implica entender bien cómo funciona el pool de conexiones. Porque si alguien reutiliza una conexión con el contexto de otro usuario, acabas sirviendo datos del usuario A al usuario B. Y eso no es un bug: es un incidente de seguridad.
Este patrón requiere no solo código en la aplicación para establecer el contexto en cada conexión sino validación en la base de datos para asegurarse de que el contexto existe y funciones RLS bien diseñadas (inline, sin llamadas externas ni joins innecesarios). Si además añades una capa de control en la aplicación tendrás doble seguridad.
Este escenario, bien montado, es el pan de cada día en escenarios como aplicaciones web SaaS, portales internos de gestión multiusuario o APIs que consumen datos con identidad propia (pero login compartido)
RLS en aplicaciones multi-tenant con varios clientes
Aquí las cosas se complican. Estamos ante una única aplicación que sirve a múltiples clientes independientes, y todos los datos viven en las mismas tablas, separados lógicamente por una columna TenantId. Este patrón es muy eficiente… hasta que alguien mete la pata y un SELECT devuelve datos de otro tenant.
RLS aquí no es una opción. Es una necesidad.
El patrón es similar al del punto anterior: la aplicación se conecta con un login compartido, pero establece el TenantId como contexto al inicio de cada sesión. La diferencia es que ahora ese TenantId define los límites legales del acceso a datos.
Es decir, ya no es un usuario con menos o más permisos. Es otro cliente, con sus propios datos. Si algo se cuela, no solo es un error, es una violación de privacidad con consecuencias legales.
La función RLS, por tanto, debe ser clara, directa, sin adornos:
CREATE FUNCTION Seguridad.fn_RLS_Tenant(@TenantId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS Permitir
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TENANT_ID') AS INT);
Y la política se aplica a todas las tablas sensibles. Además, conviene reforzar la integridad con defaults:
ALTER TABLE Facturas ADD CONSTRAINT DF_TenantId
DEFAULT CAST(SESSION_CONTEXT(N'TENANT_ID') AS INT) FOR TenantId;
Y con predicados de bloqueo (de eso hablaremos pronto) para evitar updates o deletes entre tenants.
Este patrón funciona, pero solo si se impide a la aplicación cambiar el contexto a lo loco, el TenantId se valida contra los permisos del usuario web y todas las rutas de acceso pasan por código que inicializa el contexto.
Además, como ya he remarcado, este escenario es el más sensible, y por tanto donde más cuidado debemos tener. Si no haces RLS aquí, estás confiando en que la aplicación nunca se equivoque. Buena suerte con eso.
Predicados de bloqueo en RLS
Cuando implementamos RLS, todo el mundo se queda encantado con el FILTER PREDICATE: ese que impide que el usuario vea filas que no le tocan. Pero la mayoría se olvida, o no quiere saber, que eso no impide que puedan hacer INSERT, UPDATE o DELETE sobre datos que no deberían tocar. Así de simple. Y así de peligroso. Ahí es donde entran los BLOCK PREDICATES, el otro 50% de la seguridad que RLS nos ofrece.
Un BLOCK PREDICATE define si una operación de modificación de datos está permitida. Se puede aplicar a:
- AFTER INSERT (cuando se insertan nuevas filas)
- AFTER UPDATE (cuando se actualiza una fila)
- BEFORE UPDATE (para impedir que se cambie el valor de clave de filtrado, por ejemplo TenantId)
- BEFORE DELETE (para validar que el usuario puede borrar esa fila)
Y aquí viene la parte buena, estos bloqueos se aplican antes de que la operación se realice. Si no se cumple la condición, la instrucción falla con error. Sin necesidad de triggers, sin lógica adicional. De forma declarativa y centralizada. Como debe ser.
Vamos con un ejemplo.
Supongamos que tenemos una tabla Facturas y ya tenemos aplicado un FILTER PREDICATE que impide ver facturas de otros tenants. Muy bien. Pero si no hacemos nada más, el usuario podría hacer esto:
INSERT INTO Facturas (FacturaId, TenantId, Importe) VALUES (9999, 42, 1500);
Y colar datos con el TenantId de otro cliente. Luego, como no puede verlas, ni se entera. Pero ya están en tu base. ¿Te hace ilusión explicárselo a legal? A mí tampoco.
Para evitarlo, añadimos un BLOCK PREDICATE:
CREATE FUNCTION Seguridad.fn_Bloqueo_Tenant(@TenantId INT)
RETURNS TABLE
WITH SCHEMABINDING
AS
RETURN SELECT 1 AS Permitir
WHERE @TenantId = CAST(SESSION_CONTEXT(N'TENANT_ID') AS INT);
Y luego lo aplicamos así:
CREATE SECURITY POLICY Seguridad_Politica_Tenant
ADD FILTER PREDICATE Seguridad.fn_Bloqueo_Tenant(TenantId) ON dbo.Facturas,
ADD BLOCK PREDICATE Seguridad.fn_Bloqueo_Tenant(TenantId) ON dbo.Facturas
WITH (STATE = ON);
Esto bloquea los INSERT y DELETE por defecto. Si queremos bloquear los updates también (y deberíamos), lo hacemos explícitamente:
ADD BLOCK PREDICATE Seguridad.fn_Bloqueo_Tenant(TenantId)
ON dbo.Facturas
AFTER UPDATE
Y si no queremos que nadie cambie el TenantId de una fila, lo añadimos como BEFORE UPDATE:
ADD BLOCK PREDICATE Seguridad.fn_Bloqueo_Tenant(TenantId)
ON dbo.Facturas
BEFORE UPDATE
Esto impide que alguien coja una factura legítima y le cambie el TenantId para que desaparezca mágicamente de la vista de su propietario original. Muy creativo. Muy ilegal.
Consideraciones importantes con los bloqueos de RLS
Al usar BLOCK PREDICATE, las operaciones que no cumplan la condición fallan con un error genérico del tipo:
“The attempted operation failed due to security restrictions.”
Esto es intencionado. No dice qué falló ni por qué, porque revelar detalles de seguridad no es buena idea. Pero esto también significa que debes controlar bien los errores en la aplicación, o los usuarios se encontrarán mensajes confusos.
Y como siempre, las funciones deben ser INLINE para no perder rendimiento. Y deben ser simples, sin joins ni condiciones complejas. Esto se ejecuta en cada fila afectada. Si metes un SELECT TOP 1 dentro, estás haciendo cosas malas y mereces tus bloqueos (los tuyos, no los de RLS).
Conclusión
RLS es una herramienta poderosa, pero no mágica. Su utilidad real depende de cómo te conectas, de quién eres en la base de datos y de cómo estableces el contexto. Un mismo mecanismo puede servir tanto para proteger acceso interno como para blindar un modelo multi-tenant.
Pero si no eliges el patrón correcto para tu escenario, estás poniendo una alarma en la puerta mientras dejas la ventana abierta. Y eso no es seguridad: es postureo.
Ya hemos cubierto lo esencial de RLS: qué es, cómo se implementa, cómo se adapta a escenarios reales y cómo no confiar ciegamente en que el FILTER PREDICATE lo hace todo. Porque si no añades bloqueos, estás dejando que el usuario escriba donde no puede leer. Y eso es como cerrar la puerta pero dejar las ventanas abiertas: solo te protege si el atacante es educado.
Usar RLS bien no es complicado. Pero requiere tomárselo en serio. Establecer contexto. Validar accesos. Aplicar filtros y bloqueos. Y sobre todo: asumir que si no lo defines tú, alguien lo hará por ti. Probablemente mal.
Si tenéis alguna duda o sugerencia, podéis dejarla en Twitter, por mail o dejarnos un mensaje en los comentarios. Y recuerda que también tenemos un grupo de LinkedIn y un canal de YouTube a los que te puede unir. ¡Hasta la próxima!


