Cinco maneras en que la seguridad a nivel de fila falla abierta

La seguridad a nivel de fila es la mejor defensa contra un WHERE olvidado, hasta que deja de aplicarse en silencio. Cinco maneras en que falla abierta en PostgreSQL 17, cada una con una reproducción, una corrección y una consulta al catálogo que la detecta en CI.

Zhan Lu15 min de lectura
En esta página

La seguridad a nivel de fila (RLS, por sus siglas en inglés) es la mejor defensa que Postgres te ofrece contra el WHERE user_id = … olvidado. También es fácil de desactivar sin darte cuenta: un rol con un solo atributo, una vista creada por el usuario equivocado, un ajuste que vuelve como cadena vacía en lugar de NULL. Este artículo recorre cinco maneras en que RLS deja de proteger tus datos en silencio. Cada una trae una reproducción que puedes pegar en psql, una corrección y, al final, una sola consulta al catálogo que detecta muchas de ellas en CI. Cada fragmento se ejecutó en PostgreSQL 17.11, la misma versión mayor en la que corre Relyt One.

Si seguiste Primeros pasos con la autenticación gestionada de Relyt One, ya usas el patrón que probamos aquí: la aplicación define app.user_id a partir de un token verificado al inicio de cada transacción, y una política compara el propietario de cada fila con ese valor. Ese patrón es sólido. Las fallas de abajo no son errores de Postgres. Son los lugares donde el patrón deja de aplicarse sin avisar, y todas fallan de la misma forma: sin error, sin línea en el log, solo filas que llegan a alguien que no debería verlas.

Una política, cinco maneras de dejar de aplicarse: saltarse la puerta (un rol que omite RLS, un propietario sin FORCE, vistas y funciones SECURITY DEFINER) o dejar pasar las filas equivocadas (un ajuste vacío u obsoleto, políticas más amplias o tablas sin RLS)

La preparación

Todo lo que sigue se ejecuta en una sola sesión de psql como superusuario, cambiando de identidad con SET ROLE. Las sentencias que crean o modifican objetos (tablas, políticas, vistas) deben ejecutarse como demo_owner o como superusuario; las que actúan como la aplicación empiezan con SET ROLE demo_app. Tres roles representan lo que tiene un proyecto real:

  • demo_owner es propietario de las tablas y ejecuta las migraciones;
  • demo_app es el rol con el que se conecta la aplicación, con NOBYPASSRLS;
  • demo_admin tiene BYPASSRLS. En Relyt One este es relytdb_owner, el rol propietario con el que empieza todo proyecto.
CREATE ROLE demo_owner NOLOGIN;
CREATE ROLE demo_app   NOLOGIN;
CREATE ROLE demo_admin NOLOGIN BYPASSRLS;
CREATE SCHEMA demo AUTHORIZATION demo_owner;
GRANT USAGE ON SCHEMA demo TO demo_app;
GRANT USAGE, CREATE ON SCHEMA demo TO demo_admin;

Una tabla, una política, cuatro filas. La última fila tiene un propietario vacío; verás por qué en la sección 4.

SET ROLE demo_owner;
CREATE TABLE demo.notes (id serial PRIMARY KEY, user_id text NOT NULL, body text NOT NULL);
INSERT INTO demo.notes (user_id, body)
  VALUES ('alice', 'a1'), ('alice', 'a2'), ('bob', 'b1'), ('', 'orphan');

ALTER TABLE demo.notes ENABLE ROW LEVEL SECURITY;
ALTER TABLE demo.notes FORCE ROW LEVEL SECURITY;
CREATE POLICY own ON demo.notes FOR ALL
  USING      (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));
RESET ROLE;

GRANT SELECT, INSERT, UPDATE, DELETE ON demo.notes TO demo_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA demo TO demo_app;
GRANT SELECT ON demo.notes TO demo_admin;

Una solicitud normal, tal como la enviaría la aplicación:

SET ROLE demo_app;
BEGIN;
SELECT set_config('app.user_id', 'alice', true);  -- true: local a esta transacción
SELECT body FROM demo.notes;                      -- a1, a2
COMMIT;

