diff --git a/src/handler.ts b/src/handler.ts index 3fa0085..aaa3be2 100644 --- a/src/handler.ts +++ b/src/handler.ts @@ -26,6 +26,7 @@ export interface StarbaseDBConfiguration { websocket?: boolean export?: boolean import?: boolean + allowlistBypassRest?: boolean } } diff --git a/src/index.ts b/src/index.ts index 4d08932..da61d9c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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 @@ -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, }, } diff --git a/src/literest/index.test.ts b/src/literest/index.test.ts index 51f8b31..570033b 100644 --- a/src/literest/index.test.ts +++ b/src/literest/index.test.ts @@ -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 }) + ) + }) + }) }) diff --git a/src/literest/index.ts b/src/literest/index.ts index ade8915..aa7c686 100644 --- a/src/literest/index.ts +++ b/src/literest/index.ts @@ -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. @@ -70,6 +78,7 @@ export class LiteREST { isRaw: false, dataSource: this.dataSource, config: this.config, + bypassAllowlist: this.shouldBypassAllowlist(), })) as any[] let pkColumns = [] @@ -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, diff --git a/src/operation.test.ts b/src/operation.test.ts index f52cbb9..06fb5fd 100644 --- a/src/operation.test.ts +++ b/src/operation.test.ts @@ -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', diff --git a/src/operation.ts b/src/operation.ts index 4abc0dd..80691b7 100644 --- a/src/operation.ts +++ b/src/operation.ts @@ -196,8 +196,9 @@ export async function executeQuery(opts: { isRaw: boolean dataSource: DataSource config: StarbaseDBConfiguration + bypassAllowlist?: boolean }): Promise { - let { sql, params, isRaw, dataSource, config } = opts + let { sql, params, isRaw, dataSource, config, bypassAllowlist } = opts if (!dataSource) { console.error('Data source not found.') @@ -205,12 +206,14 @@ export async function executeQuery(opts: { } // 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({ @@ -323,8 +326,9 @@ export async function executeTransaction(opts: { isRaw: boolean dataSource: DataSource config: StarbaseDBConfiguration + bypassAllowlist?: boolean }): Promise { - const { queries, isRaw, dataSource, config } = opts + const { queries, isRaw, dataSource, config, bypassAllowlist } = opts if (!dataSource) { console.error('Data source not found.') @@ -340,6 +344,7 @@ export async function executeTransaction(opts: { isRaw, dataSource, config, + bypassAllowlist, }) results.push(result) diff --git a/wrangler.toml b/wrangler.toml index 395c4ac..e6a5cfd 100644 --- a/wrangler.toml +++ b/wrangler.toml @@ -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