Authorization

RBAC

RBAC responde a una sola pregunta por solicitud: ¿tiene este principal permiso para realizar esta acción sobre este recurso? Los roles agrupan permisos, y una única comprobación can() mantiene la respuesta consistente en todas partes.

intermediate15 min readUpdated 16 sept 2026
authorize.ts
ts
// authorize.ts
export type Action = "read" | "create" | "update" | "delete" | "manage";

export function can(
  user: Principal,
  action: Action,
  resource?: Resource
): boolean {
  const permission = `${resource?.type ?? "global"}:${action}`;

  // 1. Does any role grant this action at all?
  if (!user.permissions.has(permission)) return false;

  // 2. Tenant isolation: never cross the boundary.
  if (resource && resource.tenantId !== user.tenantId) return false;

  // 3. Writes on a resource require ownership or manage.
  if (resource && action !== "read" && resource.ownerId !== user.id) {
    return user.permissions.has(`${resource.type}:manage`);
  }

  return true;
}
Modelo
Usuarios a roles a permisos
Llamada principal
can(user, action, resource)
Por defecto
Denegar a menos que esté explícitamente permitido
Almacenamiento
Tablas de unión en SQL
Granularidad
Nivel de ruta y recurso
Fallo común
IDOR y falta de comprobaciones de propiedad
Siguiente paso
ABAC y policy engines
Estado de fallo
403 Forbidden

Por que importa

Por qué RBAC es robusto en producción

Permisos, no cadenas de roles

Otorga permisos nombrados como posts:update y comprueba esos. Los nombres de los roles se convierten en etiquetas para humanos en lugar de cadenas dispersas por tus handlers.

Los roles agrupan permisos

Un rol es un conjunto reutilizable de concesiones. Añadir un moderador significa componer permisos existentes en lugar de editar cada ruta.

Un solo lugar para decidir

Un único helper can() significa que la regla de quién puede hacer qué reside en un solo archivo, evitando que los guards de ruta y las comprobaciones de recursos diverjan.

La imagen completa

Las tres capas de una comprobación

Identificar al principal, expandir sus roles en permisos y luego validar una acción contra un recurso.

Principal

Identificar

La autenticación prueba quién está llamando. RBAC comienza solo después de que la solicitud tenga un usuario y un tenant confiables asociados.

Roles

Agrupar

Los roles agrupan permisos en conjuntos basados en funciones, como viewer, editor o admin, y los usuarios reciben uno o más roles.

Permisos

Decidir

La concesión atómica de una acción sobre un recurso. Cada comprobación se reduce a probar un permiso contra un objetivo.

HTML5 de un vistazo

Las piezas que construirás

Permisos

posts:update, billing:read, users:manage — los átomos del modelo.

Roles

Paquetes nombrados como admin, editor y viewer que agrupan permisos.

Matriz

Una cuadrícula legible de roles frente a permisos, la fuente de verdad para revisiones.

Multi-tenant

Un tenant id restringe cada concesión para que un cliente no pueda ver los datos de otro.

Jerarquía

Los roles senior heredan los permisos de los junior en lugar de duplicarlos.

Auditoría

Registra cada permiso y denegación para poder explicar una decisión a posteriori.

Modelo de datos

El esquema detrás de la comprobación

Los permisos son los átomos, los roles son paquetes nombrados, y dos tablas de unión conectan usuarios con roles y roles con permisos.

Roles, permisos y tablas de uniónPostgreSQL schema
  • userstableEl principal autenticado, con una columna tenant_id
  • rolestableUn paquete nombrado como admin, editor o viewer
  • permissionstableUna única acción sobre un recurso, por ejemplo posts:update
  • role_permissionsjoin tableRelación muchos-a-muchos entre roles y permisos
  • user_rolesjoin tableRelación muchos-a-muchos entre usuarios y roles, delimitada por tenant
  • scopetextTenant id opcional que restringe una concesión a un solo cliente

Los permisos son los átomos, los roles son paquetes nombrados, y dos tablas de unión conectan usuarios con roles y roles con permisos.

