Autorización de Grano Grueso vs Grano Fino

El espectro de granularidad en el que cae toda decisión de autorización, desde los gateways perimetrales hasta las comprobaciones por relación: qué cuesta cada nivel en latencia, esfuerzo de modelado y radio de impacto, cómo las arquitecturas por capas combinan comprobaciones gruesas y finas, por qué comprobar elemento a elemento crea un problema de autorización N+1 y cómo elegir la granularidad adecuada para cada funcionalidad en lugar de por defecto.

Un rol y un error silencioso

La consola de soporte de la empresa de cashback era sencilla cuando la construyó Daniel. Cualquiera con el rol support-agent podía abrir la cuenta de cualquier cliente, porque los agentes tenían que poder ayudar a quien llamara. Un rol, una comprobación, nada que modelar. Dos años después la empresa firma con un gran socio comercial cuyos clientes solo puede atender un equipo dedicado, y una auditora hace una pregunta directa: ¿puede un agente de fuera de ese equipo abrir una de esas cuentas? La respuesta honesta es que sí. No se rompió nada y no apareció ningún error en los logs. La comprobación, sencillamente, nunca preguntó qué cuenta estaba mirando.


Los dos artículos anteriores daban por hecho que ya sabías qué tipo de pregunta estabas haciendo. Políticas como código suponía que la forma correcta era una regla plana sobre atributos y explicaba cómo escribirla, probarla y desplegarla. Autorización de grano fino suponía que la forma correcta era una pregunta sobre un grafo y explicaba cómo modelarla y consultarla. Ninguno se paró en la pregunta que va antes, en cada funcionalidad que construyes: ¿cuánto necesita saber esta decisión sobre el recurso concreto que está en juego, para este llamador concreto, antes de que puedas fiarte de ella?


La respuesta va desde “nada en absoluto” hasta “todo sobre una relación concreta entre este usuario y este recurso”, y el punto en el que cae una decisión es lo que en este artículo llamamos granularidad. Si te equivocas por un lado, construyes de más: un grafo de relaciones y una llamada Check para una decisión que siempre iba a ser “cualquier empleado con sesión iniciada, sí”. Si te equivocas por el otro, construyes de menos, que es lo que le pasó a la consola de Daniel: un solo rol protegiendo algo que tenía que ser distinto para cada cliente. Los dos errores son habituales, y los dos se evitan cuando la granularidad se elige a propósito en lugar de heredarse.


Un espectro, no dos cajas

“Grano grueso” y “grano fino” suenan a dos cajas, pero los sistemas reales toman decisiones de autorización en varias capas, y cada una responde a un tipo de pregunta distinto con un coste distinto.


El espectro de granularidad Cuatro capas de autorización, de más gruesa a más fina: perímetro, módulo/rol, recurso, relación — el costo sube y la decisión depende de datos más específicos a medida que aumenta la granularidad. El espectro de granularidad Dónde se ubica una decisión de autorización determina lo que cuesta ← más grueso más fino → costo: latencia, modelado de datos, superficie operativa bajo alto Perímetro (red y gateway) ejemplos: "¿esta IP está permitida?" "¿esta API key es válida?" Módulo y Rol (RBAC, feature flags) ejemplos: "¿es este usuario administrador?" "¿pueden los agentes de soporte ver el módulo de facturación?" Recurso (propiedad y atributos) ejemplo: "¿este usuario es dueño de esta factura?" herramientas: PDP de XACML, OPA, Cedar, Casbin Relación (alcanzabilidad en grafo) ejemplo: "¿se compartió este documento con este usuario a través de este grupo?" herramienta: Zanzibar/OpenFGA la decisión depende de: nada sobre el recurso específico una relación específica entre este sujeto y objeto exactos Misma solicitud, distinta granularidad — la forma de la pregunta determina la capa, no la herramienta.
El espectro de granularidad. Toda decisión de autorización cae en algún punto de esta barra, y ese punto decide tanto lo que puede expresar como lo que cuesta. Nada en el extremo fino es más correcto que en el grueso: una decisión que de verdad no depende del recurso concreto está bien modelada como gruesa, no le falta modelado.

El perímetro son las ACL de red, un API gateway que comprueba que una clave o un bearer token son válidos, una regla del WAF. Su pregunta es si este llamador puede hablar con el sistema siquiera. No sabe nada de recursos, roles ni relaciones, y no debería.