Alice ve sus dos filas. Ahora vamos a romperlo.

1. El rol omite RLS por completo

La documentación de PostgreSQL (está en inglés; las citas de este artículo están traducidas) es tajante: "Los superusuarios y los roles con el atributo BYPASSRLS siempre omiten el sistema de seguridad de filas al acceder a una tabla". Las políticas ni siquiera se consultan.

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;                       -- 2   (se aplica la política)
COMMIT;
SET ROLE demo_admin; SELECT count(*) FROM demo.notes;  -- 4   (BYPASSRLS: todas las filas)
RESET ROLE;          SELECT count(*) FROM demo.notes;  -- 4   (superusuario: todas las filas)

El atributo pertenece al rol, no a sus miembros. Si demo_app es miembro de demo_admin, sigue filtrado, hasta que algún código ejecuta SET ROLE demo_admin:

GRANT demo_admin TO demo_app;
SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;                       -- 2   (la pertenencia por sí sola no omite RLS)
COMMIT;
SET ROLE demo_admin; SELECT count(*) FROM demo.notes;  -- 4

En Relyt One esto importa más de lo que parece. Los usuarios de base de datos creados desde la consola tienen BYPASSRLS, y también lo tiene relytdb_owner. Lo comprobamos en una rama real: una tabla con FORCE y una política correcta, consultada como relytdb_owner, devolvió todas las filas. Una aplicación que se conecta como uno de esos roles no tiene seguridad a nivel de fila en absoluto. Crea un rol dedicado con SQL y conéctate con ese; la guía de autenticación gestionada muestra cómo, y verificamos que un rol así inicia sesión tanto con la cadena de conexión directa como con la de pooling.

La prueba: ejecuta esto como el rol con el que se conecta tu aplicación. Debe devolver false.

SET ROLE demo_app;
SELECT rolsuper OR rolbypassrls AS bypasses_rls FROM pg_roles WHERE rolname = current_user;  -- f

2. El propietario sin FORCE

De forma predeterminada, "los propietarios de las tablas normalmente también omiten la seguridad de filas, aunque un propietario de tabla puede optar por quedar sujeto a ella con ALTER TABLE ... FORCE ROW LEVEL SECURITY". ENABLE por sí solo protege a todos excepto al rol que es propietario de la tabla.

SET ROLE demo_owner;
CREATE TABLE demo.notes2 (id serial PRIMARY KEY, user_id text NOT NULL, body text NOT NULL);
INSERT INTO demo.notes2 (user_id, body) VALUES ('alice', 'x'), ('bob', 'y');
ALTER TABLE demo.notes2 ENABLE ROW LEVEL SECURITY;          -- activada, no forzada
CREATE POLICY own ON demo.notes2 FOR ALL
  USING      (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));

SELECT count(*) FROM demo.notes2;   -- 2  (las de alice y las de bob: se omitió la política)

ALTER TABLE demo.notes2 FORCE ROW LEVEL SECURITY;
SELECT count(*) FROM demo.notes2;   -- 0  (ahora el propietario está sujeto a la política)
RESET ROLE;

Si tu herramienta de migraciones y tu aplicación comparten un mismo rol, ese rol es el propietario y tu aplicación omite RLS. Usa FORCE en cada tabla que guarde datos de usuarios y mantén al propietario fuera de la ruta de las solicitudes. En Relyt One el propietario tiene BYPASSRLS, así que FORCE no le sirve de nada; ahí la corrección es la de la sección 1.

3. Código que se ejecuta como otra persona

Una vista se ejecuta con los permisos de su propietario. La documentación de CREATE VIEW: "De forma predeterminada, el acceso a las relaciones base subyacentes a las que se refiere la vista se determina por los permisos del propietario de la vista", y lo mismo ocurre con las políticas de seguridad a nivel de fila. Una función SECURITY DEFINER hace lo mismo con la identidad del propietario de la función. Ambas son útiles, y ambas son justo lo que RLS debía evitar cuando el propietario la omite.

Toma otra vez notes2, déjala de nuevo en "activada, no forzada", agrega una vista sobre ella y una función definer, ambas propiedad de demo_owner, y pide las filas de alice:

