Después de tres lecciones, Escena Viva sabe quién hace cada petición. req.usuario llega verificado —con su id y su rol— tanto por sesión como por token de acceso, y comprarEntradas ya no acepta un usuarioId del cliente. La autenticación está resuelta.

Ahora viene la otra mitad, la que decide qué puede hacer cada uno. Y conviene decirlo sin rodeos: la autorización es donde se rompen las APIs reales. Los dos primeros puestos del OWASP API Security Top 10 son fallos de autorización, no de autenticación. Un login impecable no sirve de nada si GET /api/pedidos/ped-042 le devuelve a Marc el pedido de Lucía.

Contenido

  1. Modelos de autorización
  2. Los tres roles de Escena Viva
  3. La matriz de permisos
  4. El middleware exigirRol
  5. 401 frente a 403
  6. Lo que RBAC no cubre: la propiedad del recurso
  7. Filtrar en la consulta, no comparar después
  8. La autorización nunca se hace en el cliente
  9. Cuando el rol del token está caducado
  10. Permisos más finos que los roles
  11. Centralizar la política
  12. Auditoría y escalada de privilegios
  13. Errores comunes, ejercicios y conclusión

  1. Modelos de autorización

Modelo Cómo decide Ejemplo en Escena Viva Inconveniente
ACL Permisos por recurso y por sujeto «Lucía puede leer ped-042» Ingobernable al crecer: N usuarios × M recursos
RBAC Permisos por rol; los usuarios tienen roles «Los organizadores publican eventos» No expresa «solo mis eventos»
ABAC Reglas sobre atributos de usuario, recurso y contexto «Edita eventos de su sala si están en borrador» Difícil de razonar y de probar
ReBAC Grafo de relaciones «Puedes ver el pedido si eres su comprador» Requiere infraestructura (tipo Zanzibar)

Por qué RBAC encaja en Escena Viva: hay tres roles nítidos que corresponden a funciones reales del negocio, el conjunto de acciones es acotado, y los permisos se explican en una frase a alguien no técnico. RBAC es la opción por defecto correcta para el 90 % de las aplicaciones, con un matiz honesto que domina la segunda mitad de esta lección: responde «¿puede este rol hacer esta acción?», no «¿puede sobre este recurso concreto?». Esa segunda pregunta la resolveremos añadiendo comprobación de propiedad —un toque de ABAC/ReBAC sobre una base RBAC, que es lo que hacen casi todos los sistemas reales.

  1. Los tres roles de Escena Viva

Ya están modelados desde la lección 07-02: const ROLES = Object.freeze(['asistente', 'organizador', 'administrador']);

Rol Quién es Qué hace
asistente Lucía, Marc Catálogo, compra, y ve sus pedidos y sus entradas
organizador Responsables del Teatro Almendra, la Sala Bóveda y el Auditorio Ribera Crea, publica y finaliza eventos de su sala; consulta sus ventas
administrador Equipo de la plataforma Gestiona usuarios y roles, anula entradas, ve todo

Una advertencia sobre la jerarquía: es tentador programar «administrador ⊇ organizador ⊇ asistente». Cuidado: un administrador no debería poder comprar entradas en nombre de un asistente, porque no es un permiso superior sino una acción distinta. Escena Viva trata los roles como conjuntos de permisos explícitos, con herencia solo donde la matriz lo diga. Un dato más que hará falta: el organizador tiene salaAsignada ('Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'); el rol dice qué tipo de cosas puede hacer, y la sala sobre cuáles.

  1. La matriz de permisos

Esta tabla no es documentación decorativa: es la especificación del código que viene.

Acción Ruta Anónimo Asistente Organizador Administrador
Ver catálogo y detalle GET /api/eventos, /:id Sí Sí Sí Sí
Comprar entradas POST /api/compras No Sí Sí Sí
Ver mis pedidos GET /api/pedidos No Sí (propios) Sí (propios) Sí (propios)
Ver un pedido ajeno GET /api/pedidos/:id No No No Sí
Crear, publicar o finalizar evento POST /api/eventos, PATCH /:id/publicar No No Sí (su sala) Sí
Ver ventas de una sala GET /api/salas/:sala/ventas No No Sí (su sala) Sí
Anular o usar una entrada PATCH /api/entradas/:codigo/... No No Sí (su sala) Sí
Gestionar usuarios y roles PATCH /api/usuarios/:id/rol No No No Sí

