Gerenciando credenciais que os usuários podem inserir como um objeto de credencial ou inline no painel de configuração do nó, com opção de fornecer senha ou token

Então, estou trabalhando em um nó customizado com muitas opções de configuração. Não é terrível complexo, apenas ocupado. Uma das coisas com a qual estou quebrando a cabeça é lidar com credenciais – basicamente, dadas quatro opções diferentes em dois conjuntos diferentes.

A primeira escolha que os usuários têm é se as credenciais são “codificadas” como um objeto de credencial (baseado em uma definição .credentials.ts) ou fornecidas dinamicamente nas propriedades do nó (definidas no arquivo .node.ts), para casos onde um único pipeline pode ser usado em um ambiente multi-site (permitindo que as credenciais sejam passadas dinamicamente de uma fonte segura; o “de onde” está além do meu escopo de influência, então essa pareceu ser a abordagem mais racional).

A segunda escolha é se eles fornecem um token de API diretamente ou fornecem uma senha que é usada para recuperar um token. Se o usuário fornecer um token, ele é apenas codificado e colocado em um cabeçalho Auth para qualquer requisição – sem problema. Mas se eles fornecerem uma senha, o nó precisa fazer uma requisição “especial” primeiro, com um cabeçalho Auth customizado contendo uma string codificada em base64 construída a partir do nome de usuário e da senha, e o token é retornado a partir disso.

Depois de ter avançado na implementação, percebi que tinha que lidar com muito disso manualmente, então o arquivo .credentials.ts apenas coleta dados (se o usuário seguir esse caminho) – assim como as entradas do nó. Tenho uma função apiRequest() definida e exportada a partir de um arquivo separado que lida com as requisições de API propriamente ditas, e uma função getApiToken() no mesmo arquivo (não exportada – é usada apenas por apiRequest() no momento) que especificamente lida com a requisição do token.

Os usuários podem escolher uma ou outra para uma ou outra. Independentemente disso, as entradas são as mesmas: nome de usuário, URL do site, e senha ou token de API. Eu já tinha tudo isso funcionando sem a opção de usar uma senha, mas comecei essa missão hoje, e consegui fazer funcionar com a opção de credenciais dinâmicas, mas parece que isso quebrou qualquer capacidade de puxar as credenciais da configuração baseada em .credentials.ts.

Apertei o problema em um bloco de código no arquivo .node.ts, que é apenas uma série de atribuições condicionais.

Primeiro, ele declara uma variável – credentials – e tenta usar getCredentials() para puxá-la da configuração .credentials.ts. Se não estiverem presentes, há uma captura de erro que simplesmente coloca credentials como undefined. Imediatamente depois, uma constante é declarada – dynamicConnection – que usa getNodeParameter() para obter as entradas das propriedades de entrada do nó (cada propriedade é type-safe, especificada como uma string) – então se nada estiver lá, elas são apenas null. Em seguida, um objeto connection é criado onde cada uma das quatro propriedades é atribuída com base em verificações condicionais das propriedades dos objetos credentials e dynamicConnection. Para cada uma, verificamos se o objeto credentials.<prop> contém uma string; se contiver, ela é atribuída, e se não, fazemos o mesmo para dynamicConnection. Se nenhuma contiver um valor string, a padrão é undefined. Finalmente, uma condição if() verifica para nos certificar de que temos (a) um nome de usuário, (b) uma URL de instância, e (c) uma senha ou um token; se qualquer uma dessas condições não for atendida, ela lança um erro. Essa condição if() é lida como “se NÃO nome de usuário -OU- NÃO URL de site -OU- ( NÃO token de api -E- NÃO senha): lance um erro.”

Isso funciona bem para a entrada dinâmica de credenciais, mas se eu tentar fazer isso com credenciais salvas, elas retornam o erro do bloco if(), como se algo estivesse faltando, mesmo sabendo que todos os campos estão lá. Criticamente – isso funcionava bem antes de eu adicionar a propriedade password ao connection, então genuinamente não acho que seja um problema de atribuição ou esperaria que tivesse falhado antes de implementar o campo de senha.

Sinto que estou perdendo algo absolutamente estúpido, mas espero que alguém aqui tenha mais clareza sobre isso do que eu tenho sozinho. Ou pelo menos que você possa confirmar que não estou louco? Por favor e obrigado!