Flujo

De la solicitud al acceso permitido

La autenticación ocurre una vez; la autorización se ejecuta en cada solicitud, en orden, y falla cerrando el acceso.

  1. 1

    Autenticar al principal

    Resolver la sesión o token en un user id, un tenant id y un conjunto de roles.

  2. 2

    Cargar roles

    Obtener las asignaciones de roles del usuario desde la base de datos o un caché de corta duración.

  3. 3

    Expandir a permisos

    Aplanar cada rol en sus permisos, incluyendo los heredados, para construir un único conjunto.

  4. 4

    Comprobar la acción sobre el recurso

    Llamar a can(user, action, resource) para el objetivo específico, no solo para la ruta.

  5. 5

    Permitir o devolver 403

    Proceder al handler cuando esté permitido; de lo contrario, responder con 403 y sin efectos secundarios.

  6. 6

    Registrar la decisión

    Grabar el principal, la acción, el recurso y el resultado para auditoría y depuración.

La guia completa

RBAC: Todo lo que necesitas saber

La autenticación no es autorización

Estas dos palabras se suelen utilizar indistintamente, pero no son lo mismo. La autenticación responde a “¿quién eres?” y termina con un principal confiable: un id de usuario, un tenant y una forma de verificar que la solicitud provino de ellos. La autorización responde a “¿qué tienes permitido hacer?” y se ejecuta después de la autenticación, en cada solicitud, contra un objetivo específico.

Confundirlas es el origen de algunos de los errores de seguridad más comunes en la web. Un login implementado a la perfección no dice nada sobre si el usuario conectado debería poder leer la factura de otro cliente. Una sesión válida demuestra la identidad; no otorga ningún permiso. Cada endpoint debe decidir, explícitamente, si este principal puede realizar esta acción sobre este recurso.

Esta guía trata sobre la segunda pregunta. Asume que ya tienes resuelta la primera —una sesión o token que devuelve un usuario— y se centra en el modelo y las comprobaciones que convierten a ese usuario en un permiso o una denegación. Si aún no has implementado la autenticación, lee primero la guía de session auth.

El modelo RBAC

El Role-Based Access Control (Control de Acceso Basado en Roles) es el modelo de autorización más utilizado porque se ajusta a la forma en que las organizaciones operan en la realidad. Consta de tres conceptos:

  • Principals: son las entidades que actúan: usuarios, cuentas de servicio, API keys. Cada principal pertenece a un tenant.
  • Roles: son conjuntos nombrados de capacidades: viewer, editor, admin, billing.
  • Permissions: son los átomos: una única acción sobre un único tipo de recurso, escrita como posts:update o billing:read.

A un principal se le asignan uno o más roles, y cada rol está mapeado a un conjunto de permisos. El conjunto de permisos efectivos para una solicitud es la unión de todos los permisos de todos los roles del principal. Así, una comprobación de autorización se reduce a una simple prueba de pertenencia al conjunto, además de cualquier regla específica del recurso.

La elegancia de este sistema es que los permisos son estables mientras que los roles son fluidos. Puedes añadir un rol moderator, mover posts:delete dentro de él y ningún handler tendrá que cambiar. La regla de “quién puede eliminar una publicación” reside en los datos, no en una cadena de sentencias if dispersas por todo el código base.

Usuarios, roles y permisos

La estructura relacional consta de cuatro tablas y dos uniones de muchos a muchos. Vale la pena interiorizarla, ya que casi cualquier implementación de RBAC es una variación de esta.

CREATE TABLE permissions (
  id     bigserial PRIMARY KEY,
  action text NOT NULL UNIQUE
);

CREATE TABLE roles (
  id   bigserial PRIMARY KEY,
  name text NOT NULL UNIQUE
);

CREATE TABLE role_permissions (
  role_id       bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  permission_id bigint NOT NULL REFERENCES permissions (id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
  user_id bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  role_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, role_id)
);