La capa de módulo o rol pregunta cosas como “¿es este usuario administrador?” o “¿pueden los agentes de soporte ver el módulo de facturación?”, y ahí viven también los feature flags. Es el RBAC de siempre: la decisión depende de quién llama, su rol o su grupo, pero no de qué recurso concreto pide. Un matcher de Casbin que comprueba role == "admin" vive aquí, igual que un claim booleano en un JWT.


La capa de recurso pregunta “¿es este usuario dueño de esta factura?” o “¿sigue este documento en borrador y lo creó este usuario?”. Ahora la decisión depende de atributos de una instancia concreta, comparados con quien llama. Es donde el PDP de XACML, OPA y Cedar demuestran lo que valen, contrastando sujeto, recurso, acción y entorno con la política.


La capa de relación pregunta “¿se compartió este documento con este usuario, directamente o a través de un grupo o una carpeta, a cualquier profundidad?”. La decisión depende de que exista una arista o un camino concreto entre este sujeto y este objeto, que es la razón de ser de Zanzibar y OpenFGA.


Es tentador leer esa lista como “el perímetro usa un gateway, el módulo usa RBAC, el recurso usa OPA y la relación usa OpenFGA”, como si la herramienta eligiera la capa. Funciona al revés. La forma de la pregunta te sitúa en una capa y la herramienta viene después. Podrías usar OpenFGA para responder a algo que nunca dependió de una relación, y funcionaría, pero sería muchísima más maquinaria que una comprobación de rol que ya daba la respuesta correcta.


Lo que te da el grano grueso y lo que cuesta

Una comprobación gruesa es rápida, porque es leer un claim o comparar un rol en memoria, sin datos externos que traer. Es fácil de modelar, porque la lista de roles o flags es corta y cabe en un papel. Y es fácil de auditar, porque “¿quién tiene el rol de administrador?” es una consulta. Buena parte de la autorización de cualquier aplicación es legítimamente gruesa: herramientas internas que solo usan empleados, paneles de administración, feature flags, cualquier caso en el que “¿puede este usuario hacer X?” tiene de verdad la misma respuesta para todas las instancias del recurso.


El coste aparece cuando a una comprobación gruesa se le pide un trabajo fino, que es la historia de Daniel. “Cualquier agente puede ver cualquier cuenta” era correcto cuando se escribió. En cuanto la política pasó a ser “los agentes solo ven las cuentas asignadas a su equipo”, la comprobación de rol dejó de ser imprecisa y pasó a estar mal: concede un acceso que la política real dice que no debería existir. Ese es el riesgo central del grano grueso. No falla de forma ruidosa cuando se queda demasiado amplio. Concede permisos de más en silencio, y nadie se entera hasta que lo descubre una auditoría, un incidente o un cliente.


Lo que te da el grano fino y lo que cuesta

Una comprobación fina, en la capa de recurso o de relación, cierra justo ese hueco. Evalúa la instancia concreta, así que “¿puede Sara ver la cuenta #4471?” y “¿puede Sara ver la cuenta #9902?” pueden tener respuestas distintas aunque el rol de Sara no haya cambiado. Eso es lo que significan de verdad el mínimo privilegio y la necesidad de saber en un sistema que guarda datos de otras personas, y es lo que acaban pidiendo casi todos los marcos de cumplimiento cuando lees más allá del titular. HIPAA, SOC 2 y el principio de minimización de datos del RGPD llegan ahí tarde o temprano.


El precio tiene tres partes. La primera es la latencia: una comprobación de recurso suele implicar traer atributos, a través del PIP de XACML o su equivalente, y una de relación implica recorrer un grafo. Las dos dan más trabajo que comparar un rol que ya está en un JWT. La segunda es el esfuerzo de modelado: alguien tiene que decidir qué atributos importan, o qué relaciones y reglas de reescritura existen, y mantener ese modelo correcto a medida que cambia el producto. No es un coste de una vez; crece con el producto. La tercera es la superficie operativa: un PDP, un PIP o un almacén de relaciones es una pieza más que desplegar, vigilar, mantener disponible y razonar en términos de coherencia, nada de lo cual necesita un claim de rol en un JWT.