Léela con atención, porque contiene la lección entera. Hay filas que el rol resuelve solo: «gestionar usuarios» es sí o no según el rol. Hay filas donde el rol no basta: «publicar evento» dice Sí (su sala), y ese paréntesis es la propiedad del recurso, que ningún exigirRol cubre. Y «ver mis pedidos» es la más engañosa: todos los roles pueden, pero el conjunto de resultados es distinto para cada usuario; no es un permiso de ruta, es un filtro de datos.

  1. El middleware exigirRol

// src/middleware/exigir-rol.js
'use strict';
const { ErrorDeAutenticacion, ErrorDeAutorizacion } = require('../errores.js');
const { ROLES } = require('../modelos/usuario.js');
// Factoria: recibe los roles permitidos y devuelve el middleware.
function exigirRol(...rolesPermitidos) {
  // Comprobacion al ARRANCAR, no en cada peticion: un rol mal escrito
  // ('organizadors') haria que la ruta denegase a todo el mundo en silencio.
  const malo = rolesPermitidos.find((rol) => !ROLES.includes(rol));
  if (malo) { throw new Error(`Rol desconocido en exigirRol: ${malo}`); }
  return function comprobarRol(req, res, next) {
    // 401: no sabemos quien es. Es un fallo de AUTENTICACION.
    if (!req.usuario) { return next(new ErrorDeAutenticacion('Debes iniciar sesion')); }
    // 403: sabemos quien es, y no le corresponde. Es AUTORIZACION.
    if (!rolesPermitidos.includes(req.usuario.rol)) {
      // No devolvemos el rol actual al cliente para no dar pistas sobre la
      // estructura de permisos: eso se registra en el log.
      return next(new ErrorDeAutorizacion('No tienes permiso para esta operacion',
        { requerido: rolesPermitidos }));
    }
    return next();
  };
}
module.exports = { exigirRol };
// Aplicado a las rutas reales, en src/rutas/eventos.js:
router.get('/', autenticarOpcional, listarCatalogo); // publica: se enriquece con sesion
router.get('/:id', autenticarOpcional, obtenerEvento);
// El orden es la politica: autenticar -> rol -> cargar recurso -> propiedad.
router.post('/', autenticar, exigirRol('organizador', 'administrador'),
  validar(esquemaCrearEvento, ORIGENES_VALIDOS.CUERPO), crearEvento);
router.patch('/:id/publicar', autenticar, exigirRol('organizador', 'administrador'),
  cargarEvento, exigirPropiedadDeSala, publicarEvento);

Fíjate en la cadena de PATCH /:id/publicar: autenticar (¿quién eres?) → exigirRol (¿tu rol permite publicar?) → cargarEvento (¿qué recurso es?) → exigirPropiedadDeSala (¿es tuyo?). Cada eslabón responde una pregunta y ninguno responde la del otro.

  1. 401 frente a 403

401 Unauthorized 403 Forbidden
Significado real No autenticado (nombre desafortunado de la especificación) Autenticado, sin permiso
Causa Sin credencial, caducada o inválida Rol insuficiente o recurso ajeno
Qué debe hacer el cliente Iniciar sesión o refrescar: reintentar puede cambiar el resultado No reintentar: dará lo mismo; mostrar «no tienes permiso»

Confundirlos tiene un coste muy concreto: si devuelves 401 a un asistente que intenta publicar un evento, el front-end lo manda al login, entra correctamente, vuelve a intentarlo y falla otra vez, en un bucle que el usuario no entiende; y al revés, un 403 ante un token caducado impide que el cliente sepa que solo tenía que refrescar. Con la tabla ESTADO_POR_CODIGO ampliada en 08-02 (NO_AUTENTICADO: 401, SIN_PERMISO: 403), la respuesta sale con el formato de siempre: { "error": { "codigo": "SIN_PERMISO", "mensaje": "No tienes permiso para esta operacion", "estado": 403, "detalles": { "requerido": ["organizador", "administrador"] } } }.

