|
| 1 | +// Copyright (c) 2026 Databricks, Inc. |
| 2 | +// |
| 3 | +// Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +// you may not use this file except in compliance with the License. |
| 5 | +// You may obtain a copy of the License at |
| 6 | +// |
| 7 | +// http://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +// |
| 9 | +// Unless required by applicable law or agreed to in writing, software |
| 10 | +// distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +// See the License for the specific language governing permissions and |
| 13 | +// limitations under the License. |
| 14 | + |
| 15 | +import { expect } from 'chai'; |
| 16 | +import { DBSQLClient } from '../../../lib'; |
| 17 | + |
| 18 | +/** |
| 19 | + * A throwaway self-signed CA, embedded so the suite is self-contained |
| 20 | + * (no external file dependency in CI). It is added to the trust store as |
| 21 | + * an *additional* root in the customCaCert tests; the real workspace cert |
| 22 | + * still validates via the system roots, so this cert is never actually in |
| 23 | + * the validation path — its expiry is irrelevant. |
| 24 | + */ |
| 25 | +const THROWAWAY_CA_PEM = `-----BEGIN CERTIFICATE----- |
| 26 | +MIIDGTCCAgGgAwIBAgIUdW39pdaepg+Mfb++pCKm9FxYlkswDQYJKoZIhvcNAQEL |
| 27 | +BQAwHDEaMBgGA1UEAwwRdGhyb3dhd2F5LXRlc3QtY2EwHhcNMjYwNTMxMjEzNDI1 |
| 28 | +WhcNMjYwNjAxMjEzNDI1WjAcMRowGAYDVQQDDBF0aHJvd2F3YXktdGVzdC1jYTCC |
| 29 | +ASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALWHhz4wQsBNqHJJzqJgT0BL |
| 30 | +DO9oqZT2zsAnHOWPchsLoAjeBJ4oXgNjh2ZeH1FH4kThGw/giL5VDpO+lVmMu5TG |
| 31 | +jeXXAmPIXk27qi1mGqu/tqstzuSqyxxk3oDnJuxCFxBYyA9LG+rhjH9WsW1XvzGn |
| 32 | +gOQLZ0Hjx4FkA+aWvH8AV82OUgHRRHfe1GaU4MUsLMYGU+2bwngcL059pBP/h/BS |
| 33 | +Q5brMJnFi8UrVTizuCF/QYP5dtEyvDltMKKU/E8uft/DP/2Q25r0hMZNU+I22v0N |
| 34 | +Ya8gCzA47LMPeIaZ3tGT94OEyUaU5mpwsVwL4Y6bfQRtEIph6qwJnAjWwJxQsssC |
| 35 | +AwEAAaNTMFEwHQYDVR0OBBYEFCDYsRm4L4QEbzu3Vkm+ji82xZcNMB8GA1UdIwQY |
| 36 | +MBaAFCDYsRm4L4QEbzu3Vkm+ji82xZcNMA8GA1UdEwEB/wQFMAMBAf8wDQYJKoZI |
| 37 | +hvcNAQELBQADggEBAJBXM668YVQ+PcrolpH0MisuUkqyjcrMLLDsjqU4PmBv75BZ |
| 38 | +m08hffg1qVRjYnqvFYbYaStuvousuIT5h7cS5y0I5rMEzeRi2+MaOB3gaDBofObH |
| 39 | +RYpCsehOjiNoNxoTzIIwZh+aNf/LK/Ti+DcW7dgxAlrctPbzZGQt/2wQLK/OLn6X |
| 40 | +0FyAJDMQ7l1MOL5wZ4JTd+EImlbwMIj73sug9elXfYGNu2UzqZ82iQFUxaBEuckJ |
| 41 | +O+tldQDDMuvU2m5bFypPXu/3Fe25FDFQggdOE1i+sp9WkoW5zh8GKo1y53yjbNAk |
| 42 | +7bq2fNLgT9r2D54uqpuJwCMshIUTi5p60GNNPSo= |
| 43 | +-----END CERTIFICATE----- |
| 44 | +`; |
| 45 | + |
| 46 | +/** |
| 47 | + * SEA TLS end-to-end against a live workspace. Exercises the |
| 48 | + * `checkServerCertificate` toggle and `customCaCert` passthrough end to |
| 49 | + * end (DBSQLClient → SeaBackend → SeaAuth → napi → kernel rustls → |
| 50 | + * real Databricks TLS handshake). |
| 51 | + * |
| 52 | + * Required env (exported by `~/.zshrc`): |
| 53 | + * - DATABRICKS_PECOTESTING_SERVER_HOSTNAME |
| 54 | + * - DATABRICKS_PECOTESTING_HTTP_PATH |
| 55 | + * - DATABRICKS_PECOTESTING_TOKEN_PERSONAL (or _TOKEN) |
| 56 | + * |
| 57 | + * Skipped when secrets are absent. |
| 58 | + */ |
| 59 | +describe('sea-tls e2e — checkServerCertificate + customCaCert through a live TLS handshake', function suite() { |
| 60 | + const host = process.env.DATABRICKS_PECOTESTING_SERVER_HOSTNAME; |
| 61 | + const path = process.env.DATABRICKS_PECOTESTING_HTTP_PATH; |
| 62 | + const token = process.env.DATABRICKS_PECOTESTING_TOKEN_PERSONAL || process.env.DATABRICKS_PECOTESTING_TOKEN; |
| 63 | + |
| 64 | + this.timeout(120_000); |
| 65 | + |
| 66 | + before(function gate() { |
| 67 | + if (!host || !path || !token) { |
| 68 | + // eslint-disable-next-line no-invalid-this |
| 69 | + this.skip(); |
| 70 | + } |
| 71 | + }); |
| 72 | + |
| 73 | + async function connectAndSelectOne(extra: Record<string, unknown>): Promise<Array<Record<string, unknown>>> { |
| 74 | + const client = new DBSQLClient(); |
| 75 | + await client.connect({ |
| 76 | + host: host as string, |
| 77 | + path: path as string, |
| 78 | + token: token as string, |
| 79 | + useSEA: true, |
| 80 | + ...extra, |
| 81 | + }); |
| 82 | + const session = await client.openSession(); |
| 83 | + const operation = await session.executeStatement('SELECT 1 AS one', {}); |
| 84 | + const rows = (await operation.fetchAll()) as Array<Record<string, unknown>>; |
| 85 | + await operation.close(); |
| 86 | + await session.close(); |
| 87 | + await client.close(); |
| 88 | + return rows; |
| 89 | + } |
| 90 | + |
| 91 | + it('default (permissive, no flag): connects and runs SELECT 1', async () => { |
| 92 | + const rows = await connectAndSelectOne({}); |
| 93 | + expect(rows).to.deep.equal([{ one: 1 }]); |
| 94 | + }); |
| 95 | + |
| 96 | + it('strict (checkServerCertificate: true): real workspace cert validates against system roots', async () => { |
| 97 | + // The important positive case — turning verification ON must NOT |
| 98 | + // break against a real, publicly-signed Databricks cert. |
| 99 | + const rows = await connectAndSelectOne({ checkServerCertificate: true }); |
| 100 | + expect(rows).to.deep.equal([{ one: 1 }]); |
| 101 | + }); |
| 102 | + |
| 103 | + it('strict + additive customCaCert (PEM string): extra root does not break system-root validation', async () => { |
| 104 | + // A throwaway self-signed CA is added on top of the system roots. |
| 105 | + // The real workspace cert still validates via the system roots, so |
| 106 | + // the connection succeeds — proving custom_ca_cert is additive. |
| 107 | + const rows = await connectAndSelectOne({ |
| 108 | + checkServerCertificate: true, |
| 109 | + customCaCert: THROWAWAY_CA_PEM, |
| 110 | + }); |
| 111 | + expect(rows).to.deep.equal([{ one: 1 }]); |
| 112 | + }); |
| 113 | + |
| 114 | + it('strict + additive customCaCert (Buffer): same, passed as bytes', async () => { |
| 115 | + const rows = await connectAndSelectOne({ |
| 116 | + checkServerCertificate: true, |
| 117 | + customCaCert: Buffer.from(THROWAWAY_CA_PEM, 'utf8'), |
| 118 | + }); |
| 119 | + expect(rows).to.deep.equal([{ one: 1 }]); |
| 120 | + }); |
| 121 | + |
| 122 | + it('malformed customCaCert (PEM header, garbage body): kernel rejects at connect/openSession', async () => { |
| 123 | + const garbage = '-----BEGIN CERTIFICATE-----\nnot-valid-base64-cert-bytes\n-----END CERTIFICATE-----\n'; |
| 124 | + const client = new DBSQLClient(); |
| 125 | + let threw = false; |
| 126 | + try { |
| 127 | + await client.connect({ |
| 128 | + host: host as string, |
| 129 | + path: path as string, |
| 130 | + token: token as string, |
| 131 | + useSEA: true, |
| 132 | + checkServerCertificate: true, |
| 133 | + customCaCert: garbage, |
| 134 | + }); |
| 135 | + await client.openSession(); |
| 136 | + } catch (err) { |
| 137 | + threw = true; |
| 138 | + expect(String((err as Error).message).length).to.be.greaterThan(0); |
| 139 | + } finally { |
| 140 | + await client.close().catch(() => undefined); |
| 141 | + } |
| 142 | + expect(threw, 'expected a malformed custom CA to be rejected').to.equal(true); |
| 143 | + }); |
| 144 | +}); |
0 commit comments