Así que el grano fino es un coste que asumes por un motivo, no algo por defecto. Tratarlo como la opción más sofisticada y por tanto más correcta acaba en sistemas sobredimensionados. Un grafo de relaciones que decide qué empleados pueden leer qué páginas de la wiki interna, cuando la regla real es “cualquier empleado”, es sobrecarga pura sin ninguna ganancia de seguridad.


Autorización por capas

Los sistemas en producción rara vez eligen una capa y se quedan ahí. Apilan varias, de la más barata a la más cara, para que la mayoría de las peticiones inválidas se rechacen antes de llegar a la comprobación costosa.


flowchart LR
  accTitle: Una ruta de autorización por capas que rechaza barato primero
  accDescr: El diagrama muestra una petición que avanza de izquierda a derecha por cuatro etapas. Primero, el cliente envía una petición a un API gateway, que hace una comprobación de perímetro para verificar que el token o la API key son válidos y la rechaza al momento si no lo son. Segundo, la petición llega a un servicio, que hace una comprobación de módulo o rol para verificar que el rol de quien llama permite este tipo de acción, y la rechaza si no. Tercero, solo las peticiones que superaron las dos comprobaciones baratas llegan a una comprobación de grano fino, ya sea una evaluación de atributos del recurso en un punto de decisión de políticas o una comprobación de relación en un motor de grafos, que es el paso más caro. Cuarto, si todo se supera, el servicio realiza la operación. Cada capa es más barata y rechaza una parte mayor del tráfico inválido que la siguiente.
  Client([Petición del cliente]) --> Gateway[API Gateway<br/>perímetro: ¿token válido?]
  Gateway -- rechazada --> Deny1[["Deniega (401/403)"]]
  Gateway -- válida --> Service[Servicio<br/>módulo: ¿el rol permite esta acción?]
  Service -- rechazada --> Deny2[["Deniega (403)"]]
  Service -- permitida --> Fine[Comprobación de grano fino<br/>atributos de recurso o grafo de relaciones]
  Fine -- denegada --> Deny3[["Deniega (403)"]]
  Fine -- permitida --> Op[Realiza la operación]
Una ruta de autorización por capas. El gateway rechaza al momento las peticiones sin autenticar o mal formadas (perímetro). El servicio compara el rol de quien llama con el mínimo que exige la acción (módulo) y rechaza por poco coste la mayoría de las peticiones inválidas restantes. Solo las que superan las dos capas baratas llegan a la comprobación de grano fino, una evaluación de atributos del recurso o un Check de relación, que es el paso más caro y se deja a propósito para el final.

Es la misma lección que daba el artículo de XACML sobre dónde poner el PDP, un equilibrio y no una respuesta fija, aplicada esta vez a qué comprobación se ejecuta y no a dónde. La comprobación del gateway y la de rol del servicio existen en buena parte para ahorrarle a la capa cara una carga que no necesita. Rechazar en el gateway una petición con un token malo cuesta verificar una firma. Rechazar esa misma petición en un Check de relaciones costaría recorrer un grafo para nada. Apilar de barato a caro es un patrón de rendimiento con un efecto secundario de seguridad: la mayor parte del tráfico inválido que genera un atacante o un bug nunca llega a la parte del sistema donde la lentitud haría daño.


El problema de autorización N+1

Las decisiones de granularidad no afectan solo a peticiones sueltas. Se acumulan mal en el código que pinta listas, con un patrón que reconocerá cualquiera que haya perseguido una página lenta por una consulta N+1 a base de datos.


sequenceDiagram
  accTitle: El problema de autorización N+1 y su solución con una API masiva
  accDescr: El diagrama compara dos formas de pintar una lista filtrada de cincuenta documentos. En la ingenua, la aplicación trae los documentos candidatos una vez y luego los recorre llamando a Check por separado para cada uno, lo que produce cincuenta idas y vueltas de autorización antes de poder pintar la página, igual que el clásico problema de consultas N más 1 en bases de datos. En la corregida, la aplicación trae los mismos candidatos una vez y hace una única búsqueda masiva, ListObjects, que devuelve todos los IDs permitidos en una sola ida y vuelta en lugar de cincuenta.
  App->>App: ingenuo: N+1 llamadas
  App->>DB: trae 50 documentos
  loop cada documento
    App->>AuthZ: Check(usuario, doc)
    AuthZ-->>App: permite / deniega
  end
  App->>App: corregido: una llamada
  App->>DB: trae 50 documentos
  App->>AuthZ: ListObjects(usuario, tipo)
  AuthZ-->>App: IDs permitidos
