Volver a escritos

RBAC, ABAC, ReBAC: tres formas de pensar los permisos

Cuándo bastan los roles, cuándo necesitas atributos y cuándo el problema es de relaciones. Una guía pragmática para elegir un modelo de autorización antes de que se convierta en un if anidado de quince niveles.

Toda aplicación termina haciéndose la misma pregunta: ¿puede este usuario hacer esto sobre esta cosa? Al principio se responde con un if. Luego con dos. Después aparece una columna role en la tabla de usuarios. Más tarde una de is_admin, otra de is_owner, y un día alguien pide “que los managers vean los reportes pero solo de su región, salvo en diciembre” — y el if ya tiene quince niveles.

Hay tres modelos clásicos para organizar esa respuesta sin perder la cabeza. No son religiones: son formas distintas de modelar los datos que alimentan la misma función.

la pregunta, en abstracto

Cualquier sistema de autorización se reduce a una función:

function can(user: User, action: Action, resource: Resource): boolean;

Lo único que cambia entre modelos es dónde viven los datos que esa función consulta y cómo se combinan. Roles, atributos, relaciones. Tres maneras de organizar la misma información.

RBAC — roles entre el usuario y el permiso

En Role-Based Access Control los usuarios no tienen permisos: tienen roles. Los permisos cuelgan del rol. Cambias a un usuario de rol y sus capacidades cambian de golpe.

const roles = {
  viewer: ["article:read"],
  editor: ["article:read", "article:write"],
  admin: ["article:read", "article:write", "article:delete", "user:manage"],
};

function can(user: User, action: string) {
  return roles[user.role].includes(action);
}

Funciona sorprendentemente bien cuando la realidad del negocio se deja describir con tres o cuatro etiquetas. La mayoría de los SaaS pequeños viven aquí durante años: viewer, editor, admin, y poco más.

Dónde se rompe. En cuanto un permiso depende del recurso concreto, no del tipo de recurso, RBAC empieza a empujar para todos lados. “Los editores solo pueden editar artículos de su equipo” no cabe en un rol — cabe en una relación. La salida fácil es inventar más roles: editor_equipo_a, editor_equipo_b, editor_equipo_a_solo_lectura_en_diciembre. Esa explosión combinatoria es la señal de que el modelo se quedó corto.

ABAC — el permiso es una función de atributos

Attribute-Based Access Control tira los roles a la basura (o los degrada a un atributo más) y decide en función de los atributos del usuario, del recurso y del entorno. La política es una expresión que se evalúa contra esos atributos.

function can(user: User, action: "read", article: Article, ctx: Context) {
  if (article.visibility === "public") return true;
  if (article.authorId === user.id) return true;
  if (user.department === article.department && ctx.now < article.embargoUntil)
    return false;
  if (user.department === article.department) return true;
  return false;
}

Eso es ABAC en miniatura. Con un motor de políticas de verdad (OPA, Cedar, Casbin) la lógica se escribe en un lenguaje declarativo separado del código, y se evalúa pasándole los atributos del usuario, el recurso y el contexto.

Lo que gana. Expresividad. Casi cualquier regla de negocio se puede escribir si tienes los atributos adecuados.

Lo que cuesta. Tres cosas, y conviene tenerlas claras antes de adoptarlo:

  1. Depurar es difícil. “¿Por qué no puedo entrar?” deja de tener una respuesta corta. Un buen motor te da una traza; los demás te dan un false y un encogimiento de hombros.
  2. La UI tiene que reflejar la política. No basta con que el backend diga que no — el botón no debería estar ahí. Eso significa exponer la decisión también al frontend, idealmente con la misma política.
  3. Los atributos hay que mantenerlos. Si el department del usuario está desactualizado, la política decide bien sobre datos mal.

ReBAC — los permisos son grafos

Relationship-Based Access Control es lo que está debajo de Google Drive, GitHub y casi cualquier producto donde el verbo principal es compartir. La pregunta deja de ser “¿qué rol tienes?” y pasa a ser “¿qué relación tienes con este recurso?”.

