Skip to content

Commit d5a9ac3

Browse files
committed
Merge branch 'ehesp/plugin' of https://github.com/Ehesp/starbasedb into pr/58
2 parents 620c614 + 9c3328d commit d5a9ac3

File tree

3 files changed

+67
-26
lines changed

3 files changed

+67
-26
lines changed

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
"dev": "wrangler dev",
88
"start": "wrangler dev",
99
"cf-typegen": "wrangler types",
10+
"delete": "wrangler delete",
1011
"prepare": "husky"
1112
},
1213
"devDependencies": {

src/allowlist/index.ts

Lines changed: 56 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,37 @@ function normalizeSQL(sql: string) {
1616

1717
async function loadAllowlist(dataSource: DataSource): Promise<string[]> {
1818
try {
19-
const statement = `SELECT sql_statement FROM tmp_allowlist_queries WHERE source="${dataSource.source}"`
19+
const statement = `SELECT sql_statement, source FROM tmp_allowlist_queries WHERE source="${dataSource.source}"`
2020
const result = (await dataSource.rpc.executeQuery({
2121
sql: statement,
2222
})) as QueryResult[]
23-
return result.map((row) => String(row.sql_statement))
23+
return result
24+
.filter((row) => row.source === dataSource.source)
25+
.map((row) => String(row.sql_statement))
2426
} catch (error) {
2527
console.error('Error loading allowlist:', error)
2628
return []
2729
}
2830
}
2931

32+
async function addRejectedQuery(
33+
query: string,
34+
dataSource: DataSource
35+
): Promise<string[]> {
36+
try {
37+
const statement =
38+
'INSERT INTO tmp_allowlist_rejections (sql_statement, source) VALUES (?, ?)'
39+
const result = (await dataSource.rpc.executeQuery({
40+
sql: statement,
41+
params: [query, dataSource.source],
42+
})) as QueryResult[]
43+
return result.map((row) => String(row.sql_statement))
44+
} catch (error) {
45+
console.error('Error inserting rejected allowlist query:', error)
46+
return []
47+
}
48+
}
49+
3050
export async function isQueryAllowed(opts: {
3151
sql: string
3252
isEnabled: boolean
@@ -59,34 +79,45 @@ export async function isQueryAllowed(opts: {
5979
const normalizedQuery = parser.astify(normalizeSQL(sql))
6080

6181
// Compare ASTs while ignoring specific values
62-
const isCurrentAllowed = normalizedAllowlist?.some((allowedQuery) => {
63-
// Create deep copies to avoid modifying original ASTs
64-
const allowedAst = JSON.parse(JSON.stringify(allowedQuery))
65-
const queryAst = JSON.parse(JSON.stringify(normalizedQuery))
66-
67-
// Remove or normalize value fields from both ASTs
68-
const normalizeAst = (ast: any) => {
69-
if (Array.isArray(ast)) {
70-
ast.forEach(normalizeAst)
71-
} else if (ast && typeof ast === 'object') {
72-
// Remove or normalize fields that contain specific values
73-
if ('value' in ast) {
74-
ast.value = '?'
75-
}
76-
77-
Object.values(ast).forEach(normalizeAst)
78-
}
79-
80-
return ast
82+
const deepCompareAst = (allowedAst: any, queryAst: any): boolean => {
83+
if (typeof allowedAst !== typeof queryAst) return false
84+
85+
if (Array.isArray(allowedAst) && Array.isArray(queryAst)) {
86+
if (allowedAst.length !== queryAst.length) return false
87+
return allowedAst.every((item, index) =>
88+
deepCompareAst(item, queryAst[index])
89+
)
90+
} else if (
91+
typeof allowedAst === 'object' &&
92+
allowedAst !== null &&
93+
queryAst !== null
94+
) {
95+
const allowedKeys = Object.keys(allowedAst)
96+
const queryKeys = Object.keys(queryAst)
97+
98+
if (allowedKeys.length !== queryKeys.length) return false
99+
100+
return allowedKeys.every((key) =>
101+
deepCompareAst(allowedAst[key], queryAst[key])
102+
)
81103
}
82104

83-
normalizeAst(allowedAst)
84-
normalizeAst(queryAst)
105+
// Base case: Primitive value comparison
106+
return allowedAst === queryAst
107+
}
85108

86-
return JSON.stringify(allowedAst) === JSON.stringify(queryAst)
87-
})
109+
const isCurrentAllowed = normalizedAllowlist?.some((allowedQuery) =>
110+
deepCompareAst(allowedQuery, normalizedQuery)
111+
)
88112

89113
if (!isCurrentAllowed) {
114+
// For any rejected query, we can add it to a table of rejected queries
115+
// to act both as an audit log as well as an easy way to see recent queries
116+
// that may need to be added to the allowlist in an easy way via a user
117+
// interface.
118+
addRejectedQuery(sql, dataSource)
119+
120+
// Then throw the appropriate error to the user.
90121
throw new Error('Query not allowed')
91122
}
92123

src/do.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,15 @@ export class StarbaseDBDurableObject extends DurableObject {
3838
const allowlistStatement = `
3939
CREATE TABLE IF NOT EXISTS tmp_allowlist_queries (
4040
id INTEGER PRIMARY KEY AUTOINCREMENT,
41-
sql_statement TEXT NOT NULL
41+
sql_statement TEXT NOT NULL,
42+
source TEXT DEFAULT 'external'
43+
)`
44+
const allowlistRejectedStatement = `
45+
CREATE TABLE IF NOT EXISTS tmp_allowlist_rejections (
46+
id INTEGER PRIMARY KEY AUTOINCREMENT,
47+
sql_statement TEXT NOT NULL,
48+
source TEXT DEFAULT 'external',
49+
created_at TEXT DEFAULT (datetime('now'))
4250
)`
4351

4452
const rlsStatement = `
@@ -55,6 +63,7 @@ export class StarbaseDBDurableObject extends DurableObject {
5563

5664
this.executeQuery({ sql: cacheStatement })
5765
this.executeQuery({ sql: allowlistStatement })
66+
this.executeQuery({ sql: allowlistRejectedStatement })
5867
this.executeQuery({ sql: rlsStatement })
5968
}
6069

0 commit comments

Comments
 (0)