Segurança e Multi-tenancy
O Kikwiflow trata segurança como um ponto de extensão explícito, não como uma feature embutida e opinativa.
O motor define duas SPIs pequenas em kikwi-security-api; o kikwi-core fornece implementações padrão
minimalistas (uma delas deliberadamente permissiva); e é responsabilidade de cada aplicação decidir se — e como
— reforçar controle de acesso real.
IdentityContext: quem está fazendo a chamada
public record IdentityContext(String actorId, String tenantId, Set<String> roles) {
public static IdentityContext system() {
return new IdentityContext("__KIKWIFLOW_SYSTEM__", "GLOBAL", Set.of("SYSTEM_ADMIN"));
}
public boolean isSystem() { return "__KIKWIFLOW_SYSTEM__".equals(this.actorId); }
}
Toda operação de comando exposta por KikwiflowEngine (startProcess, completeExternalTask, retryIncident,
setVariables, deleteInstance) recebe um IdentityContext — mas o motor não o extrai de nenhum lugar
automaticamente. Na camada REST (kikwi-security-spring-starter), um HttpIdentityResolver e um
IdentityContextArgumentResolver injetam o IdentityContext a partir da requisição HTTP (tipicamente de um
JWT ou header de autenticação) diretamente como parâmetro de controller — mas essa fiação é opcional e
substituível.
IdentityContext.system() é o identity usado internamente pelo motor para operações que não têm um ator humano
associado — por exemplo, o KikwiflowAutoDeployer chama processDefinitionService.deploy(content, IdentityContext.system()) ao implantar processos do classpath no boot da aplicação.
DeploymentSecurityManager: controlando quem pode implantar processos
public interface DeploymentSecurityManager {
/** @throws SecurityException se o acesso for negado. */
void validateDeployAccess(IdentityContext identity, ProcessDefinitionDeployRequest content);
}
A implementação padrão fornecida pelo kikwi-core, DefaultDeploymentSecurityManager, é um interruptor global
simples:
public class DefaultDeploymentSecurityManager implements DeploymentSecurityManager {
private final boolean isDeployEnabled;
@Override
public void validateDeployAccess(IdentityContext identity, ProcessDefinitionDeployRequest content) {
if (!isDeployEnabled) throw new SecurityException("Deploys estão desabilitados");
}
}
Ligado a uma única propriedade:
kikwiflow:
security:
deploy-enabled: true # default false — se false, TODO deploy é rejeitado, independentemente de quem chama
Note que isso não é controle de acesso por identidade/role — é um interruptor de tudo-ou-nada. Se sua
aplicação precisa de regras mais finas (ex.: só usuários com role PROCESS_ADMIN podem implantar, ou deploys
só são permitidos para processos do próprio tenant), forneça seu próprio bean DeploymentSecurityManager —
KikwiflowAutoConfiguration registra a implementação padrão apenas com @ConditionalOnMissingBean, então um
bean customizado no seu contexto Spring automaticamente toma precedência, sem nenhuma configuração adicional.
@Component
public class RoleBasedDeploymentSecurityManager implements DeploymentSecurityManager {
@Override
public void validateDeployAccess(IdentityContext identity, ProcessDefinitionDeployRequest content) {
if (!identity.roles().contains("PROCESS_ADMIN")) {
throw new SecurityException("Ator " + identity.actorId() + " não tem permissão de deploy.");
}
}
}
VariableSecurityPolicyManager: mascaramento e controle de leitura/escrita de variáveis
public interface VariableSecurityPolicyManager {
boolean canWrite(String processDefinitionId, IdentityContext identity, Set<String> variableNames);
boolean canRead(String processDefinitionId, IdentityContext identity, Set<String> variableNames);
boolean canWrite(String processDefinitionId, IdentityContext identity, String variableName);
boolean canRead(String processDefinitionId, IdentityContext identity, String variableName);
Map<String, ProcessVariable> applyReadPoliciesAndMasking(String processDefinitionId, IdentityContext identity, Map<String, ProcessVariable> rawVariables);
}
:::danger A implementação padrão é um no-op — todo mundo pode ler e escrever tudo
public class DefaultVariableSecurityPolicyManager implements VariableSecurityPolicyManager {
public boolean canWrite(...) { return true; }
public boolean canRead(...) { return true; }
public Map<String, ProcessVariable> applyReadPoliciesAndMasking(String processDefinitionId, IdentityContext identity, Map<String, ProcessVariable> rawVariables) {
return rawVariables; // sem mascaramento algum
}
}
Isso é intencional e documentado no próprio CLAUDE.md do repositório: RBAC e mascaramento de variáveis é um
ponto de extensão que não faz nada por padrão. Se o seu processo carrega dados sensíveis em variáveis (PII,
dados financeiros, documentos) e múltiplos tenants/roles compartilham a mesma instância da aplicação, você
precisa fornecer sua própria implementação de VariableSecurityPolicyManager antes de ir para produção — caso
contrário, qualquer identidade autenticada que consiga chamar a API de consulta enxerga o valor bruto de todas
as variáveis de qualquer instância.
:::
@Component
public class TenantAwareVariableSecurityPolicyManager implements VariableSecurityPolicyManager {
@Override
public Map<String, ProcessVariable> applyReadPoliciesAndMasking(
String processDefinitionId, IdentityContext identity, Map<String, ProcessVariable> rawVariables) {
if (identity.roles().contains("VIEW_SENSITIVE_DATA")) return rawVariables;
Map<String, ProcessVariable> masked = new HashMap<>(rawVariables);
masked.computeIfPresent("cpfCliente", (k, v) -> new ProcessVariable(k, "***MASKED***"));
return masked;
}
// canRead/canWrite conforme a regra de negócio
}
Isolamento por tenant (tenantId)
tenantId é um campo de primeira classe em ProcessInstance e ExternalTask (definido na criação da instância
via ProcessStarter.onTenant(tenantId)), mas o motor não aplica isolamento por tenant automaticamente em
todas as operações — o único ponto onde há checagem explícita hoje é
KikwiflowEngine.completeExternalTask:
if (!Objects.equals(taskToComplete.tenantId(), identityContext.tenantId())) {
throw new SecurityException("Tenant mismatch: Task " + externalTaskId + " does not belong to the provided tenant.");
}
Para consultas (createProcessInstanceQuery(), createExternalTaskQuery(), endpoints REST de busca), o filtro
por tenantId/tenantIds está disponível como parâmetro de busca (ver
Busca Avançada de Instâncias), mas é responsabilidade de quem monta a query incluir o
filtro de tenant do identity atual — o motor não injeta esse filtro automaticamente por trás dos panos. Trate
isolamento multi-tenant como uma responsabilidade da camada de API/controller que precede o motor, reforçada
consistentemente em toda superfície de leitura e escrita exposta a usuários finais.
Resumo de responsabilidades
| Mecanismo | Fornecido pelo motor | Aplicado automaticamente? |
|---|---|---|
IdentityContext propagado pelas chamadas | Sim (tipo) | Não — extração de request HTTP é responsabilidade de kikwi-security-spring-starter ou de código próprio |
| Bloqueio global de deploy | Sim (DefaultDeploymentSecurityManager) | Sim, mas é tudo-ou-nada, não por identidade |
| RBAC/mascaramento de variáveis | Interface apenas (DefaultVariableSecurityPolicyManager é no-op) | Não — requer implementação própria |
Isolamento por tenant em completeExternalTask | Sim | Sim, apenas nesse método específico |
| Isolamento por tenant em consultas | Suporte via parâmetro de filtro | Não — precisa ser aplicado explicitamente pelo chamador |