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: 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.
Quem pode assumir e concluir uma tarefa externa
O motor não restringe quem trabalha numa EXTERNAL_TASK. Não há, no .kikwi, como declarar que uma
tarefa só pode ser assumida ou concluída por quem tem um determinado perfil de acesso (um dos roles do
IdentityContext, como ANALISTA_CREDITO) ou por uma pessoa específica. Na prática:
| Operação | O que o motor checa |
|---|---|
claim / unclaim | Nada — qualquer chamador assume ou libera qualquer tarefa, e pode atribuí-la a qualquer assignee. |
completeExternalTask | Só o tenant (ver acima). Não checa roles nem se quem conclui é o assignee atual. |
Consultar a tarefa e seu formulário (GET /external-tasks/{id}, GET /external-tasks/{id}/form) | Nada — os endpoints nem recebem a identidade de quem chama. |
Se o seu processo tem tarefas que só certos perfis podem decidir — a aprovação de crédito só por analistas, por
exemplo —, faça essa checagem na sua aplicação, antes de chamar o motor: o controller ou backend que recebe a
ação do usuário confere os roles dele e só então chama claim/completeExternalTask. Pelo mesmo motivo, não
exponha a API de gestão diretamente ao navegador do usuário final.
extensionProperties do nó (por exemplo, "actor": "analista_credito") é um bom lugar para declarar quem deve
atuar na tarefa, para a sua aplicação ler e aplicar — o motor não interpreta esse campo.
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 |
Restringir quem assume/conclui uma tarefa externa por perfil de acesso (roles) | Não | Não — precisa ser aplicado pela aplicação antes de chamar o motor |