Sistema de permisos y roles granulares en app web
La llamada del jueves por la tarde
Eran las seis y cuarto cuando el CTO de una startup de logística convocó a su equipo a una reunión imprevista. Un comercial acababa de exportar, sin querer, la cartera completa de clientes de la competencia interna. No había hackeo ni mala fe. Solo un botón al que tenía acceso porque su rol era, literalmente, "usuario interno". Esa noche bloquearon el endpoint con un if rápido. Al lunes había tres if nuevos. Al mes, treinta.
La escena se repite en aplicaciones web a medida que crecen más rápido que su arquitectura de autorización. Al principio basta con distinguir admin y no admin: dos perfiles, un condicional y a producción.
Tres meses después llegan los matices. El responsable de ventas necesita ver los informes financieros sin tocarlos. El auditor externo pide lectura de facturación pero ni se le ocurra mirar nóminas. La becaria crea borradores pero no publica. El cliente del portal solo ve lo suyo. Cada requisito se traduce en un parche, una tabla más, una excepción mal colocada. El código de autorización se desparrama hasta que nadie sabe quién puede borrar una factura un martes a las nueve.
Diseñar un sistema permisos roles granulares app web desde el primer commit evita ese desparrame. La clave no es elegir el modelo más sofisticado, sino acertar con la combinación mínima que aguante el crecimiento sin convertirse en un laberinto.
RBAC: el caballo de batalla que resuelve casi todo
Antes de hablar de modelos exóticos conviene asentar el más probado. RBAC, Role-Based Access Control, organiza el permiso alrededor del rol. Los permisos no se asignan persona a persona, sino que se agrupan en perfiles ("editor", "gestor de ventas", "auditor financiero") y cada usuario hereda lo que su rol le concede.
Dentro de RBAC hay tres niveles, cada uno para un problema distinto.
RBAC plano. Un usuario tiene uno o varios roles. Cada rol lleva un conjunto cerrado de permisos. Los roles no se relacionan entre sí. Sirve para aplicaciones con pocos perfiles diferenciados y se implementa en una tarde.
RBAC jerárquico. Los roles forman un árbol. "Director" hereda todo lo de "gestor", que hereda lo de "empleado". Se evita duplicar permisos y el mantenimiento se vuelve manejable cuando hay diez o veinte perfiles.
RBAC con restricciones. Añade reglas que cruzan roles. El caso clásico es la separación de funciones: la misma persona no puede ser "aprobador de pagos" y "emisor de pagos" a la vez. Lo pide cualquier auditor de SOX o ISO 27001 que se siente a revisar la aplicación.
Cómo aterriza el modelo en la base de datos
El esquema mínimo viable cabe en cinco tablas:
users.roles, con nombre, descripción yparent_role_idpara la jerarquía.permissions, los permisos atómicos en formatorecurso:accion(invoices:read,invoices:create,invoices:delete).role_permissions, puente entre roles y permisos.user_roles, puente entre usuarios y roles.
Nombrar los permisos como recurso:accion parece una tontería estética hasta que toca consultarlos. Filtrar los permisos sobre facturas se reduce a un LIKE 'invoices:%' y el código queda tan legible como hasPermission(user, 'invoices:delete'). Cualquier desarrollador nuevo entiende esa línea sin abrir documentación.
La jerarquía se resuelve con un CTE recursivo en PostgreSQL o MySQL 8. Si la lectura es frecuente, conviene cachear los permisos efectivos en Redis o materializarlos en una columna que se recalcule al editar un rol. Resolver el árbol en cada request, con tráfico real, mata la latencia antes de lo que parece.
ABAC: cuando los roles se quedan cortos
El problema no termina ahí. Hay reglas que no caben en un rol por más que se estire el modelo. "Un comercial puede ver los pedidos de su región siempre que el importe sea inferior a 50.000 euros y dentro del horario laboral." Cuatro atributos en una frase: departamento, región, importe y hora. Modelarlo solo con RBAC obligaría a crear decenas de roles específicos hasta agotar la combinatoria.
ABAC, Attribute-Based Access Control, evalúa atributos del usuario, del recurso, de la acción y del contexto. La pregunta deja de ser "¿tiene el rol correcto?" y pasa a "¿cumple las condiciones?". El motor de políticas resuelve cada acceso evaluando esas reglas.
Dónde compensa pagar el coste de ABAC
Cuatro escenarios donde ABAC justifica su complejidad:
- Multitenancy estricta. El
tenant_iddel recurso debe coincidir con el del usuario. Una política se aplica a todas las tablas. - Restricciones de tiempo, ubicación o IP. Acceso solo desde la red corporativa, en horario de oficina o desde ciertos países.
- Propiedad del recurso. Editar lo que cada uno crea, leer lo de los demás.
- Niveles de confidencialidad. El clearance del usuario debe ser igual o superior al del documento.
La otra cara de la moneda es que las políticas ABAC se auditan peor. "¿Quién puede ver este registro?" obliga a ejecutar todas las reglas contra todos los usuarios. Los tests crecen, los logs crecen y explicárselo al usuario de negocio se vuelve un ejercicio de paciencia.
El híbrido que casi siempre gana
La práctica enseña que el modelo que mejor envejece es RBAC como espina dorsal y ABAC para los matices contextuales. Los roles definen el qué puede hacer cada perfil; las políticas de atributos filtran sobre qué recursos concretos puede hacerlo. El middleware encadena las dos comprobaciones: primero el permiso base, después la condición contextual. Si cualquiera falla, 403 y a otra cosa.
Permisos por recurso: el siguiente escalón
projects:read te dice que el usuario puede leer proyectos. No te dice si puede leer ese proyecto. Y esa diferencia es la que separa una aplicación correcta de una con fuga de datos.
Existen dos enfoques consolidados.
Las listas de control de acceso (ACL) asignan a cada recurso una lista de usuarios o roles con sus permisos. Una tabla resource_permissions con resource_type, resource_id, grantee_type, grantee_id y permission_level. Es flexible y se vuelve pesada cuando el catálogo crece a millones de filas.
Los permisos basados en relaciones, popularizados por Google Zanzibar y materializados en SpiceDB u OpenFGA, deducen el acceso de la estructura organizativa. Si el usuario pertenece al equipo dueño del proyecto, ve el proyecto. La potencia es enorme; el coste operativo de mantener una pieza adicional de infraestructura, también.
Empujar el filtro al SQL
Un error caro: traer 100.000 registros del servidor y filtrar en memoria cuáles ve el usuario. Funciona con diez filas y revienta con cien mil. La autorización debe traducirse en WHERE dentro de la consulta. Si el usuario es del tenant 42, la query lleva WHERE tenant_id = 42. Si tiene acceso a los proyectos 1, 5 y 12, lleva WHERE project_id IN (1, 5, 12). La técnica se conoce como push down de autorizaciones y es lo que diferencia un sistema que escala de uno que se cae con los primeros mil usuarios.
En ORMs como Prisma, Sequelize o SQLAlchemy esto se monta con scopes que inyectan las restricciones de forma automática. Una vez configurado, el desarrollador escribe Project.findAll() y el ORM hace el resto.
El middleware: tres capas que no se mezclan
Autenticación responde a quién eres. Autorización responde a qué puedes hacer. Meterlo todo en el mismo middleware produce código que nadie quiere mantener.
El patrón limpio separa tres responsabilidades:
- Autenticación. Valida el JWT o la sesión y adjunta la identidad al request.
- Carga de permisos. Recupera roles y permisos efectivos desde base de datos o desde el token. Se cachea durante la vida del request.
- Guards de autorización. Decoradores o funciones aplicados a cada endpoint para verificar permisos concretos.
Un guard reutilizable se ve más o menos así:
function requirePermission(permission) {
return function(request, response, next) {
if (request.user.permissions.includes(permission)) {
next()
} else {
response.status(403).json({ error: 'Acceso denegado' })
}
}
}
Cuando entra autorización por recurso, el guard carga primero el objeto y después llama a una función tipo canAccess(user, resource, action) que concentre la evaluación: RBAC, ABAC y ACL si las hay. Centralizar esa función paga dividendos a los seis meses, cuando alguien quiere cambiar una regla y solo tiene que tocar un sitio.
Auditoría: la cerradura sin registro no sirve
Un sistema de permisos sin trazas es una caja negra. Cuando llega la pregunta "¿cómo entró este usuario al fichero de nóminas el martes?", o se tiene respuesta o se tiene un problema con el delegado de protección de datos.
Conviene registrar al menos cuatro eventos:
- Cambios en roles y permisos: quién modificó qué, qué entró y qué salió.
- Asignaciones de roles a usuarios: quién promocionó a quién y cuándo.
- Accesos denegados: usuario, recurso, permiso faltante y timestamp.
- Accesos a datos sensibles: lecturas de datos personales, exportaciones masivas, descargas de informes financieros.
Los logs van en una tabla separada con retención larga, o en un servicio externo tipo Datadog o S3 con inmutabilidad. Lo que nunca debe ocurrir: que el usuario auditado pueda borrar sus propias trazas.
Tests: la red de seguridad que evita el titular
Los bugs de autorización son los más peligrosos del catálogo. Permisivos de más exponen datos. Restrictivos de más bloquean al cliente que paga. Necesitan cobertura específica y disciplinada.
Tests por endpoint y permiso. Para cada endpoint, tres escenarios mínimos: usuario con permiso (200), usuario sin permiso (403), usuario sin autenticar (401). La generación se automatiza a partir de la matriz de permisos.
Regresión de roles. Mantén un fixture con la configuración esperada de cada rol. Un test compara la real contra el fixture y avisa cuando alguien añade silenciosamente un permiso a "editor".
Aislamiento entre tenants. Crea dos usuarios de cuentas distintas, genera datos para ambos y verifica que ninguno ve los del otro. Este test ha evitado más filtraciones que cualquier auditoría manual de cinco cifras.
Herencia. Si hay jerarquía, comprueba que el hijo recibe los permisos del padre y que revocar arriba propaga hacia abajo.
La arquitectura que aguanta cinco y quinientos usuarios
Diseñar autorización granular es un equilibrio incómodo. Demasiado simple y cada cliente nuevo trae un parche que rompe el modelo. Demasiado complejo y el equipo pasa más tiempo configurando permisos que escribiendo producto.
El arranque sensato suele ser el mismo: RBAC jerárquico, permisos nombrados como recurso:accion, una tabla de auditoría desde el día uno y tests automatizados por endpoint. Sobre esa base se incorpora ABAC o permisos por recurso solo cuando aparece un requisito que lo justifique. Empezar al revés, con SpiceDB y políticas Rego para una app de tres roles, suele acabar en abandono.
Si tu aplicación necesita un sistema de permisos que funcione para cinco usuarios hoy y para quinientos el año que viene, sin convertirse en un laberinto de condicionales anidados, podemos diseñarlo juntos. Una arquitectura de autorización bien planteada desde el inicio escala con el producto en lugar de frenarlo.