Després de tres lliçons, Escena Viva sap qui fa cada petició. req.usuari arriba verificat —amb el seu id i el seu rol— tant per sessió com per token d'accés, i comprarEntrades ja no accepta un usuariId del client. L'autenticació està resolta.

Ara ve l'altra meitat, la que decideix què pot fer cadascú. I convé dir-ho sense embuts: l'autorització és on es trenquen les APIs reals. Els dos primers llocs de l'OWASP API Security Top 10 són fallades d'autorització, no d'autenticació. Una entrada impecable no serveix de res si GET /api/comandes/ped-042 retorna a en Marc la comanda de la Lucía.

Contingut

  1. Models d'autorització
  2. Els tres rols d'Escena Viva
  3. La matriu de permisos
  4. El middleware exigirRol
  5. 401 davant de 403
  6. El que RBAC no cobreix: la propietat del recurs
  7. Filtrar dins de la consulta, no comparar després
  8. L'autorització no es fa mai al client
  9. Quan el rol del token està caducat
  10. Permisos més fins que els rols
  11. Centralitzar la política
  12. Auditoria i escalada de privilegis
  13. Errors comuns, exercicis i conclusió

  1. Models d'autorització

Model Com decideix Exemple a Escena Viva Inconvenient
ACL Permisos per recurs i per subjecte «La Lucía pot llegir ped-042» Ingovernable en créixer: N usuaris × M recursos
RBAC Permisos per rol; els usuaris tenen rols «Els organitzadors publiquen esdeveniments» No expressa «només els meus esdeveniments»
ABAC Regles sobre atributs d'usuari, recurs i context «Edita esdeveniments de la seva sala si són en esborrany» Difícil de raonar i de provar
ReBAC Graf de relacions «Pots veure la comanda si n'ets el comprador» Requereix infraestructura (tipus Zanzibar)

Per què RBAC encaixa a Escena Viva: hi ha tres rols nítids que corresponen a funcions reals del negoci, el conjunt d'accions és acotat, i els permisos s'expliquen en una frase a algú no tècnic. RBAC és l'opció per defecte correcta per al 90 % de les aplicacions, amb un matís honest que domina la segona meitat d'aquesta lliçó: respon «pot aquest rol fer aquesta acció?», no «pot fer-la sobre aquest recurs concret?». Aquesta segona pregunta la resoldrem afegint-hi comprovació de propietat —un toc d'ABAC/ReBAC sobre una base RBAC, que és el que fan gairebé tots els sistemes reals.

  1. Els tres rols d'Escena Viva

Ja estan modelats des de la lliçó 07-02: const ROLS = Object.freeze(['assistent', 'organitzador', 'administrador']);

Rol Qui és Què fa
assistent Lucía, Marc Catàleg, compra, i veu les seves comandes i les seves entrades
organitzador Responsables del Teatro Almendra, la Sala Bóveda i l'Auditorio Ribera Crea, publica i finalitza esdeveniments de la seva sala; consulta les seves vendes
administrador Equip de la plataforma Gestiona usuaris i rols, anul·la entrades, ho veu tot

Un advertiment sobre la jerarquia: és temptador programar «administrador ⊇ organitzador ⊇ assistent». Compte: un administrador no hauria de poder comprar entrades en nom d'un assistent, perquè no és un permís superior sinó una acció diferent. Escena Viva tracta els rols com a conjunts de permisos explícits, amb herència només allà on la matriu ho digui. Una dada més que caldrà: l'organitzador té salaAssignada ('Teatro Almendra', 'Sala Boveda', 'Auditorio Ribera'); el rol diu quin tipus de coses pot fer, i la sala sobre quines.

  1. La matriu de permisos

Aquesta taula no és documentació decorativa: és l'especificació del codi que ve.

