1Por qué importa
Cuando una API expone GET /pedidos/{id} y no comprueba que el pedido pertenece a
quien pregunta, no hay ningún exploit sofisticado: el atacante cambia el número y lee lo
ajeno. Eso es un fallo de autorización, no de autenticación. El usuario estaba perfectamente
logueado; el sistema simplemente no decidió si tenía derecho a ese objeto. Año tras año, los
fallos de control de acceso encabezan las listas de vulnerabilidades reales, y casi siempre
por la misma razón: la decisión de acceso estaba dispersa, implícita o directamente ausente.
Diseñar la autorización bien hace tres cosas prácticas. Primero, convierte una intuición
(“esto solo lo pueden ver los administradores”) en una regla que se puede leer, revisar y
probar. Segundo, concentra la decisión en un sitio, de modo que añadir un endpoint no
signifique reinventar quién puede llamarlo. Tercero, hace que el acceso sea auditable: si un
día alguien pregunta “¿por qué este usuario pudo hacer esto?”, hay una respuesta escrita, no
una arqueología de if repartidos por el código.
No estás memorizando siglas. Estás aprendiendo a escribir una función que, dada una identidad, una acción y un recurso, devuelva permitido o denegado con un motivo, y a probar esa función con casos concretos. Ese es el entregable de la lección.
2Conceptos
- Autenticación: demostrar quién eres. Produce una identidad de confianza.
- Autorización: decidir qué puede hacer esa identidad. Produce permitido o denegado.
- Sujeto · acción · recurso: las tres piezas de toda decisión de acceso.
- RBAC: permisos agrupados en roles; el sujeto tiene roles.
- Explosión de roles: cuando cada matiz de permiso obliga a inventar un rol nuevo.
- ABAC: la decisión compara atributos del sujeto, del recurso y del entorno.
- PDP / PEP: el punto que decide (Policy Decision Point) y el que aplica (Policy Enforcement Point).
- Default-deny: si ninguna regla concede, se niega. El silencio es un no.
3Explicación profunda
RBAC parte de una observación cierta: los permisos se repiten por puesto de trabajo.
Un contable hace lo mismo que otro contable, así que en vez de asignar permisos a personas
se asignan a un rol contable y se mete a la persona en él. Es simple, se audita
de un vistazo (“¿quién es admin?”) y encaja con cómo piensan las organizaciones. Por eso es
el modelo por defecto y casi siempre el punto de partida correcto.
RBAC se rompe cuando el permiso depende de datos, no solo de puesto. “Un
vendedor puede ver pedidos” es un rol; “un vendedor puede ver sus pedidos, los de
su región, del trimestre actual” ya no lo es. Si intentas expresarlo con roles,
creas vendedor-norte-q1, vendedor-sur-q2 y así hasta el infinito:
es la explosión de roles. El síntoma es un sistema con cientos de roles que nadie se
atreve a tocar porque nadie sabe ya qué concede cada uno. Un catálogo de roles que no se
puede auditar es control de acceso roto con buena presentación.
ABAC ataca justo ese caso. En vez de preguntar “¿qué rol tienes?”, la política compara
atributos: sujeto.region == recurso.region, recurso.estado == "abierto",
hora ∈ horario_laboral. La decisión se evalúa por petición, con los datos
de esa petición, así que expresa relaciones que RBAC no puede. El precio es que una política
de atributos mal escrita es más difícil de leer que una lista de roles, y que necesitas que
esos atributos vengan de una fuente de confianza —normalmente los claims de un
token verificado, que es de lo que van las tres lecciones siguientes.
En la práctica no se elige uno: se combinan. RBAC pone el trazo grueso (“solo el rol admin escribe”) y ABAC pone el fino (“y además solo en recursos de su departamento”). La política del laboratorio de esta lección hace exactamente eso, y el orden importa: primero la regla de rol, que es barata y descarta la mayoría, y luego la de atributo.
Hay una separación de la que depende que todo esto sea mantenible: la del PDP y el
PEP. El PDP es el sitio que decide —una función autorizar(sujeto,
accion, recurso)—, y el PEP es cada punto del código que aplica esa decisión
antes de tocar el dato. Si la decisión vive en un solo PDP, añadir un endpoint es llamar a
una función; si vive esparcida en if por los controladores, cada endpoint es una
oportunidad nueva de olvidarse. Y sobre todo: default-deny. La función arranca negando
y solo concede si una regla lo dice explícitamente. Un fallo así falla cerrado, no abierto.
4Ejemplo sencillo
# Un edificio con recepcion AUTENTICAR la recepcion confirma que eres quien dices (DNI) AUTORIZAR tu tarjeta abre SOLO las puertas que te tocan # RBAC: la tarjeta lleva un rol impreso rol "limpieza" abre todas las plantas, de noche rol "finanzas" abre solo la planta 3 Problema: "finanzas, pero solo su propio despacho" -> ¿un rol por despacho? # ABAC: la puerta compara atributos al vuelo regla: puerta.planta == tarjeta.planta Y hora en horario Una sola regla cubre a todos los despachos, sin inventar roles.
5Ejemplo técnico
El laboratorio lab-iam-oidc trae una política real que combina los dos modelos.
Tres usuarios, dos recursos de departamentos distintos, y una función que decide:
| Sujeto | Acción | Recurso | Decisión y motivo |
|---|---|---|---|
| alice · admin · ventas | escribir | informe-compras | Permitido: admin cruza departamentos. |
| bob · lector · ventas | leer | informe-ventas | Permitido: rol lector y departamento coincide. |
| bob · lector · ventas | escribir | informe-ventas | Denegado. RBAC: escribir exige rol admin. |
| bob · lector · ventas | leer | informe-compras | Denegado. ABAC: dep del sujeto ≠ dep del recurso. |
| carol · lector · compras | leer | informe-compras | Permitido: lector y departamento coincide. |
Fíjate en los dos “denegado” de bob: uno lo para RBAC (le falta el rol para escribir) y el otro lo para ABAC (su departamento no es el del recurso). Un mismo usuario, dos motivos distintos. Devolver el motivo no es un lujo: es lo que hace la política depurable y auditable.
6Diagrama
Ver el mismo diagrama como código Mermaid (editable)
flowchart LR
P[peticion: sujeto+accion+recurso] --> PEP[PEP · aplica]
PEP --> PDP[PDP · autorizar]
RBAC[RBAC: roles] --> PDP
ABAC[ABAC: atributos] --> PDP
PDP --> OK[permitido]
PDP --> NO[denegado + motivo]
7Código
Esta es la política del laboratorio, escrita como una función pura: entra sujeto, acción y recurso; sale una decisión y su motivo. Es el PDP entero, y cabe en la pantalla:
# cliente.py — el PDP del laboratorio def autorizar(claims, accion, recurso): roles = set(claims.get("roles", [])) dep = claims.get("dep") meta = RECURSOS.get(recurso) if meta is None: return (False, "recurso desconocido") # default-deny # RBAC: primero la regla barata de rol if accion == "escribir" and "admin" not in roles: return (False, "RBAC: escribir exige rol admin") if accion == "leer" and not roles & {"admin", "lector"}: return (False, "RBAC: leer exige rol lector o admin") # ABAC: el atributo del sujeto debe casar con el del recurso if dep != meta["dep"] and "admin" not in roles: return (False, f"ABAC: dep '{dep}' no coincide") return (True, "permitido")
Lo que la hace correcta no es que sea corta: es que arranca negando, que los atributos
(roles, dep) vienen de un token verificado y no de la petición, y que
devuelve el motivo. Como es una función pura, probarla es trivial, y ese es el punto entero.
8Qué puede salir mal
Autenticar y darlo por autorizado. El error número uno: comprobar que hay sesión y servir el objeto sin comprobar que ese objeto es de ese usuario. Es exactamente el IDOR que viste en la fase de web. Autenticar no autoriza.
Explosión de roles. Modelar cada matiz como un rol nuevo lleva a un catálogo
inmanejable. Cuando veas vendedor-norte-lectura-q1, el modelo ya se rompió:
eso era un atributo disfrazado de rol.
Allow-by-default. Una política que concede salvo que una regla lo prohíba falla abierto: cualquier caso que olvidaste queda permitido. La forma segura es al revés.
Decidir con datos de la petición. Si el atributo que decide (dep, el
id de tenant) viene de un campo que el cliente controla y no de un token verificado, el
atacante se lo pone él. Los atributos de decisión salen de una fuente de confianza.
9Cómo lo abusaría un atacante
Alguien que ataca la autorización no busca romper nada: busca la petición que el servidor
atiende sin comprobar. Prueba a cambiar el identificador de un recurso por el de otro
usuario. Prueba a mandar el atributo de decisión él mismo —un ?dep=compras,
un campo role en el cuerpo— por si el backend se fía. Y prueba las acciones
que nadie testeó: el DELETE del que solo existía el botón para admins, por si
el botón era la única defensa.
El terreno más fértil es la explosión de roles. En un sistema con trescientos roles, siempre hay uno mal definido que concede de más, y nadie lo revisa porque revisarlos es imposible. El atacante no necesita un rol de admin: le basta un rol legítimo que, por acumulación histórica, arrastra un permiso que ya nadie recuerda haber concedido.
Y explota la dispersión. Si la decisión de acceso está repetida en veinte controladores, con que uno se saltara la comprobación ya hay una puerta. Por eso centralizar el PDP no es estética: reduce a uno el número de sitios donde te puedes equivocar.
10Cómo defenderlo
- Empieza en default-deny: la función niega y solo concede con una regla explícita.
- Usa RBAC para el trazo grueso y ABAC para lo que depende de datos. No fuerces roles donde toca un atributo.
- Centraliza la decisión en un PDP: una función
autorizar(), y que cada endpoint la llame. - Toma los atributos de decisión de un token verificado, nunca de la petición del cliente.
- Haz que la política devuelva el motivo. El motivo va al log de decisiones; al usuario, lo justo.
- Escribe un test por regla: un caso que se permite y uno que se niega, para cada rama.
11Laboratorio
Levanta el proveedor de identidad y su cliente:
cd cursos/seguridad/labs/lab-iam-oidc
docker compose up -d --build
docker compose ps
Abre http://localhost:8470, pulsa entrar, elige un usuario y prueba a
leer y escribir cada recurso. Verás la política decidiendo, con su motivo. La regla
está en autorizar(): la puedes ejercitar sin navegador.
Cuando termines:
docker compose down -v
12Ejercicios
Clasifica cinco decisiones
Para cada regla, di si es más natural con RBAC, con ABAC, o con los dos: (1) “solo administradores borran usuarios”; (2) “un médico ve historiales de sus propios pacientes”; (3) “nadie modifica una factura ya pagada”; (4) “el equipo de soporte lee tickets”; (5) “un usuario edita solo su propio perfil”.
(1) RBAC puro: depende del rol. (2) RBAC + ABAC: rol médico y además
paciente.medico == sujeto. (3) ABAC: depende del estado del recurso. (4)
RBAC puro. (5) ABAC: perfil.dueño == sujeto, típicamente con un rol
usuario de base.
Clave: R · R+A · A · R · A. Las que llevan “propio”, “ya” o “su” son atributos; forzarlas a roles es el camino a la explosión.
Añade una regla a la política
Con el lab-iam-oidc levantado, modifica autorizar() para añadir
una acción borrar que solo permita el rol admin y solo sobre recursos de su
propio departamento (aunque sea admin). Ejercítala con los tres usuarios y comprueba que
se comporta como esperas.
escribir y endureciéndola.
Una rama nueva antes del return final:
if accion == "borrar": if "admin" not in roles: return (False, "RBAC: borrar exige rol admin") if dep != meta["dep"]: return (False, "ABAC: borrar solo en tu departamento") return (True, "permitido")
Clave: alice borra informe-ventas (permitido) pero no informe-compras (ABAC, sin excepción de admin). bob no borra nada (RBAC). La diferencia con escribir es que aquí el atributo aplica también al admin.
RBAC o ABAC para multi-tenant
Investiga cómo modelan el aislamiento entre clientes (tenants) los sistemas SaaS reales.
¿El tenant_id es un rol o un atributo? Argumenta por qué modelarlo como rol
lleva a la explosión, y qué tiene que ser cierto del tenant_id para que sea
seguro usarlo como atributo de decisión.
tenant_id que usa la decisión.
El tenant_id es un atributo, no un rol: si fuera un rol, cada alta de
cliente crearía roles (admin-tenant-42) y en un año tendrías miles. Como
atributo, una sola regla sujeto.tenant == recurso.tenant cubre a todos.
Para que sea seguro, el tenant_id de la decisión tiene que venir del
token verificado del usuario, jamás de un parámetro que el cliente pueda cambiar; si
el atacante elige su propio tenant, el aislamiento no existe.
Clave: atributo, no rol; y el atributo debe salir de una fuente de confianza (el token), no de la petición. Un tenant como rol es la explosión de roles con nombre corporativo.
Red Team · rompe una política ingenua
Dentro del lab-iam-oidc, y solo dentro de él: modifica autorizar()
para que lea el departamento de un argumento controlable en vez de los claims del token
(simula el error de fiarse de la petición). Demuestra con una llamada que un lector de
ventas “se convierte” en compras y accede a lo ajeno. Después revierte.
dep venga de un parámetro en vez de claims["dep"].dep="compras" a mano salta el ABAC.
Si dep se lee de un parámetro que el llamante elige, bob (lector/ventas)
invoca la decisión con dep="compras" y el ABAC pasa: el atributo ya no
describe al sujeto, sino lo que el atacante quiere. Es el mismo fallo que un
?role=admin en una query. La corrección es tomar dep
solo de claims, que salen de un token verificado.
Clave: el atributo de decisión no puede venir de la petición. Revierte a claims["dep"] y vuelve a comprobar que bob no llega a compras.
Blue Team · registra las decisiones
Haz que autorizar() registre cada decisión con sujeto, acción, recurso,
veredicto y motivo. Genera tráfico de los tres usuarios y localiza en el log los
“denegado”. Responde: con esos campos, ¿podrías detectar a alguien tanteando recursos que
no le tocan? ¿Qué patrón buscarías?
Con sujeto, veredicto y motivo puedes detectar tanteo: un mismo sujeto acumulando negativas ABAC contra varios recursos en segundos es alguien probando qué le dejan. La regla de detección es una cuenta por ventana de tiempo de decisiones denegadas por sujeto; supera un umbral, alerta. Sin registrar el veredicto, esto es invisible: el log de acceso HTTP vería 403 pero no el motivo que los agrupa.
Clave: el entregable es el patrón: pico de negativas por sujeto en poco tiempo. Guárdalo: es material para la fase de detección.
Architecture Challenge · el PDP de un sistema real
Diseña la autorización de una aplicación con varios servicios. Define: dónde vive el PDP, cómo llega la política a cada servicio, de dónde salen los atributos de decisión, qué pasa cuando el PDP no responde, y cómo pruebas que una política nueva no rompe una existente antes de desplegarla.
Una propuesta razonable: la política como datos versionados en el repositorio, evaluada por una librería PDP embebida en cada servicio (baja latencia, sin punto único) o por un servicio PDP con la política replicada. Los atributos de decisión salen del token verificado y de metadatos del recurso, nunca de la petición. Si el PDP no puede decidir, se niega (fail-closed). Y cada cambio de política pasa un banco de casos —permitidos y denegados— en el pipeline, de modo que una regla nueva que rompa un caso existente no llega a producción.
Clave: no hay una única respuesta. Se evalúa que la decisión esté centralizada como concepto, que falle cerrado, que los atributos vengan de fuente de confianza y que la política se pruebe como código.
13Preguntas de comprensión
¿Qué problema aparece cuando modelas cada permiso fino como un rol nuevo?
Cuando el permiso depende de datos y lo fuerzas a roles, cada matiz crea un rol. El síntoma es un catálogo que nadie se atreve a tocar.
La política “dep del sujeto == dep del recurso” es un ejemplo de…
Comparar un atributo del sujeto con uno del recurso, en cada petición, es exactamente ABAC.
La política del laboratorio niega a bob (lector/ventas) leer informe-compras. ¿Por qué?
bob sí tiene rol lector, así que pasa el RBAC; lo para el ABAC, porque ventas ≠ compras y no es admin.
¿Por qué una política de autorización debe devolver el motivo de la negativa?
El motivo hace la política depurable y el acceso auditable. Otra cosa es cuánto de ese motivo se le enseña al usuario final.
14Reto adicional
Convierte tres reglas de negocio en política probable
Toma tres reglas de acceso de un proyecto tuyo, escritas en lenguaje de negocio, y
conviértelas en llamadas a autorizar(sujeto, accion, recurso) con su
veredicto esperado. Para cada una, escribe el caso que se permite y el que se niega. Si
alguna regla no cabe en esa forma, es señal de que aún no la entiendes del todo:
reescríbela hasta que quepa.
El ejercicio no tiene una respuesta única, pero sí una forma correcta: cada regla
queda como una o dos ramas de autorizar(), con su motivo, y con dos
casos de prueba. La dificultad real aparece con las reglas que mezclan condiciones
(“un editor publica, pero no su propio artículo sin revisión de otro”): ahí ves que
necesitas atributos del recurso (autor, estado) además del rol. Si una regla te
obliga a inventar un rol por cada valor de un dato, reescríbela como atributo.
Clave: lo que se evalúa es que cada regla tenga sujeto, acción, recurso, veredicto con motivo y un par de casos. Una regla sin caso rojo no está terminada.
15Resumen
- Autenticar es saber quién eres; autorizar es decidir qué puedes hacer. Casi todas las brechas de acceso confunden las dos.
- RBAC agrupa permisos en roles: simple y auditable, ideal para el trazo grueso.
- Cuando el permiso depende de datos, RBAC lleva a la explosión de roles: ese matiz era un atributo.
- ABAC decide comparando atributos por petición, y expresa relaciones que los roles no pueden.
- Los dos se combinan: rol para lo grueso, atributo para lo fino, con default-deny.
- La política es una función pura que devuelve permitido o denegado con motivo, con atributos de una fuente de confianza, y con un test por regla.
16Mis notas
Estas notas se guardan solo en este navegador. No se envían a ningún servidor. Expórtalas desde el panel de progreso si cambias de máquina.
17Recursos
Los comentarios se cargan solo si los pides, desde GitHub Discussions. No se carga nada de terceros mientras no pulses.