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

Add support for role query parameters #328

Merged
merged 4 commits into from
Nov 5, 2024
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
1 change: 1 addition & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +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
slvrtrn marked this conversation as resolved.
Show resolved Hide resolved

## How to run

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

void (async () => {
slvrtrn marked this conversation as resolved.
Show resolved Hide resolved
const client = createClient({
role: 'role_name_1',
})

// with a role defined in the client configuration, all queries will use the specified role
await client.command({
query: `SELECT * FROM SECURED_TABLE`,
})
slvrtrn marked this conversation as resolved.
Show resolved Hide resolved

// 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'],
})

console.log(await rows1.json())

await client.close()
})()
slvrtrn marked this conversation as resolved.
Show resolved Hide resolved
359 changes: 359 additions & 0 deletions packages/client-common/__tests__/integration/role.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,359 @@
import type { ClickHouseClient } from '@clickhouse/client-common'
import { createTestClient, TestEnv, whenOnEnv } from '@test/utils'
import { getTestDatabaseName, guid } from '../utils'
import { createSimpleTable } from '../fixtures/simple_table'
import { assertJsonValues, jsonValues } from '../fixtures/test_data'

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

let database: string
let username: string
let password: string
let roleName1: string
let roleName2: string

beforeAll(async () => {
defaultClient = createTestClient()
username = `clickhousejs__user_with_roles_${guid()}`
password = `CHJS_${guid()}`
roleName1 = `TEST_ROLE_${guid()}`
roleName2 = `TEST_ROLE_${guid()}`
database = getTestDatabaseName()

await defaultClient.command({
query: `CREATE USER ${username} IDENTIFIED WITH sha256_password BY '${password}' DEFAULT DATABASE ${database}`,
})
await defaultClient.command({
query: `CREATE ROLE IF NOT EXISTS ${roleName1}`,
})
await defaultClient.command({
query: `CREATE ROLE IF NOT EXISTS ${roleName2}`,
})
await defaultClient.command({
query: `GRANT ${roleName1}, ${roleName2} TO ${username}`,
})
await defaultClient.command({
query: `GRANT INSERT ON ${database}.* TO ${roleName1}`,
})
await defaultClient.command({
query: `GRANT CREATE TABLE ON * TO ${roleName1}`,
})
})

afterEach(async () => {
await client.close()
})

afterAll(async () => {
await defaultClient.close()
})

