Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

OAuth Authentication Support #146

Merged
merged 19 commits into from
May 24, 2024
61 changes: 59 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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 \
Expand All @@ -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 <<EOF
/opt/keycloak/bin/kcadm.sh config credentials --server http://localhost:8080 --realm master --user admin --password admin
/opt/keycloak/bin/kcadm.sh create realms -s realm=${KC_REALM} -s enabled=true
/opt/keycloak/bin/kcadm.sh update realms/${KC_REALM} -s accessTokenLifespan=3600
/opt/keycloak/bin/kcadm.sh get realms/${KC_REALM}
/opt/keycloak/bin/kcadm.sh create users -r ${KC_REALM} -s username=${KC_USER} -s enabled=true
/opt/keycloak/bin/kcadm.sh set-password -r ${KC_REALM} --username ${KC_USER} --new-password ${KC_PASSWORD}
/opt/keycloak/bin/kcadm.sh get users -r ${KC_REALM}
/opt/keycloak/bin/kcadm.sh create clients -r ${KC_REALM} -s clientId=${KC_CLIENT_ID} -s enabled=true \
-s 'redirectUris=["/*"]' -s 'webOrigins=["/*"]' -s secret=${KC_CLIENT_SECRET} -s directAccessGrantsEnabled=true -o
EOF

# Retrieving an Access Token
curl --location --request POST http://`hostname`:8080/realms/${KC_REALM}/protocol/openid-connect/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode "username=${KC_USER}" \
--data-urlencode "password=${KC_PASSWORD}" \
--data-urlencode "client_id=${KC_CLIENT_ID}" \
--data-urlencode "client_secret=${KC_CLIENT_SECRET}" \
--data-urlencode 'grant_type=password' -o oauth.json
cat oauth.json | python3 -c 'import json,sys;obj=json.load(sys.stdin);print(obj["access_token"])' > 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: |
Expand All @@ -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
1 change: 1 addition & 0 deletions DATATYPES.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 |
Expand Down
3 changes: 3 additions & 0 deletions packages/v-connection-string/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
62 changes: 35 additions & 27 deletions packages/v-connection-string/test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand All @@ -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')
})

Expand Down Expand Up @@ -82,7 +82,7 @@ describe('parse', function () {
database: 'postgres',
}
var connectionString =
'postgres://' +
'vertica://' +
sourceConfig.user +
':' +
sourceConfig.password +
Expand All @@ -105,7 +105,7 @@ describe('parse', function () {
database: 'postgres',
}
var connectionString =
'postgres://' +
'vertica://' +
sourceConfig.user +
':' +
sourceConfig.password +
Expand All @@ -120,15 +120,15 @@ 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!')
subject.host.should.equal('localhost')
})

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#')
Expand All @@ -137,79 +137,87 @@ 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')
subject.database.should.equal('dbname')
})

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')
subject.database.should.equal('dbname')
})

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')
subject.database.should.equal('%2Fdbname')
})

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')
subject.database.should.equal('%2Funix%2Fsocket/dbname')
})

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')
})

Expand All @@ -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')
})
Expand All @@ -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')
})
Expand Down
1 change: 1 addition & 0 deletions packages/v-protocol/src/backend-messages.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ export type MessageName =
| 'authenticationMD5Password'
| 'authenticationSHA512Password'
| 'authenticationCleartextPassword'
| 'authenticationOAuthPassword'
| 'error'
| 'notice'
| 'verifyFiles'
Expand Down
3 changes: 3 additions & 0 deletions packages/v-protocol/src/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
6 changes: 5 additions & 1 deletion packages/v-protocol/src/serializer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
1 change: 1 addition & 0 deletions packages/v-protocol/src/vertica-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export enum VerticaType {
LongVarbinary = 116,
Binary = 117,
Numeric = 16,
Uuid = 20,
IntervalYear = 114,
IntervalYearToMonth = 114,
IntervalMonth = 114,
Expand Down
Loading