Skip to content
Open
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
1 change: 1 addition & 0 deletions src/handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ export interface StarbaseDBConfiguration {
websocket?: boolean
export?: boolean
import?: boolean
allowlistBypassRest?: boolean
}
}

Expand Down
5 changes: 5 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ export interface Env {

ENABLE_ALLOWLIST?: boolean
ENABLE_RLS?: boolean
ALLOWLIST_BYPASS_REST?: boolean

// External database source details
OUTERBASE_API_KEY?: string
Expand Down Expand Up @@ -190,6 +191,10 @@ export default {
features: {
allowlist: env.ENABLE_ALLOWLIST,
rls: env.ENABLE_RLS,
allowlistBypassRest:
env.ALLOWLIST_BYPASS_REST !== undefined
? Boolean(Number(env.ALLOWLIST_BYPASS_REST))
: true,
},
}

Expand Down
32 changes: 32 additions & 0 deletions src/literest/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -489,4 +489,36 @@ describe('LiteREST', () => {
expect(query).not.toContain('DROP TABLE users')
})
})

describe('allowlist bypass', () => {
it('should execute queries with bypassAllowlist: true by default', async () => {
const request = new Request('http://localhost/rest/main/users', {
method: 'GET',
})
await liteRest.handleRequest(request)

expect(executeTransaction).toHaveBeenCalledWith(
expect.objectContaining({ bypassAllowlist: true })
)
})

it('should respect allowlistBypassRest: false when configured', async () => {
const strictConfig: StarbaseDBConfiguration = {
...mockConfig,
features: {
allowlistBypassRest: false,
},
}
const strictLiteRest = new LiteREST(mockDataSource, strictConfig)

const request = new Request('http://localhost/rest/main/users', {
method: 'GET',
})
await strictLiteRest.handleRequest(request)

expect(executeTransaction).toHaveBeenCalledWith(
expect.objectContaining({ bypassAllowlist: false })
)
})
})
})
10 changes: 10 additions & 0 deletions src/literest/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,14 @@ export class LiteREST {
this.config = config
}

/**
* Determines whether queries produced by LiteREST should bypass the static SQL allowlist.
* By default true so REST API works seamlessly when allowlist is toggled on.
*/
private shouldBypassAllowlist(): boolean {
return this.config?.features?.allowlistBypassRest ?? true
}

/**
* Sanitizes an identifier by removing all non-alphanumeric characters except underscores.
* @param identifier - The identifier to sanitize.
Expand Down Expand Up @@ -70,6 +78,7 @@ export class LiteREST {
isRaw: false,
dataSource: this.dataSource,
config: this.config,
bypassAllowlist: this.shouldBypassAllowlist(),
})) as any[]

let pkColumns = []
Expand Down Expand Up @@ -178,6 +187,7 @@ export class LiteREST {
isRaw: false,
dataSource: this.dataSource,
config: this.config,
bypassAllowlist: this.shouldBypassAllowlist(),
})) as any[]
return {
result: results?.length > 0 ? results[0] : undefined,
Expand Down
29 changes: 29 additions & 0 deletions src/operation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,35 @@ describe('executeQuery', () => {
)
})

it('should bypass the allowlist feature when bypassAllowlist is true', async () => {
vi.mocked(isQueryAllowed).mockClear()

await executeQuery({
sql: 'SELECT * FROM users',
params: undefined,
isRaw: false,
dataSource: mockDataSource,
config: mockConfig,
bypassAllowlist: true,
})

expect(isQueryAllowed).not.toHaveBeenCalled()
})

it('should pass bypassAllowlist flag in executeTransaction', async () => {
vi.mocked(isQueryAllowed).mockClear()

await executeTransaction({
queries: [{ sql: 'SELECT * FROM users' }],
isRaw: false,
dataSource: mockDataSource,
config: mockConfig,
bypassAllowlist: true,
})

expect(isQueryAllowed).not.toHaveBeenCalled()
})

it('should apply row-level security', async () => {
await executeQuery({
sql: 'SELECT * FROM users',
Expand Down
21 changes: 13 additions & 8 deletions src/operation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -196,21 +196,24 @@ export async function executeQuery(opts: {
isRaw: boolean
dataSource: DataSource
config: StarbaseDBConfiguration
bypassAllowlist?: boolean
}): Promise<QueryResponse> {
let { sql, params, isRaw, dataSource, config } = opts
let { sql, params, isRaw, dataSource, config, bypassAllowlist } = opts

if (!dataSource) {
console.error('Data source not found.')
return []
}

// If the allowlist feature is enabled, we should verify the query is allowed before proceeding.
await isQueryAllowed({
sql: sql,
isEnabled: config?.features?.allowlist ?? false,
dataSource,
config,
})
if (!bypassAllowlist) {
await isQueryAllowed({
sql: sql,
isEnabled: config?.features?.allowlist ?? false,
dataSource,
config,
})
}

// If the row level security feature is enabled, we should apply our policies to this SQL statement.
sql = await applyRLS({
Expand Down Expand Up @@ -323,8 +326,9 @@ export async function executeTransaction(opts: {
isRaw: boolean
dataSource: DataSource
config: StarbaseDBConfiguration
bypassAllowlist?: boolean
}): Promise<QueryResponse> {
const { queries, isRaw, dataSource, config } = opts
const { queries, isRaw, dataSource, config, bypassAllowlist } = opts

if (!dataSource) {
console.error('Data source not found.')
Expand All @@ -340,6 +344,7 @@ export async function executeTransaction(opts: {
isRaw,
dataSource,
config,
bypassAllowlist,
})

results.push(result)
Expand Down
1 change: 1 addition & 0 deletions wrangler.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ REGION = "auto"
# Toggle to enable default features
ENABLE_ALLOWLIST = 0
ENABLE_RLS = 0
ALLOWLIST_BYPASS_REST = 1

# External database source details
# This enables Starbase to connect to an external data source
Expand Down