Autorización de Grano Grueso vs Grano Fino
El espectro de granularidad en el que se ubica toda decisión de autorización, desde gateways perimetrales hasta comprobaciones por relación específica — 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é las comprobaciones ingenuas por elemento crean un problema de autorización N+1, y un marco de decisión para elegir la granularidad correcta por funcionalidad en lugar de por defecto.
La pregunta que se saltaron los dos últimos artículos
Los dos artículos anteriores asumieron que ya sabías qué tipo de pregunta estabas haciendo. Políticas como código asumió que una regla plana sobre atributos era la forma correcta y cubrió cómo escribirla, probarla y desplegarla. Autorización de grano fino asumió que una pregunta de alcanzabilidad en grafo era la forma correcta y cubrió cómo modelarla y consultarla. Ninguno hizo la pregunta que en realidad va primero, en cada funcionalidad que construyes: ¿cuánto necesita saber esta decisión de acceso sobre el recurso específico involucrado, para este llamador específico, antes de poder confiar en ella?
Esa pregunta tiene una respuesta que va continuamente desde “nada en absoluto” hasta “todo sobre una relación específica entre este usuario exacto y este recurso exacto” — y dónde cae una decisión dada en ese rango es lo que este artículo llama granularidad. Equivocarte en una dirección hace que sobreconstruyas: un grafo de relaciones y una llamada Check para una decisión que siempre iba a ser “cualquier empleado autenticado, sí”. Equivocarte en la otra dirección hace que subconstruyas: un simple booleano is_admin protegiendo una funcionalidad que en realidad debía diferir por cliente, por documento, por equipo. Ambos errores son comunes, y ambos son evitables una vez que la granularidad se convierte en una elección deliberada en lugar de un valor por defecto.
Un espectro, no un binario
“Grano grueso” y “grano fino” suenan como dos casillas, pero los sistemas de producción toman decisiones de autorización en varias capas distintas, cada una respondiendo una pregunta con una forma diferente y un costo diferente:
Vale la pena nombrar explícitamente cuatro capas, porque cada una corresponde a un componente arquitectónico real que ya conociste o conocerás:
- Perímetro. ACLs de red, un API gateway comprobando una clave o la validez de un token portador, una regla de WAF. La pregunta es “¿este llamador puede siquiera hablar con este sistema?” — no sabe nada sobre recursos, roles o relaciones, y no debería saberlo; ese no es su trabajo.
- Módulo / rol. “¿Es este usuario administrador?”, “¿pueden los agentes de soporte ver el módulo de facturación?”, un feature flag que activa toda una capacidad. Esto es RBAC clásico — la decisión depende de quién es el llamador (su rol, su grupo) pero no de cuál recurso específico está pidiendo. Un matcher de Casbin comprobando
role == "admin"vive aquí, y también un claim booleano en un JWT. - Recurso. “¿Este usuario es dueño de esta factura específica?”, “¿el estado de este documento es ‘borrador’ y fue creado por este usuario?”. La decisión ahora depende de atributos de una instancia de recurso específica, evaluados contra el llamador — aquí es donde el PDP de XACML, OPA/Rego y Cedar demuestran su valor, cotejando el sujeto/recurso/acción/entorno de una solicitud contra la política.
- Relación. “¿Se compartió este documento con este usuario, directa o indirectamente a través de un grupo, a través de una carpeta, a cualquier profundidad?”. La decisión depende de que exista una relación o camino específico entre este sujeto exacto y este objeto exacto — la razón de existir completa de Zanzibar y OpenFGA.
Qué te da el grano grueso, y qué cuesta
Una comprobación de grano grueso — capa de perímetro o de módulo/rol — es rápida (una consulta de claim o una comparación de rol en memoria, sin búsqueda de datos externa), simple de modelar (una lista corta y enumerable de roles o flags) y simple de auditar (“quién tiene el rol de administrador” es una sola consulta). Gran parte de la superficie de autorización de una aplicación es legítimamente gruesa: herramientas internas usadas solo por empleados, paneles de administración, activación de funcionalidades, cualquier cosa donde la respuesta a “¿puede este usuario hacer X?” realmente sea la misma para cada instancia del recurso en cuestión.
El costo aparece cuando se le pide a una comprobación gruesa que haga un trabajo de grano fino. Considera una consola de soporte interna donde cualquier agente con el rol support-agent puede ver la cuenta de cualquier cliente. Eso es grueso por diseño, y está bien — hasta que el producto crece un requisito de que los agentes solo deberían ver cuentas asignadas a su equipo, o cuentas para las que tienen un ticket abierto. Ahora la comprobación de rol está equivocada, no es imprecisa: otorga un acceso que una política real dice que no debería existir, silenciosamente, porque la comprobación nunca preguntó “cuál cuenta”. Este es el riesgo central de la autorización de grano grueso: no falla ruidosamente cuando es demasiado amplia — simplemente sobre-permisiona en silencio, y la brecha es invisible hasta una auditoría, un incidente, o hasta que un cliente la nota.
Qué te da el grano fino, y qué cuesta
Una comprobación de grano fino — capa de recurso o de relación — cierra exactamente esa brecha: evalúa contra la instancia específica, así que “puede Sara ver la cuenta #4471” y “puede Sara ver la cuenta #9902” pueden tener respuestas distintas aunque el rol de Sara nunca haya cambiado. Esto es lo que el mínimo privilegio y el “need-to-know” realmente exigen en sistemas que manejan datos de terceros — un modelo de grano fino es lo que la mayoría de los regímenes de cumplimiento (HIPAA, SOC 2, el principio de minimización de datos de GDPR) terminan exigiendo una vez que lees más allá del requisito titular hasta el detalle del control de acceso.
El costo es real y tiene varias partes. Latencia: una comprobación a nivel de recurso normalmente implica obtener atributos (el PIP de XACML) o, para una comprobación de relación, recorrer un grafo — ambas cosas son más trabajo que comparar una cadena de rol ya presente en un JWT. Esfuerzo de modelado: alguien tiene que definir qué atributos importan (nivel de recurso) o qué relaciones y reglas de reescritura existen (nivel de relación), y mantener ese modelo correcto a medida que el producto evoluciona — esto no es un costo único, es mantenimiento continuo que escala con la complejidad de la funcionalidad. Superficie operativa: un PDP, un PIP o un almacén de relaciones es algo nuevo que desplegar, monitorear, mantener disponible y razonar sobre su consistencia — problemas que una comprobación de rol incrustada en un JWT simplemente no tiene.
Autorización por capas: combinar grueso y fino en una misma solicitud
Los sistemas de producción rara vez eligen una capa y se detienen ahí — componen varias, empezando por la más barata, para que la mayoría de las solicitudes inválidas sean rechazadas antes de llegar a la comprobación más costosa:
flowchart LR accTitle: Una ruta de autorización por capas que rechaza barato primero accDescr: El diagrama muestra una solicitud fluyendo de izquierda a derecha por cuatro etapas. Primero, el cliente envía una solicitud a un API gateway, que realiza una comprobación de perímetro verificando si el token o la API key es válida, rechazando de inmediato si no lo es. Segundo, la solicitud llega a un servicio, que realiza una comprobación de módulo o rol verificando si el rol del llamador permite este tipo de acción, rechazando si no. Tercero, solo las solicitudes que pasaron ambas comprobaciones baratas llegan a una comprobación de grano fino, ya sea una evaluación de atributos de recurso a través de un punto de decisión de política o una comprobación de relación a través de un motor de grafo, que es el paso más costoso. Cuarto, si todas las comprobaciones pasan, el servicio realiza la operación solicitada. Una nota debajo del diagrama indica que cada capa es más barata y rechaza una mayor proporción del tráfico inválido que la capa siguiente. Client([Solicitud 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]
Esta no es una idea nueva inventada para este artículo — es la misma lección de “la ubicación del PDP es un compromiso, no una respuesta fija” del artículo de XACML, aplicada ahora a qué comprobación se ejecuta, no solo a dónde. La comprobación de perímetro del gateway y la comprobación de rol del servicio existen específicamente para proteger a la capa fina costosa de una carga que no necesita soportar: rechazar una solicitud con un token inválido en el gateway cuesta una verificación de firma de token; rechazar al mismo actor malicioso en una llamada Check de grafo de relaciones costaría un recorrido de grafo sin ninguna razón. Aplicar capas de lo barato primero es un patrón de rendimiento con un efecto secundario de seguridad — la mayoría del tráfico inválido de un atacante o de un bug nunca llega a la parte del sistema lo suficientemente costosa como para que importe si es lenta.
El problema de autorización N+1
Las decisiones de granularidad no solo afectan solicitudes individuales — se agravan mal en código de renderizado de listas, en un patrón que debería resultarte familiar si alguna vez depuraste una página lenta causada por una consulta N+1 a base de datos:
sequenceDiagram
accTitle: El problema de autorización N+1 y su solución masiva
accDescr: El diagrama contrasta dos enfoques para renderizar una lista filtrada de cincuenta documentos. En el enfoque ingenuo, la aplicación obtiene los documentos candidatos una vez y luego recorre cada uno llamando a Check individualmente, generando cincuenta idas y vueltas de autorización separadas antes de poder renderizar la página, reflejando el clásico problema de consultas N más 1 en bases de datos. En el enfoque corregido, la aplicación obtiene los mismos candidatos una vez y llama a una única búsqueda masiva, ListObjects, que devuelve todos los IDs permitidos en una sola ida y vuelta en lugar de cincuenta.
Note over App: Ingenuo: N+1 llamadas
App->>DB: obtiene 50 documentos
loop cada documento
App->>AuthZ: Check(usuario, doc)
AuthZ-->>App: permite / deniega
end
Note over App: Corregido: una llamada
App->>DB: obtiene 50 documentos
App->>AuthZ: ListObjects(usuario, tipo)
AuthZ-->>App: IDs permitidosLa solución refleja exactamente la solución de base de datos: no preguntes “está permitido este uno” en un bucle, pregunta “cuáles de estos puedo ver” una sola vez. Las APIs ListObjects y ListUsers de OpenFGA (del artículo de autorización de grano fino) existen específicamente para esto; el equivalente de un motor de atributos de recurso normalmente es empujar el filtro a la propia consulta de base de datos — WHERE owner_id = :user_id OR :user_id IN (SELECT ...) — en lugar de obtener todo y filtrarlo fila por fila en código de aplicación contra una llamada al PDP por fila. De cualquier forma, el principio es el mismo: la granularidad es una propiedad de la pregunta, y “cuáles de N cosas” es una pregunta distinta, y más barata, que “está permitida esta una cosa”, aunque parezca N copias de la misma comprobación.
Un marco de decisión
Dada una funcionalidad, haz estas preguntas en orden, y detente en la primera que la resuelva:
| Pregunta | Si sí → | Si no → |
|---|---|---|
| ¿Todo llamador tiene por completo o carece por completo de esta capacidad, sin importar cuál instancia de recurso esté involucrada? | Módulo/rol. Un claim de rol, un feature flag, un matcher de Casbin solo sobre el rol. | Continúa |
| ¿La respuesta depende solo de atributos del propio recurso y del llamador (propiedad, estado, departamento, hora del día) — no de un camino a través de otros recursos? | Recurso. Política evaluada al estilo XACML/OPA/Cedar contra atributos de sujeto/recurso/entorno. | Continúa |
| ¿La respuesta depende de una relación que puede compartirse, anidarse o delegarse — a través de un grupo, una carpeta, un organigrama — a una profundidad que no puedes enumerar de antemano? | Relación. Alcanzabilidad en grafo al estilo Zanzibar/OpenFGA. | Reconsidera — la decisión podría no necesitar lógica de autorización en absoluto (por ejemplo, es una regla de negocio, no una regla de control de acceso) |
Ejemplo trabajado: granularidad en una misma app de cashback
Vuelve a la empresa de cashback de los artículos anteriores y mira tres funcionalidades una junto a la otra, porque el contraste es toda la lección:
“¿Puede este usuario abrir el panel de recompensas de la app en absoluto?” — depende solo de si la cuenta está activa, un hecho verdadero o falso para toda la cuenta, nada específico de un recurso. Grueso: un claim de rol/estado, comprobado en la capa de perímetro o módulo, sin necesidad de llamar al PDP.
“¿Puede este agente de soporte ver el historial de transacciones de un cliente específico?” — depende del rol del agente y de atributos de este cliente específico (¿hay un ticket abierto asignado a este agente para esta cuenta?, ¿es horario laboral?). Esto es nivel de recurso: un PDP con forma de XACML evaluando atributos de sujeto, recurso y entorno por solicitud, exactamente el patrón que recorrió el ejemplo de la fintech del artículo de XACML.
“¿Puede este usuario ver un informe de ahorros que un compañero de equipo compartió con su equipo?” — depende de una cadena: ¿es el usuario miembro del equipo?, ¿se compartió el informe con el equipo?, ¿a qué profundidad? Esto es nivel de relación: un Check de OpenFGA recorriendo relaciones de membresía y de compartición, exactamente el patrón que recorrió el ejemplo de Sara del artículo de autorización de grano fino.
Tres funcionalidades, tres capas, una sola aplicación — y cada una está correctamente modelada en la capa a la que pertenece. Forzar las tres hacia una sola herramienta (un grafo de relaciones para la puerta del panel, o una comprobación de rol para el informe compartido) desperdiciaría esfuerzo o submodelaría silenciosamente la regla de acceso.
Probar y operar un sistema por capas
Las pruebas tienen que dar cuenta de la estructura por capas, no solo de cada capa por separado: una solicitud debería probarse contra la combinación que realmente experimentará — ¿el gateway la rechaza antes de que se ejecute siquiera la comprobación de rol?, ¿la comprobación de rol la rechaza antes de la costosa comprobación de grano fino?, y por separado, ¿la comprobación de grano fino da la respuesta correcta para los casos que pasan ambas puertas anteriores? Un vacío común es probar a fondo la lógica de cada capa por separado sin nunca probar que una solicitud denegada en la capa dos jamás llegue a la capa tres — algo que importa tanto para la corrección como para el costo, ya que un bug que deja pasar solicitudes denegadas hacia la capa costosa erosiona exactamente el beneficio de rendimiento que la estructura por capas existe para proveer.
Operativamente, la métrica que vale la pena vigilar es en qué punto del flujo se están rechazando las solicitudes. Un sistema saludable rechaza la abrumadora mayoría del tráfico inválido en las capas de perímetro y módulo — rechazos baratos y rápidos — y solo un pequeño remanente llega a la capa de grano fino, que es a la vez la más costosa de ejecutar y la más importante de acertar. Si esa proporción se invierte — si la mayoría de los rechazos ocurren en la costosa capa de grafo de relaciones — es una señal de que las capas gruesas están mal configuradas (demasiado permisivas, dejando pasar tráfico que las comprobaciones baratas deberían haber atrapado ya) o de que una funcionalidad se volvió silenciosamente una que necesitaba un prefiltro grueso que nunca recibió.
Recapitulación
La granularidad es la pregunta que toda decisión de autorización responde antes de elegir ninguna herramienta:
- La autorización se ubica en un espectro, no en un binario — perímetro, módulo/rol, recurso y relación responden preguntas con formas distintas, y cada una tiene un componente arquitectónico real detrás (gateway, claim de RBAC, PDP de XACML/OPA/Cedar, Zanzibar/OpenFGA).
- El grano grueso es rápido y simple pero falla en silencio cuando es demasiado amplio — no da error, sobre-permisiona, y la brecha suele ser invisible hasta una auditoría o un incidente.
- El grano fino cierra esa brecha a un costo real — latencia, esfuerzo de modelado continuo y nueva superficie operativa — que vale la pena pagar precisamente cuando la decisión genuinamente depende del recurso o la relación específica, no por defecto.
- Las arquitecturas por capas combinan ambos, empezando por las comprobaciones más baratas, para que la costosa capa de grano fino solo vea el tráfico que realmente lo necesita.
- El problema de autorización N+1 es lo que ocurre cuando comprobaciones por elemento reemplazan una sola llamada masiva o de búsqueda inversa — la misma forma de solución que el clásico problema N+1 de bases de datos.
- Aplica la pregunta de granularidad por funcionalidad, no una sola vez para todo el sistema — la mayoría de las aplicaciones reales mezclan correctamente las cuatro capas.
Tres preguntas para ponerte a prueba
- Una funcionalidad empieza como “cualquier empleado autenticado puede ver la wiki interna” y luego gana un requisito de que algunas páginas solo deberían ser visibles para equipos específicos. Explica, usando el espectro de granularidad, en qué capa empezó la funcionalidad y a cuál capa necesita moverse — y por qué simplemente añadir más roles eventualmente fallaría.
- Explica el problema de autorización N+1 con tus propias palabras, y describe por qué un bucle de llamadas Check individuales no es solo más lento sino síntoma de hacerle al motor de decisión una pregunta con la forma equivocada.
- Un sistema actualmente solo comprueba la autorización en un PDP central de nivel de recurso para cada solicitud, incluidas aquellas que un API gateway podría rechazar solo por un token inválido. ¿Qué ahorra realmente añadir una comprobación de nivel de perímetro aguas arriba, dado que la comprobación de nivel de recurso eventualmente rechazaría la misma solicitud de todas formas?
Ejercicios prácticos
- Clasifica cinco decisiones reales. Para una aplicación que uses o estés construyendo, enumera cinco decisiones de autorización distintas que tome (una puerta de página, una comprobación de visibilidad de botón, un filtro de datos, una funcionalidad de compartición, una acción de administrador) y clasifica cada una en el espectro de granularidad usando las tres preguntas del marco de decisión. Anota cuáles están actualmente sobremodeladas o submodeladas respecto a tu clasificación.
- Diseña una ruta por capas para una funcionalidad. Elige una decisión de grano fino del ejercicio anterior y diseña la ruta completa de solicitud por capas para ella: qué rechaza la comprobación de perímetro, qué rechaza la comprobación de módulo/rol, y qué evalúa la comprobación de grano fino. Estima, aproximadamente, qué proporción de solicitudes inválidas debería atrapar cada capa.
- Encuentra (o crea) un bug de autorización N+1. En un código al que tengas acceso, busca una ruta de renderizado de lista que llame a una comprobación de autorización dentro de un bucle. Si encuentras una, esboza cómo la reemplazarías con una llamada masiva o de búsqueda inversa. Si no encuentras ninguna, escribe un pequeño ejemplo (pseudocódigo está bien) que demuestre el patrón y su solución.
Preguntas frecuentes
¿Cuál es la diferencia real entre autorización de grano grueso y de grano fino?
La autorización de grano grueso decide el acceso en un límite amplio que no depende de cuál recurso específico está involucrado — una comprobación de rol ('¿es este usuario un administrador?'), un feature flag, una puerta a nivel de módulo. La autorización de grano fino decide el acceso por recurso, por campo o por relación — '¿puede este usuario específico editar esta factura específica?', algo que depende de datos que el motor de decisión debe consultar, no solo de quién es el llamador. Las dos no son tecnologías distintas; son cantidades distintas de contexto que una decisión necesita antes de poder confiarse en ella.
¿La autorización de grano fino es siempre más segura, así que debería usarla siempre?
No — más granularidad no es gratis, y aplicarla donde no se necesita añade latencia, carga de modelado de datos y superficie operativa sin ningún beneficio de seguridad. Si una decisión realmente no depende de cuál recurso está involucrado (una puerta de panel de administración, un feature flag), una comprobación de rol grueso no es una versión más débil de una de grano fino, es el modelo correcto para esa decisión. La habilidad que enseña este artículo es hacer coincidir la granularidad con aquello de lo que la decisión realmente depende, no maximizar la granularidad en todas partes.
¿Cómo decido qué granularidad necesita una funcionalidad dada?
Haz una sola pregunta: ¿la respuesta correcta depende de cuál recurso, registro o relación específica está involucrada, para este llamador específico? Si la respuesta es la misma para cada instancia de ese tipo de recurso sin importar quién sea su dueño o con quién se haya compartido, es de grano grueso — una comprobación de rol o módulo. Si la respuesta cambia por instancia — por documento, por cuenta, por relación — necesita evaluación de grano fino, a nivel de recurso. Aplica esa pregunta por 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 ocurre cuando una ruta de código que renderiza una lista comprueba la autorización de cada elemento individualmente 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, exactamente el mismo antipatrón que el clásico problema de consultas N+1 en bases de datos. La solución tiene la misma forma que la solución de base de datos: usar una API masiva o de búsqueda inversa (ListObjects de OpenFGA, un Check por lotes, o un filtro a nivel de base de datos) que responda 'cuáles de estos puedo ver' en una sola llamada en lugar de una por elemento.
¿Pueden coexistir la autorización de grano grueso y de grano fino en la misma ruta de una solicitud?
Sí, y en los sistemas de producción normalmente lo hacen — esto se llama autorización por capas o defensa en profundidad. Una comprobación gruesa barata (¿este llamador siquiera pertenece a esta organización?, ¿su rol permite este tipo de acción?) se ejecuta primero y rechaza la mayoría de las solicitudes inválidas de inmediato; solo las solicitudes que pasan la puerta gruesa llegan a una comprobación de grano fino más costosa (¿existe esta relación específica para este recurso específico?). La capa gruesa protege a la capa fina de una carga que no necesita soportar, y la capa fina detecta lo que la capa gruesa estructuralmente no puede.
¿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 específico (los grafos de relaciones de Zanzibar/OpenFGA). Este artículo es la capa por encima de todos ellos: dada una funcionalidad, ¿cuál de esas herramientas — o qué combinación — encaja realmente, según cuánto dependa la decisión de acceso de datos específicos del recurso? Una comprobación de rol podría ser un matcher de Casbin o un claim de un JWT; una comprobación de grano fino podría ser una llamada Check de OpenFGA — la pregunta de granularidad va primero, y es la que determina a cuál herramienta de los artículos anteriores recurres.