Un matiz sobre filtración de información. A veces incluso el 403 dice demasiado: confirma que el recurso existe. Si un asistente prueba GET /api/pedidos/ped-042 y recibe 403, ha aprendido que ese pedido existe; con 404, no sabe nada. La regla práctica de Escena Viva: 403 cuando el recurso es público o su existencia no es un secreto (un evento del catálogo); 404 cuando la existencia misma es información sensible (pedidos, entradas, usuarios).

  1. Lo que RBAC no cubre: la propiedad del recurso

Vuelve a la matriz. El organizador de la Sala Bóveda tiene rol organizador, así que exigirRol('organizador') lo deja pasar a PATCH /api/eventos/evt-001/publicar… y evt-001 es el Concierto de Otoño del Teatro Almendra. Rol correcto, recurso ajeno.

Esto es la referencia directa insegura a objetos (IDOR), o en lenguaje de OWASP, autorización rota a nivel de objeto: el primer puesto de su lista y, con diferencia, el fallo más común de las APIs reales. Es tan común porque el código parece razonable:

// MAL: el rol es correcto y el recurso es de otro
router.patch('/:id/publicar', autenticar, exigirRol('organizador'), async (req, res) => {
  const evento = await Evento.findById(req.params.id);
  evento.estado = 'publicado';
  await evento.save();
  res.json(evento);
});

El middleware de propiedad, que se ejecuta después de cargar el recurso:

// src/middleware/exigir-propiedad.js
'use strict';
const { RecursoNoEncontrado } = require('../errores.js');
const { repositorioEventos } = require('../repositorios/index.js');
// 1) Cargar el recurso. Sin recurso no se puede decidir nada.
async function cargarEvento(req, res, next) {
  try {
    const evento = await repositorioEventos.obtenerEventoPorId(req.params.id);
    if (!evento) { throw new RecursoNoEncontrado(`No existe el evento ${req.params.id}`); }
    req.recurso = evento; // objeto del dominio, no documento del ODM (M7)
    next();
  } catch (error) { next(error); }
}
// 2) Comparar el recurso con el usuario. Aqui, y no antes. Un administrador
// atraviesa la comprobacion, pero se registra (seccion 12).
function exigirPropiedadDeSala(req, res, next) {
  if (req.usuario.rol === 'administrador') { return next(); }
  if (req.recurso.sala !== req.usuario.salaAsignada) {
    // 404, no 403: no confirmamos que el evento de otra sala exista.
    return next(new RecursoNoEncontrado(`No existe el evento ${req.params.id}`));
  }
  return next();
}
module.exports = { cargarEvento, exigirPropiedadDeSala };

Por qué la comprobación va después de cargar: la propiedad es un atributo del recurso (evento.sala, pedido.usuarioId), y no se puede comprobar sin tenerlo delante. Cualquier intento de deducirla del identificador —codificar la sala en el id, por ejemplo— es un parche frágil.

  1. Filtrar en la consulta, no comparar después

Hay una técnica todavía mejor que comparar tras cargar: no cargar lo que no es tuyo.

// ACEPTABLE: cargar y comparar
const pedido = await Pedido.findById(id);
if (String(pedido.usuarioId) !== req.usuario.id) { throw new ErrorDeAutorizacion(); }
// MEJOR: filtrar dentro de la consulta. No distingue "no existe" de "no es tuyo".
const pedido = await Pedido.findOne({ _id: id, usuarioId: req.usuario.id });
if (!pedido) { throw new RecursoNoEncontrado(); }

Las ventajas de la segunda forma son cuatro, y son sólidas: es imposible olvidar la comprobación (no hay un if que borrar al refactorizar; si el filtro no está, no hay resultado); no se filtra la existencia («no existe» y «no es tuyo» dan la misma respuesta); el dato ajeno nunca entra en el proceso, así que no puede acabar en un log ni en una respuesta por descuido; y es más eficiente, porque el índice hace el trabajo en la base. Por eso el filtro se empuja al repositorio del módulo 7, donde queda como parte de la firma de la operación:

