Skip to content

Commit

Permalink
Snow 1487602 connection toml configuration (#859)
Browse files Browse the repository at this point in the history
* SNOW-1487602 - read connection configuration from toml file

* SNOW-1487602- TOML file connection configuration - code review fixes
  • Loading branch information
sfc-gh-pmotacki authored Jun 26, 2024
1 parent 1ed013a commit 03d557d
Show file tree
Hide file tree
Showing 7 changed files with 198 additions and 7 deletions.
32 changes: 32 additions & 0 deletions lib/configuration/connection_configuration.js
Original file line number Diff line number Diff line change
@@ -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;
21 changes: 16 additions & 5 deletions lib/connection/connection_config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -733,7 +744,7 @@ function ConnectionConfig(options, validateCredentials, qaMode, clientInfo) {
};

/**
* Returns the bind threshold
* Returns the bind threshold
*
* @returns {string}
*/
Expand Down Expand Up @@ -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 () {
Expand Down
16 changes: 15 additions & 1 deletion lib/file_transfer_agent/file_util.js
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down Expand Up @@ -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;
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
},
Expand Down
35 changes: 35 additions & 0 deletions test/connections.toml
Original file line number Diff line number Diff line change
@@ -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'
36 changes: 35 additions & 1 deletion test/integration/testManualConnection.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
});

}
64 changes: 64 additions & 0 deletions test/unit/configuration/configuration_parsing_test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 () {
Expand Down

0 comments on commit 03d557d

Please sign in to comment.