Acció Ruta Anònim Assistent Organitzador Administrador
Veure catàleg i detall GET /api/esdeveniments, /:id Sí Sí Sí Sí
Comprar entrades POST /api/compres No Sí Sí Sí
Veure les meves comandes GET /api/comandes No Sí (pròpies) Sí (pròpies) Sí (pròpies)
Veure una comanda aliena GET /api/comandes/:id No No No Sí
Crear, publicar o finalitzar esdeveniment POST /api/esdeveniments, PATCH /:id/publicar No No Sí (la seva sala) Sí
Veure vendes d'una sala GET /api/sales/:sala/vendes No No Sí (la seva sala) Sí
Anul·lar o usar una entrada PATCH /api/entrades/:codi/... No No Sí (la seva sala) Sí
Gestionar usuaris i rols PATCH /api/usuaris/:id/rol No No No Sí

Llegeix-la amb atenció, perquè conté la lliçó sencera. Hi ha files que el rol resol tot sol: «gestionar usuaris» és sí o no segons el rol. Hi ha files on el rol no basta: «publicar esdeveniment» diu Sí (la seva sala), i aquest parèntesi és la propietat del recurs, que cap exigirRol no cobreix. I «veure les meves comandes» és la més enganyosa: tots els rols poden fer-ho, però el conjunt de resultats és diferent per a cada usuari; no és un permís de ruta, és un filtre de dades.

  1. El middleware exigirRol

// src/middleware/exigir-rol.js
'use strict';
const { ErrorDAutenticacio, ErrorDAutoritzacio } = require('../errors.js');
const { ROLS } = require('../models/usuari.js');
// Factoria: rep els rols permesos i retorna el middleware.
function exigirRol(...rolsPermesos) {
  // Comprovacio en ARRENCAR, no a cada peticio: un rol mal escrit
  // ('organitzadors') faria que la ruta denegues a tothom en silenci.
  const dolent = rolsPermesos.find((rol) => !ROLS.includes(rol));
  if (dolent) { throw new Error(`Rol desconegut a exigirRol: ${dolent}`); }
  return function comprovarRol(req, res, next) {
    // 401: no sabem qui es. Es una fallada d'AUTENTICACIO.
    if (!req.usuari) { return next(new ErrorDAutenticacio("Has d'iniciar sessio")); }
    // 403: sabem qui es, i no li correspon. Es AUTORITZACIO.
    if (!rolsPermesos.includes(req.usuari.rol)) {
      // No retornem el rol actual al client per no donar pistes sobre
      // l'estructura de permisos: aixo es registra al log.
      return next(new ErrorDAutoritzacio('No tens permis per a aquesta operacio',
        { requerit: rolsPermesos }));
    }
    return next();
  };
}
module.exports = { exigirRol };
// Aplicat a les rutes reals, a src/rutes/esdeveniments.js:
router.get('/', autenticarOpcional, llistarCataleg); // publica: s'enriqueix amb sessio
router.get('/:id', autenticarOpcional, obtenirEsdeveniment);
// L'ordre es la politica: autenticar -> rol -> carregar recurs -> propietat.
router.post('/', autenticar, exigirRol('organitzador', 'administrador'),
  validar(esquemaCrearEsdeveniment, ORIGENS_VALIDS.COS), crearEsdeveniment);
router.patch('/:id/publicar', autenticar, exigirRol('organitzador', 'administrador'),
  carregarEsdeveniment, exigirPropietatDeSala, publicarEsdeveniment);

Fixa't en la cadena de PATCH /:id/publicar: autenticar (qui ets?) → exigirRol (el teu rol permet publicar?) → carregarEsdeveniment (quin recurs és?) → exigirPropietatDeSala (és teu?). Cada baula respon una pregunta i cap no respon la de l'altra.

  1. 401 davant de 403

