Backend · SaaS · Autorización

Diseño de RBAC Multiempresa para un SaaS

La autorización multiempresa se vuelve mucho más clara cuando identidad, membresía, rol y tenant activo se tratan como conceptos separados. El problema normalmente no está en los nombres de los roles, sino en concentrar demasiadas decisiones de autorización dentro del registro de usuario.

Separar identidad de membresía

Un usuario responde quién es una persona. Una membresía responde a qué empresa pertenece y qué puede hacer allí. Mantener esos conceptos separados permite que una misma identidad participe en varias empresas sin duplicar usuarios ni sobrecargar un único user.role.

En la práctica, un usuario puede tener una identidad y varias membresías. Cada membresía pertenece a un tenant y contiene el rol y las preferencias específicas de esa empresa.

User
  id
  email

TenantMembership
  userId
  tenantId
  role
  notificationsEnabled

UNIQUE(userId, tenantId)

Los roles pertenecen al contexto de la empresa

Un rol como OWNER, ADMIN, AGRONOMO, OPERADOR o VIEWER sólo tiene sentido dentro de una empresa. La misma persona puede ser propietaria en un tenant y visualizadora en otro.

Por eso los roles de empresa deben vivir en la membresía y no en el usuario. Puede existir un rol global para funciones de plataforma como SUPERADMIN, pero no debería usarse como atajo para la autorización normal de cada tenant.

Resolver el tenant activo de forma explícita

Cada request asociado a una empresa necesita un tenant activo. El backend debe resolverlo desde un contexto de sesión confiable y verificar que el usuario todavía tenga una membresía válida para ese tenant.

Un orden de fallback útil es: tenant solicitado si la membresía es válida, luego la empresa por defecto si sigue siendo válida y finalmente otra membresía vigente. Si no existe ninguna, la sesión debe quedar en estado sin tenant u onboarding en lugar de reutilizar silenciosamente contexto viejo.

tenant solicitado
  ↓ ¿membresía válida?
tenant por defecto
  ↓ ¿membresía válida?
primera membresía válida
  ↓
NO_TENANT / onboarding

Guardar el contexto de tenant en la sesión

Una vez resuelto el tenant activo, el servidor puede emitir una sesión o token con tenantId y memberRole junto con cualquier rol global de plataforma.

El cliente puede pedir un cambio de empresa, pero no debería ser confiable para declarar libremente el tenant en cada llamada. La autorización sigue derivando de la membresía validada por el backend y de la sesión generada a partir de ella.

Usar capacidades en lugar de repartir checks de roles

Condiciones directas como if role === ADMIN se multiplican rápido por los controladores y vuelven difícil entender la política completa. Una matriz de capacidades permite definir en un único lugar qué puede hacer cada rol.

Los controladores pueden proteger operaciones como fields.write, team.invite, billing.manage o analytics.view sin duplicar la semántica de roles. También permite expresar excepciones: por ejemplo, un ADMIN puede gestionar campos y equipo pero no facturación, mientras que un OWNER puede hacer ambas cosas.

OWNER  -> fields.*, team.*, billing.manage
ADMIN  -> fields.*, team.*, billing.view
AGRONOMO -> fields.read, alerts.*, analytics.*
OPERADOR -> fields.read, notes.write
VIEWER -> *.read

Validar membresía y capacidad

La autorización necesita dos comprobaciones: si el usuario pertenece al tenant y si el rol de esa membresía permite la acción solicitada. Validar sólo la capacidad puede autorizar la acción correcta dentro de la empresa equivocada.

Prefiero guardias centralizados para membresía y evaluación de rol o capacidades. Así los controladores se concentran en el comportamiento de la aplicación mientras el límite de autorización permanece consistente en campos, alertas, analítica, notificaciones, equipo y facturación.

Cambiar de empresa también es un problema de estado del frontend

El cambio de tenant no termina cuando el backend devuelve un nuevo tenantId. El cliente todavía puede tener queries cacheadas, filtros seleccionados, formularios abiertos o requests en vuelo de la empresa anterior.

Al cambiar de empresa hay que cancelar requests tenant-scoped, invalidar caches o incluir tenantId en sus keys y resetear el estado específico de empresa. De lo contrario, un backend correcto puede convivir con un frontend que durante un momento muestre datos viejos de otro tenant.

queryKey: ["fields", tenantId]
queryKey: ["alerts", tenantId]
queryKey: ["analytics", tenantId]

Las invitaciones deben crear membresía, no otra identidad

Invitar a alguien a una empresa debería converger al mismo modelo de membresía tanto si el email ya pertenece a un usuario como si esa persona todavía necesita registrarse.

Para un usuario existente y verificado, aceptar la invitación puede crear directamente la membresía. Para uno nuevo, primero ocurren registro y verificación, y luego la invitación crea la membresía. La invitación es temporal; la membresía es el registro durable de autorización.

Los cambios de rol y bajas deben aplicarse inmediatamente

La membresía es dinámica. Un usuario puede ser eliminado de una empresa o perder permisos mientras todavía conserva una sesión vieja o una pestaña abierta.

Los requests sensibles deberían validar la membresía actual en lugar de confiar en una vista antigua del rol. El frontend también debe refrescar el contexto de empresas y usuario después de modificar el equipo para que navegación y permisos no queden desactualizados.

El onboarding sin tenant es un estado válido

Un SaaS puede tener un usuario autenticado que todavía no pertenece a ninguna empresa. Tratarlo como un error de autenticación vuelve incómodo el onboarding y genera excepciones por todos lados.

Un diseño más limpio mantiene al usuario autenticado pero sin tenant activo. Sólo se habilitan onboarding y creación de empresa hasta que exista un tenant; entonces la nueva membresía pasa a ser el contexto activo.

El límite que hay que proteger

La regla más importante de un RBAC multiempresa es que toda lectura y escritura tenant-scoped derive su tenant de un contexto de autorización confiable. Filtrar por tenantId no es sólo un detalle de consulta; forma parte del límite de seguridad.

Un buen modelo hace que el camino correcto sea más fácil que el peligroso: membresías explícitas, roles contextuales, capacidades centralizadas, sesiones con tenant resuelto y caches conscientes del tenant. El resultado es más simple de extender y mucho más difícil de usar mal por accidente.

Trabajo relacionado

Ver el modelo de autorización multiempresa en el caso de estudio de Vienor

Artículos técnicos relacionados