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
- Models d'autorització
- Els tres rols d'Escena Viva
- La matriu de permisos
- El middleware
exigirRol - 401 davant de 403
- El que RBAC no cobreix: la propietat del recurs
- Filtrar dins de la consulta, no comparar després
- L'autorització no es fa mai al client
- Quan el rol del token està caducat
- Permisos més fins que els rols
- Centralitzar la política
- Auditoria i escalada de privilegis
- Errors comuns, exercicis i conclusió
- 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.
- 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.
- 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.
- El middleware
exigirRol
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.
- 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).
- 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.
- 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.
- 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.
- 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.
- 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.
- 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();
};
}
- 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
roldel 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
- Què és Node.js?
- Instal·lació i Configuració de l'Entorn
- El Teu Primer Programa en Node.js
- El REPL de Node.js
- JavaScript Modern per a Node.js
- El Projecte del Curs: la Plataforma Escena Viva
Mòdul 2: Conceptes Bàsics
- Arquitectura de Node.js
- El Bucle d'Esdeveniments (Event Loop)
- Callbacks i Programació Asíncrona
- Promeses i async/await
- Esdeveniments i EventEmitter
- Mòduls CommonJS i require()
- Mòduls ES i Interoperabilitat
Mòdul 3: Sistema de Fitxers i E/S
- Lectura i Escriptura de Fitxers
- El Mòdul fs a Fons
- Rutes Multiplataforma amb el Mòdul path
- Treballant amb Streams
- Streams de Transformació i pipeline
- Buffers i Dades Binàries
Mòdul 4: HTTP i Servidors Web
- Creant un Servidor HTTP Simple
- Gestió de Sol·licituds i Respostes
- Enrutament Manual
- Servint Fitxers Estàtics
- Rebent Dades: Cossos de Petició i JSON
- Consumint APIs Externes des de Node.js
Mòdul 5: NPM i Gestió de Paquets
- Introducció a NPM i package.json
- Instal·lació i Ús de Paquets
- Versionat Semàntic i package-lock
- Scripts d'npm i Automatització del Projecte
- Creació i Publicació de Paquets
- Seguretat i Manteniment de Dependències
Mòdul 6: Framework Express.js
- Introducció a Express.js
- Configuració d'una Aplicació Express
- Enrutament a Express
- Middleware
- Middleware de Tercers Essencials
- Validació de Dades d'Entrada
- Gestió d'Errors
Mòdul 7: Bases de Dades i ORMs
- Introducció a les Bases de Dades
- Usant MongoDB amb Mongoose
- Operacions CRUD
- Relacions, Poblat i Consultes Avançades
- Usant Bases de Dades SQL amb Sequelize
- Migracions, Transaccions i Dades de Prova
Mòdul 8: Autenticació i Autorització
- Introducció a l'Autenticació
- Registre d'Usuaris i Hash de Contrasenyes
- Sessions i Galetes amb Passport.js
- Autenticació amb JWT
- Control d'Accés Basat en Rols
- Bones Pràctiques de Seguretat en APIs
Mòdul 9: Proves i Depuració
- Introducció a les Proves
- Proves Unitàries amb Mocha i Chai
- Dobles de Prova amb Sinon
- Proves d'Integració
- Cobertura i Automatització de les Proves
- Depuració d'Aplicacions Node.js
Mòdul 10: Temes Avançats
- El Mòdul Cluster
- Fils de Treball (Worker Threads)
- Memòria Cau i Cues de Treball amb Redis
- Optimització del Rendiment
- Construcció d'APIs RESTful
- GraphQL amb Node.js
Mòdul 11: Desplegament i DevOps
- Configuració i Variables d'Entorn
- Registre i Monitoratge en Producció
- Usant PM2 per a la Gestió de Processos
- Empaquetatge amb Docker
- Desplegant a Heroku i Altres PaaS
- Integració i Desplegament Continus