ALTER TABLE demo.notes2 NO FORCE ROW LEVEL SECURITY;
GRANT SELECT ON demo.notes2 TO demo_app, demo_admin;

SET ROLE demo_owner;
CREATE VIEW demo.v_notes2 AS SELECT * FROM demo.notes2;
CREATE FUNCTION demo.count_notes2() RETURNS bigint
  LANGUAGE sql SECURITY DEFINER SET search_path = demo, pg_temp
  AS $$ SELECT count(*) FROM demo.notes2 $$;
RESET ROLE;
GRANT SELECT ON demo.v_notes2 TO demo_app;
GRANT EXECUTE ON FUNCTION demo.count_notes2() TO demo_app;

SET ROLE demo_app;
BEGIN;
SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes2;     -- 1
SELECT count(*) FROM demo.v_notes2;   -- 2   fuga
SELECT demo.count_notes2();           -- 2   fuga
COMMIT;

Agregar FORCE a la tabla cierra ambos huecos, porque ahora el propietario también queda sujeto a la política. Pero si el propietario mismo omite RLS, FORCE no hace nada. Crea una vista como demo_admin (nuestro sustituto de relytdb_owner) y filtra datos aun en una tabla forzada:

Dónde se ejecuta la consulta de aliceFilas que ve
Directamente sobre la tabla1
A través de una vista del propietario de la tabla (tabla no forzada)2
A través de una función SECURITY DEFINER (tabla no forzada)2
La misma vista y función después de FORCE1
A través de una vista de un rol con BYPASSRLS, tabla forzada2
La misma vista con security_invoker = true1

Para ver las dos últimas filas, crea una vista como demo_admin y fuerza la tabla:

RESET ROLE;
ALTER TABLE demo.notes2 FORCE ROW LEVEL SECURITY;
SET ROLE demo_admin;
CREATE VIEW demo.v_admin AS SELECT * FROM demo.notes2;     -- propiedad de un rol con BYPASSRLS
RESET ROLE;
GRANT SELECT ON demo.v_admin TO demo_app;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.v_admin;    -- 2   fuga, aunque la tabla está forzada
COMMIT;

La corrección real es security_invoker, agregado en PostgreSQL 15 ("Permitir que los accesos a tablas hechos por una vista se controlen, opcionalmente, con los privilegios de quien llama a la vista"). Con él, se aplican las políticas del usuario que ejecuta la consulta:

CREATE VIEW demo.v_safe WITH (security_invoker = true) AS SELECT * FROM demo.notes2;
ALTER VIEW demo.v_admin SET (security_invoker = true);   -- corrige una vista existente; ahora alice ve 1 fila

Para las funciones SECURITY DEFINER sobre tablas con datos de usuarios, lo mejor es no tenerlas. Si no hay más remedio, pasa el usuario como argumento y filtra explícitamente por él dentro de la función, y fija siempre search_path, como en el ejemplo de arriba.

4. El ajuste que no es lo que crees

El patrón depende de current_setting('app.user_id', true). El segundo argumento significa "devuelve NULL en lugar de lanzar un error si el ajuste no existe", y eso es lo que devuelve una sesión nueva. Luego una transacción define el valor con set_config(…, true) y termina. ¿Qué ve la siguiente consulta?

Empieza primero con una sesión nueva, porque las solicitudes anteriores ya dejaron el ajuste definido:

-- psql: reconéctate para obtener una sesión nueva
\c
SET ROLE demo_app;
SELECT coalesce(current_setting('app.user_id', true), '<NULL>');       -- <NULL>

BEGIN; SELECT set_config('app.user_id', 'alice', true); COMMIT;

SELECT '[' || coalesce(current_setting('app.user_id', true), '<NULL>') || ']';  -- []
SELECT user_id || '/' || body FROM demo.notes;                         -- /orphan

