ADR-013: RBAC simple: 3 roles¶
- Status: Accepted
- Date: 2026-05-14
- Tags: security, authorization, identity
Context and Problem Statement¶
Camunda tiene authorization model complejo: 20 resource types × 40 permission types × 3 scope types (ID/PROPERTY/ANY). Esto es flexible pero complejo de configurar y mantener. ¿Replicamos esta granularidad o simplificamos?
Decision Drivers¶
- 95% de use cases requieren solo 2-3 roles
- Complexity de auth model bug-prone (escalation vulnerabilities)
- Fine-grained permissions raro implementados correctamente en producción
- Simple model = onboarding easier
- Si negocio requiere fine-grained, build later
Considered Options¶
- 3 roles fixed: admin, operator, worker
- Camunda model full (20 × 40 × 3)
- Sin authorization (todos pueden todo en su tenant)
- Custom role model (usuario define roles + permissions)
- OAuth2 scopes (delegate al IdP)
Decision Outcome¶
Chosen option: 3 roles fixed (admin, operator, worker) por tenant. Phase 1.
Positive Consequences¶
- Onboarding trivial (3 conceptos, no 20×40)
- Bug surface mínima (impossible escalate vía granular missing permission)
- Code simpler (3 checks vs miles de combinations)
- Documentation clara
- Tenant-scoped (no cross-tenant authorization)
Negative Consequences¶
- Sin fine-grained permissions
- Compliance audits stricter pueden requerir más granularity
- Migrating to fine-grained later requires schema change
- Algunos use cases no representables (e.g., "puede ver process X pero no Y")
Roles definidos¶
| Role | Permissions |
|---|---|
| admin | Todo en su tenant: deploy, manage forms/processes, manage users, batch ops, resolve incidents, cancel instances |
| operator | Monitor (read): list processes/instances/incidents. Operate: resolve incidents, cancel instances, batch ops |
| worker | Activate jobs, complete/fail/throwError jobs, publish messages, read process definitions |
Schema¶
CREATE TABLE user_tenants (
user_id TEXT NOT NULL, -- sub claim from OIDC
tenant_id TEXT REFERENCES tenants(tenant_id),
role TEXT NOT NULL CHECK (role IN ('admin', 'operator', 'worker')),
granted_at TIMESTAMPTZ DEFAULT NOW(),
granted_by TEXT,
PRIMARY KEY (user_id, tenant_id)
);
CREATE TABLE api_keys (
api_key_id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
tenant_id TEXT REFERENCES tenants(tenant_id),
name TEXT NOT NULL,
key_hash TEXT NOT NULL UNIQUE, -- bcrypt/argon2
role TEXT NOT NULL,
expires_at TIMESTAMPTZ,
created_at TIMESTAMPTZ DEFAULT NOW()
);
Authorization checks¶
Middleware approach:
function requireRole(roles: string[]) {
return async (req, res, next) => {
const userRole = await getUserRole(req.user.sub, req.tenantId);
if (!roles.includes(userRole)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}
// Apply to routes
app.post('/v2/deployments', requireRole(['admin']), handleDeploy);
app.post('/v2/process-instances', requireRole(['admin', 'operator', 'worker']), handleCreate);
app.delete('/v2/process-instances/:key', requireRole(['admin', 'operator']), handleCancel);
app.post('/v2/jobs/:key/complete', requireRole(['worker', 'admin']), handleComplete);
Tenant isolation enforced¶
TODAS las queries deben incluir tenant_id:
async function listInstances(req) {
return db.query(`
SELECT * FROM process_instances
WHERE tenant_id = $1 AND state = $2
`, [req.tenantId, req.query.state]);
}
Postgres RLS como defense in depth:
ALTER TABLE process_instances ENABLE ROW LEVEL SECURITY;
CREATE POLICY tenant_isolation ON process_instances
FOR ALL USING (tenant_id = current_setting('app.current_tenant'));
Cuándo upgradear¶
Build fine-grained authorization SI:
- Cliente regulatory pide audit logs with specific permission grants
- Use case "process X visible, process Y oculto" para mismo role
- Compliance HIPAA/SOC2/etc. requires granularity
Migration: agregar permissions table, keep roles como aggregations.
Links¶
- concepts/authorization-model — Camunda model detalle
- adrs/adr-014-oidc-single-idp — Authentication
- entities/identity — Identity component