// src/repositorios/pedidos.js  (ampliado en el modulo 8)
// El usuarioId es OBLIGATORIO en la firma: es imposible llamar a estas funciones
// "sin querer" sin restringir por propietario.
async function listarPedidosDeUsuario(usuarioId, { pagina = 1, tamano = 20 } = {}) {
  const documentos = await Pedido.find({ usuarioId }).sort({ creadoEn: -1 })
    .skip((pagina - 1) * tamano).limit(Math.min(tamano, 100)).lean(); // tope duro (08-06)
  return documentos.map(aPedidoDeDominio);
}
async function obtenerPedidoDeUsuario(pedidoId, usuarioId) {
  const documento = await Pedido.findOne({ _id: pedidoId, usuarioId }).lean();
  return documento ? aPedidoDeDominio(documento) : null;
}
module.exports = { crearPedido, listarPedidosDeUsuario, obtenerPedidoDeUsuario };
// Y el controlador queda tan simple que es dificil equivocarse:
async function obtenerPedido(req, res, next) {
  try {
    const pedido = await obtenerPedidoDeUsuario(req.params.id, req.usuario.id);
    if (!pedido) { throw new RecursoNoEncontrado('Pedido no encontrado'); } // 404 en ambos casos
    res.json({ pedido });
  } catch (error) { next(error); }
}

Regla de oro: si el usuarioId que restringe la consulta viene de req.usuario, la autorización es correcta por construcción. Si viene de req.params o de req.body, tienes un IDOR.

  1. La autorización nunca se hace en el cliente

Ocultar un botón no es un permiso. El front-end de Escena Viva puede esconder «Publicar evento» a los asistentes, y debe hacerlo por usabilidad, pero eso solo evita clics accidentales. Cualquiera puede lanzar curl -X PATCH https://api.escenaviva.test/api/eventos/evt-001/publicar -H "Authorization: Bearer <token de asistente>". El navegador es territorio del usuario: se pueden editar las variables de JavaScript, modificar el HTML e ignorar el enrutador. La única frontera de seguridad es el servidor.

Corolarios: no confíes en campos ocultos (un <input type="hidden" name="usuarioId"> es editable); no confíes en la validación del cliente, que es usabilidad, mientras la de verdad es la de zod en el borde (M6); y no devuelvas datos que el usuario no puede ver esperando que el front-end los oculte —si la respuesta los lleva, están filtrados. Esto es la exposición excesiva de datos de OWASP, que veremos en 08-06.

  1. Cuando el rol del token está caducado

En 08-04 vimos que el rol del JWT es una fotografía del momento de la emisión. Si un administrador degrada al organizador de la Sala Bóveda a asistente, su token de acceso sigue diciendo organizador hasta 15 minutos.

Riesgo Estrategia Cuándo
Bajo Confiar en el rol del token Lecturas y acciones reversibles: listar mis pedidos
Medio Confiar en el token; el cambio se propaga al refrescar (/auth/refrescar relee el usuario) Crear un evento en borrador
Alto Releer el usuario en la base Anular entradas, cambiar roles, ver ventas
// src/middleware/exigir-rol-fresco.js
// Para operaciones criticas: el rol se relee de la base, no del token. Cuesta
// una consulta; a cambio, un rol revocado deja de valer YA.
function exigirRolFresco(...rolesPermitidos) {
  return async function comprobar(req, res, next) {
    try {
      if (!req.usuario) { throw new ErrorDeAutenticacion('Debes iniciar sesion'); }
      const actual = await Usuario.findById(req.usuario.id).select('rol salaAsignada').lean();
      if (!actual || !rolesPermitidos.includes(actual.rol)) {
        throw new ErrorDeAutorizacion('No tienes permiso para esta operacion');
      }
      req.usuario = { ...req.usuario, rol: actual.rol, sala: actual.salaAsignada };
      next();
    } catch (error) { next(error); }
  };
}

