Skip to content
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
18 changes: 7 additions & 11 deletions src/internal/database/pg-connection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -771,18 +771,14 @@ export class PgTenantConnection implements TenantConnection {
}

await tnx.query(
buildScopeStatement({
role: this.role,
jwt: this.options.user.jwt || '',
subject: this.options.user.payload.sub || '',
claims: this.getUserPayload(),
headers: this.headersPayload,
method: this.options.method || '',
path: this.options.path || '',
operation: this.options.operation?.() || '',
buildScopeStatement(
this.options,
this.role,
this.getUserPayload(),
this.headersPayload,
statementTimeoutMs,
searchPath: pendingSearchPath,
})
pendingSearchPath
)
)
}

Expand Down
58 changes: 39 additions & 19 deletions src/internal/database/postgres/scope.test.ts
Original file line number Diff line number Diff line change
@@ -1,20 +1,25 @@
import { describe, expect, it } from 'vitest'
import { buildScopeStatement, type Scope } from './scope'

const baseScope: Scope = {
role: 'authenticated',
jwt: 'jwt',
subject: 'user-id',
claims: '{"role":"authenticated"}',
headers: '{"x-client-info":"test"}',
import { buildScopeStatement, type ScopeConnectionOptions } from './scope'

const baseScopeOptions: ScopeConnectionOptions = {
user: {
jwt: 'jwt',
payload: {
role: 'authenticated',
sub: 'user-id',
},
},
method: 'POST',
path: '/object/bucket/name',
operation: 'object.create',
operation: () => 'object.create',
}
const role = 'authenticated'
const claims = '{"role":"authenticated"}'
const headers = '{"x-client-info":"test"}'

describe('PostgreSQL scope statement', () => {
it('builds the common request scope without optional transaction settings', () => {
const statement = buildScopeStatement(baseScope)
const statement = buildScopeStatement(baseScopeOptions, role, claims, headers)

expect(statement.text).toContain("set_config('role', $1, true)")
expect(statement.text).not.toContain("set_config('statement_timeout'")
Expand All @@ -33,24 +38,39 @@ describe('PostgreSQL scope statement', () => {
})

it('keeps timeout and search path placeholder ordering stable', () => {
const statement = buildScopeStatement({
...baseScope,
statementTimeoutMs: 4321,
searchPath: 'storage,public,extensions',
})
const statement = buildScopeStatement(
baseScopeOptions,
role,
claims,
headers,
4321,
'storage,public,extensions'
)

expect(statement.text).toContain("set_config('statement_timeout', $10, true)")
expect(statement.text).toContain("set_config('search_path', $11, true)")
expect(statement.values.slice(9)).toEqual(['4321ms', 'storage,public,extensions'])
})

it('uses placeholder ten when only search path is present', () => {
const statement = buildScopeStatement({
...baseScope,
searchPath: 'storage,public,extensions',
})
const statement = buildScopeStatement(
baseScopeOptions,
role,
claims,
headers,
undefined,
'storage,public,extensions'
)

expect(statement.text).toContain("set_config('search_path', $10, true)")
expect(statement.values[9]).toBe('storage,public,extensions')
})

it('uses placeholder ten when only statement timeout is present', () => {
const statement = buildScopeStatement(baseScopeOptions, role, claims, headers, 4321)

expect(statement.text).toContain("set_config('statement_timeout', $10, true)")
expect(statement.text).not.toContain("set_config('search_path'")
expect(statement.values[9]).toBe('4321ms')
})
})
126 changes: 82 additions & 44 deletions src/internal/database/postgres/scope.ts
Original file line number Diff line number Diff line change
@@ -1,61 +1,99 @@
export interface Scope {
role: string
jwt: string
subject: string
claims: string
headers: string
method: string
path: string
operation: string
statementTimeoutMs?: number
searchPath?: string
}
import type { TenantConnectionOptions } from '../pool'

export type ScopeConnectionOptions = Pick<
TenantConnectionOptions,
'user' | 'method' | 'path' | 'operation'
>

export interface ScopeStatement {
text: string
values: unknown[]
}

export function buildScopeStatement(scope: Scope): ScopeStatement {
const setters = [
"set_config('role', $1, true)",
"set_config('request.jwt.claim.role', $2, true)",
"set_config('request.jwt', $3, true)",
"set_config('request.jwt.claim.sub', $4, true)",
"set_config('request.jwt.claims', $5, true)",
"set_config('request.headers', $6, true)",
"set_config('request.method', $7, true)",
"set_config('request.path', $8, true)",
"set_config('storage.operation', $9, true)",
"set_config('storage.allow_delete_query', 'true', true)",
]
const SCOPE_CONFIG_SETTERS = `set_config('role', $1, true),
set_config('request.jwt.claim.role', $2, true),
set_config('request.jwt', $3, true),
set_config('request.jwt.claim.sub', $4, true),
set_config('request.jwt.claims', $5, true),
set_config('request.headers', $6, true),
set_config('request.method', $7, true),
set_config('request.path', $8, true),
set_config('storage.operation', $9, true),
set_config('storage.allow_delete_query', 'true', true)`

const SCOPE_CONFIG_SQL = `
SELECT
${SCOPE_CONFIG_SETTERS};
`

const SCOPE_CONFIG_SQL_WITH_STATEMENT_TIMEOUT = `
SELECT
${SCOPE_CONFIG_SETTERS},
set_config('statement_timeout', $10, true);
`

const SCOPE_CONFIG_SQL_WITH_SEARCH_PATH = `
SELECT
${SCOPE_CONFIG_SETTERS},
set_config('search_path', $10, true);
`

const SCOPE_CONFIG_SQL_WITH_STATEMENT_TIMEOUT_AND_SEARCH_PATH = `
SELECT
${SCOPE_CONFIG_SETTERS},
set_config('statement_timeout', $10, true),
set_config('search_path', $11, true);
`

export function buildScopeStatement(
options: ScopeConnectionOptions,
role: string,
claimsPayload: string,
headersPayload: string,
statementTimeoutMs?: number,
searchPath?: string
): ScopeStatement {
const values: unknown[] = [
scope.role,
scope.role,
scope.jwt,
scope.subject,
scope.claims,
scope.headers,
scope.method,
scope.path,
scope.operation,
role,
role,
options.user.jwt || '',
options.user.payload.sub || '',
claimsPayload,
headersPayload,
options.method || '',
options.path || '',
options.operation?.() || '',
]

if (scope.statementTimeoutMs) {
values.push(`${scope.statementTimeoutMs}ms`)
setters.push(`set_config('statement_timeout', $${values.length}, true)`)
if (statementTimeoutMs) {
values.push(`${statementTimeoutMs}ms`)
}

if (scope.searchPath) {
values.push(scope.searchPath)
setters.push(`set_config('search_path', $${values.length}, true)`)
if (searchPath) {
values.push(searchPath)
}

return {
text: `
SELECT
${setters.join(',\n ')};
`,
text: getScopeConfigSql(statementTimeoutMs, searchPath),
values,
}
}

function getScopeConfigSql(
statementTimeoutMs: number | undefined,
searchPath: string | undefined
): string {
if (statementTimeoutMs && searchPath) {
return SCOPE_CONFIG_SQL_WITH_STATEMENT_TIMEOUT_AND_SEARCH_PATH
}

if (statementTimeoutMs) {
return SCOPE_CONFIG_SQL_WITH_STATEMENT_TIMEOUT
}

if (searchPath) {
return SCOPE_CONFIG_SQL_WITH_SEARCH_PATH
}

return SCOPE_CONFIG_SQL
}