describe('for queries', () => {
async function queryCurrentRoles(role?: string | Array<string>) {
const rs = await client.query({
query: 'select currentRoles() as roles',
format: 'JSONEachRow',
role,
})

const jsonResults = (await rs.json()) as { roles: string[] }[]
return jsonResults[0].roles
}

whenOnEnv(TestEnv.LocalSingleNode).it(
'should use a single role from the client configuration',
async () => {
client = createTestClient({
username,
password,
role: roleName1,
})

const actualRoles = await queryCurrentRoles()
expect(actualRoles).toEqual([roleName1])
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should use multiple roles from the client configuration',
async () => {
client = createTestClient({
username,
password,
role: [roleName1, roleName2],
})

const actualRoles = await queryCurrentRoles()
expect(actualRoles.length).toBe(2)
expect(actualRoles).toContain(roleName1)
expect(actualRoles).toContain(roleName2)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should use single role from the query options',
async () => {
client = createTestClient({
username,
password,
role: [roleName1, roleName2],
})

const actualRoles = await queryCurrentRoles(roleName2)
expect(actualRoles).toEqual([roleName2])
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should use multiple roles from the query options',
async () => {
client = createTestClient({
username,
password,
})

const actualRoles = await queryCurrentRoles([roleName1, roleName2])
expect(actualRoles.length).toBe(2)
expect(actualRoles).toContain(roleName1)
expect(actualRoles).toContain(roleName2)
},
)
})

describe('for inserts', () => {
let tableName: string

beforeEach(async () => {
tableName = `insert_test_${guid()}`
await createSimpleTable(defaultClient, tableName)
})

async function tryInsert(role?: string | Array<string>) {
await client.insert({
table: tableName,
values: jsonValues,
format: 'JSONEachRow',
role,
})
}

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully insert when client specifies a role that is allowed to insert',
async () => {
client = createTestClient({
username,
password,
role: roleName1,
})

await tryInsert()
await assertJsonValues(defaultClient, tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully insert when client specifies multiple roles and at least one is allowed to insert',
async () => {
client = createTestClient({
username,
password,
role: [roleName1, roleName2],
})

await tryInsert()
await assertJsonValues(defaultClient, tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should fail to insert when client specifies a role that is not allowed to insert',
async () => {
client = createTestClient({
username,
password,
role: roleName2,
})

await expectAsync(tryInsert()).toBeRejectedWith(
jasmine.objectContaining({
message: jasmine.stringContaining('Not enough privileges'),
code: '497',
type: 'ACCESS_DENIED',
}),
)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully insert when insert specifies a role that is allowed to insert',
async () => {
client = createTestClient({
username,
password,
role: roleName2,
})

await tryInsert(roleName1)
await assertJsonValues(defaultClient, tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully insert when insert specifies multiple roles and at least one is allowed to insert',
async () => {
client = createTestClient({
username,
password,
role: roleName2,
})

await tryInsert([roleName1, roleName2])
await assertJsonValues(defaultClient, tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should fail to insert when insert specifies a role that is not allowed to insert',
async () => {
client = createTestClient({
username,
password,
role: roleName1,
})

await expectAsync(tryInsert(roleName2)).toBeRejectedWith(
jasmine.objectContaining({
message: jasmine.stringContaining('Not enough privileges'),
code: '497',
type: 'ACCESS_DENIED',
}),
)
},
)
})

describe('for commands', () => {
let tableName: string

beforeEach(async () => {
tableName = `command_role_test_${guid()}`
})

async function tryCreateTable(role?: string | Array<string>) {
const query = `
CREATE TABLE ${tableName}
(id UInt64, name String, sku Array(UInt8), timestamp DateTime)
ENGINE = MergeTree()
ORDER BY (id)
`
await client.command({ query, role })
}
slvrtrn marked this conversation as resolved.
Show resolved Hide resolved

async function checkCreatedTable(tableName: string) {
const selectResult = await defaultClient.query({
query: `SELECT * from system.tables where name = '${tableName}'`,
format: 'JSON',
})

const { data, rows } = await selectResult.json<{ name: string }>()
expect(rows).toBe(1)
expect(data[0].name).toBe(tableName)
}

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully create a table when client specifies a role that is allowed to create tables',
async () => {
client = createTestClient({
username,
password,
role: roleName1,
})

await tryCreateTable()
await checkCreatedTable(tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully create table when client specifies multiple roles and at least one is allowed to create tables',
async () => {
client = createTestClient({
username,
password,
role: [roleName1, roleName2],
})

await tryCreateTable()
await checkCreatedTable(tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should fail to create a table when client specifies a role that is not allowed to create tables',
async () => {
client = createTestClient({
username,
password,
role: roleName2,
})

await expectAsync(tryCreateTable()).toBeRejectedWith(
jasmine.objectContaining({
message: jasmine.stringContaining('Not enough privileges'),
code: '497',
type: 'ACCESS_DENIED',
}),
)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully create table when command specifies a role that is allowed to create tables',
async () => {
client = createTestClient({
username,
password,
role: roleName2,
})

await tryCreateTable(roleName1)
await checkCreatedTable(tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should successfully create table when command specifies multiple roles and at least one is allowed to create tables',
async () => {
client = createTestClient({
username,
password,
role: roleName2,
})

await tryCreateTable([roleName1, roleName2])
await checkCreatedTable(tableName)
},
)

whenOnEnv(TestEnv.LocalSingleNode).it(
'should fail to create table when command specifies a role that is not allowed to create tables',
async () => {
client = createTestClient({
username,
password,
role: roleName1,
})

await expectAsync(tryCreateTable(roleName2)).toBeRejectedWith(
jasmine.objectContaining({
message: jasmine.stringContaining('Not enough privileges'),
code: '497',
type: 'ACCESS_DENIED',
}),
)
},
)
})
})
Loading