Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions lib/DBSQLClient.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import thrift from 'thrift';
import os from 'os';
import tls from 'tls';

import { EventEmitter } from 'events';
import TCLIService from '../thrift/TCLIService';
Expand Down Expand Up @@ -196,6 +197,15 @@ export default class DBSQLClient extends EventEmitter implements IDBSQLClient, I
https: true,
socketTimeout: options.socketTimeout,
proxy: options.proxy,
// `customCaCert` is ADDITIVE: Node's `ca` option replaces the system trust
// store, so we append the custom cert to the built-in roots to keep public
// Databricks warehouses trusted while also trusting the caller's CA.
ca: options.customCaCert === undefined ? undefined : [...tls.rootCertificates, options.customCaCert.toString()],
// Client certificate + key for mutual TLS (mTLS). Both must be supplied together.
cert: options.clientCert,
key: options.clientKey,
// Validate the server certificate unless the caller explicitly opts out.
rejectUnauthorized: options.checkServerCertificate ?? true,
headers: {
'User-Agent': buildUserAgentString(options.userAgentEntry),
},
Expand Down
4 changes: 3 additions & 1 deletion lib/connection/connections/HttpConnection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,9 @@ export default class HttpConnection implements IConnectionProvider {
const httpsAgentOptions: https.AgentOptions = {
...this.getAgentDefaultOptions(),
minVersion: 'TLSv1.2',
rejectUnauthorized: false,
// Validate the server certificate by default; only skip verification when
// the caller explicitly opts out via `rejectUnauthorized: false`.
rejectUnauthorized: this.options.rejectUnauthorized ?? true,
ca: this.options.ca,
cert: this.options.cert,
key: this.options.key,
Expand Down
6 changes: 5 additions & 1 deletion lib/connection/contracts/IConnectionOptions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,11 @@ export default interface IConnectionOptions {
socketTimeout?: number;
proxy?: ProxyOptions;

ca?: Buffer | string;
ca?: Buffer | string | Array<Buffer | string>;
cert?: Buffer | string;
key?: Buffer | string;

// Whether the TLS server certificate is validated against the trusted CA set.
// When omitted the connection provider treats it as `true` (validation enabled).
rejectUnauthorized?: boolean;
}
40 changes: 40 additions & 0 deletions lib/contracts/IDBSQLClient.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,46 @@ export type ConnectionOptions = {
proxy?: ProxyOptions;
enableMetricViewMetadata?: boolean;

/**
* Verify the server's TLS certificate on the primary Thrift transport.
* Secure-by-default: omitting this leaves full chain + hostname verification
* enabled (`true`), matching Node's `https` default, the JDBC/ODBC drivers,
* and the SEA/kernel backend.
*
* Setting it to `false` disables server certificate verification entirely
* (any self-signed, expired, or wrong-hostname certificate is accepted),
* which exposes the connection — including bearer-token auth headers — to
* man-in-the-middle attacks. Only use `false` for local development against a
* trusted endpoint, and prefer supplying `customCaCert` instead.
*
* Mirrors the `checkServerCertificate` option on the SEA backend.
*/
checkServerCertificate?: boolean;

/**
* PEM-encoded CA certificate (string or `Buffer`) added to the trust store
* **on top of** the system roots — for TLS-inspecting proxies or on-prem
* internal CAs. Because it is additive, connections to public Databricks
* warehouses (trusted via the system roots) keep working.
*
* Mirrors the `customCaCert` option on the SEA backend.
*/
customCaCert?: Buffer | string;

/**
* PEM-encoded client certificate (string or `Buffer`) presented to the server
* for mutual TLS (mTLS). Must be supplied together with `clientKey`. Leave
* both unset for the usual token/OAuth flows, which do not require a client
* certificate.
*/
clientCert?: Buffer | string;

/**
* PEM-encoded private key (string or `Buffer`) for `clientCert`, used for
* mutual TLS (mTLS). Must be supplied together with `clientCert`.
*/
clientKey?: Buffer | string;

/**
* Retry-policy knobs governing how the driver retries retryable requests.
* They apply to **both** backends: the Thrift `HttpRetryPolicy` reads them
Expand Down
61 changes: 61 additions & 0 deletions tests/unit/DBSQLClient.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,67 @@ describe('DBSQLClient.connect', () => {
expect(connectionOptions.path).to.equal(path);
});

it('should validate the server certificate by default', async () => {
const client = new DBSQLClient();

const connectionOptions = client['getConnectionOptions'](connectOptions);

expect(connectionOptions.rejectUnauthorized).to.be.true;
});

it('should map checkServerCertificate to rejectUnauthorized', async () => {
const client = new DBSQLClient();

expect(client['getConnectionOptions']({ ...connectOptions, checkServerCertificate: false }).rejectUnauthorized).to
.be.false;
expect(client['getConnectionOptions']({ ...connectOptions, checkServerCertificate: true }).rejectUnauthorized).to.be
.true;
});

it('should not set a custom CA when customCaCert is omitted', async () => {
const client = new DBSQLClient();

const connectionOptions = client['getConnectionOptions'](connectOptions);

expect(connectionOptions.ca).to.be.undefined;
});

it('should add customCaCert on top of the system root certificates (additive)', async () => {
const client = new DBSQLClient();
const customCaCert = '-----BEGIN CERTIFICATE-----\ncustom\n-----END CERTIFICATE-----\n';

const connectionOptions = client['getConnectionOptions']({ ...connectOptions, customCaCert });

expect(connectionOptions.ca).to.be.an('array');
const ca = connectionOptions.ca as Array<string>;
// The custom cert must be present...
expect(ca).to.include(customCaCert);
// ...alongside the built-in system roots (so public warehouses still validate).
expect(ca.length).to.be.greaterThan(1);
});

it('should not set client cert/key when mTLS options are omitted', async () => {
const client = new DBSQLClient();

const connectionOptions = client['getConnectionOptions'](connectOptions);

expect(connectionOptions.cert).to.be.undefined;
expect(connectionOptions.key).to.be.undefined;
});

it('should map clientCert/clientKey to cert/key for mutual TLS', async () => {
const client = new DBSQLClient();

const connectionOptions = client['getConnectionOptions']({
...connectOptions,
clientCert: 'client-cert',
clientKey: 'client-key',
});

expect(connectionOptions.cert).to.equal('client-cert');
expect(connectionOptions.key).to.equal('client-key');
});

it('should initialize connection state', async () => {
const client = new DBSQLClient();

Expand Down
38 changes: 36 additions & 2 deletions tests/unit/connection/connections/HttpConnection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ describe('HttpConnection.connect', () => {
expect(anotherConnection).to.eq(thriftConnection);
});

it('should set SSL certificates and disable rejectUnauthorized', async () => {
it('should set SSL certificates and validate the server certificate by default', async () => {
const connection = new HttpConnection(
{
host: 'localhost',
Expand All @@ -42,12 +42,46 @@ describe('HttpConnection.connect', () => {

const thriftConnection = await connection.getThriftConnection();

expect(thriftConnection.config.agent.options.rejectUnauthorized).to.be.false;
expect(thriftConnection.config.agent.options.rejectUnauthorized).to.be.true;
expect(thriftConnection.config.agent.options.ca).to.be.eq('ca');
expect(thriftConnection.config.agent.options.cert).to.be.eq('cert');
expect(thriftConnection.config.agent.options.key).to.be.eq('key');
});

it('should allow disabling server certificate validation via rejectUnauthorized', async () => {
const connection = new HttpConnection(
{
host: 'localhost',
port: 10001,
path: '/hive',
https: true,
rejectUnauthorized: false,
},
new ClientContextStub(),
);

const thriftConnection = await connection.getThriftConnection();

expect(thriftConnection.config.agent.options.rejectUnauthorized).to.be.false;
});

it('should keep server certificate validation enabled when rejectUnauthorized is true', async () => {
const connection = new HttpConnection(
{
host: 'localhost',
port: 10001,
path: '/hive',
https: true,
rejectUnauthorized: true,
},
new ClientContextStub(),
);

const thriftConnection = await connection.getThriftConnection();

expect(thriftConnection.config.agent.options.rejectUnauthorized).to.be.true;
});

it('should initialize http agents', async () => {
const connection = new HttpConnection(
{
Expand Down
Loading