Es importante realizar el seeding de estos datos en una migración. Los permisos y los mapeos de roles forman parte del contrato de tu aplicación, no son algo que un administrador improvise en producción. Mantén el seed en el control de versiones para que todos los entornos coincidan en lo que significa editor, y trata cualquier cambio en él con el mismo cuidado que un cambio de esquema.

Cargar los permisos efectivos requiere una sola consulta:

SELECT DISTINCT p.action
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p       ON p.id = rp.permission_id
WHERE ur.user_id = $1;

Almacena el resultado en caché por solicitud. Cargarlo una vez y adjuntarlo a req.user evita repetir la consulta en cada comprobación, y una caché con un TTL corto indexada por el id del usuario mantiene la base de datos fuera de la ruta crítica.

Jerarquías de roles

Las organizaciones reales tienen niveles. Un rol senior suele hacer todo lo que puede hacer un rol junior, y más. Modelar esto copiando cada permiso en cada rol es una trampa de mantenimiento: si cambias posts:read, debes recordar los cinco roles que lo incluyen.

En su lugar, permite que los roles hereden. Añade una tabla de unión parent_role_id o role_inherits, y expande la jerarquía al construir el conjunto de permisos. Una estructura común es viewer → editor → admin, donde cada nivel añade capacidades.

CREATE TABLE role_inherits (
  role_id   bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  parent_id bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, parent_id)
);

La expansión es una consulta recursiva o, más sencillamente, una closure table precomputada que almacena cada par de ancestros. La closure table sacrifica un poco de almacenamiento a cambio de una búsqueda trivial y optimizada para índices, lo cual suele ser la decisión correcta ya que las comprobaciones de permisos son mucho más frecuentes que las ediciones de roles.

Protégete contra los ciclos. Un rol que hereda de sí mismo, ya sea directamente o a través de una cadena, hará que la expansión entre en un bucle infinito. Valida al escribir: rechaza cualquier padre que pueda crear un ciclo. Mantén las jerarquías superficiales —tres o cuatro niveles son suficientes— porque los árboles profundos son difíciles de razonar para los humanos y fáciles de implementar incorrectamente.

Por qué los permisos son mejores que los strings de roles

El error más común al implementar RBAC es no construir un sistema de RBAC en absoluto. En su lugar, se suelen esparcir comprobaciones de roles por todo el código:

if (req.user.role !== "admin") return res.sendStatus(403);

Esto parece inofensivo y es una decisión de política integrada en un handler. Indica que solo admin puede hacer esto, lo cual pudo ser cierto en el momento en que se escribió. Cuando el producto añade un rol de support que también necesita acceso, alguien tiene que buscar cada una de estas comprobaciones y editarlas —y se saltarán alguna. La regla ahora está distribuida en docenas de archivos sin una única fuente de verdad.

Comprobar un permiso invierte la dependencia. El handler pregunta “¿puede este principal actualizar un post?” y la respuesta proviene de los datos:

if (!can(req.user, "update", post)) return res.sendStatus(403);

Ahora, otorgar a support la capacidad de actualizar posts es una fila en role_permissions, no un cambio de código. La política es revisable, testeable y consistente. El handler describe la intención en lugar de codificar un rol específico.

La regla de oro: los roles son para humanos, los permisos son para el código. Una UI puede decir “Los administradores pueden gestionar la facturación”, pero la comprobación subyacente debe solicitar billing:manage.

El pipeline de autorización

Cada solicitud sigue la misma secuencia, y cada etapa tiene exactamente una función.

  1. Autenticar. Resolver la sesión o el token en un principal: un id de usuario, un id de tenant y una lista de roles. Si esto falla, la solicitud es anónima y las rutas protegidas devuelven un 401.
  2. Cargar roles. Obtener las asignaciones de roles, generalmente desde la base de datos o un caché poblado durante el login.
  3. Expandir a permisos. Aplanar los roles, incluyendo los heredados, en un único conjunto de strings de permisos.
  4. Verificar la acción sobre el recurso. Llamar a can(user, action, resource) para el objetivo concreto, después de haberlo cargado.
  5. Permitir o denegar. Si tiene éxito, ejecutar el handler. Si falla, devolver un 403 sin efectos secundarios.
  6. Registrar la decisión. Grabar el principal, la acción, el recurso y el resultado.

