Yago Marinho
Volver a los artículos
  • Auth
  • Arquitectura
  • Python
  • Functional Programming

Autorización que también filtra datos: los beneficios de un motor embebido

Llevé la decisión de autorización dentro de la aplicación y gané más que disponibilidad: las concesiones empezaron a decir qué datos ve cada persona.

Foto de Yago MarinhoPor: Yago MarinhoÚltima actualización realizada el:
💡
Este es el segundo de tres artículos sobre un motor de autorización que creé bajo un contrato privado. El primero cuenta el problema y la idea; este cuenta lo que el motor entregó. El código original y los detalles del cliente quedan fuera, y los ejemplos se escribieron desde cero, en un dominio inventado.
“¿Puede o no puede?” — esa es la pregunta que prácticamente todo sistema de autorización responde. Parece suficiente, y en la mayoría de los tutoriales lo es. Pero, cuando la aplicación empieza a manejar varias organizaciones, personas con roles distintos en cada una y pantallas que listan datos de más de una al mismo tiempo, un simple sí o no deja de responder lo que realmente importa.
En el artículo anterior, mostré cómo resolví la autorización de una aplicación sin poder tocar la API central de permisos: llevé las concesiones a la base de datos de la propia aplicación y la decisión dentro del proceso, con un motor que compara reglas declarativas. Ahora quiero hablar del otro lado de esa decisión — lo que entregó.
Y, para mí, el beneficio más importante no fue el más obvio.

Antes que nada: lo que se entregó

Para quien llega ahora, un resumen rápido de lo que se construyó:
  • Las concesiones viven en la base de datos de la aplicación: cada concesión es un rol en un alcance, como “administradora en la organización A”.
  • Cada ruta declara lo que exige: en una línea, con el privilegio mínimo, el recurso y la acción.
  • Un motor compara el requisito con las concesiones: campo por campo, según un esquema, y devuelve las concesiones que autorizaron — y no un simple verdadero o falso.
También vale recordar el contexto: yo era uno de los desarrolladores de la aplicación, recibí la tarea con la condición de no tocar la API central, y la solución fue mía. Todo lo que cuento aquí vale para esa aplicación y para el momento en que se entregó.
Vamos a los beneficios.

Beneficio 1: la aplicación dejó de depender de otro servicio para responder

El primer beneficio es el más directo, y fue justamente el motivo de la tarea. Antes, cada solicitud tenía que esperar la respuesta de otro servicio antes de hacer cualquier cosa. Ahora, decidir es una función que compara datos que ya están dentro de la aplicación.
En otras palabras: si la API central se pone lenta o se cae, la aplicación sigue respondiendo. Su disponibilidad deja de multiplicarse por la de otro servicio, y cada solicitud se ahorra un viaje de ida y vuelta por la red.
Vale destacar un cuidado: este beneficio viene del diseño, y no de una medición. No tengo números de producción para mostrar aquí — y prefiero no inventar ninguno. Lo que puedo afirmar es lo que la arquitectura garantiza por construcción: ninguna decisión de autorización de esa aplicación pasa por la red.

Beneficio 2: el requisito de cada ruta se volvió legible

Con las reglas esparcidas en ifs, saber quién puede hacer qué exige leer todo el código. Con el requisito declarado en la propia ruta, la respuesta está en una línea:
@requiere("usuario", "documento", "leer") async def listar_documentos(solicitud): ... @requiere("admin", "documento", "eliminar") async def eliminar_documento(solicitud): ...
¿Pero por qué importa? Porque, cuando la regla es un dato, se puede listar, revisar y auditar. Cualquier persona del equipo puede leer lo que exige cada ruta sin entender la implementación del motor, y un cambio de regla ocurre en un solo lugar, y no en todos los lugares donde se copió.

Beneficio 3: la autorización empezó a decir “sobre qué”

Este es, para mí, el beneficio más importante — y el menos obvio.
Piensa en una pantalla que lista documentos. La pregunta “¿esta persona puede leer documentos?” no basta, porque puede leer los documentos de algunas organizaciones y no de otras. En un motor que solo responde sí o no, la autorización dice “puede”, y el filtro de cuáles documentos mostrar queda escrito en otro lugar — normalmente en una consulta, a veces por otra persona, con otra interpretación de la misma regla.
Como el motor que construí devuelve las concesiones que autorizaron, la misma decisión ya entrega el filtro. Observa el ejemplo de abajo, con el motor del artículo anterior:
concesiones_de_ana = [ {"privilegio": "admin", "recurso": "*", "accion": "*", "organizacion": "org-a"}, {"privilegio": "usuario", "recurso": "documento", "accion": "leer", "organizacion": "org-b"}, {"privilegio": "usuario", "recurso": "reporte", "accion": "leer", "organizacion": "org-c"}, ] # el listado pide "leer documentos" sin fijar una organización ESQUEMA_DEL_LISTADO = { campo: comparador for campo, comparador in ESQUEMA.items() if campo != "organizacion" } requisito = {"privilegio": "usuario", "recurso": "documento", "accion": "leer"} autorizaron = autorizar(requisito, concesiones_de_ana, ESQUEMA_DEL_LISTADO) def organizaciones_visibles(concesiones): organizaciones = {concesion["organizacion"] for concesion in concesiones} # una concesión con comodín en la organización libera todas return None if "*" in organizaciones else organizaciones organizaciones_visibles(autorizaron) # {"org-a", "org-b"}
¿Qué está pasando aquí?
  • La concesión de org-a cubre la lectura de documentos, porque el comodín de recurso y de acción vale para todo en esa organización.
  • La concesión de org-b cubre exactamente lo que se pidió.
  • La concesión de org-c queda fuera: permite leer reportes, y no documentos.
