From 4302c5f474e96412f7a1c5d724b1d1c05acc2068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E0=A4=95=E0=A4=BE=E0=A4=B0=E0=A4=A4=E0=A5=8B=E0=A4=AB?= =?UTF-8?q?=E0=A5=8D=E0=A4=AB=E0=A5=87=E0=A4=B2=E0=A4=B8=E0=A5=8D=E0=A4=95?= =?UTF-8?q?=E0=A5=8D=E0=A4=B0=E0=A4=BF=E0=A4=AA=E0=A5=8D=E0=A4=9F=E2=84=A2?= Date: Tue, 13 May 2025 10:43:54 +0200 Subject: [PATCH] feat(Snowflake Node): Add support for Key-Pair authentication (#14833) --- .../credentials/Snowflake.credentials.ts | 44 +++++++++++++++++++ .../nodes/Snowflake/GenericFunctions.ts | 38 ++++++++++++++++ .../nodes/Snowflake/Snowflake.node.ts | 20 +++++---- .../__tests__/GenericFunctions.test.ts | 43 ++++++++++++++++++ 4 files changed, 137 insertions(+), 8 deletions(-) create mode 100644 packages/nodes-base/nodes/Snowflake/__tests__/GenericFunctions.test.ts diff --git a/packages/nodes-base/credentials/Snowflake.credentials.ts b/packages/nodes-base/credentials/Snowflake.credentials.ts index 0c42dac88a..0ba9afb1ec 100644 --- a/packages/nodes-base/credentials/Snowflake.credentials.ts +++ b/packages/nodes-base/credentials/Snowflake.credentials.ts @@ -30,11 +30,33 @@ export class Snowflake implements ICredentialType { description: 'The default virtual warehouse to use for the session after connecting. Used for performing queries, loading data, etc.', }, + { + displayName: 'Authentication', + name: 'authentication', + type: 'options', + options: [ + { + name: 'Password', + value: 'password', + }, + { + name: 'Key-Pair', + value: 'keyPair', + }, + ], + default: 'password', + description: 'The way to authenticate with Snowflake', + }, { displayName: 'Username', name: 'username', type: 'string', default: '', + displayOptions: { + show: { + authentication: ['password'], + }, + }, }, { displayName: 'Password', @@ -44,6 +66,28 @@ export class Snowflake implements ICredentialType { password: true, }, default: '', + displayOptions: { + show: { + authentication: ['password'], + }, + }, + }, + { + displayName: 'Private Key', + name: 'privateKey', + type: 'string', + typeOptions: { + password: true, + rows: 4, + }, + default: '', + required: true, + displayOptions: { + show: { + authentication: ['keyPair'], + }, + }, + description: 'Private PEM key for Key-pair authentication with Snowflake', }, { displayName: 'Schema', diff --git a/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts b/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts index 406752a5c5..58c2828e31 100644 --- a/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts +++ b/packages/nodes-base/nodes/Snowflake/GenericFunctions.ts @@ -1,5 +1,43 @@ +import pick from 'lodash/pick'; import type snowflake from 'snowflake-sdk'; +const commonConnectionFields = [ + 'account', + 'database', + 'schema', + 'warehouse', + 'role', + 'clientSessionKeepAlive', +] as const; + +export type SnowflakeCredential = Pick< + snowflake.ConnectionOptions, + (typeof commonConnectionFields)[number] +> & + ( + | { + authentication: 'password'; + username?: string; + password?: string; + } + | { + authentication: 'keyPair'; + privateKey: string; + } + ); + +export const getConnectionOptions = (credential: SnowflakeCredential) => { + const connectionOptions: snowflake.ConnectionOptions = pick(credential, commonConnectionFields); + if (credential.authentication === 'keyPair') { + connectionOptions.authenticator = 'SNOWFLAKE_JWT'; + connectionOptions.privateKey = credential.privateKey; + } else { + connectionOptions.username = credential.username; + connectionOptions.password = credential.password; + } + return connectionOptions; +}; + export async function connect(conn: snowflake.Connection) { return await new Promise((resolve, reject) => { conn.connect((error) => (error ? reject(error) : resolve())); diff --git a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts index f41387a057..83591603f4 100644 --- a/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts +++ b/packages/nodes-base/nodes/Snowflake/Snowflake.node.ts @@ -10,7 +10,13 @@ import snowflake from 'snowflake-sdk'; import { getResolvables } from '@utils/utilities'; -import { connect, destroy, execute } from './GenericFunctions'; +import { + connect, + destroy, + execute, + getConnectionOptions, + type SnowflakeCredential, +} from './GenericFunctions'; export class Snowflake implements INodeType { description: INodeTypeDescription = { @@ -164,16 +170,14 @@ export class Snowflake implements INodeType { }; async execute(this: IExecuteFunctions): Promise { - const credentials = (await this.getCredentials( - 'snowflake', - )) as unknown as snowflake.ConnectionOptions; - const returnData: INodeExecutionData[] = []; - let responseData; + const credentials = await this.getCredentials('snowflake'); - const connection = snowflake.createConnection(credentials); + const connectionOptions = getConnectionOptions(credentials); + const connection = snowflake.createConnection(connectionOptions); await connect(connection); + const returnData: INodeExecutionData[] = []; const items = this.getInputData(); const operation = this.getNodeParameter('operation', 0); @@ -189,7 +193,7 @@ export class Snowflake implements INodeType { query = query.replace(resolvable, this.evaluateExpression(resolvable, i) as string); } - responseData = await execute(connection, query, []); + const responseData = await execute(connection, query, []); const executionData = this.helpers.constructExecutionMetaData( this.helpers.returnJsonArray(responseData as IDataObject[]), { itemData: { item: i } }, diff --git a/packages/nodes-base/nodes/Snowflake/__tests__/GenericFunctions.test.ts b/packages/nodes-base/nodes/Snowflake/__tests__/GenericFunctions.test.ts new file mode 100644 index 0000000000..13577d3852 --- /dev/null +++ b/packages/nodes-base/nodes/Snowflake/__tests__/GenericFunctions.test.ts @@ -0,0 +1,43 @@ +import { getConnectionOptions } from '../GenericFunctions'; + +describe('getConnectionOptions', () => { + const commonOptions = { + account: 'test-account', + database: 'test-database', + schema: 'test-schema', + warehouse: 'test-warehouse', + role: 'test-role', + clientSessionKeepAlive: true, + }; + + describe('should return connection options', () => { + it('with username and password for password authentication', () => { + const result = getConnectionOptions({ + ...commonOptions, + authentication: 'password', + username: 'test-username', + password: 'test-password', + }); + + expect(result).toEqual({ + ...commonOptions, + username: 'test-username', + password: 'test-password', + }); + }); + + it('with private key for keyPair authentication', () => { + const result = getConnectionOptions({ + ...commonOptions, + authentication: 'keyPair', + privateKey: 'test-private-key', + }); + + expect(result).toEqual({ + ...commonOptions, + authenticator: 'SNOWFLAKE_JWT', + privateKey: 'test-private-key', + }); + }); + }); +});