El orden es fundamental por dos razones. La autenticación debe ir primero porque todo lo demás depende de un principal confiable. Las verificaciones de recursos deben ocurrir después de cargar el recurso, ya que no se puede evaluar la propiedad de un registro que aún no se ha recuperado.

El “failing closed” (fallar cerrando el acceso) es innegociable. Si la carga de roles lanza un error o el caché no está disponible, el valor predeterminado es denegar. Un sistema de autorización que devuelve “permitir” ante un error es peor que no tener ningún sistema, ya que genera una falsa sensación de seguridad.

Creando un helper can()

Centralizar la decisión en una sola función es lo que evita que los route guards y las comprobaciones de recursos se desincronicen. La firma es sencilla: un principal, una acción y un recurso opcional.

export type Action = "read" | "create" | "update" | "delete" | "manage";

export function can(
  user: Principal,
  action: Action,
  resource?: Resource
): boolean {
  const permission = `${resource?.type ?? "global"}:${action}`;
  if (!user.permissions.has(permission)) return false;

  if (resource && resource.tenantId !== user.tenantId) return false;

  if (resource && action !== "read" && resource.ownerId !== user.id) {
    return user.permissions.has(`${resource.type}:manage`);
  }

  return true;
}

Aquí se codifican tres reglas, en orden de importancia. El conjunto de permisos es la puerta principal: si ningún rol concede la acción, se detiene el proceso. A continuación viene el aislamiento del tenant, que es absoluto: un principal nunca debe actuar fuera de su tenant, independientemente de sus permisos. Por último, las escrituras en un recurso requieren propiedad o una concesión explícita de manage, que es lo que permite que un editor edite sus propios borradores mientras que un admin puede editar cualquier cosa.

El helper es puro. Recibe datos simples y devuelve un booleano, sin llamadas a la base de datos en su interior. Esto hace que sea trivial realizar unit tests con una matriz de principals, acciones y recursos, y significa que la misma función puede ejecutarse en un route guard, un servicio, un background job o un componente de UI que decida si debe renderizar un botón.

Para la UI, expón la misma función al cliente a través de un endpoint o un objeto de permisos renderizado en el servidor. El cliente debe ocultar los controles que el usuario no puede utilizar, pero el servidor debe seguir aplicando cada comprobación, ya que un botón oculto no es un control de seguridad.

Aplicando restricciones a nivel de ruta

Un route guard es la primera línea de defensa: decide si este tipo de acción está disponible para este principal en absoluto. Se ejecuta antes del handler y antes de cualquier operación de base de datos, lo que lo convierte en una forma económica de rechazar denegaciones evidentes.

export function requirePermission(
  action: Action,
  type: string
): RequestHandler {
  return (req, res, next) => {
    if (!req.user) return res.status(401).json({ error: "unauthorized" });

    if (!can(req.user, action, { type, ownerId: req.user.id, tenantId: req.user.tenantId })) {
      return res.status(403).json({ error: "forbidden" });
    }
    next();
  };
}

Montalo en el router para que la regla sea visible donde se definen las rutas:

router.get("/posts", requirePermission("read", "post"), listPosts);
router.post("/posts", requirePermission("create", "post"), createPost);

Es importante devolver un 401 cuando falta el principal y un 403 cuando este ha sido denegado. 401 significa “no sé quién eres”; 403 significa “sé quién eres y no tienes permiso para hacer esto”. Los clientes y las herramientas de monitoreo los tratan de manera diferente, y confundirlos dificulta la depuración.

Los route guards son necesarios pero no suficientes. Responden a “¿puede este principal actualizar posts en general?”, no a “¿puede actualizar el post 42?”. Esta segunda pregunta requiere el recurso.

Validación a nivel de recurso

