diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8695e55b..e0e07065 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,6 +15,11 @@ env: V_PORT: 5433 V_USER: dbadmin V_DATABASE: VMart + KC_REALM: test + KC_USER: oauth_user + KC_PASSWORD: password + KC_CLIENT_ID: vertica + KC_CLIENT_SECRET: P9f8350QQIUhFfK1GF5sMhq4Dm3P6Sbs jobs: build: @@ -44,13 +49,23 @@ jobs: - name: boostrap run: yarn lerna bootstrap + + - name: Set up a Keycloak docker container + timeout-minutes: 5 + run: | + docker network create -d bridge my-network + docker run -d -p 8080:8080 \ + --name keycloak --network my-network \ + -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin \ + quay.io/keycloak/keycloak:23.0.4 start-dev + docker container ls - - name: Setup Vertica + - name: Setup Vertica server docker container timeout-minutes: 15 run: | docker run -d -p 5433:5433 -p 5444:5444 \ --mount type=volume,source=vertica-data,target=/data \ - --name vertica_ce \ + --name vertica_ce --network my-network \ opentext/vertica-ce:24.2.0-1 echo "Vertica startup ..." until docker exec vertica_ce test -f /data/vertica/VMart/agent_start.out; do \ @@ -61,6 +76,47 @@ jobs: docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "\l" docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "select version()" + - name: Configure Keycloak + run: | + echo "Wait for keycloak ready ..." + bash -c 'while true; do curl -s localhost:8080 &>/dev/null; ret=$?; [[ $ret -eq 0 ]] && break; echo "..."; sleep 3; done' + + docker exec -i keycloak /bin/bash < access_token.txt + + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "CREATE AUTHENTICATION v_oauth METHOD 'oauth' HOST '0.0.0.0/0';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET client_id = '${KC_CLIENT_ID}';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET client_secret = '${KC_CLIENT_SECRET}';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET discovery_url = 'http://`hostname`:8080/realms/${KC_REALM}/.well-known/openid-configuration';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_oauth SET introspect_url = 'http://`hostname`:8080/realms/${KC_REALM}/protocol/openid-connect/token/introspect';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "SELECT * FROM client_auth WHERE auth_name='v_oauth';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "CREATE USER ${KC_USER};" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "GRANT AUTHENTICATION v_oauth TO ${KC_USER};" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "GRANT ALL ON SCHEMA PUBLIC TO ${KC_USER};" + # A dbadmin-specific authentication record (connect remotely) is needed after setting up an OAuth user + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "CREATE AUTHENTICATION v_dbadmin_hash METHOD 'hash' HOST '0.0.0.0/0';" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "ALTER AUTHENTICATION v_dbadmin_hash PRIORITY 10000;" + docker exec -u dbadmin vertica_ce /opt/vertica/bin/vsql -c "GRANT AUTHENTICATION v_dbadmin_hash TO dbadmin;" + - name: test-v-connection-string if: always() run: | @@ -82,5 +138,6 @@ jobs: - name: test-vertica-nodejs if: always() run: | + export VTEST_OAUTH_ACCESS_TOKEN=`cat access_token.txt` cd packages/vertica-nodejs yarn test diff --git a/DATATYPES.md b/DATATYPES.md index 5c77a2f1..fca5421f 100644 --- a/DATATYPES.md +++ b/DATATYPES.md @@ -14,6 +14,7 @@ The result set metadata currently just displays the Type ID for each column as a | 116 | Long Varbinary | LongVarbinary | | 117 | Binary | Binary | | 16 | Numeric | Numeric | +| 20 | UUID | Uuid | | 114 | Interval Year | IntervalYear | | 114 | Interval Year to Month | IntervalYearToMonth | | 114 | Interval Month | IntervalMonth | diff --git a/packages/v-connection-string/index.js b/packages/v-connection-string/index.js index a66107af..b79fdd6e 100644 --- a/packages/v-connection-string/index.js +++ b/packages/v-connection-string/index.js @@ -56,7 +56,10 @@ function parse(str) { config.database = result.query.db config.client_encoding = result.query.encoding return config + } else if (result.protocol !== 'vertica:') { + throw new Error("Invalid connection string. Only vertica:// scheme is supported."); } + if (!config.host) { // Only set the host if there is no equivalent query param. config.host = result.hostname diff --git a/packages/v-connection-string/test/parse.js b/packages/v-connection-string/test/parse.js index ab53c99b..298bb20c 100644 --- a/packages/v-connection-string/test/parse.js +++ b/packages/v-connection-string/test/parse.js @@ -22,7 +22,7 @@ var parse = require('../').parse describe('parse', function () { it('using connection string in client constructor', function () { - var subject = parse('postgres://brian:pw@boom:381/lala') + var subject = parse('vertica://brian:pw@boom:381/lala') subject.user.should.equal('brian') subject.password.should.equal('pw') subject.host.should.equal('boom') @@ -31,12 +31,12 @@ describe('parse', function () { }) it('escape spaces if present', function () { - var subject = parse('postgres://localhost/post gres') + var subject = parse('vertica://localhost/post gres') subject.database.should.equal('post gres') }) it('do not double escape spaces', function () { - var subject = parse('postgres://localhost/post%20gres') + var subject = parse('vertica://localhost/post%20gres') subject.database.should.equal('post gres') }) @@ -82,7 +82,7 @@ describe('parse', function () { database: 'postgres', } var connectionString = - 'postgres://' + + 'vertica://' + sourceConfig.user + ':' + sourceConfig.password + @@ -105,7 +105,7 @@ describe('parse', function () { database: 'postgres', } var connectionString = - 'postgres://' + + 'vertica://' + sourceConfig.user + ':' + sourceConfig.password + @@ -120,7 +120,7 @@ describe('parse', function () { }) it('username or password contains weird characters', function () { - var strang = 'pg://my f%irst name:is&%awesome!@localhost:9000' + var strang = 'vertica://my f%irst name:is&%awesome!@localhost:9000' var subject = parse(strang) subject.user.should.equal('my f%irst name') subject.password.should.equal('is&%awesome!') @@ -128,7 +128,7 @@ describe('parse', function () { }) it('url is properly encoded', function () { - var encoded = 'pg://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl' + var encoded = 'vertica://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl' var subject = parse(encoded) subject.user.should.equal('bi%na%%ry ') subject.password.should.equal('s@f#') @@ -137,24 +137,24 @@ describe('parse', function () { }) it('relative url sets database', function () { - var relative = 'different_db_on_default_host' + var relative = 'vertica:///different_db_on_default_host' var subject = parse(relative) subject.database.should.equal('different_db_on_default_host') }) it('no pathname returns null database', function () { - var subject = parse('pg://myhost') + var subject = parse('vertica://myhost') ;(subject.database === null).should.equal(true) }) it('pathname of "/" returns null database', function () { - var subject = parse('pg://myhost/') + var subject = parse('vertica://myhost/') subject.host.should.equal('myhost') ;(subject.database === null).should.equal(true) }) it('configuration parameter host', function () { - var subject = parse('pg://user:pass@/dbname?host=/unix/socket') + var subject = parse('vertica://user:pass@/dbname?host=/unix/socket') subject.user.should.equal('user') subject.password.should.equal('pass') subject.host.should.equal('/unix/socket') @@ -162,12 +162,12 @@ describe('parse', function () { }) it('configuration parameter host overrides url host', function () { - var subject = parse('pg://user:pass@localhost/dbname?host=/unix/socket') + var subject = parse('vertica://user:pass@localhost/dbname?host=/unix/socket') subject.host.should.equal('/unix/socket') }) it('url with encoded socket', function () { - var subject = parse('pg://user:pass@%2Funix%2Fsocket/dbname') + var subject = parse('vertica://user:pass@%2Funix%2Fsocket/dbname') subject.user.should.equal('user') subject.password.should.equal('pass') subject.host.should.equal('/unix/socket') @@ -175,7 +175,7 @@ describe('parse', function () { }) it('url with real host and an encoded db name', function () { - var subject = parse('pg://user:pass@localhost/%2Fdbname') + var subject = parse('vertica://user:pass@localhost/%2Fdbname') subject.user.should.equal('user') subject.password.should.equal('pass') subject.host.should.equal('localhost') @@ -183,7 +183,7 @@ describe('parse', function () { }) it('configuration parameter host treats encoded socket as part of the db name', function () { - var subject = parse('pg://user:pass@%2Funix%2Fsocket/dbname?host=localhost') + var subject = parse('vertica://user:pass@%2Funix%2Fsocket/dbname?host=localhost') subject.user.should.equal('user') subject.password.should.equal('pass') subject.host.should.equal('localhost') @@ -191,25 +191,33 @@ describe('parse', function () { }) it('configuration parameter options', function () { - var connectionString = 'pg:///?options=-c geqo=off' + var connectionString = 'vertica:///?options=-c geqo=off' var subject = parse(connectionString) subject.options.should.equal('-c geqo=off') }) + it('configuration parameter oauth_access_token, workload, client_label', function () { + var connectionString = 'vertica:///dbname?oauth_access_token=xxx&workload=analytics&client_label=vertica-nodejs' + var subject = parse(connectionString) + subject.oauth_access_token.should.equal('xxx') + subject.workload.should.equal('analytics') + subject.client_label.should.equal('vertica-nodejs') + }) + it('configuration parameter tls_mode=require', function () { - var connectionString = 'pg:///?tls_mode=require' + var connectionString = 'vertica:///?tls_mode=require' var subject = parse(connectionString) subject.tls_mode.should.equal('require') }) it('configuration parameter tls_mode=disable', function () { - var connectionString = 'pg:///?tls_mode=disable' + var connectionString = 'vertica:///?tls_mode=disable' var subject = parse(connectionString) subject.tls_mode.should.equal('disable') }) it('set tls_mode', function () { - var subject = parse('pg://myhost/db?tls_mode=require') + var subject = parse('vertica://myhost/db?tls_mode=require') subject.tls_mode.should.equal('require') }) @@ -233,43 +241,43 @@ describe('parse', function () { */ it('configuration parameter tls_trusted_certs=/path/to/ca', function () { - var connectionString = 'pg:///?tls_trusted_certs=' + __dirname + '/example.ca' + var connectionString = 'vertica:///?tls_trusted_certs=' + __dirname + '/example.ca' var subject = parse(connectionString) subject.tls_trusted_certs.should.eql(__dirname + '/example.ca') }) it('configuration parameter tls_mode=no-verify', function () { - var connectionString = 'pg:///?tls_mode=no-verify' // not a supported tls_mode, should instead default to disable + var connectionString = 'vertica:///?tls_mode=no-verify' // not a supported tls_mode, should instead default to disable var subject = parse(connectionString) subject.tls_mode.should.eql('disable') }) it('configuration parameter tls_mode=verify-ca', function () { - var connectionString = 'pg:///?tls_mode=verify-ca' + var connectionString = 'vertica:///?tls_mode=verify-ca' var subject = parse(connectionString) subject.tls_mode.should.eql('verify-ca') }) it('configuration parameter tls_mode=verify-full', function () { - var connectionString = 'pg:///?tls_mode=verify-full' + var connectionString = 'vertica:///?tls_mode=verify-full' var subject = parse(connectionString) subject.tls_mode.should.eql('verify-full') }) it('allow other params like max, ...', function () { - var subject = parse('pg://myhost/db?max=18&min=4') + var subject = parse('vertica://myhost/db?max=18&min=4') subject.max.should.equal('18') subject.min.should.equal('4') }) it('configuration parameter keepalives', function () { - var connectionString = 'pg:///?keepalives=1' + var connectionString = 'vertica:///?keepalives=1' var subject = parse(connectionString) subject.keepalives.should.equal('1') }) it('unknown configuration parameter is passed into client', function () { - var connectionString = 'pg:///?ThereIsNoSuchPostgresParameter=1234' + var connectionString = 'vertica:///?ThereIsNoSuchPostgresParameter=1234' var subject = parse(connectionString) subject.ThereIsNoSuchPostgresParameter.should.equal('1234') }) @@ -282,7 +290,7 @@ describe('parse', function () { }) it('return last value of repeated parameter', function () { - var connectionString = 'pg:///?keepalives=1&keepalives=0' + var connectionString = 'vertica:///?keepalives=1&keepalives=0' var subject = parse(connectionString) subject.keepalives.should.equal('0') }) diff --git a/packages/v-protocol/src/backend-messages.ts b/packages/v-protocol/src/backend-messages.ts index 57ee286d..a21e1d0e 100644 --- a/packages/v-protocol/src/backend-messages.ts +++ b/packages/v-protocol/src/backend-messages.ts @@ -40,6 +40,7 @@ export type MessageName = | 'authenticationMD5Password' | 'authenticationSHA512Password' | 'authenticationCleartextPassword' + | 'authenticationOAuthPassword' | 'error' | 'notice' | 'verifyFiles' diff --git a/packages/v-protocol/src/parser.ts b/packages/v-protocol/src/parser.ts index 098d0ca2..729eb5d1 100644 --- a/packages/v-protocol/src/parser.ts +++ b/packages/v-protocol/src/parser.ts @@ -419,6 +419,9 @@ export class Parser { return new AuthenticationMD5Password(length, salt) } break + case 12: // AuthenticationOAuthPassword + message.name = 'authenticationOAuthPassword' + break case 65536: // AuthenticationHashPassword case 66048: // AuthenticationHashSHA512Password if(message.length === 32) { diff --git a/packages/v-protocol/src/serializer.ts b/packages/v-protocol/src/serializer.ts index 978ab9f8..7910ab08 100644 --- a/packages/v-protocol/src/serializer.ts +++ b/packages/v-protocol/src/serializer.ts @@ -286,7 +286,11 @@ function getFileSize(filePath: string): number { //numFiles: number, fileNames: string[], fileLengths: number[] const verifiedFiles = (config: genericConfig): Buffer => { - writer.addInt16(config.numFiles) // In 3.15 this will be 'writer.addInt32(config.numFiles) + if (config.protocol_version < (3 << 16 | 15)) { + writer.addInt16(config.numFiles) + } else { + writer.addInt32(config.numFiles) + } for(let i = 0; i < config.numFiles; i++) { writer.addCString(config.fileNames[i]) writer.addInt32(0) diff --git a/packages/v-protocol/src/vertica-types.ts b/packages/v-protocol/src/vertica-types.ts index 6834ca55..90796851 100644 --- a/packages/v-protocol/src/vertica-types.ts +++ b/packages/v-protocol/src/vertica-types.ts @@ -27,6 +27,7 @@ export enum VerticaType { LongVarbinary = 116, Binary = 117, Numeric = 16, + Uuid = 20, IntervalYear = 114, IntervalYearToMonth = 114, IntervalMonth = 114, diff --git a/packages/v-protocol/test/inbound-parser.test.ts b/packages/v-protocol/test/inbound-parser.test.ts index 6ef9bc93..b4c3f633 100644 --- a/packages/v-protocol/test/inbound-parser.test.ts +++ b/packages/v-protocol/test/inbound-parser.test.ts @@ -203,8 +203,9 @@ var testForMessage = function (buffer: Buffer, expectedMessage: any) { }) } -var plainPasswordBuffer = buffers.authenticationCleartextPassword() -var md5PasswordBuffer = buffers.authenticationMD5Password() +var authCleartextBuffer = buffers.authenticationCleartextPassword() +var authMD5Buffer = buffers.authenticationMD5Password() +var authOAuthBuffer = buffers.authenticationOAuthPassword() var expectedPlainPasswordMessage = { name: 'authenticationCleartextPassword', @@ -215,6 +216,10 @@ var expectedMD5PasswordMessage = { salt: Buffer.from([1, 2, 3, 4]), } +var expectedOAuthPasswordMessage = { + name: 'authenticationOAuthPassword', +} + const parseBuffers = async (buffers: Buffer[]): Promise => { const stream = new PassThrough() for (const buffer of buffers) { @@ -226,10 +231,11 @@ const parseBuffers = async (buffers: Buffer[]): Promise => { return msgs } -describe('PgPacketStream', function () { +describe('BackendPacket', function () { testForMessage(authOkBuffer, expectedAuthenticationOkayMessage) - testForMessage(plainPasswordBuffer, expectedPlainPasswordMessage) - testForMessage(md5PasswordBuffer, expectedMD5PasswordMessage) + testForMessage(authCleartextBuffer, expectedPlainPasswordMessage) + testForMessage(authMD5Buffer, expectedMD5PasswordMessage) + testForMessage(authOAuthBuffer, expectedOAuthPasswordMessage) testForMessage(paramStatusBuffer, expectedParameterStatusMessage) testForMessage(backendKeyDataBuffer, expectedBackendKeyDataMessage) testForMessage(readyForQueryBuffer, expectedReadyForQueryMessage) diff --git a/packages/v-protocol/test/outbound-serializer.test.ts b/packages/v-protocol/test/outbound-serializer.test.ts index 279ccf28..080bd971 100644 --- a/packages/v-protocol/test/outbound-serializer.test.ts +++ b/packages/v-protocol/test/outbound-serializer.test.ts @@ -20,7 +20,7 @@ import assert from 'assert' import { serialize } from '../src/serializer' import BufferList from './buffer-list' -describe('serializer', () => { +describe('FrontendPacket', () => { /*it('builds startup message', function () { const actual = serialize.startup({ user: 'brian', diff --git a/packages/v-protocol/test/test-buffers.ts b/packages/v-protocol/test/test-buffers.ts index 6e07cdcf..abe9ff41 100644 --- a/packages/v-protocol/test/test-buffers.ts +++ b/packages/v-protocol/test/test-buffers.ts @@ -41,6 +41,10 @@ const buffers = { .join(true, 'R') }, + authenticationOAuthPassword: function () { + return new BufferList().addInt32(12).join(true, 'R') + }, + parameterStatus: function (name: string, value: string) { return new BufferList().addCString(name).addCString(value).join(true, 'S') }, diff --git a/packages/vertica-nodejs/README.md b/packages/vertica-nodejs/README.md index 2e8e78e4..69503eba 100644 --- a/packages/vertica-nodejs/README.md +++ b/packages/vertica-nodejs/README.md @@ -9,11 +9,11 @@ Non-blocking Vertica client for Node.js made with pure Javascript. ## Jump to 1. [Features](#Features) 2. [Contributing](#Contributing) -3. [Vertica Data Types](#Vertica-Data-Types) -4. [Support](#Support) -5. [Troubleshooting and FAQ](#Troubleshooting-and-FAQ) -6. [Installation](#Installation) -7. [Post Installation Setup](#Post-Installation-Setup) +3. [Installation](#Installation) +4. [Post Installation Setup](#Post-Installation-Setup) +5. [Vertica Data Types](#Vertica-Data-Types) +6. [Support](#Support) +7. [Troubleshooting and FAQ](#Troubleshooting-and-FAQ) 8. [Usage Examples](#Usage-Examples) - [Establishing Connections](#Establishing-Connections) - [Executing Queries and Accessing Results](#Executing-Queries-and-Accessing-Results) @@ -45,6 +45,7 @@ Ensure that the applicable environment variables are configured for connecting t - V_PORT: 5433 - V_USER: process.env.USER/USERNAME - V_PASSWORD: '' + - V_OAUTH_ACCESS_TOKEN: '' - V_DATABASE: '' - V_BACKUP_SERVER_NODE: '' @@ -222,6 +223,43 @@ Connection strings work the same way with connection pools }) ``` +### Connection with OAuth Authentication +Set `V_OAUTH_ACCESS_TOKEN` environment variable or use the `oauth_access_token` connection property to pass the OAuth access token to authenticate to Vertica server. +```javascript + // with Config Object + const {Client} = require('vertica-nodejs') + var client = new Client({ + host: 'localhost', + port: 5433, + database: 'db0', + oauth_access_token: 'xxx'}) + client.connect() +``` + +```javascript + // with Connection String + const {Client} = require('vertica-nodejs') + const connectionString = 'vertica://:@localhost:5433/db0?oauth_access_token=xxx' + var client = new Client({ connectionString }) + client.connect() +``` +### Workload Connection Property +The `workload` connection property is the name of the workload for the session. Valid values are workload names that already exist in a workload routing rule on the server. It will be set to the default if a workload name that doesn't exist is entered. + +```javascript + const {Client} = require('vertica-nodejs') + var client = new Client({workload: 'analytics'}) + client.connect() +``` + +### Client Label Connection Property +The `client_label` connection property is a string that sets a label for the connection on the server. This value appears in the *client_label* column of the SESSIONS system table. + +```javascript + const {Client} = require('vertica-nodejs') + var client = new Client({client_label: 'xxxxx'}) + client.connect() +``` ## TLS ### TLS Modes @@ -245,23 +283,6 @@ The `tls_trusted_certs` connection property is an optional override of the trust client.connect() ``` -## Workload Connection Property -The `workload` connection property is the name of the workload for the session. Valid values are workload names that already exist in a workload routing rule on the server. It will be set to the default if a workload name that doesn't exist is entered. - -```javascript - const {Client} = require('vertica-nodejs') - var client = new Client({workload: 'analytics'}) - client.connect() -``` - -## Client Label Connection Property -The `client_label` connection property is a string that sets a label for the connection on the server. This value appears in the *client_label* column of the SESSIONS system table. - -```javascript - const {Client} = require('vertica-nodejs') - var client = new Client({client_label: 'xxxxx'}) - client.connect() -``` ## Executing Queries and Accessing Results diff --git a/packages/vertica-nodejs/lib/client.js b/packages/vertica-nodejs/lib/client.js index 38764f17..8d70c370 100644 --- a/packages/vertica-nodejs/lib/client.js +++ b/packages/vertica-nodejs/lib/client.js @@ -45,6 +45,12 @@ class Client extends EventEmitter { writable: true, value: this.connectionParameters.password, }) + Object.defineProperty(this, 'oauth_access_token', { + configurable: true, + enumerable: false, + writable: true, + value: this.connectionParameters.oauth_access_token, + }) this.protocol_version = this.connectionParameters.protocol_version; @@ -262,6 +268,7 @@ class Client extends EventEmitter { // password request handling con.on('authenticationMD5Password', this._handleAuthMD5Password.bind(this)) con.on('authenticationSHA512Password', this._handleAuthSHA512Password.bind(this)) + con.on('authenticationOAuthPassword', this._handleOAuthPassword.bind(this)) con.on('backendKeyData', this._handleBackendKeyData.bind(this)) con.on('error', this._handleErrorEvent.bind(this)) con.on('errorMessage', this._handleErrorMessage.bind(this)) @@ -321,7 +328,7 @@ class Client extends EventEmitter { _handleParameterStatus(msg) { const min_supported_version = (3 << 16 | 5) // 3.5 - const max_supported_version = this.protocol_version // for now we are enforcing 3.5 + const max_supported_version = this.connectionParameters.protocol_version // requested protocol version switch(msg.parameterName) { // right now we only care about the protocol_version // if we want to have the parameterStatus message update any other connection properties, add them here @@ -334,7 +341,7 @@ class Client extends EventEmitter { // error throw new Error("Unsupported Protocol Version returned by Server. Connection Disallowed."); } - this.connectionParameters.protocol_version = parseInt(msg.ParameterValue) // likely to be the same, meaning this has no affect + this.protocol_version = parseInt(msg.parameterValue) // effective protocol version break; default: // do nothing @@ -366,6 +373,10 @@ class Client extends EventEmitter { }) } + _handleOAuthPassword(msg) { + this.connection.password(this.oauth_access_token) + } + _handleBackendKeyData(msg) { this.processID = msg.processID this.secretKey = msg.secretKey @@ -494,7 +505,7 @@ class Client extends EventEmitter { } _handleVerifyFiles(msg) { - this.activeQuery.handleVerifyFiles(msg, this.connection) + this.activeQuery.handleVerifyFiles(msg, this.connection, this.protocol_version) } _handleEndOfBatchResponse() { @@ -517,7 +528,9 @@ class Client extends EventEmitter { client_os: params.client_os, client_os_user_name: params.client_os_user_name, client_os_hostname: params.client_os_hostname, - client_pid: params.client_pid + client_pid: params.client_pid, + binary_data_protocol: '0', // Defaults to text format '0' + protocol_compat: 'VER', } if (params.replication) { @@ -538,6 +551,11 @@ class Client extends EventEmitter { if (params.workload) { data.workload = params.workload } + if (params.oauth_access_token) { + data.auth_category = 'OAuth' + } else if (params.password) { + data.auth_category = 'User' + } return data } diff --git a/packages/vertica-nodejs/lib/connection-parameters.js b/packages/vertica-nodejs/lib/connection-parameters.js index 9134de91..e6a19164 100644 --- a/packages/vertica-nodejs/lib/connection-parameters.js +++ b/packages/vertica-nodejs/lib/connection-parameters.js @@ -72,7 +72,6 @@ class ConnectionParameters { config = Object.assign({}, config, parse(config.connectionString)) } - this.user = val('user', config) this.database = val('database', config) if (this.database === undefined) { @@ -90,6 +89,19 @@ class ConnectionParameters { writable: true, value: val('password', config), }) + Object.defineProperty(this, 'oauth_access_token', { + configurable: true, + enumerable: false, + writable: true, + value: val('oauth_access_token', config), + }) + + // user is required for non-OAuth connections + this.user = val('user', config) + if (!this.user && !this.oauth_access_token) { + // TODO: log a notice to the user that the user property was taken from the environment once we fully support logging + this.user = process.platform === 'win32' ? process.env.USERNAME : process.env.USER + } this.binary = val('binary', config) this.options = val('options', config) @@ -143,8 +155,8 @@ class ConnectionParameters { this.client_os_user_name = "" } - //NOTE: The client has only been tested to support 3.5, which was chosen in order to include SHA512 support - this.protocol_version = (3 << 16 | 5) // 3.5 -> (major << 16 | minor) -> (3 << 16 | 5) -> 196613 + // The frontend sends a requested protocol version to the backend + this.protocol_version = (3 << 16 | 16) // 3.16 -> (major << 16 | minor) -> (3 << 16 | 16) -> 196624 if (config.connectionTimeoutMillis === undefined) { this.connect_timeout = process.env.PGCONNECT_TIMEOUT || 0 diff --git a/packages/vertica-nodejs/lib/defaults.js b/packages/vertica-nodejs/lib/defaults.js index ed3486a8..77ae4e84 100644 --- a/packages/vertica-nodejs/lib/defaults.js +++ b/packages/vertica-nodejs/lib/defaults.js @@ -19,7 +19,7 @@ module.exports = { host: 'localhost', // database user's name - user: process.platform === 'win32' ? process.env.USERNAME : process.env.USER, + user: '', // name of database to connect database: '', @@ -27,6 +27,9 @@ module.exports = { // database user's password password: '', + // database user's OAuth access token + oauth_access_token: '', + // a Postgres connection string to be used instead of setting individual connection items // NOTE: Setting this value will cause it to override any other value (such as database or user) defined // in the defaults object. diff --git a/packages/vertica-nodejs/lib/query.js b/packages/vertica-nodejs/lib/query.js index 5d7171b3..2026e458 100644 --- a/packages/vertica-nodejs/lib/query.js +++ b/packages/vertica-nodejs/lib/query.js @@ -261,7 +261,8 @@ class Query extends EventEmitter { connection.sendCopyDataStream(this.copyStream) } - async handleVerifyFiles(msg, connection) { + async handleVerifyFiles(msg, connection, protocol_version) { + msg.protocol_version = protocol_version if (msg.numFiles !== 0) { // we are copying from file, not stdin let expandedFileNames = [] for (const fileName of msg.files) { diff --git a/packages/vertica-nodejs/lib/type-overrides.js b/packages/vertica-nodejs/lib/type-overrides.js index c7d77ece..09f5e3f8 100644 --- a/packages/vertica-nodejs/lib/type-overrides.js +++ b/packages/vertica-nodejs/lib/type-overrides.js @@ -24,7 +24,8 @@ types.setTypeParser(VerticaType.Integer, types.getTypeParser(21, 'text')) types.setTypeParser(VerticaType.Float, types.getTypeParser(700, 'text')) types.setTypeParser(VerticaType.Numeric, types.getTypeParser(9, 'text')) types.setTypeParser(VerticaType.Varbinary, types.getTypeParser(9, 'text')) -//types.setTypeParser(VerticaType.Uuid, types.getTypeParser(9, 'text')) //Uuid not introduced yet in the current protocol 3.5 +types.setTypeParser(VerticaType.Uuid, types.getTypeParser(2950, 'text')) + function TypeOverrides(userTypes) { this._types = userTypes || types this.text = {} diff --git a/packages/vertica-nodejs/mochatest/integration/client/oauth-tests.js b/packages/vertica-nodejs/mochatest/integration/client/oauth-tests.js new file mode 100644 index 00000000..a6fa6212 --- /dev/null +++ b/packages/vertica-nodejs/mochatest/integration/client/oauth-tests.js @@ -0,0 +1,35 @@ +// Copyright (c) 2024 Open Text. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +'use strict' +const vertica = require('../../../../vertica-nodejs') +const assert = require('assert') + + +describe('OAuth authentication', function () { + it('connect with an OAuth access token', function(done) { + const access_token = process.env['VTEST_OAUTH_ACCESS_TOKEN'] + if (!access_token) this.skip() + const client = new vertica.Client({oauth_access_token: access_token}) + client.connect(err => { + if (err) return done(err) + client.query("SELECT authentication_method FROM sessions WHERE session_id = current_session()", (err, res) => { + if (err) return done(err) + assert.equal(res.rows[0].authentication_method, 'OAuth') + client.end() + done() + }) + }) + }) +}) diff --git a/packages/vertica-nodejs/mochatest/integration/client/vertica-connection-params-tests.js b/packages/vertica-nodejs/mochatest/integration/client/vertica-connection-params-tests.js index 848f97d3..18d80f9a 100644 --- a/packages/vertica-nodejs/mochatest/integration/client/vertica-connection-params-tests.js +++ b/packages/vertica-nodejs/mochatest/integration/client/vertica-connection-params-tests.js @@ -30,7 +30,7 @@ describe('vertica client label connection parameter', function () { client_default.query('SELECT GET_CLIENT_LABEL()', (err, res) => { if (err){ console.log(err) - assert(false) + done(err) } assert.equal(res.rows[0]['GET_CLIENT_LABEL'], vertica.defaults.client_label) client_default.end() @@ -46,7 +46,7 @@ describe('vertica client label connection parameter', function () { client_test.query('SELECT GET_CLIENT_LABEL()', (err, res) => { if (err){ console.log(err) - assert(false) + done(err) } assert.equal(res.rows[0]['GET_CLIENT_LABEL'], 'distinctLabel') client_test.end() @@ -62,10 +62,10 @@ describe('vertica protocol_version connection parameter', function () { }) it('provides a maximum value for the protocol version used by the server', function(done) { - const client = new vertica.Client({client_label: 'pvTest'}) // make easy to find session + const client = new vertica.Client() client.connect() - client.query("SELECT effective_protocol from sessions where client_label = 'pvTest'", (err, res) => { - if (err) assert(false) + client.query("SELECT effective_protocol FROM sessions WHERE session_id = current_session()", (err, res) => { + if (err) done(err) var pv = res.rows[0]['effective_protocol'] // string of form "Major.minor" var int32pv = (parseInt(pv.split(".")[0]) << 16 | parseInt(pv.split(".")[1])) // int32 from (M << 16 | m) assert(int32pv <= client.protocol_version) // server isn't trying to talk in a protocol newer than we know @@ -106,11 +106,11 @@ describe('vertica backup_server_node connection parameter', function() { }) describe('vertica-nodejs handling auditing connection properties', function() { - it('are provided automatically when establishing a connection', function() { + it('are provided automatically when establishing a connection', function(done) { const client = new vertica.Client() client.connect() client.query("SELECT client_pid, client_type, client_version, client_os, client_os_user_name, client_os_hostname FROM CURRENT_SESSION", (err, res) => { - if (err) assert(false) + if (err) done(err) assert.equal(res.rows[0].client_pid, process.pid) assert.equal(res.rows[0].client_type, "Node.js Driver") assert.equal(res.rows[0].client_version, vertica.version) @@ -118,21 +118,23 @@ describe('vertica-nodejs handling auditing connection properties', function() { assert.equal(res.rows[0].client_os_user_name, os.userInfo().username) assert.equal(res.rows[0].client_os_hostname, os.hostname()) client.end() + done() }) }) }) describe('vertica workload connection parameter', function() { - it('can be set and is sent in the startup packet', function() { + it('can be set and is sent in the startup packet', function(done) { const client = new vertica.Client({workload: 'testNodeWorkload'}) client.connect() client.query(`SELECT contents FROM dc_client_server_messages WHERE session_id = current_session() AND message_type = '^+' AND contents like '%workload%'`, (err, res) => { - if (err) assert(false) + if (err) done(err) assert.equal(res.rows[0].contents, 'workload: testNodeWorkload') client.end() + done() }) }) }) diff --git a/packages/vertica-nodejs/test/integration/client/api-tests.js b/packages/vertica-nodejs/test/integration/client/api-tests.js index 55eea4dd..1a011bba 100644 --- a/packages/vertica-nodejs/test/integration/client/api-tests.js +++ b/packages/vertica-nodejs/test/integration/client/api-tests.js @@ -162,7 +162,7 @@ suite.test('executing nested queries', function (done) { }) suite.test('raises error if cannot connect', function () { - var connectionString = 'pg://sfalsdkf:asdf@localhost/ieieie' + var connectionString = 'vertica://sfalsdkf:asdf@localhost/ieieie' const pool = new vertica.Pool({ connectionString: connectionString }) pool.connect( assert.calls(function (err, client, done) { diff --git a/packages/vertica-nodejs/test/integration/client/configuration-tests.js b/packages/vertica-nodejs/test/integration/client/configuration-tests.js index d8aabe9b..7b4ae8e9 100644 --- a/packages/vertica-nodejs/test/integration/client/configuration-tests.js +++ b/packages/vertica-nodejs/test/integration/client/configuration-tests.js @@ -7,13 +7,15 @@ var suite = new helper.Suite() // clear process.env var realEnv = {} for (var key in process.env) { - realEnv[key] = process.env[key] - if (!key.indexOf('PG')) delete process.env[key] + if (key.startsWith('V_')) { + realEnv[key] = process.env[key] + delete process.env[key] + } } suite.test('default values are used in new clients', function () { assert.same(vertica.defaults, { - user: process.env.USER, + user: '', database: '', password: '', port: 5433, diff --git a/packages/vertica-nodejs/test/integration/client/promise-api-tests.js b/packages/vertica-nodejs/test/integration/client/promise-api-tests.js index b892e454..c3fe9db8 100644 --- a/packages/vertica-nodejs/test/integration/client/promise-api-tests.js +++ b/packages/vertica-nodejs/test/integration/client/promise-api-tests.js @@ -12,13 +12,6 @@ suite.test('valid connection completes promise', () => { }) }) -suite.test('valid connection completes promise', () => { - const client = new vertica.Client() - return client.connect().then(() => { - return client.end().then(() => {}) - }) -}) - suite.test('invalid connection rejects promise', (done) => { const client = new vertica.Client({ host: 'alksdjflaskdfj', port: 1234 }) return client.connect().catch((e) => { diff --git a/packages/vertica-nodejs/test/integration/connection/test-helper.js b/packages/vertica-nodejs/test/integration/connection/test-helper.js index 868a140e..22bc7a89 100644 --- a/packages/vertica-nodejs/test/integration/connection/test-helper.js +++ b/packages/vertica-nodejs/test/integration/connection/test-helper.js @@ -16,7 +16,7 @@ var connect = function (callback) { con.startup({ user: username, database: database, - protocol_version: (3 << 16 | 5), + protocol_version: (3 << 16 | 16), }) con.once('authenticationCleartextPassword', function () { con.password(helper.args.password) @@ -24,6 +24,9 @@ var connect = function (callback) { con.once('authenticationMD5Password', function (msg) { con.password(utils.postgresMd5PasswordHash(helper.args.user, helper.args.password, msg.salt)) }) + con.once('authenticationSHA512Password', function (msg) { + con.password(utils.postgresSha512PasswordHash(helper.args.password, msg.salt, msg.userSalt)) + }) con.once('readyForQuery', function () { con.query('create local temp table ids(id integer)') con.once('readyForQuery', function () { diff --git a/packages/vertica-nodejs/test/unit/client/configuration-tests.js b/packages/vertica-nodejs/test/unit/client/configuration-tests.js index 4a46f44b..47eb7487 100644 --- a/packages/vertica-nodejs/test/unit/client/configuration-tests.js +++ b/packages/vertica-nodejs/test/unit/client/configuration-tests.js @@ -38,7 +38,7 @@ test('client settings', function () { test('initializing from a config string', function () { test('uses connectionString property', function () { var client = new Client({ - connectionString: 'postgres://brian:pass@host1:333/databasename', + connectionString: 'vertica://brian:pass@host1:333/databasename', }) assert.equal(client.user, 'brian') assert.equal(client.password, 'pass') @@ -48,7 +48,7 @@ test('initializing from a config string', function () { }) test('uses the correct values from the config string', function () { - var client = new Client('postgres://brian:pass@host1:333/databasename') + var client = new Client('vertica://brian:pass@host1:333/databasename') assert.equal(client.user, 'brian') assert.equal(client.password, 'pass') assert.equal(client.host, 'host1') @@ -57,7 +57,7 @@ test('initializing from a config string', function () { }) test('uses the correct values from the config string with space in password', function () { - var client = new Client('postgres://brian:pass word@host1:333/databasename') + var client = new Client('vertica://brian:pass word@host1:333/databasename') assert.equal(client.user, 'brian') assert.equal(client.password, 'pass word') assert.equal(client.host, 'host1') @@ -66,7 +66,7 @@ test('initializing from a config string', function () { }) test('when not including all values the defaults are used', function () { - var client = new Client('postgres://host1') + var client = new Client('vertica://host1') assert.equal(client.user, process.env['V_USER'] || process.env.USER) assert.equal(client.password, process.env['V_PASSWORD'] || '') assert.equal(client.host, 'host1') @@ -93,7 +93,7 @@ test('initializing from a config string', function () { process.env['V_HOST'] = 'utHost1' process.env['V_PORT'] = 5464 - var client = new Client('postgres://host1') + var client = new Client('vertica://host1') assert.equal(client.user, process.env['V_USER']) assert.equal(client.password, process.env['V_PASSWORD']) diff --git a/packages/vertica-nodejs/test/unit/connection-parameters/creation-tests.js b/packages/vertica-nodejs/test/unit/connection-parameters/creation-tests.js index c5543393..51b4b7dd 100644 --- a/packages/vertica-nodejs/test/unit/connection-parameters/creation-tests.js +++ b/packages/vertica-nodejs/test/unit/connection-parameters/creation-tests.js @@ -7,7 +7,9 @@ const dns = require('dns') // clear process.env for (var key in process.env) { - delete process.env[key] + if (key.startsWith('V_')) { + delete process.env[key] + } } const suite = new helper.Suite() @@ -19,9 +21,10 @@ suite.test('ConnectionParameters construction', function () { }) var compare = function (actual, expected, type) { + const expectedUser = (!expected.user && !expected.oauth_access_token) ? (process.platform === 'win32' ? process.env.USERNAME : process.env.USER) : expected.user const expectedDatabase = expected.database === undefined ? '' : expected.database - assert.equal(actual.user, expected.user, type + ' user') + assert.equal(actual.user, expectedUser, type + ' user') assert.equal(actual.database, expectedDatabase, type + ' database') assert.equal(actual.port, expected.port, type + ' port') assert.equal(actual.host, expected.host, type + ' host') @@ -186,7 +189,7 @@ suite.test('password contains < and/or > characters', function () { suite.test('username or password contains weird characters', function () { var defaults = require('../../../lib/defaults') defaults.tls_mode = 'require' - var strang = 'pg://my f%irst name:is&%awesome!@localhost:9000' + var strang = 'vertica://my f%irst name:is&%awesome!@localhost:9000' var subject = new ConnectionParameters(strang) assert.equal(subject.user, 'my f%irst name') assert.equal(subject.password, 'is&%awesome!') @@ -195,7 +198,7 @@ suite.test('username or password contains weird characters', function () { }) suite.test('url is properly encoded', function () { - var encoded = 'pg://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl' + var encoded = 'vertica://bi%25na%25%25ry%20:s%40f%23@localhost/%20u%2520rl' var subject = new ConnectionParameters(encoded) assert.equal(subject.user, 'bi%na%%ry ') assert.equal(subject.password, 's@f#') diff --git a/packages/vertica-nodejs/test/unit/connection-parameters/environment-variable-tests.js b/packages/vertica-nodejs/test/unit/connection-parameters/environment-variable-tests.js index 77188ea5..52802a5d 100644 --- a/packages/vertica-nodejs/test/unit/connection-parameters/environment-variable-tests.js +++ b/packages/vertica-nodejs/test/unit/connection-parameters/environment-variable-tests.js @@ -60,7 +60,7 @@ suite.test('ConnectionParameters initialized from mix', function () { suite.test('connection string parsing', function () { clearEnv() - var string = 'postgres://brian:pw@boom:381/lala' + var string = 'vertica://brian:pw@boom:381/lala' var subject = new ConnectionParameters(string) assert.equal(subject.host, 'boom', 'string host') assert.equal(subject.user, 'brian', 'string user') @@ -73,19 +73,19 @@ suite.test('connection string parsing - tls_mode', function () { // clear process.env clearEnv() - var string = 'postgres://brian:pw@boom:381/lala?tls_mode=require' + var string = 'vertica://brian:pw@boom:381/lala?tls_mode=require' var subject = new ConnectionParameters(string) assert.equal(subject.tls_mode, 'require') - string = 'postgres://brian:pw@boom:381/lala?tls_mode=disable' + string = 'vertica://brian:pw@boom:381/lala?tls_mode=disable' subject = new ConnectionParameters(string) assert.equal(subject.tls_mode, 'disable') - string = 'postgres://brian:pw@boom:381/lala' + string = 'vertica://brian:pw@boom:381/lala' subject = new ConnectionParameters(string) assert.equal(subject.tls_mode, 'disable') - string = 'postgres://brian:pw@boom:381/lala?tls_mode=verify-ca' + string = 'vertica://brian:pw@boom:381/lala?tls_mode=verify-ca' subject = new ConnectionParameters(string) assert.equal(subject.tls_mode, 'verify-ca') }) diff --git a/packages/vertica-nodejs/test/unit/connection/inbound-parser-tests.js b/packages/vertica-nodejs/test/unit/connection/inbound-parser-tests.js index 931a9967..3bf52099 100644 --- a/packages/vertica-nodejs/test/unit/connection/inbound-parser-tests.js +++ b/packages/vertica-nodejs/test/unit/connection/inbound-parser-tests.js @@ -1,5 +1,11 @@ 'use strict' require('./test-helper') +/* + * TODO: + * This is very similar to packages/v-protocol/test/inbound-parser.test.ts, + * see testForMessage function in both files. + * Do we really need this test? + */ const BufferList = require('../../buffer-list') var Connection = require('../../../lib/connection') var buffers = require('../../test-buffers')