diff --git a/lib/configuration/connection_configuration.js b/lib/configuration/connection_configuration.js new file mode 100644 index 000000000..b960d4742 --- /dev/null +++ b/lib/configuration/connection_configuration.js @@ -0,0 +1,32 @@ +/* + * Copyright (c) 2015-2024 Snowflake Computing Inc. All rights reserved. + */ + +const toml = require('toml'); +const os = require('os'); +const fs = require('fs'); +const { validateOnlyUserReadWritePermission } = require('../file_transfer_agent/file_util'); + +function defaultIfNotSet(value, defaultValue) { + if (value === null || typeof value === 'undefined' || value === '') { + return defaultValue; + } else { + return value; + } +} + +function loadConnectionConfiguration() { + const path = defaultIfNotSet(process.env.SNOWFLAKE_HOME, os.homedir() + '/.snowflake/'); + const filePath = path + 'connections.toml'; + validateOnlyUserReadWritePermission(filePath); + const str = fs.readFileSync(filePath, { encoding: 'utf8' }); + const parsingResult = toml.parse(str); + const configurationName = defaultIfNotSet(process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME, 'default'); + if (parsingResult[configurationName] !== undefined) { + return parsingResult[configurationName]; + } else { + throw new Error(`Connection configuration with name ${configurationName} does not exist`); + } +} + +exports.loadConnectionConfiguration = loadConnectionConfiguration; diff --git a/lib/connection/connection_config.js b/lib/connection/connection_config.js index 8ddacf5f9..f143f8099 100644 --- a/lib/connection/connection_config.js +++ b/lib/connection/connection_config.js @@ -6,6 +6,7 @@ const os = require('os'); const url = require('url'); const Util = require('../util'); const Errors = require('../errors'); +const fs = require('fs'); const ErrorCodes = Errors.codes; const NativeTypes = require('./result/data_types').NativeTypes; const GlobalConfig = require('../global_config'); @@ -59,6 +60,7 @@ const DEFAULT_PARAMS = 'disableSamlURLCheck', ]; const Logger = require('../logger'); +const { validateOnlyUserReadWritePermission } = require('../file_transfer_agent/file_util'); function consolidateHostAndAccount(options) { let dotPos = -1; @@ -291,11 +293,20 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) { ErrorCodes.ERR_CONN_CREATE_INVALID_PRIVATE_KEY_PASS); } - const token = options.token; + let token = options.token; if (Util.exists(options.token)) { Errors.checkArgumentValid(Util.isString(token), ErrorCodes.ERR_CONN_CREATE_INVALID_OAUTH_TOKEN); } + if (authenticator === authenticationTypes.OAUTH_AUTHENTICATOR && !Util.string.isNotNullOrEmpty(token)) { + const tokenFilePath = options.token_file_path ? options.token_file_path : '/snowflake/session/token'; + if (Util.exists(tokenFilePath)) { + validateOnlyUserReadWritePermission(tokenFilePath); + token = fs.readFileSync(tokenFilePath, 'utf-8').trim(); + Errors.checkArgumentValid(Util.isString(token), + ErrorCodes.ERR_CONN_CREATE_INVALID_OAUTH_TOKEN); + } + } const warehouse = options.warehouse; const database = options.database; @@ -501,7 +512,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) { DataTypes.setIsRepresentNullAsStringNull(options.representNullAsStringNull); } - + let disableSamlURLCheck = false; if (Util.exists(options.disableSamlURLCheck)) { Errors.checkArgumentValid(Util.isBoolean(options.disableSamlURLCheck), @@ -733,7 +744,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) { }; /** - * Returns the bind threshold + * Returns the bind threshold * * @returns {string} */ @@ -791,8 +802,8 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) { }; /** - * Returns whether the SAML URL check is enabled or not. - * + * Returns whether the SAML URL check is enabled or not. + * * @returns {Boolean} */ this.getDisableSamlURLCheck = function () { diff --git a/lib/file_transfer_agent/file_util.js b/lib/file_transfer_agent/file_util.js index b533d4e67..b0629f3a4 100644 --- a/lib/file_transfer_agent/file_util.js +++ b/lib/file_transfer_agent/file_util.js @@ -3,12 +3,13 @@ */ const crypto = require('crypto'); -const fs = require('fs'); +const fs = require('fs'); const path = require('path'); const struct = require('python-struct'); const zlib = require('zlib'); const os = require('os'); const glob = require('glob'); +const Logger = require('../logger'); const resultStatus = { ERROR: 'ERROR', @@ -144,4 +145,17 @@ function getMatchingFilePaths(dir, fileName) { return glob.sync(pathWithWildcardDependsOnPlatform); } +function validateOnlyUserReadWritePermission(filePath) { + fs.accessSync(filePath, fs.constants.F_OK); + const mode = (fs.statSync(filePath)).mode; + const permission = mode & 0o600; + //This should be 600 permission, which means the file permission has not been changed by others. + if (permission.toString(8) === '600') { + Logger.getInstance().debug('Validated that the user has only read and write permission'); + } else { + throw new Error('File permissions different than read/write for user.'); + } +} + exports.getMatchingFilePaths = getMatchingFilePaths; +exports.validateOnlyUserReadWritePermission = validateOnlyUserReadWritePermission; diff --git a/package.json b/package.json index 63452d000..f2de197df 100644 --- a/package.json +++ b/package.json @@ -30,6 +30,7 @@ "open": "^7.3.1", "python-struct": "^1.1.3", "simple-lru-cache": "^0.0.2", + "toml": "^3.0.0", "uuid": "^8.3.2", "winston": "^3.1.0" }, diff --git a/test/connections.toml b/test/connections.toml new file mode 100644 index 000000000..87099c693 --- /dev/null +++ b/test/connections.toml @@ -0,0 +1,35 @@ +[default] +account = 'snowdriverswarsaw.us-west-2.aws' +user = 'test_user' +password = 'test_pass' +warehouse = 'testw' +database = 'test_db' +schema = 'test_nodejs' +protocol = 'https' +port = '443' + +[aws-oauth] +account = 'snowdriverswarsaw.us-west-2.aws' +user = 'test_user' +password = 'test_pass' +warehouse = 'testw' +database = 'test_db' +schema = 'test_nodejs' +protocol = 'https' +port = '443' +authenticator = 'oauth' +testNot = 'problematicParameter' +token = 'token_value' + +[aws-oauth-file] +account = 'snowdriverswarsaw.us-west-2.aws' +user = 'test_user' +password = 'test_pass' +warehouse = 'testw' +database = 'test_db' +schema = 'test_nodejs' +protocol = 'https' +port = '443' +authenticator = 'oauth' +testNot = 'problematicParameter' +token_file_path = '/Users/test/.snowflake/token' \ No newline at end of file diff --git a/test/integration/testManualConnection.js b/test/integration/testManualConnection.js index 0efa20a78..80c34b65d 100644 --- a/test/integration/testManualConnection.js +++ b/test/integration/testManualConnection.js @@ -8,6 +8,7 @@ const assert = require('assert'); const connOption = require('./connectionOptions'); const testUtil = require('./testUtil'); const Logger = require('../../lib/logger'); +const { loadConnectionConfiguration } = require('../../lib/configuration/connection_configuration'); if (process.env.RUN_MANUAL_TESTS_ONLY === 'true') { describe.only('Run manual tests', function () { @@ -265,7 +266,7 @@ if (process.env.RUN_MANUAL_TESTS_ONLY === 'true') { }); }); - describe.only('keepAlive test', function () { + describe('keepAlive test', function () { let connection; const loopCount = 10; const rowCount = 10; @@ -330,4 +331,37 @@ if (process.env.RUN_MANUAL_TESTS_ONLY === 'true') { assert.ok(sumWithoutKeepAlive * 0.66 > sumWithKeepAlive, 'With keep alive the queries should work faster'); }); }); + + + // Before run below tests you should prepare files connections.toml and token + describe('Connection file configuration test', function () { + afterEach( function () { + delete process.env.SNOWFLAKE_HOME; + delete process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME; + }); + + it('test simple connection', async function () { + const configuration = await loadConnectionConfiguration(); + await verifyConnectionWorks(configuration); + }); + it('test connection with token', async function () { + process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME = 'aws-oauth'; + const configuration = await loadConnectionConfiguration(); + await verifyConnectionWorks(configuration); + }); + it('test connection with token from file', async function () { + process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME = 'aws-oauth-file'; + const configuration = await loadConnectionConfiguration(); + await verifyConnectionWorks(configuration); + }); + + async function verifyConnectionWorks(configuration) { + const connection = snowflake.createConnection(configuration); + await testUtil.connectAsync(connection); + assert.ok(connection.isUp(), 'not active'); + await testUtil.executeCmdAsync(connection, 'Select 1'); + await testUtil.destroyConnectionAsync(connection); + } + }); + } diff --git a/test/unit/configuration/configuration_parsing_test.js b/test/unit/configuration/configuration_parsing_test.js index 4daef0423..377ffa6aa 100644 --- a/test/unit/configuration/configuration_parsing_test.js +++ b/test/unit/configuration/configuration_parsing_test.js @@ -4,12 +4,76 @@ const assert = require('assert'); const { Levels, ConfigurationUtil } = require('./../../../lib/configuration/client_configuration'); +const { loadConnectionConfiguration } = require('./../../../lib/configuration/connection_configuration'); const getClientConfig = new ConfigurationUtil().getClientConfig; const fsPromises = require('fs/promises'); const os = require('os'); const path = require('path'); +const Util = require('../../../lib/util'); let tempDir = null; + +describe('should parse toml connection configuration', function () { + + afterEach( function () { + delete process.env.SNOWFLAKE_HOME; + delete process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME; + }); + + it('should parse toml with connection configuration: ', async function () { + process.env.SNOWFLAKE_HOME = process.cwd() + '/test/'; + const configuration = await loadConnectionConfiguration(); + assert.strictEqual(configuration['account'], 'snowdriverswarsaw.us-west-2.aws'); + assert.strictEqual(configuration['user'], 'test_user'); + assert.strictEqual(configuration['password'], 'test_pass'); + assert.strictEqual(configuration['warehouse'], 'testw'); + assert.strictEqual(configuration['database'], 'test_db'); + assert.strictEqual(configuration['schema'], 'test_nodejs'); + assert.strictEqual(configuration['protocol'], 'https'); + assert.strictEqual(configuration['port'], '443'); + }); + + it('should parse toml with connection configuration - oauth', async function () { + process.env.SNOWFLAKE_HOME = process.cwd() + '/test/'; + process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME = 'aws-oauth'; + const configuration = await loadConnectionConfiguration(); + assert.strictEqual(configuration['token'], 'token_value'); + assert.strictEqual(configuration['authenticator'], 'oauth'); + }); + + it('should parse toml with connection configuration - oauth and token in file', async function () { + process.env.SNOWFLAKE_HOME = process.cwd() + '/test/'; + process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME = 'aws-oauth-file'; + const configuration = await loadConnectionConfiguration(); + assert.ok(Util.string.isNotNullOrEmpty(configuration['token_file_path'])); + assert.strictEqual(configuration['authenticator'], 'oauth'); + }); + + it('should throw error toml when file does not exist', function (done) { + process.env.SNOWFLAKE_HOME = '/unknown/'; + try { + loadConnectionConfiguration(); + assert.fail(); + } catch (error) { + assert.match(error.message, /ENOENT: no such file or directory/); + done(); + } + }); + + it('should throw exception if configuration does not exists', function (done) { + process.env.SNOWFLAKE_DEFAULT_CONNECTION_NAME = 'unknown'; + process.env.SNOWFLAKE_HOME = process.cwd() + '/test/'; + + try { + loadConnectionConfiguration(); + assert.fail(); + } catch (error) { + assert.strictEqual(error.message, 'Connection configuration with name unknown does not exist'); + done(); + } + }); +}); + describe('Configuration parsing tests', function () { before(async function () {