Skip to content

Commit 03c5dbd

Browse files
committed
perf(rls): sale_write pasa de FOR ALL a por-operacion; la rejilla baja 1,0 s
Migracion F. Una tabla: sale. Ninguna funcion de RLS tocada. menu_item_channel_economics sigue en a3600331debb4402709f2f05e43ac173. Sin cambios de cliente: sin OTA. EL CAMBIO sale_write era FOR ALL, y en Postgres FOR ALL incluye SELECT. Al combinarse con OR con sale_read, cada fila leida evaluaba current_user_is_admin_of(account_id), que recibiendo una COLUMNA no puede resolverse una vez y se paga por fila. Ahora son tres politicas por operacion con la MISMA expresion. El WITH CHECK de sale_write era explicito, no heredado del USING, y se conserva en insert y update: si se hubiera perdido, el INSERT dejaria de validarse. RESULTADO, medido como authenticated con uid real usage_30d suelto, 2.632 filas 46,20 ms -> 4,53 ms brand_price_grid entera 3.530 ms -> 2.529 / 2.524 ms El plan explica el salto: desaparece el Filter y la condicion de RLS entra en el Index Cond junto a cuenta y fecha. El indice de la migracion E, que por si solo valia un 6 %, es lo que permite esto. Sigue por encima de 1,5 s. Se para aqui: la regla es volver a medir antes de volver a tocar. NEUTRALIDAD, DEMOSTRADA Y COMPROBADA current_user_is_admin_of(X) implica X = ANY(current_user_account_ids()) por sus dos ramas, asi que para el SELECT la politica de escritura concedia un subconjunto estricto de la de lectura. Los dos huecos de esa demostracion se cerraron con datos ANTES de aplicar: account_id es NOT NULL con 0 nulos, y hay 0 filas huerfanas. No hay clave ajena de sale.account_id a accounts, asi que lo segundo es cierto hoy pero no esta impuesto: queda anotado. Y comprobado sobre datos, no solo sobre logica: 12 usuarios reales x 3 cuentas con ventas, 0 casos en que la expresion de escritura concediera una fila que la de lectura no concediera. VERIFICADO ANTES Y DESPUES - Visibilidad: admin de Foodint 6.727, worker de Foodint 6.727. Identico. - Escritura del NO admin: INSERT rechazado por RLS, UPDATE 0 filas, DELETE 0 filas. Identico. Es la mitad que cambia de forma aunque no de efecto, y es la que mandaba revertir si fallaba. - Escritura del admin: INSERT 1, UPDATE 1, DELETE 1. Sigue funcionando. - Rejilla: 297 filas, huella completa 7a210315c250570fe29573e08d536f3c y huella de margenes c0249f056f9e5789d3934691efaf1e65 IDENTICAS, suma de recuentos 70956, 216 combinaciones permitidas. - sale con 4 politicas: sale_read(r), sale_insert(a), sale_update(w), sale_delete(d). - 7.636 ventas y 62 overrides intactos. Cola de pg_net vacia, 0 publicaciones. PARA EL PROXIMO ENCARGO, CON DATO El mismo patron FOR ALL + current_user_is_admin_of existe en mas tablas de las que estaban en la lista. Por tamano: sale_line (11 MB, no estaba en la lista), external_catalog_product (7,9 MB), channel_settlement_order (7,0 MB), menu_item (1,4 MB), menu_item_override (624 kB). Aviso: appcc_audit_items_write NO tiene la expresion mecanica sino una con joins a plantillas, asi que la demostracion del subconjunto hay que rehacerla ahi, no darla por buena. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Vy4yW6xBAtXcaSGgKybquB
1 parent a85c397 commit 03c5dbd

1 file changed

Lines changed: 81 additions & 0 deletions