Es un intercambio consciente: una consulta por petición a cambio de coherencia inmediata; aplícalo donde el daño de 15 minutos de retraso sea inaceptable, no en todas partes.

  1. Permisos más finos que los roles

Llega un momento en que los roles se quedan cortos: Escena Viva quiere que ciertos organizadores puedan crear eventos pero no publicarlos sin revisión, y con roles puros la salida es inventar organizador_junior, de donde a tener quince roles hay un paso. La alternativa son los permisos con nombre de acción, y roles que son conjuntos de permisos:

// src/autorizacion/permisos.js
'use strict';
const PERMISOS = Object.freeze({
  EVENTO_CREAR: 'evento:crear', EVENTO_PUBLICAR: 'evento:publicar',
  EVENTO_FINALIZAR: 'evento:finalizar', PEDIDO_LEER_PROPIO: 'pedido:leer:propio',
  PEDIDO_LEER_CUALQUIERA: 'pedido:leer:cualquiera', ENTRADA_ANULAR: 'entrada:anular',
  ENTRADA_USAR: 'entrada:usar', USUARIO_GESTIONAR: 'usuario:gestionar', VENTAS_LEER: 'ventas:leer',
});
// El rol deja de ser el permiso: pasa a ser un ATAJO para un conjunto.
const PERMISOS_POR_ROL = Object.freeze({
  asistente: [PERMISOS.PEDIDO_LEER_PROPIO],
  organizador: [PERMISOS.EVENTO_CREAR, PERMISOS.EVENTO_PUBLICAR, PERMISOS.EVENTO_FINALIZAR,
    PERMISOS.ENTRADA_ANULAR, PERMISOS.ENTRADA_USAR, PERMISOS.VENTAS_LEER, PERMISOS.PEDIDO_LEER_PROPIO],
  administrador: Object.values(PERMISOS),
});
module.exports = { PERMISOS, PERMISOS_POR_ROL };

Cuándo merece la pena el salto: cuando empiezas a crear roles cuya única diferencia es una acción, cuando el negocio pide excepciones («este organizador sí, aquel no»), o cuando necesitas conceder permisos temporales. Mientras tres roles describan bien la realidad, añadir permisos es complejidad sin beneficio. Escena Viva prepara la estructura pero mantiene los tres roles.

  1. Centralizar la política

El antipatrón: if (req.usuario.rol === 'administrador' || ...) esparcido por doce controladores. Cuando cambie una regla habrá que encontrarlos todos, y el que se olvide será un agujero.

// src/autorizacion/politica.js
'use strict';
const { PERMISOS, PERMISOS_POR_ROL } = require('./permisos.js');
// Funciones PURAS: sin req, sin base de datos, sin Express. Por eso se pueden
// probar solas y de forma exhaustiva (modulo 9).
const tienePermiso = (usuario, permiso) =>
  Boolean(usuario?.rol) && (PERMISOS_POR_ROL[usuario.rol] || []).includes(permiso);
// Regla de propiedad: la sala del evento debe ser la asignada al organizador.
const puedeGestionarEvento = (usuario, evento) => tienePermiso(usuario, PERMISOS.EVENTO_PUBLICAR)
  && (usuario.rol === 'administrador' || evento.sala === usuario.salaAsignada);
const puedeVerPedido = (usuario, pedido) =>
  tienePermiso(usuario, PERMISOS.PEDIDO_LEER_CUALQUIERA)
  || String(pedido.usuarioId) === String(usuario.id);
// Nadie cambia su propio rol, ni siquiera un administrador: evita tanto la
// escalada como el auto-bloqueo accidental.
const puedeCambiarRol = (usuario, objetivo) => tienePermiso(usuario, PERMISOS.USUARIO_GESTIONAR)
  && String(usuario.id) !== String(objetivo.id);
module.exports = { tienePermiso, puedeGestionarEvento, puedeVerPedido, puedeCambiarRol };