Los datos son tuplas:

documento:reporte-q1   owner    usuario:maria
documento:reporte-q1   editor   grupo:finanzas
grupo:finanzas         member   usuario:luis

Y las reglas dicen cómo se heredan los permisos a través de esas relaciones:

relation owner: User
relation editor: User | Group#member
relation viewer: User | Group#member

permission read = viewer + editor + owner
permission write = editor + owner
permission delete = owner

Para preguntar “¿puede Luis leer el reporte?” el motor recorre el grafo: Luis es member de finanzas, que es editor de reporte-q1, y editor implica read. Sí.

Esa idea — autorización como grafo + motor que lo recorre — es la que Google describió en el paper de Zanzibar y hoy implementan productos como SpiceDB, OpenFGA o Permify.

Dónde brilla. Cuando el producto es compartir: documentos, repos, carpetas anidadas, equipos con sub-equipos, herencia por jerarquía. Tratar de modelar GitHub con RBAC puro es una tortura; con ReBAC es casi natural.

Lo que cuesta. Una pieza más de infraestructura. Necesitas mantener las tuplas sincronizadas con tu base de datos principal — cada vez que alguien se une a un equipo, cada vez que se comparte un documento, hay que escribir en los dos sitios. Y los problemas de consistencia entre ellos son reales.

cuál elegir

La trampa es elegir por estética. ABAC suena general, ReBAC suena moderno, y RBAC suena aburrido. El criterio útil es otro: ¿cuál es el modelo más simple que describe tu dominio sin obligarte a inventar roles imposibles?

  • Empieza con RBAC. Tres o cuatro roles. Resuelve el 90% de los SaaS B2B antes de que el producto necesite algo más.
  • Súbete a ABAC cuando aparezcan reglas que dependen de atributos del recurso o del contexto — horarios, regiones, estados — y estés inventando roles cuya única razón de existir es codificar esos atributos.
  • Vete a ReBAC cuando el verbo central del producto sea compartir o heredar: documentos, carpetas, equipos anidados, repos. Si el usuario va a invitar a otros usuarios a cosas concretas, ReBAC es la abstracción correcta.

Y nada impide combinarlos. La mayoría de los sistemas reales son RBAC para lo general + reglas ABAC para los casos finos, o ReBAC para los recursos compartidos + roles globales para distinguir clientes de administradores de la plataforma.

detalles que importan

Un único punto de decisión. Sea cual sea el modelo, la decisión debería pasar siempre por la misma función. Si en algunos sitios consultas user.role === "admin" directamente, has perdido — el día que cambie la política, te quedan cien sitios que actualizar y no sabes cuáles.

La UI tiene que coincidir con la política. No es seguridad — el backend ya bloquea — pero un botón que al pulsarse muestra “no tienes permiso” es una mala experiencia. Expón los resultados de can(...) al frontend y úsalos para esconder o deshabilitar acciones.

Auditoría. Cada decisión interesante debería dejar rastro: quién, qué, sobre qué, cuándo, con qué política. El día que alguien pregunte “¿quién borró esto?” vas a querer poder responder en menos de cinco minutos.

Tests con casos negativos. Los tests de autorización son mucho más útiles probando lo que no se puede hacer que lo que sí. “Un viewer no puede borrar”, “un editor de otro equipo no puede editar”, “un usuario expirado no puede leer”. Si solo pruebas la ruta feliz, no estás probando autorización.

el resumen, en una frase

  • RBAC: el usuario tiene un rol, el rol tiene permisos.
  • ABAC: el permiso es una función de atributos del usuario, del recurso y del entorno.
  • ReBAC: el permiso se deduce recorriendo un grafo de relaciones.

Tres formas de organizar los mismos datos. La buena noticia es que casi nunca tienes que elegir una sola — la mala es que tienes que saber cuál es cada cuál para no terminar implementando ReBAC a mano sobre una columna role.

· ·