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

Role query parameters #351

Merged
merged 4 commits into from
Nov 6, 2024
Merged
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,9 @@
# 1.8.0 (Common, Node.js, Web)

## New features

- Added support for specifying roles via request query parameters. See [this example](examples/role.ts) for more details. ([@pulpdrew](https://github.com/pulpdrew), [#328](https://github.com/ClickHouse/clickhouse-js/pull/328))

# 1.7.0 (Common, Node.js, Web)

## Bug fixes
Expand Down
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,7 @@ If something is missing, or you found a mistake in one of these examples, please
- [default_format_setting.ts](default_format_setting.ts) - sending queries using `exec` method without a `FORMAT` clause; the default format will be set from the client settings.
- [session_id_and_temporary_tables.ts](session_id_and_temporary_tables.ts) - creating a temporary table, which requires a session_id to be passed to the server.
- [session_level_commands.ts](session_level_commands.ts) - using SET commands, memorized for the specific session_id.
- [role.ts](role.ts) - using one or more roles without explicit `USE` commands or session IDs

## How to run

Expand Down
129 changes: 129 additions & 0 deletions examples/role.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import type { ClickHouseError } from '@clickhouse/client'
import { createClient } from '@clickhouse/client' // or '@clickhouse/client-web'

/**
* An example of specifying a role using query parameters
* See https://clickhouse.com/docs/en/interfaces/http#setting-role-with-query-parameters
*/
void (async () => {
const format = 'JSONEachRow'
const username = 'clickhouse_js_role_user'
const password = 'role_user_password'
const table1 = 'clickhouse_js_role_table_1'
const table2 = 'clickhouse_js_role_table_2'

// Create 2 tables, a role for each table allowing SELECT, and a user with access to those roles
const defaultClient = createClient()
await createOrReplaceUser(username, password)
const table1Role = await createTableAndGrantAccess(table1, username)
const table2Role = await createTableAndGrantAccess(table2, username)
await defaultClient.close()

// Create a client using a role that only has permission to query table1
const client = createClient({
username,
password,
// This role will be applied to all the queries by default,
// unless it is overridden in a specific method call
role: table1Role,
})

// Selecting from table1 is allowed using table1Role
const resultSet1 = await client.query({
query: `select count(*) from ${table1}`,
format,
})
console.log(
`Successfully queried from ${table1} using ${table1Role}. Result: `,
await resultSet1.json(),
)

// Selecting from table2 is not allowed using table1Role,
// which is set by default in the client instance
await client
.query({ query: `select count(*) from ${table2}`, format })
.catch((e: ClickHouseError) => {
console.error(
`Failed to query from ${table2}, as ${table1Role} does not have sufficient privileges. Expected error:`,
e,
)
})

// Override the client's role to table2Role, allowing a query to table2
const resultSet2 = await client.query({
query: `select count(*) from ${table2}`,
role: table2Role,
format,
})
console.log(
`Successfully queried from ${table2} using ${table2Role}. Result:`,
await resultSet2.json(),
)

// Selecting from table1 is no longer allowed, since table2Role is being used
await client
.query({
query: `select count(*) from ${table1}`,
role: table2Role,
format,
})
.catch((e: ClickHouseError) => {
console.error(
`Failed to query from ${table1}, as ${table2Role} does not have sufficient privileges. Expected error:`,
e,
)
})

// Multiple roles can be specified to allowed querying from either table
const resultSet3 = await client.query({
query: `select count(*) from ${table1}`,
role: [table1Role, table2Role],
format,
})
console.log(
`Successfully queried from ${table1} using roles: [${table1Role}, ${table2Role}]. Result:`,
await resultSet3.json(),
)

const resultSet4 = await client.query({
query: `select count(*) from ${table2}`,
role: [table1Role, table2Role],
format,
})
console.log(
`Successfully queried from ${table2} using roles: [${table1Role}, ${table2Role}]. Result: `,
await resultSet4.json(),
)

await client.close()

async function createOrReplaceUser(username: string, password: string) {
await defaultClient.command({
query: `CREATE USER OR REPLACE ${username} IDENTIFIED WITH plaintext_password BY '${password}'`,
})
}

async function createTableAndGrantAccess(
tableName: string,
username: string,
) {
const role = `${tableName}_role`

await defaultClient.command({
query: `
CREATE OR REPLACE TABLE ${tableName}
(id UInt32, name String, sku Array(UInt32))
ENGINE MergeTree()
ORDER BY (id)
`,
})

await defaultClient.command({ query: `CREATE ROLE OR REPLACE ${role}` })
await defaultClient.command({
query: `GRANT SELECT ON ${tableName} TO ${role}`,
})
await defaultClient.command({ query: `GRANT ${role} TO ${username}` })

return role
}
})()
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ import { type ClickHouseClient } from '@clickhouse/client-common'
import {
createTestClient,
getClickHouseTestEnvironment,
getTestDatabaseName,
guid,
TestEnv,
validateUUID,
Expand Down Expand Up @@ -51,8 +50,8 @@ describe('exec and command', () => {
})
})

it('does not swallow ClickHouse error', async () => {
const { ddl, tableName } = getDDL()
it('should not swallow ClickHouse error', async () => {
const { ddl } = getDDL()
const commands = async () => {
const command = () =>
runExec({
Expand All @@ -65,9 +64,6 @@ describe('exec and command', () => {
jasmine.objectContaining({
code: '57',
type: 'TABLE_ALREADY_EXISTS',
message: jasmine.stringContaining(
`Table ${getTestDatabaseName()}.${tableName} already exists. `,
),
}),
)
})
Expand Down
Loading