El problema de autorización N+1. Pintar una lista de 50 documentos llamando a Check una vez por documento convierte una carga de página en 50 idas y vueltas de autorización. Una API masiva o de búsqueda inversa responde a la misma pregunta, cuáles de estos puede ver este usuario, en una sola llamada.

El arreglo es el mismo que en la base de datos. No preguntes “¿está permitido este?” dentro de un bucle; pregunta una vez “¿cuáles de estos puedo ver?”. ListObjects y ListUsers de OpenFGA existen exactamente para esto. Con un motor de atributos de recurso, lo equivalente suele ser llevar el filtro a la propia consulta, algo como WHERE owner_id = :user_id OR :user_id IN (SELECT ...), en lugar de traerlo todo y llamar al PDP fila a fila desde el código de la aplicación. El bucle no solo es lento. Es una señal de que le estás haciendo al motor la pregunta equivocada: “¿cuáles de N cosas?” es una pregunta distinta y más barata que “¿esta cosa?”, aunque parezca N copias de la misma comprobación.


Elegir capa para cada funcionalidad

Ante una funcionalidad, suelen bastar tres preguntas en orden, y te detienes en la primera que lo resuelve.


PreguntaSi la respuesta es síSi es no
¿Cada llamador tiene o no tiene esta capacidad por completo, sea cual sea la instancia del recurso?Módulo o rol. Un claim de rol, un feature flag, un matcher de Casbin solo sobre el rol.Siguiente pregunta
¿La respuesta depende solo de atributos del recurso y de quien llama (propiedad, estado, departamento, hora del día), no de un camino a través de otros recursos?Recurso. Una política al estilo de XACML, OPA o Cedar sobre atributos de sujeto, recurso y entorno.Siguiente pregunta
¿La respuesta depende de una relación que puede compartirse, anidarse o delegarse, a través de un grupo, una carpeta o un organigrama, a una profundidad que no puedes enumerar de antemano?Relación. Alcanzabilidad en un grafo al estilo de Zanzibar u OpenFGA.Replantéatelo: quizá sea una regla de negocio y no de control de acceso

El error habitual es responder a esto una vez, en la fase de diseño, y elegir una sola herramienta para toda la aplicación. Los sistemas reales son a la vez por capas y mixtos: la entrada al panel de administración es una comprobación de rol, editar una factura es una comprobación de recurso sobre propiedad y estado, y “compartir este informe con mi equipo” es una comprobación de relación, todo en el mismo producto. Vuelve a hacerte las preguntas cada vez que cambien los requisitos de una funcionalidad. Que “los agentes pueden ver cualquier cuenta” pase a ser “los agentes pueden ver las cuentas de su equipo” es justo el salto de módulo a recurso que se le escapó a la consola de Daniel.


Tres funcionalidades de la app de cashback

Mira tres funcionalidades de la misma app de cashback una al lado de otra, porque el contraste es la lección.


Que un usuario pueda abrir el panel de recompensas depende solo de si la cuenta está activa, algo que es verdad o mentira para toda la cuenta. Es grano grueso: un claim de estado comprobado en el perímetro o en la capa de módulo, sin llamar a ningún PDP.


Que un agente de soporte pueda ver el historial de transacciones de un cliente concreto depende del rol del agente y de atributos de ese cliente: si hay un ticket abierto para esa cuenta asignado a ese agente, si es horario laboral, si la cuenta pertenece al equipo dedicado del socio comercial. Es la capa de recurso, un PDP con la forma del de XACML que evalúa sujeto, recurso y entorno en cada petición. También es donde debería haber acabado la consola de Daniel.


Que un usuario pueda ver un informe de ahorro que un compañero compartió con su equipo depende de una cadena: si el usuario está en el equipo, si el informe se compartió con el equipo, a través de cuántas carpetas. Es la capa de relación, un Check de OpenFGA que recorre tuplas de pertenencia y de compartición, igual que el resumen directivo de Sara en el artículo anterior.


