Un vendedor inicia sesión y abre uno de sus pedidos. Después cambia el identificador de la URL y aparece el pedido de otro vendedor. La autenticación funcionó: sabemos quién hizo la petición. Lo que falló fue la decisión sobre qué podía hacer esa persona con ese recurso concreto.
RBAC y ABAC son modelos para organizar esas decisiones. Entenderlos ayuda a evitar que el control de acceso termine reducido a un if que pregunta si alguien es administrador.
Vamos a diseñar una política para una aplicación de ventas ficticia. Tendrá vendedores, supervisores y pedidos de distintas organizaciones. Usaremos TypeScript para expresar las reglas y probar tanto los permisos como las denegaciones, sin depender de sistemas internos ni datos reales.
Autenticación y autorización responden preguntas distintas
La autenticación comprueba la identidad presentada. La autorización decide si esa identidad puede realizar una acción sobre un recurso. OWASP distingue ambas responsabilidades en su Authorization Cheat Sheet.
Una sesión válida no concede acceso a todos los pedidos. Tampoco basta con verificar la firma de un JWT: después hay que aplicar la política de la aplicación. Si el token contiene roles, su firma protege lo que afirmó el emisor; no demuestra que esos roles sigan vigentes ni que permitan acceder al objeto solicitado.
Para nuestro diseño, una decisión necesita cuatro entradas:
| Entrada | Pregunta | Ejemplo ficticio |
|---|---|---|
| Sujeto | ¿Quién intenta actuar? | Vendedor activo de la organización Norte |
| Acción | ¿Qué quiere hacer? | Leer, editar o aprobar un pedido |
| Recurso | ¿Sobre qué objeto? | Pedido de un propietario y un equipo concretos |
| Contexto | ¿Bajo qué condiciones? | Escrituras habilitadas y autenticación reforzada vigente |
Es útil separar estas piezas desde el principio: tener permiso para leer no implica tenerlo para aprobar, y poder aprobar un pedido no implica poder aprobar cualquiera.
RBAC: asignar permisos mediante roles
RBAC, Role-Based Access Control, organiza el acceso alrededor de roles. Los usuarios reciben roles y los roles agrupan permisos sobre operaciones y recursos. Así podemos mantener una asignación como «vendedor puede leer pedidos» sin repetir esa autorización individualmente para cada vendedor.
NIST describe también jerarquías de roles, activación de roles en sesiones y restricciones de separación de funciones. Nuestro ejemplo usará solo una asignación sencilla de roles y permisos; no pretende implementar todo el modelo formal. Véanse las preguntas frecuentes de NIST sobre RBAC.
La primera capa de nuestra política será:
| Rol | Acciones candidatas |
|---|---|
| Vendedor | Leer y editar pedidos |
| Supervisor | Leer y aprobar pedidos |
La palabra candidatas importa. Esta tabla todavía no determina qué pedidos puede leer cada persona ni en qué estado se pueden editar.
Además, supervisor no significa «vendedor con acceso a todo». En este negocio, supervisar permite aprobar; no concede automáticamente edición. Una persona podría tener ambos roles si existe una razón para ello, pero las restricciones de cada operación seguirían vigentes.
RBAC resulta cómodo cuando las funciones laborales son relativamente estables. El problema aparece si intentamos representar todas las combinaciones con nombres como supervisor-equipo-a-limite-alto-turno-tarde: cada variación del negocio empieza a producir otro rol.
RBAC no prohíbe comprobar condiciones adicionales. NIST aclara que las restricciones sobre recursos o relaciones pueden intervenir al acceder. Por eso conviene distinguir el modelo de una implementación demasiado simple que solo comprueba roles.includes('supervisor').
ABAC: decidir con atributos y condiciones
ABAC, Attribute-Based Access Control, evalúa atributos del sujeto, del objeto y del entorno, junto con la operación solicitada y una política. Esta es la base de la definición de NIST SP 800-162, con su actualización de 2019.
Un atributo es una característica que la política puede consultar. En nuestra aplicación:
- Del usuario: organización, estado activo, roles, equipos supervisados y límite de aprobación.
- Del pedido: organización, propietario, creador, equipo, estado e importe.
- Del contexto: disponibilidad de escrituras y resultado vigente de una autenticación reforzada.
El rol también puede ser un atributo. RBAC y ABAC no son dos mundos incomunicados: podemos usar roles para organizar capacidades generales y atributos para limitar su alcance.
Nuestra regla de aprobación combinará varias condiciones: supervisor del equipo, pedido pendiente de la misma organización, importe dentro de su límite y creador distinto del aprobador. Exigiremos además autenticación reforzada y escrituras habilitadas.
Estas son reglas de negocio elegidas para el ejemplo, no requisitos universales de NIST u OWASP. Otra organización puede necesitar una política distinta, pero debería poder expresarla con la misma precisión.
Los atributos también necesitan una fuente de confianza
Aceptar organizacionId, roles o autenticacionReforzada del cuerpo de la petición permitiría que quien solicita acceso fabricara las condiciones para obtenerlo.
En nuestro diseño, el servidor obtiene el usuario desde una identidad autenticada y consulta sus permisos vigentes. Carga el pedido desde almacenamiento y construye el contexto con información del servidor y de la sesión. Una autenticación reforzada debe tener un significado verificable —por ejemplo, un segundo factor reciente— y una caducidad definida; no basta con guardar un booleano indefinidamente.
La confianza en los atributos merece tanta atención como la expresión de la regla. NIST SP 800-205 estudia precisamente las consideraciones sobre atributos para sistemas de control de acceso. En la práctica, debemos identificar quién los emite, quién puede modificarlos y cómo se actualizan cuando una persona cambia de equipo o pierde permisos.
Mínimo privilegio y denegación por defecto
Mínimo privilegio significa conceder solo el acceso necesario para la tarea. Denegar por defecto significa que una petición sin una regla que la permita termina rechazada. OWASP recomienda ambos principios y comprobar los permisos en cada petición en su guía de autorización.
Para nuestra aplicación, el contrato completo queda así:
| Acción | Vendedor | Supervisor |
|---|---|---|
| Leer | Pedidos propios | Pedidos de equipos que supervisa |
| Editar | Pedidos propios en borrador | No permitido por este rol |
| Aprobar | No permitido por este rol | Pedidos pendientes de sus equipos, dentro de su límite y creados por otra persona |
Todas las filas requieren una cuenta activa y la misma organización. Editar y aprobar requieren escrituras habilitadas. Aprobar exige también autenticación reforzada. Cualquier acción no contemplada queda denegada.
La prohibición de aprobar lo que uno mismo creó es una restricción de separación de funciones de este negocio. Conservar ambos roles no permite saltársela. Tampoco es necesario convertir cada combinación de atributos en un rol nuevo.
El recorrido de una petición será:
No se pudo mostrar el diagrama. Puedes consultar su definición.
flowchart TD
A[Petición autenticada] --> B[Cargar usuario y pedido desde fuentes confiables]
B --> C[Evaluar rol, acción, objeto y contexto]
C --> D{¿La política permite?}
D -->|No| E[Denegar sin exponer el recurso]
D -->|Sí| F[Ejecutar la operación autorizada]
F --> G[Devolver solo campos permitidos]
Separaremos la decisión —una función que responde sí o no— del punto donde se hace cumplir. La función no protege nada si un endpoint olvida llamarla antes de leer o modificar datos.
Una política híbrida en TypeScript
Guarda los siguientes bloques TypeScript, en el orden indicado, en un archivo llamado acceso.mts. Puedes ejecutarlo con Node.js 24 o superior, sin instalar dependencias. Node admite este TypeScript con tipos eliminables y trata .mts como módulo ES; ese mecanismo no realiza comprobación de tipos. Véase la documentación oficial de Node.js sobre TypeScript.
El ejemplo recibe objetos internos ya validados. Los tipos ayudan durante el desarrollo, pero no validan datos HTTP en ejecución. La autenticación, la validación de entradas y la carga de atributos quedan fuera de este pequeño programa; son controles complementarios que tratamos en cómo proteger una API REST en Node.js.
import assert from 'node:assert/strict';
type Rol = 'vendedor' | 'supervisor';
type Accion = 'pedido:leer' | 'pedido:editar' | 'pedido:aprobar';
type Usuario = {
id: string;
organizacionId: string;
activo: boolean;
roles: readonly Rol[];
equiposSupervisados: readonly string[];
limiteAprobacionCentavos: number;
};
type Pedido = {
id: string;
organizacionId: string;
propietarioId: string;
creadorId: string;
equipoId: string;
estado: 'borrador' | 'pendiente' | 'aprobado';
totalCentavos: number;
};
type Contexto = {
escriturasHabilitadas: boolean;
autenticacionReforzada: boolean;
};
const permisos: ReadonlyMap<Rol, readonly Accion[]> = new Map([
['vendedor', ['pedido:leer', 'pedido:editar']],
['supervisor', ['pedido:leer', 'pedido:aprobar']],
]);
function puede(
usuario: Usuario | null,
accion: Accion,
pedido: Pedido | null,
contexto: Contexto,
): boolean {
if (!usuario || !pedido || usuario.activo !== true) {
return false;
}
if (usuario.organizacionId !== pedido.organizacionId) {
return false;
}
const rolPermite = usuario.roles.some(
(rol) => permisos.get(rol)?.includes(accion) === true,
);
if (!rolPermite) return false;
const esVendedorPropietario =
usuario.roles.includes('vendedor') &&
pedido.propietarioId === usuario.id;
const supervisaEquipo =
usuario.roles.includes('supervisor') &&
usuario.equiposSupervisados.includes(pedido.equipoId);
switch (accion) {
case 'pedido:leer':
return esVendedorPropietario || supervisaEquipo;
case 'pedido:editar':
return esVendedorPropietario &&
pedido.estado === 'borrador' &&
contexto.escriturasHabilitadas === true;
case 'pedido:aprobar': {
const importe = pedido.totalCentavos;
const limite = usuario.limiteAprobacionCentavos;
const importePermitido =
Number.isSafeInteger(importe) && importe >= 0 &&
Number.isSafeInteger(limite) && limite >= 0 &&
importe <= limite;
return supervisaEquipo &&
pedido.estado === 'pendiente' &&
pedido.creadorId !== usuario.id &&
importePermitido &&
contexto.escriturasHabilitadas === true &&
contexto.autenticacionReforzada === true;
}
default:
return false;
}
}La tabla de permisos expresa la parte RBAC. Las comparaciones de organización, propietario, equipo, estado, importe y contexto limitan cada operación mediante atributos.
Los importes son enteros en la unidad menor de una única moneda ficticia: no comparamos importes de monedas distintas. Comprobar enteros seguros evita que un valor como NaN o una cantidad fuera del rango entero preciso se use para aprobar. Esto no reemplaza la validación del pedido cuando se crea o modifica.
Autorizar el objeto antes de devolverlo
Añade esta función al mismo archivo. El mapa representa un repositorio local para que el ejemplo sea ejecutable; en una API cargaríamos el pedido desde almacenamiento confiable.
function leerPedido(
usuario: Usuario | null,
id: string,
pedidos: ReadonlyMap<string, Pedido>,
contexto: Contexto,
) {
const pedido = pedidos.get(id) ?? null;
if (!pedido || !puede(usuario, 'pedido:leer', pedido, contexto)) {
return null;
}
return {
id: pedido.id,
estado: pedido.estado,
totalCentavos: pedido.totalCentavos,
};
}El identificador selecciona un candidato, no concede acceso. La función devuelve datos únicamente después de autorizar ese pedido y construye una respuesta con campos explícitos. En una integración HTTP puedes decidir cómo representar una denegación, por ejemplo con 404 para no revelar existencia; la elección del código no sustituye la comprobación de permisos.
Probar también lo que debe fallar
Completa acceso.mts con estos datos ficticios y comprobaciones. Las funciones de assert de Node.js interrumpen el programa si una expectativa no se cumple.
const vendedor: Usuario = {
id: 'usuario-ana',
organizacionId: 'org-norte',
activo: true,
roles: ['vendedor'],
equiposSupervisados: [],
limiteAprobacionCentavos: 0,
};
const supervisor: Usuario = {
...vendedor,
id: 'usuario-luis',
roles: ['supervisor'],
equiposSupervisados: ['equipo-a'],
limiteAprobacionCentavos: 100_000,
};
const pedido: Pedido = {
id: 'pedido-1',
organizacionId: 'org-norte',
propietarioId: vendedor.id,
creadorId: vendedor.id,
equipoId: 'equipo-a',
estado: 'borrador',
totalCentavos: 50_000,
};
const contexto: Contexto = {
escriturasHabilitadas: true,
autenticacionReforzada: true,
};
const pendiente: Pedido = { ...pedido, estado: 'pendiente' };
const ajeno: Pedido = {
...pedido,
id: 'pedido-2',
propietarioId: 'usuario-bea',
creadorId: 'usuario-bea',
};
assert.equal(puede(vendedor, 'pedido:editar', pedido, contexto), true);
assert.equal(puede(vendedor, 'pedido:editar', pendiente, contexto), false);
assert.equal(puede(vendedor, 'pedido:aprobar', pendiente, contexto), false);
assert.equal(puede(supervisor, 'pedido:aprobar', pendiente, contexto), true);
const denegados: Pedido[] = [
{ ...pendiente, organizacionId: 'org-sur' },
{ ...pendiente, equipoId: 'equipo-b' },
{ ...pendiente, creadorId: supervisor.id },
{ ...pendiente, totalCentavos: 100_001 },
{ ...pendiente, totalCentavos: Number.NaN },
{ ...pendiente, estado: 'aprobado' },
];
for (const candidato of denegados) {
assert.equal(puede(supervisor, 'pedido:aprobar', candidato, contexto), false);
}
assert.equal(
puede(supervisor, 'pedido:aprobar', pendiente, {
...contexto,
autenticacionReforzada: false,
}),
false,
);
assert.equal(
puede({ ...vendedor, activo: false }, 'pedido:leer', pedido, contexto),
false,
);
assert.equal(puede(null, 'pedido:leer', pedido, contexto), false);
const pedidos = new Map([
[pedido.id, pedido],
[ajeno.id, ajeno],
]);
assert.deepEqual(leerPedido(vendedor, pedido.id, pedidos, contexto), {
id: pedido.id,
estado: pedido.estado,
totalCentavos: pedido.totalCentavos,
});
assert.equal(leerPedido(vendedor, ajeno.id, pedidos, contexto), null);
assert.equal(leerPedido(vendedor, 'inexistente', pedidos, contexto), null);
console.log('Política verificada: permisos y denegaciones esperados.');Ejecuta node acceso.mts. Si las comprobaciones pasan, verás el mensaje final. El programa trabaja en memoria y no levanta servicios; para retirarlo basta con borrar el archivo.
Estas pruebas comprueban las decisiones descritas, no certifican la seguridad de una API. En una aplicación real debemos añadir pruebas que atraviesen autenticación, rutas, consultas y escritura: una función correcta no detecta por sí sola un endpoint que la omite.
IDOR y BOLA: el permiso debe alcanzar al objeto
IDOR, Insecure Direct Object Reference, describe un fallo de acceso a objetos mediante referencias controlables cuando faltan comprobaciones adecuadas. BOLA, Broken Object Level Authorization, es la categoría de autorización por objeto de OWASP API Security Top 10. Comparten un problema central: poder invocar una operación no implica poder usarla sobre cualquier objeto.
Por ejemplo, Ana puede llamar a GET /pedidos/:id, pero no leer el pedido de Bea. Cambiar un ID secuencial por un UUID dificulta adivinarlo; no convierte el identificador en un permiso. Son distinciones recogidas en OWASP API1:2023 — BOLA y la guía de prevención de IDOR.
Nuestro leerPedido evalúa el objeto recuperado. En un repositorio real también podemos expresar el alcance autorizado en la consulta, por ejemplo organización y propietario para un vendedor. La consulta debe reproducir la política completa aplicable a esa operación; un filtro de organización por sí solo no separa a vendedores de la misma empresa.
La protección debe extenderse a lecturas individuales, listados, exportaciones, operaciones masivas y modificaciones. Para un listado, delimita los objetos autorizados antes de paginar y calcular totales: filtrar solo la pantalla no protege los datos que la API ya envió.
Conviene distinguir además tres preguntas:
| Nivel | Pregunta | Riesgo OWASP relacionado |
|---|---|---|
| Función | ¿Puede un vendedor invocar la aprobación? | API5: autorización por función |
| Objeto | ¿Puede este supervisor aprobar este pedido? | API1: autorización por objeto |
| Propiedad | ¿Puede cambiar su organización o consultar un campo reservado? | API3: autorización por propiedad |
La separación entre funciones está tratada en OWASP API5:2023. La autorización sobre campos se aborda en OWASP API3:2023: tener acceso a un objeto no concede acceso irrestricto a todas sus propiedades.
En nuestra política, «editar» autoriza cambios comerciales permitidos de un borrador, no cambiar organizacionId, creadorId, propietarioId o el estado para fabricar una aprobación. Un endpoint de edición debe aceptar una lista explícita de campos. Reasignar propietarios o equipos requiere una operación y una política propias; no aparece autorizado en este ejemplo.
Dónde suelen aparecer las brechas
Estos son puntos que revisaría al integrar nuestra política en una API:
- Comprobar solo en la interfaz. Ocultar el botón sirve para la experiencia de uso; el servidor sigue obligado a decidir antes de actuar.
- Leer atributos enviados por quien pide acceso. El cliente puede elegir un identificador, pero no establecer los roles, el límite de aprobación o la organización con los que se evaluará.
- Reutilizar permisos indefinidamente. Una caché o una sesión puede conservar un equipo anterior. Define vigencia e invalidación para que revocar acceso tenga el efecto esperado.
- Tratar un error como permiso. Si no se puede obtener un atributo necesario o evaluar la política, detén la operación. No conviertas una excepción en
true. - Autorizar y después escribir con datos distintos. Si el importe o el equipo cambia entre la lectura y la aprobación, la decisión puede quedar obsoleta. Usa una transacción con el aislamiento o bloqueo apropiado, o una actualización condicional que compruebe la versión y los valores autorizados. Abrir una transacción por sí solo no garantiza esa propiedad.
- Confundir registros con controles. Registrar una aprobación no evita una aprobación indebida. En auditoría, guarda actor, acción, identificador del recurso, resultado y versión de política cuando corresponda, evitando tokens y contenido sensible.
Son recomendaciones de integración para este diseño. El ejemplo ejecutable solo evalúa decisiones y lectura en memoria: no implementa persistencia, concurrencia, revocación ni un sistema de auditoría.
¿Cuándo elegir RBAC, ABAC o una combinación?
| Enfoque | Cuándo encaja | Costo que hay que vigilar |
|---|---|---|
| RBAC | Funciones estables y permisos que se administran bien por rol | Roles excesivos o permisos demasiado amplios al representar todas las excepciones |
| ABAC | Decisiones que dependen de muchos atributos y condiciones cambiantes | Calidad y vigencia de atributos, conflictos entre reglas y dificultad de explicación |
| Híbrido | Roles claros con restricciones de organización, propiedad, equipo o estado | Mantener una política coherente entre consultas, funciones y operaciones |
OWASP recomienda preferir controles basados en atributos y relaciones para necesidades granulares en su comparación de modelos de autorización. Eso no convierte cada aplicación pequeña en candidata a un motor externo de políticas.
Para la aplicación ficticia elegiría el enfoque híbrido que acabamos de implementar: los roles hacen comprensibles las capacidades, y las condiciones delimitan cada pedido. Esta es una decisión técnica para este caso, no una obligación normativa. Las publicaciones NIST y las guías OWASP citadas tampoco establecen, por sí solas, una obligación legal para todas las aplicaciones.
Empezaría con funciones pequeñas, centralizadas y probadas. Evaluaría un motor de políticas cuando varios servicios necesiten compartir reglas, administrarlas fuera del despliegue o explicar decisiones más complejas. Incorporarlo no elimina la responsabilidad de obtener atributos confiables y hacer cumplir el resultado en cada operación.
El permiso termina en una operación concreta
El control de acceso queda mejor definido cuando podemos explicar una decisión completa: este usuario puede realizar esta acción sobre este recurso, bajo estas condiciones.
Un rol aporta parte de esa respuesta. La pertenencia a una organización, la relación con el pedido, su estado y el contexto completan el alcance. Mantener esas reglas explícitas, comprobarlas antes de actuar y probar los casos denegados permite revisar el diseño con mucha más precisión que una lista creciente de excepciones para administradores.
Referencias
- NIST — Role Based Access Control: Frequently Asked Questions.
- NIST SP 800-162 — Guide to Attribute Based Access Control (ABAC) Definition and Considerations, actualización de 2019.
- NIST SP 800-205 — Attribute Considerations for Access Control Systems, 2019.
- OWASP — Authorization Cheat Sheet.
- OWASP — Insecure Direct Object Reference Prevention Cheat Sheet.
- OWASP API Security Top 10 — API1:2023 Broken Object Level Authorization.
- OWASP API Security Top 10 — API3:2023 Broken Object Property Level Authorization.
- OWASP API Security Top 10 — API5:2023 Broken Function Level Authorization.
- Node.js 24 — Modules: TypeScript y Assert.
