RLS y auditoría¶
RLS: filtrado por fila¶
RLS aplica un predicado a la consulta, resolviendo por su cuenta el camino de relaciones hasta
el modelo objetivo. Es el mecanismo para que cada usuario vea solo lo suyo sin repetir el mismo
WHERE en cada consulta.
from database.public import RLS
regla = RLS(
target_model=Empresa, # el modelo sobre el que se filtra
target_column='id', # su columna
values=[1, 2], # los valores permitidos
operator='in', # cómo se comparan
)
posts = public_sync_api.post.find_many(rls=regla) # una regla
posts = public_sync_api.post.find_many(rls=[regla, otra]) # varias, en AND
Post no tiene por qué apuntar a Empresa directamente: si hay un camino de relaciones
(Post → Usuario → Empresa), tai-sql lo encuentra y lo aplica con los JOIN necesarios. Los
caminos se cachean, así que el coste de resolverlos se paga una vez.
Una regla sin camino hasta el modelo se ignora
Si no existe ninguna cadena de relaciones entre el modelo consultado y el target_model, la
regla no se aplica y la consulta devuelve todo. No es un error: es la diferencia entre
«esta regla no habla de esta tabla» y «esta tabla no debe verse». Si una entidad tiene que
quedar restringida, comprueba que hay relación hasta el modelo objetivo.
En cambio, si la regla sí aplica pero su predicado no se puede traducir a SQL, la consulta deniega: ignorarla habría devuelto filas de más.
Operadores¶
in (por defecto), eq, ne, gt, ge, lt, le, like, ilike.
Dónde se puede usar¶
En todas las lecturas y agregaciones: find, find_many, count, exists, agg, sum,
mean, max, min y las búsquedas vectoriales.
RLS no protege las escrituras
create, update, delete y sus variantes masivas no aceptan rls. Si necesitas
restringir quién puede escribir qué, eso va en tu capa de aplicación o en un
trigger que valide y aborte.
No es Row-Level Security de PostgreSQL
Pese al nombre, esto lo impone el cliente generado añadiendo condiciones a la consulta, no la base de datos. Quien tenga la URL de conexión puede saltárselo. Es una capa de conveniencia para tu aplicación, no un control de seguridad del motor.
Un ejemplo completo¶
def contexto_del_usuario(usuario) -> RLS:
return RLS(target_model=Empresa, target_column='id',
values=usuario.empresas_permitidas)
@app.get('/posts')
def listar(usuario = Depends(auth)):
return public_sync_api.post.find_many(
rls=contexto_del_usuario(usuario),
limit=50,
includes=['autor'],
)
Auditoría: quién escribe¶
Cuando el schema declara operative_fields=True, el cliente rellena created_by y updated_by
con el usuario del contexto:
from database.public import set_username, get_username, username_context
set_username('alice') # thread-safe y async-safe (ContextVar)
public_sync_api.usuario.create(...) # created_by = 'alice'
with username_context('system'): # temporal: al salir se restaura el anterior
public_sync_api.usuario.create(...) # created_by = 'system'
get_username() # 'alice'
Al ser un ContextVar, cada hilo y cada tarea asíncrona mantienen su propio valor. En una
aplicación web, set_username() en un middleware hace que cada request use el suyo:
@app.middleware('http')
async def con_usuario(request, call_next):
set_username(request.state.user.email)
return await call_next(request)
El valor por defecto, si nadie lo establece, es 'triplealpha'.