Las ventajas son concretas: se prueba sola —sin servidor ni base de datos; en el módulo 9 escribiremos una tabla de casos que recorre la matriz de la sección 3 entera—; se lee como la especificación, porque puedeGestionarEvento(usuario, evento) se contrasta con la fila de la tabla; cambia en un sitio; y se audita leyendo un fichero, no doce controladores. Los middleware pasan a ser envoltorios finísimos:

// Uso: router.patch('/:id/publicar', autenticar, cargarEvento,
//        exigirPolitica(puedeGestionarEvento), publicarEvento);
function exigirPolitica(comprobacion) {
  return function aplicar(req, res, next) {
    if (!req.usuario) { return next(new ErrorDeAutenticacion('Debes iniciar sesion')); }
    if (!comprobacion(req.usuario, req.recurso)) {
      return next(new ErrorDeAutorizacion('No tienes permiso para esta operacion'));
    }
    return next();
  };
}

  1. Auditoría y escalada de privilegios

Auditoría. Toda decisión sensible deja rastro: quién, qué, cuándo, sobre qué y con qué resultado.

// src/servicios/auditoria.js -> accion: 'evento:publicar' | 'usuario:cambiar-rol';
// resultado: 'permitido' | 'denegado'. Estructurado (JSON), no texto libre, para
// poder consultarlo y alertar; idPeticion viene del modulo 6 y correlaciona todo.
function registrarAccion({ actor, accion, recurso, resultado, idPeticion }) {
  console.log(JSON.stringify({ tipo: 'auditoria', marca: new Date().toISOString(),
    idPeticion, actorId: actor.id, actorRol: actor.rol, accion, recurso, resultado }));
}

Qué merece auditoría: cambios de rol, anulación de entradas, publicación y finalización de eventos, accesos de administrador a datos ajenos, y los intentos denegados (un asistente que prueba diez rutas de administrador es una señal). Nunca se registran credenciales ni tokens. El logging estructurado y su envío a un sistema centralizado se desarrollan en el módulo 11.

Escalada de privilegios. El fallo clásico cabe en una línea inocente: Usuario.create(req.body) con un cuerpo que incluye "rol":"administrador". Es asignación masiva, y por eso el esquema zod de registro de 08-02 lleva .strict() y el controlador construye el objeto campo a campo con rol: 'asistente' fijado por el servidor. El rol nunca es un dato de entrada del registro.

// src/controladores/usuarios.js
async function cambiarRol(req, res, next) {
  try {
    const { rol } = req.datosValidados.cuerpo; // zod: enum(ROLES), .strict()
    const objetivo = await Usuario.findById(req.params.id);
    if (!objetivo) { throw new RecursoNoEncontrado('Usuario no encontrado'); }
    // La politica decide; el controlador solo obedece.
    if (!puedeCambiarRol(req.usuario, objetivo)) {
      throw new ErrorDeAutorizacion('No tienes permiso para esta operacion');
    }
    const rolAnterior = objetivo.rol;
    objetivo.rol = rol;
    await objetivo.save();
    // Un cambio de rol revoca las sesiones: se aplica ya, sin esperar a que
    // caduque el token de acceso (08-04).
    await revocarTodosLosRefrescos(objetivo._id);
    registrarAccion({ actor: req.usuario, accion: 'usuario:cambiar-rol',
      recurso: String(objetivo._id), resultado: 'permitido', idPeticion: req.idPeticion });
    res.json({ usuario: { id: objetivo._id, rol: objetivo.rol, rolAnterior } });
  } catch (error) { next(error); }
}