No es NULL: es una cadena vacía. Lo vimos en PostgreSQL 15, 16 y 17: el ajuste sigue definido después de la transacción, con un valor vacío. No es un comportamiento documentado, así que compruébalo en tu propia versión. Una política que compara user_id = '' coincide con cualquier fila cuyo propietario sea la cadena vacía, y nuestra tabla tiene una. Filas así vienen de errores y migraciones: un sub vacío que se coló sin validar, un valor predeterminado que nadie quiso. Son las filas que menos quieres exponer.

La otra mitad de esta sección es el error en el tercer argumento. Con false, el ajuste dura toda la sesión, y con un pool de conexiones la siguiente solicitud en esa conexión lo hereda:

SET ROLE demo_app;
SELECT set_config('app.user_id', 'alice', false);   -- a nivel de sesión
SELECT count(*) FROM demo.notes;                    -- 2, para quien use esta conexión después

La corrección es hacer que la política rechace un ajuste vacío y evitar que existan propietarios vacíos:

RESET ROLE;
ALTER POLICY own ON demo.notes
  USING      (nullif(current_setting('app.user_id', true), '') IS NOT NULL
              AND user_id = current_setting('app.user_id', true))
  WITH CHECK (nullif(current_setting('app.user_id', true), '') IS NOT NULL
              AND user_id = current_setting('app.user_id', true));

ALTER TABLE demo.notes ADD CONSTRAINT user_id_not_empty CHECK (user_id <> '') NOT VALID;

Compruébalo desde una sesión nueva:

-- psql: reconéctate para obtener una sesión nueva
\c
SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true); COMMIT;
SELECT count(*) FROM demo.notes;                         -- 0   (ahora el ajuste vacío no coincide con nada)
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;                         -- 2   (la solicitud normal de alice sigue funcionando)
COMMIT;

(NOT VALID te permite agregar la restricción mientras existe una fila vacía antigua; limpia la fila y luego ejecuta VALIDATE CONSTRAINT). Que set_config(…, true) sea la única forma que llame tu función de acceso a datos.

5. Una política más amplia, o ninguna política

Las políticas se suman. "Cuando varias políticas se aplican a una misma consulta, se combinan con OR (para las políticas permisivas, que son las predeterminadas) o con AND (para las políticas restrictivas)". Una política permisiva adicional, agregada para un reporte o una herramienta de soporte, amplía el acceso de todos:

RESET ROLE;
CREATE POLICY pub ON demo.notes FOR SELECT USING (true);

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;    -- 4, alice lo ve todo
COMMIT;

Una política RESTRICTIVE se combina con AND, así que mantiene un piso por debajo de cualquier política permisiva que se agregue después:

RESET ROLE;
CREATE POLICY tenant_guard ON demo.notes AS RESTRICTIVE FOR SELECT
  USING (nullif(current_setting('app.user_id', true), '') IS NOT NULL
         AND user_id = current_setting('app.user_id', true));

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes;    -- 2 otra vez, aun con `pub` vigente
COMMIT;

Las tablas nuevas empiezan sin RLS. Si además le otorgaste privilegios predeterminados al rol de la aplicación, la primera tabla que crees y olvides será legible para todos:

RESET ROLE;
ALTER DEFAULT PRIVILEGES FOR ROLE demo_owner IN SCHEMA demo GRANT SELECT ON TABLES TO demo_app;

SET ROLE demo_owner;
CREATE TABLE demo.notes_new (user_id text NOT NULL, body text NOT NULL);   -- nadie ejecuta ENABLE ROW LEVEL SECURITY
INSERT INTO demo.notes_new VALUES ('alice', 'n1'), ('bob', 'n2');
RESET ROLE;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
SELECT count(*) FROM demo.notes_new;    -- 2, alice también ve la fila de bob
COMMIT;

Lo contrario es seguro: una tabla con RLS activada y sin políticas es "denegar por defecto", sin filas visibles ni actualizables.

Dos cosas que no son huecos

  • Una política con solo USING. Para las políticas ALL y UPDATE, "si no se define ninguna expresión WITH CHECK, se usará la expresión USING" también para las filas nuevas. Las políticas INSERT ni siquiera pueden tener una cláusula USING. En una rama real de Relyt One, nuestra prueba de un usuario que escribía una fila cuyo propietario era otra persona fue rechazada por la política.
  • Un comando sin política. Con RLS activada y sin ninguna política aplicable, la documentación dice que se asume una política de "denegar por defecto". Una tabla que solo tiene una política FOR SELECT rechaza los inserts.

