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
43 changes: 25 additions & 18 deletions public/.vite/manifest.json
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
{
"_components.Dd6m7Hsm.js": {
"file": "assets/components.Dd6m7Hsm.js",
"name": "components",
"imports": ["_vendor.DLA8GOwG.js"]
},
"_vendor.DLA8GOwG.js": {
"file": "assets/vendor.DLA8GOwG.js",
"name": "vendor"
},
"plugins/interface/pages/template/index.tsx": {
"file": "assets/template.Cbhlkt6E.js",
"name": "template",
"src": "plugins/interface/pages/template/index.tsx",
"isEntry": true,
"imports": ["_components.Dd6m7Hsm.js", "_vendor.DLA8GOwG.js"],
"css": ["assets/template.BByNnpth.css"]
}
}
"_components.BPtxb7Pe.js": {
"file": "assets/components.BPtxb7Pe.js",
"name": "components",
"imports": [
"_vendor.CdOFJqZD.js"
]
},
"_vendor.CdOFJqZD.js": {
"file": "assets/vendor.CdOFJqZD.js",
"name": "vendor"
},
"plugins/interface/pages/template/index.tsx": {
"file": "assets/template.BnE4n9BY.js",
"name": "template",
"src": "plugins/interface/pages/template/index.tsx",
"isEntry": true,
"imports": [
"_components.BPtxb7Pe.js",
"_vendor.CdOFJqZD.js"
],
"css": [
"assets/template.CNfysZMx.css"
]
}
}
198 changes: 198 additions & 0 deletions src/allowlist/index.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
import { describe, it, expect, vi, beforeEach } from 'vitest'
import { isQueryAllowed } from './index'
import { StarbaseDBConfiguration } from '../handler'

const mockDataSource = {
source: 'internal',
rpc: {
executeQuery: vi.fn(),
},
} as any

const clientConfig: StarbaseDBConfiguration = {
role: 'client',
features: { allowlist: true },
}

const adminConfig: StarbaseDBConfiguration = {
role: 'admin',
features: { allowlist: true },
}

function mockAllowlistRows(rows: { sql_statement: string; source: string }[]) {
vi.mocked(mockDataSource.rpc.executeQuery).mockImplementation(
async (opts: any) => {
const sql: string = opts?.sql ?? ''
if (sql.startsWith('SELECT')) {
return rows as any
}
// INSERT into rejections — record call, return empty
return [] as any
}
)
}

describe('isQueryAllowed - allowlist query checker', () => {
beforeEach(() => {
vi.resetAllMocks()
vi.spyOn(console, 'error').mockImplementation(() => {})
})

it('allows any query when the feature is disabled without touching the DB', async () => {
const result = await isQueryAllowed({
sql: 'DROP TABLE users',
isEnabled: false,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBe(true)
expect(mockDataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('allows any query for admin role without touching the DB', async () => {
const result = await isQueryAllowed({
sql: 'DELETE FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: adminConfig,
})

expect(result).toBe(true)
expect(mockDataSource.rpc.executeQuery).not.toHaveBeenCalled()
})

it('allows an exact allowlisted query', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

const result = await isQueryAllowed({
sql: 'SELECT * FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBe(true)
})

it('treats trailing semicolon as equivalent (normalizeSQL)', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

const result = await isQueryAllowed({
sql: 'SELECT * FROM users;',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBe(true)
})

it('ignores allowlist rows from other sources', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'external' },
{ sql_statement: 'SELECT id FROM orders', source: 'internal' },
])

const allowed = await isQueryAllowed({
sql: 'SELECT id FROM orders',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
expect(allowed).toBe(true)

await expect(
isQueryAllowed({
sql: 'SELECT * FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')
})

it('rejects non-allowlisted query, logs rejection, and throws', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

await expect(
isQueryAllowed({
sql: 'DROP TABLE users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')

// SELECT for load + INSERT for rejection audit
const calls = vi.mocked(mockDataSource.rpc.executeQuery).mock.calls
expect(calls.length).toBeGreaterThanOrEqual(2)
expect(String(calls[1][0]?.sql)).toContain('INSERT INTO tmp_allowlist_rejections')
expect(calls[1][0]?.params).toEqual(['DROP TABLE users', 'internal'])
})

it('returns an Error object when SQL is missing', async () => {
mockAllowlistRows([
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
])

const result = await isQueryAllowed({
sql: '',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})

expect(result).toBeInstanceOf(Error)
expect((result as Error).message).toContain('No SQL provided')
})

it('denies when allowlist fails to load (empty list fail-closed)', async () => {
vi.mocked(mockDataSource.rpc.executeQuery).mockImplementation(
async (opts: any) => {
if (String(opts?.sql).startsWith('SELECT')) {
throw new Error('Database error')
}
return [] as any
}
)

await expect(
isQueryAllowed({
sql: 'SELECT * FROM users',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')
})

it('still rejects cleanly when rejection audit insert fails', async () => {
vi.mocked(mockDataSource.rpc.executeQuery).mockImplementation(
async (opts: any) => {
const sql: string = opts?.sql ?? ''
if (sql.startsWith('SELECT')) {
return [
{ sql_statement: 'SELECT * FROM users', source: 'internal' },
] as any
}
throw new Error('audit table missing')
}
)

await expect(
isQueryAllowed({
sql: 'SELECT * FROM secrets',
isEnabled: true,
dataSource: mockDataSource,
config: clientConfig,
})
).rejects.toThrow('Query not allowed')
})
})
Loading