La validación a nivel de recurso es donde se encuentran la mayoría de las vulnerabilidades reales, ya que es la que la gente suele olvidar. Un endpoint como PATCH /posts/:id recibe un id del cliente. Si confía en ese id sin verificar la propiedad, cualquier usuario autenticado puede modificar cualquier post adivinando o enumerando ids. Esto es una Referencia Directa Insegura a Objetos, o IDOR.

La solución siempre tiene la misma estructura: cargar el recurso y luego validarlo.

router.patch("/posts/:id", requireAuth(), async (req, res) => {
  const post = await db.post.findById(req.params.id);
  if (!post) return res.status(404).json({ error: "not_found" });

  const allowed = can(req.user!, "update", {
    type: "post",
    ownerId: post.authorId,
    tenantId: post.tenantId,
  });

  if (!allowed) return res.status(403).json({ error: "forbidden" });

  const updated = await db.post.update(post.id, req.body);
  res.json(updated);
});

Existe una sutil elección de orden para los recursos entre tenants. Si un usuario del tenant A solicita un post del tenant B, devolver un 403 confirma que el post existe, lo que filtra información entre tenants. Muchos sistemas devuelven un 404 en ese caso para que el recurso sea indistinguible de uno que no existe. Independientemente de lo que elijas, sé consistente y documéntalo.

El mismo patrón se aplica a los recursos anidados. Antes de actuar sobre /teams/:teamId/projects/:projectId, verifica que el principal pueda acceder al equipo y que el proyecto pertenezca a este. Cada id en la ruta está controlado por el atacante y debe ser validado.

La matriz de permisos

La matriz de permisos es una tabla donde las filas son los roles y las columnas son los permisos, completada con las concesiones (grants). Es el artefacto que permite que un sistema de autorización sea revisable.

              posts:read  posts:create  posts:update  posts:delete  billing:read
viewer            x
editor            x            x             x
admin             x            x             x             x             x
billing           x                                                     x

Manténla en el control de versiones junto al código y genera las migraciones de seed a partir de ella, para que la documentación y los datos no diverjan. Cuando alguien propone un nuevo rol, la primera pregunta es qué columnas obtiene; y la respuesta es un diff de esta tabla, no una búsqueda exhaustiva entre handlers.

Dos hábitos hacen que la matriz sea útil. Primero, nombra los permisos de manera consistente como resource:action, para que la tabla se lea con claridad y los strings sean predecibles. Segundo, revisa la matriz cada vez que un rol cambie, ya que una sola columna adicional es fácil de pasar por alto en una migración y puede otorgar mucho más de lo previsto.

Roles multi-tenant

En una aplicación multi-tenant, una misma persona puede tener diferentes roles en distintas organizaciones. El dueño de una agencia es administrador de su propio tenant y un visor en el de un cliente. Una única columna global role no puede expresar eso.

La solución es limitar las asignaciones de roles por tenant. Añade tenant_id a user_roles y haz que forme parte de la clave primaria, para que un usuario pueda tener roles distintos por tenant. Cuando construyas el conjunto de permisos para una solicitud, hazlo para un solo tenant: aquel dentro del cual se está ejecutando la solicitud.

SELECT DISTINCT p.action
FROM user_roles ur
JOIN role_permissions rp ON rp.role_id = ur.role_id
JOIN permissions p       ON p.id = rp.permission_id
WHERE ur.user_id = $1 AND ur.tenant_id = $2;

El tenant debe provenir de una fuente confiable: la sesión, un subdominio que controles o el token. Nunca lo aceptes desde el cuerpo de una solicitud o una cadena de consulta (query string) sin verificar que el principal pertenezca a él. Una vez establecido, el aislamiento del tenant es la primera regla en can() y se aplica antes de considerar cualquier permiso, de modo que ninguna concesión pueda cruzar el límite.

Cambiar de tenant es un cambio de privilegios. Si el tenant activo reside en la sesión, actualízalo en el servidor y regenera cualquier conjunto de permisos almacenado en caché para que las concesiones del tenant anterior no se filtren en el nuevo contexto.

ABAC y motores de políticas