Tres funcionalidades, tres capas, una aplicación, y cada una está bien modelada en su capa. Meter las tres en una sola herramienta o desperdiciaría esfuerzo, con un grafo de relaciones protegiendo el panel, o se quedaría corto en silencio, con una comprobación de rol protegiendo el informe compartido.


Probar y operar un sistema por capas

Las pruebas tienen que cubrir el apilamiento, no solo cada capa por separado. Una petición debería probarse contra la combinación que de verdad va a atravesar: ¿la rechaza el gateway antes de que corra la comprobación de rol?, ¿la rechaza la comprobación de rol antes de la llamada cara?, y, aparte, ¿la comprobación fina da la respuesta correcta en los casos que superan las dos barreras? Un hueco frecuente es probar con cuidado la lógica de cada capa y no comprobar nunca que una petición denegada en la segunda capa no llega a la tercera. Eso importa para la corrección y para el coste, porque un bug que deja pasar peticiones denegadas a la capa cara se come en silencio la ventaja de rendimiento para la que existían las capas.


En producción, el dato que conviene vigilar es dónde se están rechazando las peticiones. En un sistema sano, la inmensa mayoría del tráfico inválido se rechaza en el perímetro y en la capa de módulo, a bajo coste, y solo un resto pequeño llega a la capa fina, que es la más cara de ejecutar y la más importante de acertar. Si esa proporción se invierte y la mayoría de los rechazos ocurren en el grafo de relaciones, o las capas gruesas están mal configuradas y dejan pasar tráfico que deberían haber parado, o una funcionalidad se ha convertido sin que nadie lo notara en una que necesitaba un filtro grueso previo que nunca tuvo.


Resumen

La granularidad es una pregunta que cada decisión de autorización responde antes de elegir ninguna herramienta. Las decisiones caen en un espectro, perímetro, módulo o rol, recurso y relación, y cada capa tiene una pieza real detrás: un gateway, un claim de RBAC, un PDP de XACML, OPA o Cedar, un grafo al estilo de Zanzibar. Las comprobaciones gruesas son rápidas y sencillas, pero cuando se quedan demasiado amplias no fallan, conceden de más en silencio, que es lo que le pasó a la consola de soporte de Daniel. Las finas cierran ese hueco a un precio real en latencia, trabajo de modelado continuo y superficie operativa, que merece la pena pagar cuando la decisión depende de verdad del recurso o la relación concretos. Los sistemas por capas combinan ambas, de barato a caro, para que la capa cara solo vea el tráfico que la necesita. Comprobar elemento a elemento dentro de un bucle es el problema de autorización N+1, y se arregla igual que su versión de base de datos, con una llamada masiva o de búsqueda inversa. Y la pregunta se hace para cada funcionalidad, no una vez para todo el sistema, porque la mayoría de las aplicaciones reales usan con razón las cuatro capas.



Tres preguntas para ponerte a prueba

  1. Una funcionalidad empieza como “cualquier empleado con sesión iniciada puede ver la wiki interna” y más tarde aparece el requisito de que algunas páginas solo las vean ciertos equipos. Con el espectro de granularidad, explica en qué capa empezó la funcionalidad, a cuál tiene que pasar y por qué añadir más roles acabaría dejando de funcionar.
  2. Explica con tus palabras el problema de autorización N+1 y por qué un bucle de llamadas Check sueltas no solo es más lento, sino una señal de que le estás haciendo al motor la pregunta equivocada.
  3. Un sistema comprueba la autorización solo en un PDP central de la capa de recurso para todas las peticiones, incluidas las que un API gateway podría rechazar solo por llevar un token inválido. ¿Qué ahorra de verdad añadir antes una comprobación de perímetro, si el PDP acabaría rechazando la misma petición de todas formas?

Ejercicios prácticos

  1. Clasifica cinco decisiones reales. Para una aplicación que uses o estés construyendo, apunta cinco decisiones de autorización distintas que tome (la entrada a una página, la visibilidad de un botón, un filtro de datos, una función de compartir, una acción de administración) y sitúa cada una en el espectro con las tres preguntas de arriba. Señala cuáles están ahora modeladas de más o de menos respecto a tu clasificación.
  2. Diseña una ruta por capas para una funcionalidad. Elige una decisión de grano fino del primer ejercicio y diseña su ruta completa: qué rechaza la comprobación de perímetro, qué rechaza la de rol y qué evalúa la fina. Calcula a ojo qué parte de las peticiones inválidas debería parar cada capa.
  3. Encuentra (o crea) un bug de autorización N+1. En un código al que tengas acceso, busca una ruta que pinte una lista y llame a una comprobación de autorización dentro de un bucle. Si la encuentras, esboza cómo la sustituirías por una llamada masiva o de búsqueda inversa. Si no, escribe un ejemplo pequeño, vale con pseudocódigo, que muestre el patrón y su arreglo.