File tree

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
-- ============================================================================
2+
-- ENCARGO CODE "Sacar la politica de escritura de sale del camino de lectura"
3+
-- 18/08/2026
4+
-- ============================================================================
5+
-- UNA tabla: sale. Ninguna funcion de RLS se toca. menu_item_channel_economics
6+
-- sigue en a3600331debb4402709f2f05e43ac173. Sin cambios de cliente: sin OTA.
7+
--
8+
-- EL PROBLEMA
9+
-- sale tiene dos politicas, las dos PERMISSIVE, las dos para authenticated:
10+
-- sale_read SELECT using account_id = ANY (current_user_account_ids())
11+
-- sale_write ALL using current_user_is_admin_of(account_id)
12+
-- check current_user_is_admin_of(account_id)
13+
-- En Postgres FOR ALL incluye SELECT, y las permisivas se combinan con OR. Asi
14+
-- que CADA fila leida evalua tambien current_user_is_admin_of(account_id).
15+
-- Recibiendo una COLUMNA no se puede resolver una vez: se paga por fila.
16+
--
17+
-- Medido como authenticated con uid real (no como superusuario):
18+
-- usage_30d suelto, 2.632 filas, sin RLS 4,05 ms
19+
-- usage_30d suelto, 2.632 filas, con RLS 46,20 ms x11
20+
-- la misma funcion con argumento CONSTANTE 0,557 ms / 2.632 evaluaciones
21+
-- Esa ultima linea es la prueba: constante -> se resuelve una vez; columna ->
22+
-- por fila. El 91 % del nodo es la politica, no el recuento.
23+
--
24+
-- POR QUE ES NEUTRO PARA LA LECTURA
25+
-- current_user_is_admin_of(X) = EXISTS(user_profiles: uid, account_id=X,
26+
-- role='admin', active)
27+
-- OR current_user_is_admin()
28+
-- current_user_account_ids() = si current_user_is_admin() -> todas las cuentas
29+
-- si no -> account_id de user_profiles(uid, active)
30+
-- Rama 1: si es admin de X, tiene fila activa con ese account_id, y
31+
-- current_user_account_ids() recoge sus account_id activos SIN filtrar
32+
-- por rol. X esta dentro.
33+
-- Rama 2: si es superadmin, current_user_account_ids() devuelve todas. X esta
34+
-- dentro siempre que X exista en accounts.
35+
-- No hay tercera rama, y las dos filtran active = true.
36+
-- => Para el SELECT, sale_write concede un SUBCONJUNTO ESTRICTO de sale_read.
37+
-- Sacarla del camino de lectura no puede cambiar lo que ve nadie.
38+
--
39+
-- LOS DOS HUECOS DE ESA DEMOSTRACION, COMPROBADOS ANTES DE APLICAR
40+
-- a) sale.account_id NULO: current_user_is_admin_of(NULL) es false por la
41+
-- rama 1 pero TRUE para un superadmin por la rama 2, mientras que
42+
-- account_id = ANY(...) con NULL nunca es true. Una fila asi la veria hoy
43+
-- un superadmin y dejaria de verla. VERIFICADO: account_id es NOT NULL y
44+
-- hay 0 filas nulas.
45+
-- b) sale.account_id HUERFANO (no existe en accounts): para un superadmin,
46+
-- current_user_account_ids() saca los ids DE accounts, asi que un huerfano
47+
-- quedaria fuera. VERIFICADO: 0 filas huerfanas.
48+
-- OJO: no hay clave ajena de sale.account_id a accounts, asi que esto es
49+
-- cierto HOY pero no esta impuesto. Anotado; si alguna vez aparece un
50+
-- huerfano dejaria de verse, que probablemente sea lo correcto, pero
51+
-- conviene que sea una decision y no una sorpresa.
52+
--
53+
-- Y ademas comprobado sobre datos, no solo sobre logica: para los 12 usuarios
54+
-- reales x las 3 cuentas con ventas, 0 casos en que la expresion de escritura
55+
-- concediera una fila que la de lectura no concediera.
56+
--
57+
-- LO QUE NO SE HACE
58+
-- Solo sale. Las mismas parejas *_read / *_write FOR ALL existen en menu_item,
59+
-- menu_item_override, sales_channel y channel_rate y la demostracion vale igual,
60+
-- pero son tablas pequenas: una tabla, una medicion, una conclusion.
61+
62+
begin;
63+
64+
-- El WITH CHECK de sale_write era EXPLICITO (no heredado del USING): se
65+
-- conserva tal cual en insert y update, o el INSERT dejaria de validarse.
66+
drop policy sale_write on public.sale;
67+
68+
create policy sale_insert on public.sale
69+
for insert to authenticated
70+
with check (current_user_is_admin_of(account_id));
71+
72+
create policy sale_update on public.sale
73+
for update to authenticated
74+
using (current_user_is_admin_of(account_id))
75+
with check (current_user_is_admin_of(account_id));
76+
77+
create policy sale_delete on public.sale
78+
for delete to authenticated
79+
using (current_user_is_admin_of(account_id));
80+
81+
commit;

0 commit comments

Comments
 (0)