La consulta recibe ese conjunto y devuelve solo los documentos de las organizaciones A y B. Es decir: la autorización y el filtro de datos ya no pueden divergir, porque los dos salen de la misma decisión.
El mismo razonamiento vale fuera de la API. En uno de los paneles de la aplicación, los reportes venían de una herramienta de BI embebida en la página. Herramientas como Power BI aceptan, al embeber el reporte, identidades para la seguridad a nivel de fila (row-level security) — el reporte solo muestra las filas que esas identidades pueden ver. Las concesiones que autorizaron se convertían en esas identidades, y cada persona veía en el reporte exactamente lo que la autorización permitía, sin una segunda regla escrita dentro del BI.

Beneficio 4: el mismo rol, en alcances distintos

Otro beneficio que apareció pronto fue poder dar roles distintos a la misma persona, cada uno en su alcance, sin crear tablas ni columnas nuevas para eso. Observa el caso de Bruno, administrador en la organización A y agente de soporte en la organización B:
concesiones_de_bruno = [ { "rol": "admin", "privilegio": "admin", "recurso": "*", "accion": "*", "organizacion": "org-a", }, { "rol": "agente_de_soporte", "privilegio": "usuario", "recurso": "documento", "accion": "leer", "organizacion": "org-b", }, ] eliminar_en_org_b = { "privilegio": "admin", "recurso": "documento", "accion": "eliminar", "organizacion": "org-b", } autorizar(eliminar_en_org_b, concesiones_de_bruno) # []
Ser administrador en la organización A no le presta nada en la organización B. Para el motor, cada concesión es independiente, y el aislamiento entre organizaciones es solo un campo más que se compara. Para quien administra los accesos, dar o quitar un rol en una organización es crear o borrar una fila — y nada más.

Beneficio 5: reglas nuevas sin tocar el motor

En el artículo anterior, mostré cómo una dimensión nueva entra como un campo y un comparador. Para mí, eso cambió la forma de mirar un requisito nuevo. Cuando aparecía una regla de contexto — un grupo dentro de la organización, por ejemplo —, la pregunta dejaba de ser “¿cuánto del código voy a tener que tocar?” y pasaba a ser “¿qué comparador necesita esta regla?”.
El motor queda cerrado a la modificación y abierto a la extensión. Es el principio abierto/cerrado aplicado donde más ayuda: en la parte del sistema que más cambia con el negocio.

Beneficio 6: pruebas rápidas, sin base de datos y sin red

Como el motor es una función pura, cada regla ganó una prueba que solo escribe datos y verifica el resultado. No hay base de datos que levantar ni servicio que simular. Además de rápidas, esas pruebas funcionan como documentación viva: para saber si un administrador de una organización alcanza otra, basta con buscar la prueba con ese nombre.

¿Y cuánto costó?

Claro que no es un enfoque que solo tenga ventajas. Aparecieron tres costos, y prefiero dejarlos claros:
  • Generalidad de más: el motor quedó más flexible de lo que la aplicación necesitaba en ese momento. Un esquema con campos y comparadores enchufables exige más cuidado para leer y mantener que media docena de ifs. Creo que vale la pena cuando las reglas crecen, pero es una inversión, y no un beneficio gratuito.
  • Una segunda fuente de verdad: la aplicación pasó a guardar sus propios usuarios y concesiones, mientras las demás seguían con la API central.
  • Los datos cerca exigen cuidado: o se leen las concesiones en cada solicitud, o van a una caché — y, en la caché, una revocación tiene que valer al instante.
Los dos últimos puntos merecen un artículo propio, y es exactamente el próximo.

Roadmap y próximos pasos

En la entrega, algunos pasos quedaron registrados como evolución natural del motor:
  • Caché de las concesiones, con la revocación borrando la caché de la persona al instante.
  • Validación de las reglas al arrancar la aplicación, para que un error de configuración aparezca antes de la primera solicitud, y no durante ella.
  • El tercer artículo de la serie, sobre los desafíos: el legado, la caché y los riesgos de un motor genérico.

Conclusión

Empecé este artículo con la pregunta “¿puede o no puede?”. Después de construir este motor, creo que está incompleta. La pregunta que una aplicación con varias organizaciones realmente necesita responder es “¿puede, sobre qué?” — y, cuando la autorización responde las dos al mismo tiempo, el filtro de datos deja de ser una regla paralela y pasa a ser consecuencia de la propia decisión.
Los otros beneficios — la independencia de la API central, las reglas legibles, las pruebas sin infraestructura — también importan, pero todos nacen de la misma elección: tratar la regla como un dato y la decisión como una función. Esa elección fue la que permitió entregar más de lo que pedía la tarea, sin tocar lo que no me correspondía cambiar.
Al final, la lección que me queda es esta:
una buena decisión de diseño no resuelve solo el problema que la motivó; abre espacio para resolver los siguientes.
Foto de Yago Marinho
Yago MarinhoIngeniero de Software Sénior. Construyo productos con IA, del diseño al despliegue.
Ver portafolio ↗