Skip to content

Commit

Permalink
Allow credentials override for a particular request (#279)
Browse files Browse the repository at this point in the history
  • Loading branch information
slvrtrn committed Jun 6, 2024
1 parent ad8611e commit bd262b9
Show file tree
Hide file tree
Showing 17 changed files with 236 additions and 70 deletions.
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.1.0 (Common, Node.js, Web)

## New features

- Added an option to override the credentials for a particular `query`/`command`/`exec`/`insert` request via the `BaseQueryParams.auth` setting; when set, the credentials will be taken from there instead of the username/password provided during the client instantiation.

# 1.0.2 (Common, Node.js, Web)

## Bug fixes
Expand Down
81 changes: 75 additions & 6 deletions packages/client-common/__tests__/integration/auth.test.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import { type ClickHouseClient } from '@clickhouse/client-common'
import { createTestClient } from '../utils'
import { createSimpleTable } from '@test/fixtures/simple_table'
import { getAuthFromEnv } from '@test/utils/env'
import { createTestClient, guid } from '../utils'

describe('authentication', () => {
let client: ClickHouseClient
afterEach(async () => {
await client.close()
})

it('provides authentication error details', async () => {
beforeEach(() => {
client = createTestClient({
username: 'gibberish',
password: 'gibberish',
})
})
afterEach(async () => {
await client.close()
})

it('provides authentication error details', async () => {
await expectAsync(
client.query({
query: 'SELECT number FROM system.numbers LIMIT 3',
Expand All @@ -25,4 +28,70 @@ describe('authentication', () => {
}),
)
})

describe('request auth override', () => {
let defaultClient: ClickHouseClient
beforeAll(() => {
defaultClient = createTestClient()
})
afterAll(async () => {
await defaultClient.close()
})

let tableName: string
const values = [
{
id: '1',
name: 'foo',
sku: [3, 4],
},
]
const auth = getAuthFromEnv()

it('should with with insert and select', async () => {
tableName = `simple_table_${guid()}`
await createSimpleTable(defaultClient, tableName)
await client.insert({
table: tableName,
format: 'JSONEachRow',
values,
auth,
})
const rs = await client.query({
query: `SELECT * FROM ${tableName} ORDER BY id ASC`,
format: 'JSONEachRow',
auth,
})
expect(await rs.json()).toEqual(values)
})

it('should work with command and select', async () => {
tableName = `simple_table_${guid()}`
await createSimpleTable(defaultClient, tableName)
await client.command({
query: `INSERT INTO ${tableName} VALUES (1, 'foo', [3, 4])`,
auth,
})
const rs = await client.query({
query: `SELECT * FROM ${tableName} ORDER BY id ASC`,
format: 'JSONEachRow',
auth,
})
expect(await rs.json()).toEqual(values)
})

it('should work with exec', async () => {
const { stream } = await client.exec({
query: 'SELECT 42, 144 FORMAT CSV',
auth,
})
let result = ''
const textDecoder = new TextDecoder()
// @ts-expect-error - ReadableStream (Web) or Stream.Readable (Node.js); same API.
for await (const chunk of stream) {
result += textDecoder.decode(chunk, { stream: true })
}
expect(result).toEqual('42,144\n')
})
})
})
6 changes: 3 additions & 3 deletions packages/client-common/__tests__/utils/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import type {
ClickHouseClient,
ClickHouseSettings,
} from '@clickhouse/client-common'
import { getFromEnv } from './env'
import { EnvKeys, getFromEnv } from './env'
import { guid } from './guid'
import {
getClickHouseTestEnvironment,
Expand Down Expand Up @@ -55,8 +55,8 @@ export function createTestClient<Stream = unknown>(
}
if (isCloudTestEnv()) {
const cloudConfig: BaseClickHouseClientConfigOptions = {
url: `https://${getFromEnv('CLICKHOUSE_CLOUD_HOST')}:8443`,
password: getFromEnv('CLICKHOUSE_CLOUD_PASSWORD'),
url: `https://${getFromEnv(EnvKeys.host)}:8443`,
password: getFromEnv(EnvKeys.password),
database: databaseName,
...logging,
...config,
Expand Down
12 changes: 12 additions & 0 deletions packages/client-common/__tests__/utils/env.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
export const EnvKeys = {
host: 'CLICKHOUSE_CLOUD_HOST',
username: 'CLICKHOUSE_CLOUD_USERNAME',
password: 'CLICKHOUSE_CLOUD_PASSWORD',
}

export function getFromEnv(key: string): string {
const value = process.env[key]
if (value === undefined) {
throw Error(`Environment variable ${key} is not set`)
}
return value
}

export function getAuthFromEnv() {
const username = process.env[EnvKeys.username]
const password = process.env[EnvKeys.password]
return { username: username ?? 'default', password: password ?? '' }
}
24 changes: 17 additions & 7 deletions packages/client-common/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type {
BaseClickHouseClientConfigOptions,
ClickHouseSettings,
Connection,
ConnectionParams,
ConnExecResult,
IsSame,
LogWriter,
Expand All @@ -23,9 +24,16 @@ export interface BaseQueryParams {
/** AbortSignal instance to cancel a request in progress. */
abort_signal?: AbortSignal
/** A specific `query_id` that will be sent with this request.
* If it is not set, a random identifier will be generated automatically by the client. */
* If it is not set, a random identifier will be generated automatically by the client. */
query_id?: string
session_id?: string
/** When defined, overrides the credentials from the {@link BaseClickHouseClientConfigOptions.username}
* and {@link BaseClickHouseClientConfigOptions.password} settings for this particular request.
* @default undefined (no override) */
auth?: {
username: string
password: string
}
}

export interface QueryParams extends BaseQueryParams {
Expand Down Expand Up @@ -62,7 +70,7 @@ export type InsertResult = {
* Indicates whether the INSERT statement was executed on the server.
* Will be `false` if there was no data to insert.
* For example: if {@link InsertParams.values} was an empty array,
* the client does not any requests to the server, and {@link executed} is false.
* the client does not send any requests to the server, and {@link executed} is false.
*/
executed: boolean
/**
Expand Down Expand Up @@ -113,6 +121,7 @@ export interface InsertParams<Stream = unknown, T = unknown>

export class ClickHouseClient<Stream = unknown> {
private readonly clientClickHouseSettings: ClickHouseSettings
private readonly connectionParams: ConnectionParams
private readonly connection: Connection<Stream>
private readonly makeResultSet: MakeResultSet<Stream>
private readonly valuesEncoder: ValuesEncoder<Stream>
Expand All @@ -130,13 +139,13 @@ export class ClickHouseClient<Stream = unknown> {
logger,
config.impl.handle_specific_url_params ?? null,
)
const connectionParams = getConnectionParams(configWithURL, logger)
this.logWriter = connectionParams.log_writer
this.clientClickHouseSettings = connectionParams.clickhouse_settings
this.connectionParams = getConnectionParams(configWithURL, logger)
this.logWriter = this.connectionParams.log_writer
this.clientClickHouseSettings = this.connectionParams.clickhouse_settings
this.sessionId = config.session_id
this.connection = config.impl.make_connection(
configWithURL,
connectionParams,
this.connectionParams,
)
this.makeResultSet = config.impl.make_result_set
this.valuesEncoder = config.impl.values_encoder
Expand Down Expand Up @@ -247,10 +256,11 @@ export class ClickHouseClient<Stream = unknown> {
...this.clientClickHouseSettings,
...params.clickhouse_settings,
},
session_id: this.sessionId,
query_params: params.query_params,
abort_signal: params.abort_signal,
query_id: params.query_id,
session_id: this.sessionId,
auth: params.auth,
}
}
}
Expand Down
1 change: 1 addition & 0 deletions packages/client-common/src/connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export interface ConnBaseQueryParams {
abort_signal?: AbortSignal
session_id?: string
query_id?: string
auth?: { username: string; password: string }
}

export interface ConnInsertParams<Stream> extends ConnBaseQueryParams {
Expand Down
2 changes: 1 addition & 1 deletion packages/client-common/src/version.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export default '1.0.2'
export default '1.1.0'
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import {
import type {
ClickHouseClient,
ClickHouseLogLevel,
ErrorLogParams,
Logger,
LogParams,
} from '@clickhouse/client-common'
import { ClickHouseLogLevel } from '@clickhouse/client-common'
import { createTestClient } from '@test/utils'

describe('[Node.js] logger support', () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -600,7 +600,7 @@ xdescribe('[Node.js] Query and ResultSet types', () => {
const rs = await runQuery('JSON')

// All possible JSON variants are now allowed
// $ExpectType unknown[] | Record<string, unknown> | ResponseJSON<unknown>
// FIXME: this line produces a ESLint error due to a different order (which is insignificant). -$ExpectType unknown[] | Record<string, unknown> | ResponseJSON<unknown>
await rs.json() // IDE error here, different type order
// $ExpectType Data[] | ResponseJSON<Data> | Record<string, Data>
await rs.json<Data>()
Expand Down
53 changes: 49 additions & 4 deletions packages/client-node/__tests__/tls/tls.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ describe('[Node.js] TLS connection', () => {

it('should work with basic TLS', async () => {
client = createClient({
host: 'https://server.clickhouseconnect.test:8443',
url: 'https://server.clickhouseconnect.test:8443',
tls: {
ca_cert,
},
Expand All @@ -34,7 +34,7 @@ describe('[Node.js] TLS connection', () => {

it('should work with mutual TLS', async () => {
client = createClient({
host: 'https://server.clickhouseconnect.test:8443',
url: 'https://server.clickhouseconnect.test:8443',
username: 'cert_user',
tls: {
ca_cert,
Expand All @@ -51,7 +51,7 @@ describe('[Node.js] TLS connection', () => {

it('should fail when hostname does not match', async () => {
client = createClient({
host: 'https://localhost:8443',
url: 'https://localhost:8443',
username: 'cert_user',
tls: {
ca_cert,
Expand All @@ -75,7 +75,7 @@ describe('[Node.js] TLS connection', () => {

it('should fail with invalid certificates', async () => {
client = createClient({
host: 'https://server.clickhouseconnect.test:8443',
url: 'https://server.clickhouseconnect.test:8443',
username: 'cert_user',
tls: {
ca_cert,
Expand All @@ -91,4 +91,49 @@ describe('[Node.js] TLS connection', () => {
}),
).toBeRejectedWithError()
})

// query only; the rest of the methods are tested in the auth.test.ts in the common package
describe('request auth override', () => {
it('should override the credentials with basic TLS', async () => {
client = createClient({
url: 'https://server.clickhouseconnect.test:8443',
username: 'gibberish',
password: 'gibberish',
tls: {
ca_cert,
},
})
const resultSet = await client.query({
query: 'SELECT number FROM system.numbers LIMIT 3',
format: 'CSV',
auth: {
username: 'default',
password: '',
},
})
expect(await resultSet.text()).toEqual('0\n1\n2\n')
})

it('should override the credentials with mutual TLS', async () => {
client = createClient({
url: 'https://server.clickhouseconnect.test:8443',
username: 'gibberish',
password: 'gibberish',
tls: {
ca_cert,
cert,
key,
},
})
const resultSet = await client.query({
query: 'SELECT number FROM system.numbers LIMIT 3',
format: 'CSV',
auth: {
username: 'cert_user',
password: '',
},
})
expect(await resultSet.text()).toEqual('0\n1\n2\n')
})
})
})
2 changes: 1 addition & 1 deletion packages/client-node/__tests__/utils/http_stubs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,6 @@ export class MyTestHttpConnection extends NodeBaseConnection {
return {} as any
}
public getDefaultHeaders() {
return this.headers
return this.buildRequestHeaders()
}
}
Loading

0 comments on commit bd262b9

Please sign in to comment.