Dos cosas que RLS nunca cubre

Las restricciones únicas. "Las verificaciones de integridad referencial, como las restricciones únicas o de clave primaria y las referencias de clave foránea, siempre omiten la seguridad de filas para garantizar que se mantenga la integridad de los datos". La documentación llama a la consecuencia un "canal encubierto". Bob no puede leer el slug de alice, pero sí puede enterarse de que existe:

SET ROLE demo_owner;
CREATE TABLE demo.slugs (id serial PRIMARY KEY, user_id text NOT NULL, slug text NOT NULL UNIQUE);
INSERT INTO demo.slugs (user_id, slug) VALUES ('alice', 'secret-project');
ALTER TABLE demo.slugs ENABLE ROW LEVEL SECURITY;
ALTER TABLE demo.slugs FORCE ROW LEVEL SECURITY;
CREATE POLICY own ON demo.slugs FOR ALL
  USING      (user_id = current_setting('app.user_id', true))
  WITH CHECK (user_id = current_setting('app.user_id', true));
RESET ROLE;
GRANT SELECT, INSERT ON demo.slugs TO demo_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA demo TO demo_app;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'bob', true);
SELECT count(*) FROM demo.slugs WHERE slug = 'secret-project';              -- 0
INSERT INTO demo.slugs (user_id, slug) VALUES ('bob', 'secret-project');
-- ERROR:  duplicate key value violates unique constraint "slugs_slug_key"
ROLLBACK;

Haz que la unicidad sea por tenant y la fuga desaparece: el insert de bob funciona. Si un valor debe ser único entre todos los usuarios, acepta que su existencia es pública.

RESET ROLE;
ALTER TABLE demo.slugs DROP CONSTRAINT slugs_slug_key, ADD UNIQUE (user_id, slug);

Las vistas materializadas. La documentación no las menciona, así que prueba las tuyas. Una vista materializada guarda las filas tal como estaban cuando alguien la refrescó, y leer las filas guardadas no ejecuta las políticas de la tabla. En nuestra prueba, una vista creada mientras estaba activo el ajuste de alice le mostró a bob las dos filas de alice:

SET ROLE demo_owner;
BEGIN; SELECT set_config('app.user_id', 'alice', true);
CREATE MATERIALIZED VIEW demo.m_notes AS SELECT * FROM demo.notes;   -- guarda las filas de alice
COMMIT;
RESET ROLE;
GRANT SELECT ON demo.m_notes TO demo_app;

SET ROLE demo_app;
BEGIN; SELECT set_config('app.user_id', 'bob', true);
SELECT count(*) FROM demo.m_notes;    -- 2   bob lee las filas de alice
COMMIT;

No le otorgues a los roles de la aplicación acceso a vistas materializadas sobre datos de usuarios. Pon la columna del tenant en la vista y filtra por ella en la consulta, o lee la tabla base.

Una nota más de la documentación: una expresión de política se evalúa para cada fila "antes de cualquier condición o función proveniente de la consulta del usuario", con una excepción: "las funciones leakproof, que tienen garantizado no filtrar información; el optimizador puede decidir aplicar tales funciones antes de la verificación de seguridad de filas". Trata una marca LEAKPROOF como una promesa que tienes que poder cumplir.

Una autoauditoría que puedes ejecutar en CI

La mayor parte de la lista anterior deja huellas en el catálogo. Esta consulta devuelve una fila por cada hallazgo. Revisa el esquema public (cambia el nombre para otros). Ejecútala como el rol con el que se conecta tu aplicación y haz fallar la compilación cuando devuelva algo:

-- rls_audit.sql: qué puede hacer que las filas de un usuario lleguen a otro (esquema "public")
WITH tbl AS (
  SELECT c.oid, c.relname, c.relrowsecurity, c.relforcerowsecurity
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname = 'public' AND c.relkind IN ('r', 'p')
)
SELECT 'connected role bypasses RLS' AS problem, current_user::text AS object
  FROM pg_roles WHERE rolname = current_user AND (rolsuper OR rolbypassrls)