RBAC responde a la mayoría de las preguntas, pero algunas reglas dependen de más factores que el rol y la propiedad: la hora del día, la sensibilidad de los datos, el departamento del usuario o la puntuación de riesgo de la solicitud. Estas son reglas basadas en atributos, e intentar codificarlas como roles produce una explosión combinatoria.

ABAC (Attribute-Based Access Control) evalúa políticas basándose en los atributos del sujeto, el recurso, la acción y el entorno. Una regla podría decir: “un usuario puede leer un documento si su departamento coincide con el del documento y la clasificación no es secreta”. Esto es expresable, testeable y auditable de una manera que una matriz de roles no lo es.

Los motores de políticas hacen que esto sea práctico. Open Policy Agent evalúa políticas escritas en Rego y puede consultarse como un sidecar o una librería, permitiendo que las mismas reglas se apliquen en diferentes servicios y lenguajes. Casbin ofrece un enfoque de modelo y adaptador más ligero con soporte para RBAC, ABAC y combinaciones, siendo muy popular dentro del código de la aplicación.

Adopta un motor de políticas solo cuando las reglas realmente superen las capacidades de RBAC, no antes. Esto añade un nuevo lenguaje, una superficie de despliegue y una curva de aprendizaje. Un helper de can() bien factorizado con reglas claras puede gestionar una cantidad sorprendente de casos, y siempre puedes envolverlo en un motor de políticas más adelante cuando una decisión específica requiera más contexto.

Pruebas de autorización

Los errores de autorización son vulnerabilidades de seguridad, por lo que las pruebas deben tratar los casos de denegación como ciudadanos de primera clase. Para cada acción protegida, escribe una matriz de pruebas: un llamador anónimo, un principal sin el permiso, un propietario, un no propietario con el permiso y un principal de otro tenant.

describe("PATCH /posts/:id", () => {
  it("rejects anonymous users", async () => {
    await request(app).patch("/posts/1").send({ title: "x" }).expect(401);
  });

  it("rejects users without posts:update", async () => {
    await request(app).patch("/posts/1").set("Cookie", viewerCookie).expect(403);
  });

  it("allows the owner", async () => {
    await request(app).patch("/posts/1").set("Cookie", ownerCookie).expect(200);
  });

  it("rejects a non-owner editor", async () => {
    await request(app).patch("/posts/1").set("Cookie", editorCookie).expect(403);
  });

  it("rejects a user from another tenant", async () => {
    await request(app).patch("/posts/1").set("Cookie", otherTenantCookie).expect(404);
  });
});

Prueba can() directamente como una función pura, utilizando una tabla de principals, acciones y recursos. Esto cubre la lógica de manera exhaustiva y económica, mientras que las pruebas del endpoint demuestran que la comprobación está realmente implementada. Un fallo común es tener un helper correcto que el handler olvidó llamar, y solo una prueba de integración puede detectar eso.

Sembrado de permisos mediante migraciones

Los permisos y el mapeo de roles forman parte del contrato de tu aplicación, por lo que deben residir en las migraciones y no en un panel de administración que difiere entre entornos.

Escribe una migración de sembrado (seed) que realice un upsert de los permisos por nombre y luego concilie las concesiones de cada rol con la matriz. Los upserts mantienen la migración idempotente, lo cual es fundamental ya que podría ejecutarse contra bases de datos que ya contienen algunas filas.

INSERT INTO permissions (action) VALUES
  ('posts:read'), ('posts:create'), ('posts:update'), ('posts:delete'),
  ('billing:read'), ('billing:manage')
ON CONFLICT (action) DO NOTHING;

INSERT INTO role_permissions (role_id, permission_id)
SELECT r.id, p.id
FROM roles r
JOIN permissions p ON p.action IN ('posts:read', 'posts:create', 'posts:update')
WHERE r.name = 'editor'
ON CONFLICT DO NOTHING;

Eliminar un permiso es más arriesgado que añadir uno. Verifica sus usos en el código y, si se referencia en algún lugar, renómbralo o retíralo gradualmente. Una migración que elimine posts:update mientras un handler todavía lo verifica convertirá cada solicitud en un denegado; esto es seguro, pero confuso hasta que alguien revise el diff del sembrado.

