@@ -16,17 +16,37 @@ function normalizeSQL(sql: string) {
1616
1717async 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+
3050export 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
0 commit comments