Preguntas frecuentes

¿Cuál es la diferencia real entre la autorización de grano grueso y la de grano fino?

La autorización de grano grueso decide el acceso en una frontera amplia que no depende de qué recurso concreto está en juego: una comprobación de rol ('¿es este usuario administrador?'), un feature flag, una barrera a nivel de módulo. La de grano fino decide por recurso, por campo o por relación, como '¿puede este usuario editar esta factura en concreto?', y eso depende de datos que el motor de decisión tiene que consultar, no solo de quién llama. No son tecnologías distintas; son cantidades distintas de contexto que una decisión necesita antes de que puedas fiarte de ella.

¿La autorización de grano fino es siempre más segura y debería usarla siempre?

No. Más granularidad no sale gratis, y aplicarla donde no hace falta añade latencia, trabajo de modelado de datos y superficie operativa sin ningún beneficio de seguridad. Si una decisión de verdad no depende de qué recurso está en juego (la entrada a un panel de administración, un feature flag), una comprobación de rol no es una versión más débil de una de grano fino; es el modelo correcto para esa decisión. La habilidad está en ajustar la granularidad a aquello de lo que depende la decisión, no en maximizarla en todas partes.

¿Cómo decido qué granularidad necesita una funcionalidad?

Hazte una pregunta: ¿la respuesta correcta depende de qué recurso, registro o relación concretos están en juego, para este llamador concreto? Si la respuesta es la misma para todas las instancias de ese tipo de recurso, sea quien sea su dueño o con quien se haya compartido, es de grano grueso, una comprobación de rol o de módulo. Si cambia de una instancia a otra, por documento, por cuenta, por relación, necesita una evaluación de grano fino a nivel de recurso. Hazla para cada funcionalidad, no una sola vez para todo el sistema; la mayoría de los sistemas reales son una mezcla.

¿Qué es el problema de autorización N+1?

Es lo que pasa cuando el código que pinta una lista comprueba la autorización de cada elemento por separado dentro de un bucle: listar 50 documentos y luego llamar a Check() 50 veces para filtrarlos, convirtiendo una sola acción del usuario en decenas de idas y vueltas de autorización. Es el mismo antipatrón que la clásica consulta N+1 a base de datos, y se arregla igual: con una API masiva o de búsqueda inversa (ListObjects de OpenFGA, un Check por lotes o un único filtro en la base de datos) que responda 'cuáles de estos puedo ver' en una llamada y no en una por elemento.

¿Pueden convivir la autorización de grano grueso y la de grano fino en la misma petición?

Sí, y en los sistemas en producción lo normal es que convivan. Se llama autorización por capas o defensa en profundidad. Una comprobación gruesa y barata (¿pertenece siquiera este llamador a esta organización?, ¿su rol permite este tipo de acción?) va primero y rechaza al momento la mayoría de las peticiones inválidas; solo las que la superan llegan a una comprobación de grano fino más cara (¿existe esta relación concreta para este recurso concreto?). La capa gruesa le ahorra a la fina una carga que no tiene por qué soportar, y la fina detecta lo que la gruesa no puede ver por su propia estructura.

¿Cómo se relaciona esto con XACML, OPA/Cedar/Casbin y Zanzibar de los artículos anteriores?

Esos artículos te dieron los componentes (PEP, PDP, PAP), los lenguajes (Rego, Cedar, Casbin) y un modelo de grano fino concreto (los grafos de relaciones de Zanzibar y OpenFGA). Este artículo está por encima de todos ellos: dada una funcionalidad, ¿qué herramienta, o qué combinación, encaja de verdad según lo mucho que dependa la decisión de datos del recurso concreto? Una comprobación de rol puede ser un matcher de Casbin o un claim de un JWT; una de grano fino puede ser una llamada Check a OpenFGA. La pregunta de la granularidad va primero y decide a qué herramienta recurres.