401 Unauthorized 403 Forbidden
Significat real No autenticat (nom desafortunat de l'especificació) Autenticat, sense permís
Causa Sense credencial, caducada o invàlida Rol insuficient o recurs aliè
Què ha de fer el client Iniciar sessió o refrescar: reintentar pot canviar el resultat No reintentar: donarà el mateix; mostrar «no tens permís»

Confondre'ls té un cost molt concret: si retornes 401 a un assistent que intenta publicar un esdeveniment, el front-end l'envia a l'entrada, hi entra correctament, ho torna a intentar i falla un altre cop, en un bucle que l'usuari no entén; i a l'inrevés, un 403 davant d'un token caducat impedeix que el client sàpiga que només havia de refrescar. Amb la taula ESTAT_PER_CODI ampliada a 08-02 (NO_AUTENTICAT: 401, SENSE_PERMIS: 403), la resposta surt amb el format de sempre: { "error": { "codi": "SENSE_PERMIS", "missatge": "No tens permis per a aquesta operacio", "estat": 403, "detalls": { "requerit": ["organitzador", "administrador"] } } }.

Un matís sobre filtració d'informació. De vegades fins i tot el 403 diu massa: confirma que el recurs existeix. Si un assistent prova GET /api/comandes/ped-042 i rep 403, ha après que aquella comanda existeix; amb 404, no en sap res. La regla pràctica d'Escena Viva: 403 quan el recurs és públic o la seva existència no és cap secret (un esdeveniment del catàleg); 404 quan l'existència mateixa és informació sensible (comandes, entrades, usuaris).

  1. El que RBAC no cobreix: la propietat del recurs

Torna a la matriu. L'organitzador de la Sala Bóveda té rol organitzador, així que exigirRol('organitzador') el deixa passar a PATCH /api/esdeveniments/evt-001/publicar… i evt-001 és el Concierto de Otoño del Teatro Almendra. Rol correcte, recurs aliè.

Això és la referència directa insegura a objectes (IDOR), o en llenguatge d'OWASP, autorització trencada a nivell d'objecte: el primer lloc de la seva llista i, amb diferència, la fallada més comuna de les APIs reals. És tan comuna perquè el codi sembla raonable:

// MALAMENT: el rol es correcte i el recurs es d'un altre
router.patch('/:id/publicar', autenticar, exigirRol('organitzador'), async (req, res) => {
  const esdeveniment = await Esdeveniment.findById(req.params.id);
  esdeveniment.estat = 'publicat';
  await esdeveniment.save();
  res.json(esdeveniment);
});

El middleware de propietat, que s'executa després de carregar el recurs:

// src/middleware/exigir-propietat.js
'use strict';
const { RecursNoTrobat } = require('../errors.js');
const { repositoriEsdeveniments } = require('../repositoris/index.js');
// 1) Carregar el recurs. Sense recurs no es pot decidir res.
async function carregarEsdeveniment(req, res, next) {
  try {
    const esdeveniment = await repositoriEsdeveniments.obtenirEsdevenimentPerId(req.params.id);
    if (!esdeveniment) { throw new RecursNoTrobat(`No existeix l'esdeveniment ${req.params.id}`); }
    req.recurs = esdeveniment; // objecte del domini, no document de l'ODM (M7)
    next();
  } catch (error) { next(error); }
}
// 2) Comparar el recurs amb l'usuari. Aqui, i no abans. Un administrador
// travessa la comprovacio, pero es registra (seccio 12).
function exigirPropietatDeSala(req, res, next) {
  if (req.usuari.rol === 'administrador') { return next(); }
  if (req.recurs.sala !== req.usuari.salaAssignada) {
    // 404, no 403: no confirmem que l'esdeveniment d'una altra sala existeixi.
    return next(new RecursNoTrobat(`No existeix l'esdeveniment ${req.params.id}`));
  }
  return next();
}
module.exports = { carregarEsdeveniment, exigirPropietatDeSala };

Per què la comprovació va després de carregar: la propietat és un atribut del recurs (esdeveniment.sala, comanda.usuariId), i no es pot comprovar sense tenir-lo al davant. Qualsevol intent de deduir-la de l'identificador —codificar la sala dins l'id, per exemple— és un pedaç fràgil.

  1. Filtrar dins de la consulta, no comparar després

Hi ha una tècnica encara millor que comparar després de carregar: no carregar el que no és teu.

// ACCEPTABLE: carregar i comparar
const comanda = await Comanda.findById(id);
if (String(comanda.usuariId) !== req.usuari.id) { throw new ErrorDAutoritzacio(); }
// MILLOR: filtrar dins de la consulta. No distingeix "no existeix" de "no es teu".
const comanda = await Comanda.findOne({ _id: id, usuariId: req.usuari.id });
if (!comanda) { throw new RecursNoTrobat(); }

Els avantatges de la segona forma són quatre, i són sòlids: és impossible oblidar la comprovació (no hi ha cap if per esborrar en refactoritzar; si el filtre no hi és, no hi ha resultat); no es filtra l'existència («no existeix» i «no és teu» donen la mateixa resposta); la dada aliena no entra mai al procés, així que no pot acabar en un log ni en una resposta per descuit; i és més eficient, perquè l'índex fa la feina a la base. Per això el filtre s'empeny al repositori del mòdul 7, on queda com a part de la signatura de l'operació:

// src/repositoris/comandes.js  (ampliat al modul 8)
// L'usuariId es OBLIGATORI a la signatura: es impossible cridar aquestes funcions
// "sense voler" sense restringir per propietari.
async function llistarComandesDUsuari(usuariId, { pagina = 1, mida = 20 } = {}) {
  const documents = await Comanda.find({ usuariId }).sort({ creatEl: -1 })
    .skip((pagina - 1) * mida).limit(Math.min(mida, 100)).lean(); // sostre dur (08-06)
  return documents.map(aComandaDeDomini);
}
async function obtenirComandaDUsuari(comandaId, usuariId) {
  const document = await Comanda.findOne({ _id: comandaId, usuariId }).lean();
  return document ? aComandaDeDomini(document) : null;
}
module.exports = { crearComanda, llistarComandesDUsuari, obtenirComandaDUsuari };
// I el controlador queda tan simple que es dificil equivocar-se:
async function obtenirComanda(req, res, next) {
  try {
    const comanda = await obtenirComandaDUsuari(req.params.id, req.usuari.id);
    if (!comanda) { throw new RecursNoTrobat('Comanda no trobada'); } // 404 en tots dos casos
    res.json({ comanda });
  } catch (error) { next(error); }
}

Regla d'or: si l'usuariId que restringeix la consulta ve de req.usuari, l'autorització és correcta per construcció. Si ve de req.params o de req.body, tens un IDOR.

  1. L'autorització no es fa mai al client

Amagar un botó no és un permís. El front-end d'Escena Viva pot amagar «Publicar esdeveniment» als assistents, i ho ha de fer per usabilitat, però això només evita clics accidentals. Qualsevol pot llançar curl -X PATCH https://api.escenaviva.test/api/esdeveniments/evt-001/publicar -H "Authorization: Bearer <token d'assistent>". El navegador és territori de l'usuari: es poden editar les variables de JavaScript, modificar l'HTML i ignorar l'enrutador. L'única frontera de seguretat és el servidor.

Corol·laris: no confiïs en camps ocults (un <input type="hidden" name="usuariId"> és editable); no confiïs en la validació del client, que és usabilitat, mentre que la de debò és la de zod a la vora (M6); i no retornis dades que l'usuari no pot veure esperant que el front-end les amagui —si la resposta les porta, estan filtrades. Això és l'exposició excessiva de dades d'OWASP, que veurem a 08-06.

  1. Quan el rol del token està caducat

A 08-04 vam veure que el rol del JWT és una fotografia del moment de l'emissió. Si un administrador degrada l'organitzador de la Sala Bóveda a assistent, el seu token d'accés continua dient organitzador fins a 15 minuts.

Risc Estratègia Quan
Baix Confiar en el rol del token Lectures i accions reversibles: llistar les meves comandes
Mitjà Confiar en el token; el canvi es propaga en refrescar (/auth/refrescar rellegeix l'usuari) Crear un esdeveniment en esborrany
Alt Rellegir l'usuari a la base Anul·lar entrades, canviar rols, veure vendes
// src/middleware/exigir-rol-fresc.js
// Per a operacions critiques: el rol es rellegeix de la base, no del token. Costa
// una consulta; a canvi, un rol revocat deixa de valer JA.
function exigirRolFresc(...rolsPermesos) {
  return async function comprovar(req, res, next) {
    try {
      if (!req.usuari) { throw new ErrorDAutenticacio("Has d'iniciar sessio"); }
      const actual = await Usuari.findById(req.usuari.id).select('rol salaAssignada').lean();
      if (!actual || !rolsPermesos.includes(actual.rol)) {
        throw new ErrorDAutoritzacio('No tens permis per a aquesta operacio');
      }
      req.usuari = { ...req.usuari, rol: actual.rol, sala: actual.salaAssignada };
      next();
    } catch (error) { next(error); }
  };
}

És un intercanvi conscient: una consulta per petició a canvi de coherència immediata; aplica'l on el dany de 15 minuts de retard sigui inacceptable, no a tot arreu.

  1. Permisos més fins que els rols

Arriba un moment en què els rols es queden curts: Escena Viva vol que certs organitzadors puguin crear esdeveniments però no publicar-los sense revisió, i amb rols purs la sortida és inventar organitzador_junior, d'on a tenir quinze rols hi ha un pas. L'alternativa són els permisos amb nom d'acció, i rols que són conjunts de permisos:

// src/autoritzacio/permisos.js
'use strict';
const PERMISOS = Object.freeze({
  ESDEVENIMENT_CREAR: 'esdeveniment:crear', ESDEVENIMENT_PUBLICAR: 'esdeveniment:publicar',
  ESDEVENIMENT_FINALITZAR: 'esdeveniment:finalitzar', COMANDA_LLEGIR_PROPIA: 'comanda:llegir:propia',
  COMANDA_LLEGIR_QUALSEVOL: 'comanda:llegir:qualsevol', ENTRADA_ANULLAR: 'entrada:anullar',
  ENTRADA_USAR: 'entrada:usar', USUARI_GESTIONAR: 'usuari:gestionar', VENDES_LLEGIR: 'vendes:llegir',
});
// El rol deixa de ser el permis: passa a ser una DRECERA per a un conjunt.
const PERMISOS_PER_ROL = Object.freeze({
  assistent: [PERMISOS.COMANDA_LLEGIR_PROPIA],
  organitzador: [PERMISOS.ESDEVENIMENT_CREAR, PERMISOS.ESDEVENIMENT_PUBLICAR, PERMISOS.ESDEVENIMENT_FINALITZAR,
    PERMISOS.ENTRADA_ANULLAR, PERMISOS.ENTRADA_USAR, PERMISOS.VENDES_LLEGIR, PERMISOS.COMANDA_LLEGIR_PROPIA],
  administrador: Object.values(PERMISOS),
});
module.exports = { PERMISOS, PERMISOS_PER_ROL };

Quan val la pena el salt: quan comences a crear rols l'única diferència dels quals és una acció, quan el negoci demana excepcions («aquest organitzador sí, aquell no»), o quan necessites concedir permisos temporals. Mentre tres rols descriguin bé la realitat, afegir permisos és complexitat sense benefici. Escena Viva prepara l'estructura però manté els tres rols.

  1. Centralitzar la política

L'antipatró: if (req.usuari.rol === 'administrador' || ...) escampat per dotze controladors. Quan canviï una regla caldrà trobar-los tots, i el que s'oblidi serà un forat.

// src/autoritzacio/politica.js
'use strict';
const { PERMISOS, PERMISOS_PER_ROL } = require('./permisos.js');
// Funcions PURES: sense req, sense base de dades, sense Express. Per aixo es poden
// provar soles i de manera exhaustiva (modul 9).
const tePermis = (usuari, permis) =>
  Boolean(usuari?.rol) && (PERMISOS_PER_ROL[usuari.rol] || []).includes(permis);
// Regla de propietat: la sala de l'esdeveniment ha de ser l'assignada a l'organitzador.
const potGestionarEsdeveniment = (usuari, esdeveniment) => tePermis(usuari, PERMISOS.ESDEVENIMENT_PUBLICAR)
  && (usuari.rol === 'administrador' || esdeveniment.sala === usuari.salaAssignada);
const potVeureComanda = (usuari, comanda) =>
  tePermis(usuari, PERMISOS.COMANDA_LLEGIR_QUALSEVOL)
  || String(comanda.usuariId) === String(usuari.id);
// Ningu no canvia el seu propi rol, ni tan sols un administrador: evita tant
// l'escalada com l'autobloqueig accidental.
const potCanviarRol = (usuari, objectiu) => tePermis(usuari, PERMISOS.USUARI_GESTIONAR)
  && String(usuari.id) !== String(objectiu.id);
module.exports = { tePermis, potGestionarEsdeveniment, potVeureComanda, potCanviarRol };

Els avantatges són concrets: es prova sola —sense servidor ni base de dades; al mòdul 9 escriurem una taula de casos que recorre la matriu de la secció 3 sencera—; es llegeix com l'especificació, perquè potGestionarEsdeveniment(usuari, esdeveniment) es contrasta amb la fila de la taula; canvia en un sol lloc; i s'audita llegint un fitxer, no dotze controladors. Els middleware passen a ser embolcalls finíssims:

// Us: router.patch('/:id/publicar', autenticar, carregarEsdeveniment,
//        exigirPolitica(potGestionarEsdeveniment), publicarEsdeveniment);
function exigirPolitica(comprovacio) {
  return function aplicar(req, res, next) {
    if (!req.usuari) { return next(new ErrorDAutenticacio("Has d'iniciar sessio")); }
    if (!comprovacio(req.usuari, req.recurs)) {
      return next(new ErrorDAutoritzacio('No tens permis per a aquesta operacio'));
    }
    return next();
  };
}

  1. Auditoria i escalada de privilegis

Auditoria. Tota decisió sensible deixa rastre: qui, què, quan, sobre què i amb quin resultat.

// src/serveis/auditoria.js -> accio: 'esdeveniment:publicar' | 'usuari:canviar-rol';
// resultat: 'permes' | 'denegat'. Estructurat (JSON), no text lliure, per
// poder consultar-lo i alertar; idPeticio ve del modul 6 i ho correlaciona tot.
function registrarAccio({ actor, accio, recurs, resultat, idPeticio }) {
  console.log(JSON.stringify({ tipus: 'auditoria', marca: new Date().toISOString(),
    idPeticio, actorId: actor.id, actorRol: actor.rol, accio, recurs, resultat }));
}

Què mereix auditoria: canvis de rol, anul·lació d'entrades, publicació i finalització d'esdeveniments, accessos d'administrador a dades alienes, i els intents denegats (un assistent que prova deu rutes d'administrador és un senyal). Mai no es registren credencials ni tokens. El registre estructurat i el seu enviament a un sistema centralitzat es desenvolupen al mòdul 11.

Escalada de privilegis. La fallada clàssica cap en una línia innocent: Usuari.create(req.body) amb un cos que inclou "rol":"administrador". És assignació massiva, i per això l'esquema zod de registre de 08-02 porta .strict() i el controlador construeix l'objecte camp a camp amb rol: 'assistent' fixat pel servidor. El rol mai no és una dada d'entrada del registre.

// src/controladors/usuaris.js
async function canviarRol(req, res, next) {
  try {
    const { rol } = req.dadesValidades.cos; // zod: enum(ROLS), .strict()
    const objectiu = await Usuari.findById(req.params.id);
    if (!objectiu) { throw new RecursNoTrobat('Usuari no trobat'); }
    // La politica decideix; el controlador nomes obeeix.
    if (!potCanviarRol(req.usuari, objectiu)) {
      throw new ErrorDAutoritzacio('No tens permis per a aquesta operacio');
    }
    const rolAnterior = objectiu.rol;
    objectiu.rol = rol;
    await objectiu.save();
    // Un canvi de rol revoca les sessions: s'aplica ja, sense esperar que
    // caduqui el token d'acces (08-04).
    await revocarTotsElsRefrescos(objectiu._id);
    registrarAccio({ actor: req.usuari, accio: 'usuari:canviar-rol',
      recurs: String(objectiu._id), resultat: 'permes', idPeticio: req.idPeticio });
    res.json({ usuari: { id: objectiu._id, rol: objectiu.rol, rolAnterior } });
  } catch (error) { next(error); }
}

Errors Comuns i Consells

  • Comprovar el rol i oblidar la propietat. És l'IDOR de manual, la fallada d'autorització més freqüent que hi ha. I el seu revers: comprovar la propietat abans de carregar el recurs, impossible perquè la propietat és un atribut del recurs.
  • Retornar 403 on tocava 404, confirmant l'existència de recursos privats; o confiar en el rol del JWT per a accions crítiques, quan pot portar fins a 15 minuts caducat.
  • Assumir que administrador implica tots els permisos «per lògica»: escriu-ho a la matriu, perquè l'implícit acaba sent inconsistent. I autoritzar al client, o escriure Usuari.create(req.body) / Object.assign(usuari, req.body): escalada de privilegis servida.
  • Consell: escriu la matriu de permisos abans que el codi i converteix-la en la taula de casos de prova del mòdul 9: si una cel·la no té prova, aquella cel·la és una suposició. I fes que el filtre per propietari visqui a la signatura del repositori —la seguretat que no es pot oblidar és millor que la que cal recordar— i, per defecte, denega: una ruta nova sense política explícita hauria de ser inaccessible, no pública.

Exercicis

Exercici 1: trobar la fallada

Aquests dos punts d'entrada passen una revisió superficial i tenen una fallada d'autorització cadascun. Identifica-les i reescriu-los.

router.get('/api/comandes/:id', autenticar, async (req, res, next) => {
  const comanda = await Comanda.findById(req.params.id).lean();
  if (!comanda) { return next(new RecursNoTrobat('Comanda no trobada')); }
  res.json({ comanda });
});
router.get('/api/sales/:sala/vendes', autenticar, exigirRol('organitzador'), async (req, res) => {
  res.json({ vendes: await calcularVendes(req.params.sala) });
});

Exercici 2: implementar una fila de la matriu

Implementa PATCH /api/entrades/:codi/usar (marcar una entrada com a usada en entrar al recinte) respectant la matriu: només organitzador d'aquella sala o administrador. L'entrada ha d'estar en estat valida; si ja és usada o anullada, correspon un ConflicteDEstat. Escriu la ruta amb la seva cadena de middleware i la funció de política.

Exercici 3: rol nou

Escena Viva contracta personal de taquilla que pot marcar entrades com a usades, però no anul·lar-les, ni crear esdeveniments, ni veure vendes. Decideix si afegiries un rol taquilla o un permís solt, justifica l'elecció i escriu el canvi a permisos.js.

Solucions

Exercici 1. El primer punt d'entrada no comprova la propietat: qualsevol usuari autenticat llegeix la comanda de qualsevol altre coneixent-ne l'identificador. És un IDOR. El segon comprova el rol però no la sala, així que l'organitzador de la Sala Bóveda veu les vendes del Teatro Almendra; a més exclou els administradors, que segons la matriu sí que poden.

router.get('/api/comandes/:id', autenticar, async (req, res, next) => {
  try {
    // Filtre dins de la consulta: impossible oblidar-lo, i no distingeix
    // "no existeix" de "no es teu".
    const comanda = await obtenirComandaDUsuari(req.params.id, req.usuari.id);
    if (!comanda) { throw new RecursNoTrobat('Comanda no trobada'); }
    res.json({ comanda });
  } catch (error) { next(error); }
});
router.get('/api/sales/:sala/vendes', autenticar, exigirRol('organitzador', 'administrador'),
  (req, res, next) => {
    // La sala demanada ha de ser l'assignada, llevat d'administrador.
    if (req.usuari.rol !== 'administrador' && req.params.sala !== req.usuari.salaAssignada) {
      return next(new RecursNoTrobat('Sala no trobada'));
    }
    next();
  },
  async (req, res, next) => {
    try { res.json({ vendes: await calcularVendes(req.params.sala) }); } catch (e) { next(e); }
  });

Un administrador amb COMANDA_LLEGIR_QUALSEVOL necessitaria una branca a part, resolta amb potVeureComanda i registrada a l'auditoria (secció 12).

Exercici 2. Nota prèvia: marcarEntradaUsada ha de fer la transició de manera atòmica al repositori (findOneAndUpdate amb { codi, estat: 'valida' }), o dues validacions simultànies a la porta de l'Auditorio Ribera podrien passar totes dues. És la mateixa lliçó de concurrència del mòdul 7.

// src/autoritzacio/politica.js  (addicio)
const potUsarEntrada = (usuari, entrada) => tePermis(usuari, PERMISOS.ENTRADA_USAR)
  && (usuari.rol === 'administrador' || entrada.sala === usuari.salaAssignada);
// src/rutes/entrades.js
router.patch('/:codi/usar', autenticar, exigirRol('organitzador', 'administrador'),
  carregarEntradaPerCodi,            // deixa req.recurs
  exigirPolitica(potUsarEntrada),    // propietat de sala
  async (req, res, next) => {
    try {
      const entrada = req.recurs;
      // Transicio valida -> usada. Qualsevol altre origen es conflicte.
      if (entrada.estat !== 'valida') {
        throw new ConflicteDEstat(`L'entrada esta ${entrada.estat}`,
          { estatActual: entrada.estat, transicioEsperada: 'valida -> usada' });
      }
      registrarAccio({ actor: req.usuari, accio: 'entrada:usar', recurs: entrada.codi,
        resultat: 'permes', idPeticio: req.idPeticio });
      res.json({ entrada: await marcarEntradaUsada(entrada.codi) });
    } catch (error) { next(error); }
  });

Exercici 3. Un rol taquilla, no un permís solt: correspon a una funció real i estable del negoci, és explicable en una frase, i no és una excepció puntual sobre un altre rol. Els permisos individuals es justifiquen quan cal fer excepcions cas per cas; aquí hi ha una categoria de persones.

const PERMISOS_PER_ROL = Object.freeze({
  assistent: [PERMISOS.COMANDA_LLEGIR_PROPIA],
  taquilla: [PERMISOS.ENTRADA_USAR], // ni anullar, ni crear, ni veure vendes
  organitzador: [/* ...igual que abans */],
  administrador: Object.values(PERMISOS),
});

Canvis necessaris: afegir 'taquilla' a ROLS al model Usuari (amb migració, M7), afegir la fila a la matriu de la secció 3, i actualitzar la política i les seves proves. Que el canvi es localitzi en tan pocs llocs és el benefici d'haver centralitzat la política.

Conclusió

Escena Viva ja no sap només qui truca: sap què pot fer cadascú. Hem comparat els models d'autorització i triat RBAC amb coneixement de causa, escrit la matriu de permisos com a especificació executable, i construït exigirRol com a factoria amb la distinció correcta entre 401 (no sé qui ets) i 403 (sé qui ets i no et toca). Sobretot, hem afrontat el que RBAC no cobreix: la propietat del recurs, comprovada després de carregar el recurs i, encara millor, filtrada dins de la consulta des del repositori, que és la manera de fer impossible l'IDOR en lloc de recordar d'evitar-lo. Hem vist per què el client no autoritza mai, quan rellegir el rol a la base en comptes de fiar-se del token, com fer el salt a permisos fins si cal, i per què la política viu a src/autoritzacio/politica.js en funcions pures que es proven soles. I hem tancat l'escalada de privilegis on neix: ningú no s'assigna el seu propi rol.

Queda una lliçó per acabar el mòdul. Tenim autenticació sòlida i autorització correcta, però una API defensable és alguna cosa més: límits de peticions afinats per ruta, sostres de mida i de paginació, protecció davant d'injecció i assignació massiva, capçaleres de seguretat amb criteri, HTTPS obligatori, gestió de secrets i monitoratge d'esdeveniments de seguretat. A Bones Pràctiques de Seguretat en APIs recorrem l'OWASP API Security Top 10 punt per punt sobre Escena Viva, i convertim tot el que hem après en una llista de comprovació que es pot fer servir de debò.

Curs de Node.js: De Principiant a Avançat

Mòdul 1: Introducció a Node.js

Mòdul 2: Conceptes Bàsics

Mòdul 3: Sistema de Fitxers i E/S

Mòdul 4: HTTP i Servidors Web

Mòdul 5: NPM i Gestió de Paquets

Mòdul 6: Framework Express.js

Mòdul 7: Bases de Dades i ORMs

Mòdul 8: Autenticació i Autorització

Mòdul 9: Proves i Depuració

Mòdul 10: Temes Avançats

Mòdul 11: Desplegament i DevOps

Mòdul 12: Projectes del Món Real

© Copyright 2026. Tots els drets reservats