Multi-Tenancy en Camunda 8¶
Resumen: Camunda 8 implementa multi-tenancy con un flag global
camunda.security.multi-tenancy.checksEnabledque habilita/deshabilita los checks de tenant. El sistema usaTenantCheckcomo filtro en queries,TenantAccesscon semántica allowed/denied/wildcard, resolución transitiva de tenants en login (user → mapping rules → groups → roles → tenants), yDbTenantAwareKeyen RocksDB para scoping de datos por tenant.
Control global: checksEnabled¶
La multi-tenancy en Camunda 8 se controla con un único flag de configuración:
Comportamiento con checksEnabled = false (default)¶
- No hay aislamiento de tenant: todos los usuarios ven todos los recursos.
- No hay tenant ID en las entidades: procesos, instancias, jobs, etc., no tienen tenant asociado.
- TenantCheck no se aplica: los filtros de tenant no se inyectan en queries.
- Uso: entornos single-tenant, desarrollo local, demos.
Comportamiento con checksEnabled = true¶
- Todo recurso tiene un tenant ID: obligatorio en deployments, start de instancias, etc.
- TenantCheck activo: las queries solo retornan recursos del tenant del usuario.
- Default tenant: si no se especifica un tenant, se usa el tenant por defecto (
<default>). - Validación estricta: operaciones sin tenant válido son rechazadas.
TenantCheck: filtro en queries¶
Concepto¶
TenantCheck es un componente que se inyecta en el pipeline de queries para filtrar resultados por tenant. Es un filtro que puede estar habilitado o deshabilitado según la configuración global.
Implementación¶
flowchart TD
Query[Query entrante] --> Check{checksEnabled?}
Check -->|NO| NoFilter[Ejecutar query<br/>sin filtro de tenant]
Check -->|SÍ| TC[TenantCheck]
TC --> Step1[1. Obtener tenants del SecurityContext<br/>del usuario actual]
Step1 --> Step2[2. Generar filtro:<br/>WHERE tenantId IN allowedTenants]
Step2 --> Step3[3. Inyectar en la query]
Step3 --> Exec[Ejecutar query con filtro]
Interacción con autorización¶
El TenantCheck opera en conjunto con el modelo de autorización (ver concepts/authorization-model):
- Primero autorización: ¿tiene el usuario permiso para el tipo de recurso y operación?
- Después tenant check: de los recursos autorizados, ¿cuáles pertenecen a los tenants del usuario?
- Resultado: solo recursos que pasan ambos checks son visibles/accesibles.
TenantAccess: modelo de acceso¶
Tres niveles de acceso¶
El modelo de acceso de tenant tiene tres niveles:
ALLOWED¶
El usuario tiene acceso a tenants específicos, listados explícitamente:
- Solo recursos de
tenant-aytenant-bson visibles. - Deployments solo pueden hacerse a estos tenants.
- Process instances solo pueden iniciarse en estos tenants.
DENIED¶
El usuario no tiene acceso a ningún tenant:
- Ningún recurso es visible.
- Todas las operaciones son rechazadas.
- Usado como default cuando un usuario no tiene asignaciones de tenant.
WILDCARD¶
El usuario tiene acceso a todos los tenants:
- Todos los recursos son visibles independientemente del tenant.
- Puede desplegar y operar en cualquier tenant.
- Típicamente asignado solo a administradores del sistema.
Resolución transitiva de tenants en login¶
Cuando un usuario se autentica, sus tenants se resuelven a través de una cadena transitiva que agrega tenants de múltiples fuentes:
flowchart LR
User((User)) --> Direct[Tenants directos del usuario]
User --> MR[Mapping Rules]
MR --> MRT[Claims OIDC → match con rules<br/>→ tenants de la rule]
User --> Groups
subgraph Groups[Groups]
GD[Grupos directos del usuario]
GM[Grupos asignados via mapping rules]
end
Groups --> GT[Tenants de cada grupo]
User --> Roles
subgraph Roles[Roles]
RD[Roles directos del usuario]
RM[Roles asignados via mapping rules]
RG[Roles asignados via grupos]
end
Roles --> RT[Tenants de cada rol]
Algoritmo de resolución¶
función resolverTenants(user, oidcClaims):
tenants = Set()
// 1. Tenants directos
tenants.addAll(user.tenants)
// 2. Mapping rules (si OIDC)
si oidcClaims != null:
matchingRules = evaluarMappingRules(oidcClaims)
para cada rule en matchingRules:
tenants.addAll(rule.tenants)
user.addRoles(rule.roles)
user.addGroups(rule.groups)
// 3. Grupos → tenants
para cada group en user.groups:
tenants.addAll(group.tenants)
user.addRoles(group.roles) // roles heredados de grupos
// 4. Roles → tenants
para cada role en user.roles:
tenants.addAll(role.tenants)
return tenants
Ejemplo concreto¶
Configuración:
- User "alice": grupo "engineering"
- Mapping Rule: si claim "team"="platform" → rol "platform-dev"
- Grupo "engineering": tenant "company-main", rol "developer"
- Rol "platform-dev": tenant "platform-services"
- Rol "developer": tenant "dev-sandbox"
Login de alice con OIDC claim team=platform:
1. Directos: (ninguno)
2. Mapping: team=platform → rol "platform-dev" → tenant "platform-services"
3. Grupo "engineering" → tenant "company-main", rol "developer"
4. Rol "developer" → tenant "dev-sandbox"
Resultado: tenants = {"company-main", "platform-services", "dev-sandbox"}
DbTenantAwareKey en RocksDB¶
Scoping de datos por tenant¶
En el broker de Zeebe, los datos se almacenan en RocksDB. Para multi-tenancy, las keys de RocksDB incluyen el tenant ID como parte de su estructura:
DbTenantAwareKey {
tenantId: String, // prefijo de la key
entityKey: Long, // key del recurso
columnFamily: CF // column family de RocksDB
}
Estructura de la key¶
flowchart LR
subgraph Key["Key layout en RocksDB"]
direction LR
T["tenantId<br/>(var)"]
CF["columnFamilyTag<br/>(1 byte)"]
EK["entityKey<br/>(8 bytes)"]
T --- CF --- EK
end
Queries con tenant scoping¶
Cuando se consultan datos en RocksDB con multi-tenancy habilitado:
- Prefix scan: se usa el
tenantIdcomo prefijo para escanear solo datos del tenant relevante. - Point lookup: la key completa incluye el
tenantId, por lo que un lookup solo retorna datos del tenant correcto. - Iteration: los iteradores se acotan al rango del
tenantIdprefix.
Ventajas del scoping en key¶
- Aislamiento a nivel de storage: los datos de diferentes tenants no se mezclan a nivel de bytes.
- Rendimiento de scan: un prefix scan por tenant solo lee datos de ese tenant, sin filtrar post-lectura.
- Compaction efficiency: RocksDB puede organizar datos por tenant en diferentes SST files.
Sin checksEnabled¶
Cuando multi-tenancy está deshabilitada, el tenantId en las keys es un string vacío o un valor default, efectivamente eliminando la dimensión de tenant del key space.
Impacto en componentes¶
Broker (Zeebe)¶
DbTenantAwareKeyen todas las column families de RocksDB.- Validación de tenant ID en todos los comandos entrantes.
- Tenant ID propagado en el log replicado.
Exporters¶
- Los records exportados incluyen el
tenantId. - Los indices de Elasticsearch/OpenSearch pueden particionarse por tenant.
Webapps (Operate, Tasklist)¶
- Queries a Elasticsearch/OpenSearch incluyen filtro de tenant.
- UI muestra selector de tenant para usuarios con acceso a múltiples tenants.
- REST API acepta
tenantIdcomo parámetro en queries.
Connectors¶
- Los connectors operan en el contexto de un tenant específico.
- Las variables del connector incluyen el tenant ID del process instance.
Consideraciones para un MVP¶
- Diferible: multi-tenancy es una feature avanzada que la mayoría de deployments iniciales no necesitan. Puede implementarse después.
- Diseñar para el futuro: aunque no se implemente multi-tenancy inicialmente, incluir un
tenantIden las entidades desde el inicio (con un valor default) facilita enormemente la implementación posterior. - Si se implementa: el enfoque de
DbTenantAwareKey(tenant como prefijo de key) es el correcto para RocksDB y evita filtrado post-lectura costoso. - Simplificable: la resolución transitiva (user → groups → roles → tenants) puede simplificarse a asignación directa de tenants a usuarios para un MVP.
- Simplificable: el flag global
checksEnabledes un buen patrón — permite activar/desactivar multi-tenancy sin cambios de código.