Almacenamiento en caché del conjunto de permisos

Una comprobación de permisos nunca debería realizar una consulta a la base de datos. Construye el conjunto efectivo una vez por solicitud, adjúntalo al principal y reutilízalo para cada comprobación dentro de esa misma solicitud.

Para sistemas de alto tráfico, almacena el conjunto en caché por usuario durante un periodo corto, utilizando como clave el usuario y el tenant. Un TTL de 30 a 60 segundos suele ser suficiente para eliminar la consulta de la ruta crítica (hot path) manteniendo los cambios de roles visibles rápidamente. Cuando un rol cambie, invalida la caché explícitamente en lugar de esperar al TTL, para que un permiso revocado deje de funcionar inmediatamente.

async function permissionsFor(userId: string, tenantId: string) {
  const key = `perm:${tenantId}:${userId}`;
  const cached = await redis.get(key);
  if (cached) return new Set(JSON.parse(cached));

  const rows = await db.query(permissionQuery, [userId, tenantId]);
  const set = new Set(rows.map((r) => r.action));
  await redis.set(key, JSON.stringify([...set]), "EX", 60);
  return set;
}

Existe un compromiso de seguridad con el TTL. Cuanto más larga sea la caché, mayor será la ventana de tiempo en la que un rol revocado seguirá funcionando. Prioriza la invalidación explícita en cada cambio de asignación de roles y mantén el TTL corto como medida de respaldo para las invalidaciones omitidas.

Mejores prácticas

  • Verifica los permisos, no los nombres de los roles, en el código de la aplicación; mantén los roles como paquetes organizados para los humanos.
  • Centraliza la decisión en una única función can(user, action, resource) pura.
  • Deniega por defecto y falla en estado cerrado si los roles o permisos no pueden cargarse.
  • Aplica el aislamiento de tenants antes de cualquier verificación de permisos, y obtén el tenant de una fuente confiable.
  • Carga el recurso antes de autorizar una acción sobre él para prevenir IDOR.
  • Devuelve 401 para solicitudes no autenticadas y 403 para las denegadas.
  • Almacena en caché el conjunto de permisos efectivos por solicitud e invalídalo cuando cambien los roles.
  • Mantén la matriz de permisos en el control de versiones y genera los seeds a partir de ella.
  • Prueba los casos negativos: anónimos, permiso incorrecto, no propietario, otro tenant.
  • Registra las decisiones de permitir y denegar con suficiente contexto para poder explicarlas posteriormente.

Errores comunes

  • Tratar una sesión o token válido como prueba de autorización.
  • Crear ramificaciones basadas en user.role === "admin" en todo el código base.
  • Verificar la ruta pero nunca el recurso, dejando un agujero de IDOR.
  • Confiar en un tenant id proveniente del cuerpo de la solicitud o de la query string.
  • Otorgar permisos de manage demasiado amplios para evitar modelar una regla real.
  • Construir jerarquías de roles profundas que nadie puede comprender.
  • Cachear los permisos indefinidamente y dejar permisos obsoletos tras un cambio de rol.
  • Devolver un 403 cuando un 404 evitaría filtrar la existencia del recurso de otro tenant.
  • Permitir que los botones ocultos de la UI sustituyan la validación en el servidor.
  • Olvidar verificar cada id en una ruta anidada.

Próximos pasos

RBAC es el modelo de autorización que utilizarás con más frecuencia y se integra perfectamente con todo lo que hayas construido. Si tus principals llegan como tokens, la guía de JWT muestra dónde encajan los claims como los roles y por qué aún debes verificarlos en el servidor. El principal en sí proviene de la autenticación de sesión o, en el caso de máquinas, de API keys. Y dado que la autorización siempre se refiere a un objetivo, la guía de REST es el complemento ideal para modelar recursos y sus ids. Cuando tus reglas empiecen a depender del contexto en lugar de los roles, vuelve a la sección de ABAC y recurre a un motor de políticas.

En la practica

Esquema, helper, guard, propiedad

