Skip to content

Commit

Permalink
Improve role examples
Browse files Browse the repository at this point in the history
  • Loading branch information
pulpdrew committed Sep 28, 2024
1 parent 93e5673 commit 601be11
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 17 deletions.
2 changes: 1 addition & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,7 +67,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 more more roles, without explicit `USE` commands or session IDs
- [role.ts](role.ts) - using one or more roles without explicit `USE` commands or session IDs

## How to run

Expand Down
121 changes: 111 additions & 10 deletions examples/role.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,124 @@
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 = 'JSON'
const username = 'role_user'
const password = 'role_user_password'
const table1 = 'table_1'
const table2 = '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({
role: 'role_name_1',
username,
password,
role: table1Role,
})

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

// Selecting from table2 is not allowed using table1Role
await client
.query({ query: `select count(*) from ${table2}`, format })
.catch((e: ClickHouseError) => {
console.error(
`Failed to qeury from ${table2} due to error with type: ${e.type}. Message: ${e.message}`,
)
})

// with a role defined in the client configuration, all queries will use the specified role
await client.command({
query: `SELECT * FROM SECURED_TABLE`,
// Override the client's role to table2Role, allowing a query to table2
rs = await client.query({
query: `select count(*) from ${table2}`,
format,
role: table2Role,
})
console.log(
`Successfully queried from ${table2} using ${table2Role}. Result: `,
(await rs.json()).data,
)

// Selecting from table1 is no longer allowed, since table2Role is being used
await client
.query({
query: `select count(*) from ${table1}`,
format,
role: table2Role,
})
.catch((e: ClickHouseError) => {
console.error(
`Failed to qeury from ${table1} due to error with type: ${e.type}. Message: ${e.message}`,
)
})

// one or more roles can be specified in a query as well, to override the role(s) set for the client
const rows1 = await client.query({
query: `SELECT * FROM VERY_SECURED_TABLE`,
format: 'JSONEachRow',
role: ['highly_privileged_role'],
// Multiple roles can be specified to allowed querying from either table
rs = await client.query({
query: `select count(*) from ${table1}`,
format,
role: [table1Role, table2Role],
})
console.log(
`Successfully queried from ${table1} using roles: [${table1Role}, ${table2Role}]. Result: `,
(await rs.json()).data,
)

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

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
}
})()
2 changes: 1 addition & 1 deletion packages/client-common/__tests__/integration/role.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { getTestDatabaseName, guid } from '../utils'
import { createSimpleTable } from '../fixtures/simple_table'
import { assertJsonValues, jsonValues } from '../fixtures/test_data'

describe('role settings', () => {
fdescribe('role settings', () => {
let defaultClient: ClickHouseClient
let client: ClickHouseClient

Expand Down
16 changes: 16 additions & 0 deletions packages/client-common/__tests__/unit/to_search_params.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,22 @@ describe('toSearchParams', () => {
['wait_end_of_query', '1'],
])
})

it('should set a single role', async () => {
const query = 'SELECT * FROM system.query_log'
const params = toSearchParams({
database: 'some_db',
query,
query_id: 'my-query-id',
role: 'single-role',
})!
const result = toSortedArray(params)
expect(result).toEqual([
['database', 'some_db'],
['query', 'SELECT * FROM system.query_log'],
['role', 'single-role'],
])
})
})

function toSortedArray(params: URLSearchParams): [string, string][] {
Expand Down
12 changes: 7 additions & 5 deletions packages/client-common/src/utils/url.ts
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,13 @@ export function toSearchParams({
params.set('session_id', session_id)
}

if (role && typeof role === 'string') {
params.set('role', role)
} else if (role && Array.isArray(role)) {
for (const r of role) {
params.append('role', r)
if (role) {
if (typeof role === 'string') {
params.set('role', role)
} else if (Array.isArray(role)) {
for (const r of role) {
params.append('role', r)
}
}
}

Expand Down

0 comments on commit 601be11

Please sign in to comment.