Skip to content

Commit

Permalink
OAuth Authentication Support (#146)
Browse files Browse the repository at this point in the history
  • Loading branch information
sitingren authored May 24, 2024
1 parent 62173ea commit 7af0dd0
Show file tree
Hide file tree
Showing 27 changed files with 291 additions and 103 deletions.
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

0 comments on commit 7af0dd0

Please sign in to comment.