UNION ALL
SELECT 'table without RLS', relname FROM tbl WHERE NOT relrowsecurity
UNION ALL
SELECT 'RLS enabled but not forced', relname FROM tbl WHERE relrowsecurity AND NOT relforcerowsecurity
UNION ALL
SELECT 'view without security_invoker', c.relname
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname = 'public' AND c.relkind = 'v'
    AND NOT EXISTS (SELECT 1 FROM unnest(coalesce(c.reloptions, '{}')) o
                    WHERE o ~* '^security_invoker=(true|t|yes|y|on|1)$')
UNION ALL
SELECT 'materialized view or foreign table (RLS does not apply)', c.relname
  FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
  WHERE n.nspname = 'public' AND c.relkind IN ('m', 'f')
UNION ALL
SELECT 'SECURITY DEFINER function', p.proname
  FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
  WHERE n.nspname = 'public' AND p.prosecdef
UNION ALL
SELECT 'permissive policy that allows every row', p.polname
  FROM pg_policy p
  WHERE p.polrelid IN (SELECT oid FROM tbl) AND p.polpermissive AND pg_get_expr(p.polqual, p.polrelid) = 'true'
ORDER BY 1, 2;
set -euo pipefail
out=$(psql "$DATABASE_URL" -X -A -t -v ON_ERROR_STOP=1 -f rls_audit.sql)   # un error de conexión o de SQL detiene el build
if [ -n "$out" ]; then echo "$out"; exit 1; fi

La ejecutamos contra un esquema construido para contener todos los problemas anteriores. Reportó todos (el rol, la tabla nueva sin RLS, la tabla no forzada, la vista simple, la función definer, la vista materializada, la tabla foránea y la política que permite todo), ignoró las vistas con security_invoker activado en cualquiera de sus grafías y no devolvió nada una vez corregido el esquema. No puede ver si el ajuste está vacío, ni si las políticas se combinan en algo más amplio de lo que pretendías (solo detecta una política que sea literalmente true). Para eso hacen falta pruebas que ejecuten solicitudes como un usuario y cuenten filas, como las del ejemplo TODO. Las tablas que legítimamente no guardan datos de usuarios, como las de búsqueda, aparecerán en "table without RLS"; agrega una lista de excepciones para ellas.

La versión corta

  • La aplicación se conecta como un rol con NOBYPASSRLS y sin superusuario, y la primera verificación de la auditoría no devuelve nada. En Relyt One ese nunca es relytdb_owner ni un usuario creado desde la consola.
  • Cada tabla con datos de usuarios tiene ENABLE y FORCE ROW LEVEL SECURITY, y el propietario se mantiene fuera de la ruta de las solicitudes.
  • Las vistas sobre esas tablas usan security_invoker = true; las funciones SECURITY DEFINER expuestas a usuarios se evitan o filtran de forma explícita.
  • Las políticas rechazan un ajuste vacío, y un CHECK (user_id <> '') las respalda. set_config solo se llama con true.
  • Una política RESTRICTIVE queda por debajo de las permisivas, y la auditoría corre en CI para que una tabla nueva no pueda salir sin RLS.
  • Las restricciones únicas sobre datos de usuarios incluyen la columna del tenant, y los roles de la aplicación no tienen acceso a vistas materializadas sobre datos de usuarios.

La seguridad a nivel de fila merece que la pruebes como cualquier otro código de control de acceso: inicia sesión como cada tipo de usuario, pide cada tipo de consulta y cuenta las filas. Si construyes sobre la autenticación gestionada, el artículo de la vista previa explica cómo llega el id del usuario a la base de datos, y el ejemplo TODO incluye las pruebas.

Crear un proyecto gratis

EtiquetasPostgresSeguridadrls

Escrito por

Zhan Lu

Tu próxima base de datos está a una rama de distancia.

Crea un proyecto gratis en menos de un minuto. Sin tarjeta de crédito ni configuración.

Volver al blog