Cuatro archivos que llevan una solicitud desde un usuario autenticado hasta una acción autorizada.

migrations/rbac.sql
CREATE TABLE roles (
  id   bigserial PRIMARY KEY,
  name text NOT NULL UNIQUE
);

CREATE TABLE permissions (
  id     bigserial PRIMARY KEY,
  action text NOT NULL UNIQUE -- e.g. "posts:update"
);

CREATE TABLE role_permissions (
  role_id       bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  permission_id bigint NOT NULL REFERENCES permissions (id) ON DELETE CASCADE,
  PRIMARY KEY (role_id, permission_id)
);

CREATE TABLE user_roles (
  user_id  bigint NOT NULL REFERENCES users (id) ON DELETE CASCADE,
  role_id  bigint NOT NULL REFERENCES roles (id) ON DELETE CASCADE,
  tenant_id bigint NOT NULL REFERENCES tenants (id) ON DELETE CASCADE,
  PRIMARY KEY (user_id, role_id, tenant_id)
);

Comprobar permisos, no nombres de roles

Una comprobación de rol hardcodea la suposición de que solo los admins pueden editar. Una comprobación de permiso expresa la regla directamente, por lo que los nuevos roles funcionan sin tocar el handler.

Preferir
if (!can(user, "update", post)) {
  return res.status(403).json({ error: "forbidden" });
}
Evitar
if (user.role !== "admin") {
  return res.status(403).json({ error: "forbidden" });
}
// An "editor" role can never be granted
// this action without editing the code.

Autorizar el recurso, no solo la ruta

Un guard de ruta prueba que el usuario puede editar algún post. Solo una comprobación de recurso prueba que puede editar este post. Omitirlo es la clásica vulnerabilidad IDOR.

Preferir
const post = await db.post.findById(req.params.id);
if (!post) return res.sendStatus(404);

if (!can(req.user!, "update", {
  type: "post",
  ownerId: post.authorId,
  tenantId: post.tenantId,
})) {
  return res.sendStatus(403);
}
Evitar
router.patch("/posts/:id", requirePermission("update", "post"),
  async (req, res) => {
    // Any user who can update any post
    // can now update every post by id.
    await db.post.update(req.params.id, req.body);
    res.sendStatus(204);
  });

Compromisos

¿Es RBAC el modelo adecuado?

RBAC es el estándar pragmático para la mayoría de las aplicaciones. Identifica dónde deja de ser suficiente.

Strengths

  • Fácil de explicar y revisar

    Una matriz de permisos es algo que alguien que no sea ingeniero puede leer. Las revisiones y auditorías se convierten en una cuestión de revisar una tabla en lugar de rastrear rutas de código.

  • Reutilizable y componible

    Los roles empaquetan los permisos una vez y los aplican en todas partes. Las nuevas funcionalidades reutilizan átomos existentes en lugar de inventar nuevas comprobaciones.

  • Se ajusta a cómo trabajan los equipos

    Las funciones laborales como soporte, editor y admin se alinean naturalmente con los roles, lo que hace que el modelo sea intuitivo para quienes lo usan.

Trade-offs

  • La explosión de roles es real

    Sin un vocabulario de permisos disciplinado, los equipos crean un nuevo rol para cada caso borde hasta que nadie sabe qué significa ninguno de ellos.

  • El contexto es difícil de expresar

    Reglas como "los editores pueden publicar sus propios borradores durante el horario laboral" no encajan limpiamente en los roles. Ahí es donde ABAC o un policy engine justifican su uso.

  • El caché debe invalidarse

    Los permisos suelen almacenarse en caché por solicitud o por token. Un cambio de rol debe invalidar ese caché o la concesión antigua persistirá hasta su expiración.

Preguntas frecuentes

Preguntas frecuentes

Keep learning

Related topics from the roadmap.

$ comienza a aprender

Listo para aprender RBAC & Permissions?

Nuestro tutorial interactivo te guia a traves de RBAC & Permissions paso a paso — con quizzes y codigo real que puedes ejecutar en el navegador.