@robby.emslie você não está louco, aqui está minha aposta: adicionar a opção de senha adicionou uma chamada getNodeParameter (ou um auth-selector displayOptions que oculta um campo), e getNodeParameter lança “Could not get parameter” quando não consegue resolver a prop, o que é o caso no caminho saved-cred. se esse throw cair no mesmo try/catch que você está usando para anular credenciais, todo o objeto saved-cred é apagado e seu if() dispara como ausente. dê a cada getNodeParameter um padrão (this.getNodeParameter(‘password’, i, ‘’)) para que não possa lançar, e mantenha o try com escopo apenas para getCredentials.

também, para a troca de senha para token que você está hand-rolling em getApiToken(), as creds do n8n têm um hook preAuthentication built-in que executa um pré-request, obtém o token e o armazena em cache, depois authenticate o injeta. As creds MetabaseApi e Auth0ManagementApi fazem exatamente username/password para token, vale a pena copiar esse padrão para a rota saved-cred.

1 curtida

@robby.emslie isso parece um desajuste entre como os campos são atribuídos versus como são validados.

Forma mais rápida de encontrar o culpado — faça log dos objetos resolvidos logo antes do if(), no caminho de credencial salva:

console.log(JSON.stringify({ credentials, dynamicConnection, connection }, null, 2));

Duas coisas em que eu apostaria:

  1. String vazia vs undefined. Através de getCredentials(), um campo não utilizado (como password na rota de token) frequentemente volta como “” em vez de undefined. “” passa em uma verificação de atribuição “typeof === ‘string’” mas é falsy no if() — então o validador lança “missing” mesmo que o campo tecnicamente exista. Faça os dois concordarem, por exemplo:
    const val = (typeof x === ‘string’ && x.trim() !== ‘’) ? x : undefined;

  2. A precedência/agrupamento daquele if() final. !user || !url || !token && !password é avaliado como !user || !url || (!token && !password) — que é o que você quer. Mas se o código real é ... || !token || !password, ele lança um erro sempre que password está vazio mesmo quando um token válido está presente. Esse simples swap OR/AND corresponde exatamente ao seu sintoma “funcionava antes de eu adicionar a propriedade password” — antes, não havia nenhum termo password na condição.

Se você colar o bloco .node.ts (as quatro atribuições + o if), consigo identificar com precisão.

1 curtida

As duas respostas acima cobrem o provável bug. Eu adicionaria uma proteção após: faça com que tanto as credenciais salvas quanto a entrada inline se resolvam em um mesmo objeto de conexão normalizado, com strings vazias removidas, modo de autenticação registrado e sem segredos brutos sendo registrados em logs. Depois valide apenas esse objeto. Isso torna mais fácil debugar sem vazar tokens.

3 curtidas

Algo ficou bem bagunçado aqui – haha! Basicamente vou ter que reconstruir tudo isso esta tarde, acho. Cometi um erro em algum lugar. Mas estou curioso sobre o hook integrado – vou dar uma olhada em Metabase e Auth0Management como modelo. Na verdade, tentei usar isso originalmente, e acho que quase consegui fazer funcionar, mas quando implementei a opção de credenciamento dinâmico, quebrei algo.

Mas isso é muito útil. Obrigado, muito mesmo. Acho que o problema está acontecendo em algum lugar com getCredentials(), mas sinto que, neste ponto, vai dar mais trabalho descobrir o que está errado do que fazer rebase do meu repo a partir da minha última configuração que funcionava.

Obrigado, amigo!

Queria voltar nesse assunto e agradecer. A sugestão de olhar para Metabase e Auth0Management foi um salva-vidas.

Acho que resolve o problema que eu tinha, mas vou na verdade remover a autenticação baseada em senha do dynamicCredentials, acho que vou. Sinto que é uma falha de segurança esperando para acontecer. Já funciona, mas vou comentar isso e se alguém do outro lado quiser ativar, que seja responsabilidade deles. :slightly_smiling_face: lol

De qualquer forma, obrigado novamente, @achamm !

1 curtida

@robby.emslie Fico feliz em ter ajudado! Fique à vontade para marcar qualquer uma das respostas como a solução e boa sorte!