~/blog $ git show 2026-08-13
Los roles no son permisos: por qué esconder una página no es un guard
· 6 min de lectura · Victor Benavides
Al inicio de mi carrera "aseguré" una funcionalidad de admin escondiendo el botón.
Se sintió como un fix. Podía verlo funcionar: entras como usuario normal, el item del menú no está. Entras como admin, vuelve a aparecer. Lo demostré. Todos asintieron. Dos semanas después alguien pegó la URL en un chat, un colega hizo clic por curiosidad, y la página cargó preciosa para una persona que no tenía nada que hacer ahí. Nada había sido protegido. Yo había cambiado lo que la aplicación ofrecía, no lo que permitía.
Esa es toda la lección, y he visto versiones de ella repetirse en cada codebase en el que he trabajado desde entonces — incluido el mío, cuando empecé a diseñar autorización para una plataforma multi-tenant y descubrí que el problema tiene más capas que "admin o no admin".
Tres preguntas distintas
La mayoría de los bugs de autorización vienen de colapsar tres preguntas en una:
- ¿Quién eres? — autenticación. Un token prueba identidad.
- ¿Qué puedes hacer? — autorización. Una regla decide una acción.
- ¿Qué deberías ver? — presentación. Una UI decide un layout.
La tercera es la única con feedback instantáneo. Escondes el item del menú y puedes ver cómo desaparece. No existe un momento igual de satisfactorio al agregar un check del lado del servidor: no pasa nada visible, la funcionalidad sigue igual, y nadie te lo agradece. Así que lo visible se hace y lo invisible se convierte en un ticket.
Pero el navegador no es un límite de seguridad. Es una sugerencia de renderizado, corriendo en una máquina que no controlas, dentro de un programa que el usuario puede inspeccionar, con una pestaña de red que muestra cada endpoint que tu app ha llamado alguna vez. Si lo único entre un usuario y una acción es un componente que no se renderizó, tienes decoración, no autorización.
"Admin" no es un hecho sobre una persona
Aquí es donde se puso genuinamente interesante para mí, y donde creo que el consejo estándar se queda corto.
Las apps single-tenant te enseñan a pensar en los roles como propiedades de los usuarios: esta persona es admin. Los sistemas multi-tenant rompen eso de inmediato, porque "admin" nunca es una verdad global. Eres admin de algo. De un local, un workspace, una organización — y probablemente un don nadie en el de al lado.
En el momento en que la misma cuenta puede aparecer en dos contextos, un rol deja de ser un adjetivo y se vuelve una relación. Lo que significa que el objeto interesante de tu sistema no es el usuario ni el rol — es la membresía: esta identidad, en este contexto, con estos permisos.
Esa distinción suena académica hasta que miras dónde viven normalmente los roles: en el token. Los tokens son planos. Son una lista de strings, entregada al hacer login, y no tienen idea de qué local está mirando el usuario en este momento. Pregúntale a un claim plano "¿esta persona es admin?" y te va a responder que sí, felizmente — sin que nadie le haya dicho jamás admin de qué. El check pasa. Se sirven los datos equivocados. Nada falla, nada se loguea, y todos los dashboards siguen en verde.
La regla con la que terminé: nunca autorices desde un string de rol solo. Autoriza desde la tupla — identidad, contexto, permiso — resuelta en el servidor, para la cosa específica que se está tocando.
Los tokens son una foto, no una suscripción
La segunda trampa es el tiempo.
Un token es una fotografía de tus permisos en el instante en que hiciste login. Revócale el acceso a alguien, bájale el rol, sácalo de una organización — nada de eso alcanza a un token que ya está en el navegador de esa persona. Hasta que expire o se refresque, ese usuario carga una autoridad que ya le quitaste. Si tus sesiones son largas y nunca se refrescan, "removido" puede significar "sigue con todos los privilegios hasta que se le ocurra cerrar sesión".
Lo que produce una pregunta genuinamente incómoda que vale la pena hacerle a tu propio sistema: si revoco el acceso de alguien ahora mismo, ¿cuándo surte efecto de verdad? Si la respuesta honesta es "cuando expire su token", entonces tu revocación es una sugerencia con retraso incluido.
La única prueba que cuenta
Hay una prueba simple que corta todo esto, y toma como un minuto.
Toma un token válido de un usuario que no debería poder hacer algo. Llama al endpoint directo — curl, Postman, lo que sea. Sáltate la UI por completo, porque los atacantes y los colegas curiosos también se la van a saltar.
Si obtienes un 403, tienes autorización. Si obtienes datos, tienes un botón escondido. Y si tu reacción al leer ese párrafo fue "la verdad no estoy seguro de qué pasaría" — esa incertidumbre ya es el hallazgo. La autorización que no has probado desde fuera de la UI es autorización que estás asumiendo.
El trade-off, porque siempre hay uno
Hacer esto bien no es gratis, y prefiero decirlo a fingir lo contrario.
Cada endpoint paga un check — en latencia, en código, en una cosa más que se puede hacer mal. Un modelo de permisos lo bastante rico para expresar organizaciones reales se complica rápido, y los modelos de seguridad complicados fallan a su manera especial: nadie los entiende, así que todos otorgan permisos amplios "para que funcione". Vas a romper tus propias herramientas internas durante el rollout, varias veces, y cada rotura se va a sentir como si el problema fuera el enforcement y no el permiso faltante.
Ahí también se esconde una decisión real: ¿enforzar de inmediato y descubrir cada hueco a la mala, o correr primero en modo auditoría y loguear lo que se habría denegado? El modo auditoría es más amable y también es una trampa — un log que nadie lee es el equivalente en seguridad de un dashboard verde sobre un sistema que no funciona. Ponle fecha al cambio, o nunca lo vas a hacer.
Lo que ganas a cambio es un sistema donde la respuesta a "¿esta persona puede hacer eso?" vive en un solo lugar, es testeable sin un navegador, y no cambia según lo que alguien escribió en la barra de direcciones.
La versión incómoda
Si soy honesto, el instinto del botón escondido nunca se va del todo. Es la forma más rápida de hacer que un demo se vea bien, y todos nosotros lo hemos entregado al menos una vez diciéndonos que el check de verdad venía después.
Así que la pregunta que aprendí a hacer no es "¿lo escondimos?". Es: ¿qué pasa si igual encuentran la URL? Si la respuesta es nada malo, estás bien. Si la respuesta es un encogimiento de hombros, no tienes un permiso — tienes un layout.
El sábado: le puse una terminal funcional a este blog, y escribir sudo ahí produce una decisión de autorización sobre la que estoy bastante más seguro. ✨