Pular para o conteúdo principal

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çãoO que o motor checa
claim / unclaimNada — qualquer chamador assume ou libera qualquer tarefa, e pode atribuí-la a qualquer assignee.
completeExternalTaskSó 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​

MecanismoFornecido pelo motorAplicado automaticamente?
IdentityContext propagado pelas chamadasSim (tipo)Não — extração de request HTTP é responsabilidade de kikwi-security-spring-starter ou de código próprio
Bloqueio global de deploySim (DefaultDeploymentSecurityManager)Sim, mas é tudo-ou-nada, não por identidade
RBAC/mascaramento de variáveisInterface apenas (DefaultVariableSecurityPolicyManager é no-op)Não — requer implementação própria
Isolamento por tenant em completeExternalTaskSimSim, apenas nesse método específico
Isolamento por tenant em consultasSuporte via parâmetro de filtroNão — precisa ser aplicado explicitamente pelo chamador
Restringir quem assume/conclui uma tarefa externa por perfil de acesso (roles)NãoNão — precisa ser aplicado pela aplicação antes de chamar o motor