Errores Comunes y Consejos

  • Comprobar el rol y olvidar la propiedad. Es el IDOR de manual, el fallo de autorización más frecuente que existe. Y su reverso: comprobar la propiedad antes de cargar el recurso, imposible porque la propiedad es un atributo del recurso.
  • Devolver 403 donde tocaba 404, confirmando la existencia de recursos privados; o confiar en el rol del JWT para acciones críticas, cuando puede llevar hasta 15 minutos caducado.
  • Asumir que administrador implica todos los permisos «por lógica»: escríbelo en la matriz, porque lo implícito acaba siendo inconsistente. Y autorizar en el cliente, o escribir Usuario.create(req.body) / Object.assign(usuario, req.body): escalada de privilegios servida.
  • Consejo: escribe la matriz de permisos antes que el código y conviértela en la tabla de casos de prueba del módulo 9: si una celda no tiene prueba, esa celda es una suposición. Y haz que el filtro por propietario viva en la firma del repositorio —la seguridad que no se puede olvidar es mejor que la que hay que recordar— y, por defecto, deniega: una ruta nueva sin política explícita debería ser inaccesible, no pública.

Ejercicios

Ejercicio 1: encontrar el fallo

Estos dos endpoints pasan una revisión superficial y tienen un fallo de autorización cada uno. Identifícalos y reescríbelos.

router.get('/api/pedidos/:id', autenticar, async (req, res, next) => {
  const pedido = await Pedido.findById(req.params.id).lean();
  if (!pedido) { return next(new RecursoNoEncontrado('Pedido no encontrado')); }
  res.json({ pedido });
});
router.get('/api/salas/:sala/ventas', autenticar, exigirRol('organizador'), async (req, res) => {
  res.json({ ventas: await calcularVentas(req.params.sala) });
});

Ejercicio 2: implementar una fila de la matriz

Implementa PATCH /api/entradas/:codigo/usar (marcar una entrada como usada al entrar al recinto) respetando la matriz: solo organizador de esa sala o administrador. La entrada debe estar en estado valida; si ya está usada o anulada, corresponde un ConflictoDeEstado. Escribe la ruta con su cadena de middleware y la función de política.

Ejercicio 3: nuevo rol

Escena Viva contrata personal de taquilla que puede marcar entradas como usadas, pero no anularlas, ni crear eventos, ni ver ventas. Decide si añadirías un rol taquilla o un permiso suelto, justifica la elección y escribe el cambio en permisos.js.

Soluciones

Ejercicio 1. El primer endpoint no comprueba la propiedad: cualquier usuario autenticado lee el pedido de cualquier otro conociendo el identificador. Es un IDOR. El segundo comprueba el rol pero no la sala, así que el organizador de la Sala Bóveda ve las ventas del Teatro Almendra; además excluye a los administradores, que según la matriz sí pueden.

router.get('/api/pedidos/:id', autenticar, async (req, res, next) => {
  try {
    // Filtro dentro de la consulta: imposible olvidarlo, y no distingue
    // "no existe" de "no es tuyo".
    const pedido = await obtenerPedidoDeUsuario(req.params.id, req.usuario.id);
    if (!pedido) { throw new RecursoNoEncontrado('Pedido no encontrado'); }
    res.json({ pedido });
  } catch (error) { next(error); }
});
router.get('/api/salas/:sala/ventas', autenticar, exigirRol('organizador', 'administrador'),
  (req, res, next) => {
    // La sala pedida debe ser la asignada, salvo administrador.
    if (req.usuario.rol !== 'administrador' && req.params.sala !== req.usuario.salaAsignada) {
      return next(new RecursoNoEncontrado('Sala no encontrada'));
    }
    next();
  },
  async (req, res, next) => {
    try { res.json({ ventas: await calcularVentas(req.params.sala) }); } catch (e) { next(e); }
  });

Un administrador con PEDIDO_LEER_CUALQUIERA necesitaría una rama aparte, resuelta con puedeVerPedido y registrada en auditoría (sección 12).

Ejercicio 2. Nota previa: marcarEntradaUsada debe hacer la transición de forma atómica en el repositorio (findOneAndUpdate con { codigo, estado: 'valida' }), o dos validaciones simultáneas en la puerta del Auditorio Ribera podrían pasar las dos. Es la misma lección de concurrencia del módulo 7.

// src/autorizacion/politica.js  (adicion)
const puedeUsarEntrada = (usuario, entrada) => tienePermiso(usuario, PERMISOS.ENTRADA_USAR)
  && (usuario.rol === 'administrador' || entrada.sala === usuario.salaAsignada);
// src/rutas/entradas.js
router.patch('/:codigo/usar', autenticar, exigirRol('organizador', 'administrador'),
  cargarEntradaPorCodigo,             // deja req.recurso
  exigirPolitica(puedeUsarEntrada),   // propiedad de sala
  async (req, res, next) => {
    try {
      const entrada = req.recurso;
      // Transicion valida -> usada. Cualquier otro origen es conflicto.
      if (entrada.estado !== 'valida') {
        throw new ConflictoDeEstado(`La entrada esta ${entrada.estado}`,
          { estadoActual: entrada.estado, transicionEsperada: 'valida -> usada' });
      }
      registrarAccion({ actor: req.usuario, accion: 'entrada:usar', recurso: entrada.codigo,
        resultado: 'permitido', idPeticion: req.idPeticion });
      res.json({ entrada: await marcarEntradaUsada(entrada.codigo) });
    } catch (error) { next(error); }
  });

Ejercicio 3. Un rol taquilla, no un permiso suelto: corresponde a una función real y estable del negocio, es explicable en una frase, y no es una excepción puntual sobre otro rol. Los permisos individuales se justifican cuando hay que hacer excepciones caso por caso; aquí hay una categoría de personas.

const PERMISOS_POR_ROL = Object.freeze({
  asistente: [PERMISOS.PEDIDO_LEER_PROPIO],
  taquilla: [PERMISOS.ENTRADA_USAR], // ni anular, ni crear, ni ver ventas
  organizador: [/* ...igual que antes */],
  administrador: Object.values(PERMISOS),
});

Cambios necesarios: añadir 'taquilla' a ROLES en el modelo Usuario (con migración, M7), añadir la fila a la matriz de la sección 3, y actualizar la política y sus pruebas. Que el cambio se localice en tan pocos sitios es el beneficio de haber centralizado la política.

Conclusión

Escena Viva ya no solo sabe quién llama: sabe qué puede hacer cada uno. Hemos comparado los modelos de autorización y elegido RBAC con conocimiento de causa, escrito la matriz de permisos como especificación ejecutable, y construido exigirRol como factoría con la distinción correcta entre 401 (no sé quién eres) y 403 (sé quién eres y no te toca). Sobre todo, hemos afrontado lo que RBAC no cubre: la propiedad del recurso, comprobada después de cargar el recurso y, mejor todavía, filtrada dentro de la consulta desde el repositorio, que es la forma de hacer imposible el IDOR en lugar de recordar evitarlo. Hemos visto por qué el cliente nunca autoriza, cuándo releer el rol en la base en lugar de fiarse del token, cómo dar el salto a permisos finos si hace falta, y por qué la política vive en src/autorizacion/politica.js en funciones puras que se prueban solas. Y hemos cerrado la escalada de privilegios donde nace: nadie se asigna su propio rol.

Queda una lección para terminar el módulo. Tenemos autenticación sólida y autorización correcta, pero una API defendible es algo más: límites de peticiones afinados por ruta, topes de tamaño y de paginación, protección frente a inyección y asignación masiva, cabeceras de seguridad con criterio, HTTPS obligatorio, gestión de secretos y monitorización de eventos de seguridad. En Buenas Prácticas de Seguridad en APIs recorremos el OWASP API Security Top 10 punto por punto sobre Escena Viva, y convertimos todo lo aprendido en una lista de comprobación que se puede usar de verdad.

Curso de Node.js: De Principiante a Avanzado

Módulo 1: Introducción a Node.js

Módulo 2: Conceptos Básicos

Módulo 3: Sistema de Archivos y E/S

Módulo 4: HTTP y Servidores Web

Módulo 5: NPM y Gestión de Paquetes

Módulo 6: Framework Express.js

Módulo 7: Bases de Datos y ORMs

Módulo 8: Autenticación y Autorización

Módulo 9: Pruebas y Depuración

Módulo 10: Temas Avanzados

Módulo 11: Despliegue y DevOps

Módulo 12: Proyectos del Mundo Real

© Copyright 2026. Todos los derechos reservados