Skip to content

Commit 2176b65

Browse files
gusinaciosuchapalaver
authored andcommitted
feat: validate queries according to query language
fix: add selection set to query validation Fixes STU-218 Signed-off-by: Gustavo Inacio <[email protected]> feat: share functionality across sql- and non-sql gateways
1 parent 615c8e1 commit 2176b65

File tree

3 files changed

+361
-0
lines changed

3 files changed

+361
-0
lines changed

graph-gateway/src/client_query.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,7 @@ use crate::{
5858
block_constraints::{resolve_block_requirements, rewrite_query, BlockRequirements},
5959
indexer_client::{check_block_error, IndexerClient, ResponsePayload},
6060
reports::{self, serialize_attestation},
61+
sql_constraints::{validate_query, SqlFieldBehavior},
6162
unattestable_errors::{miscategorized_attestable, miscategorized_unattestable},
6263
};
6364

@@ -283,6 +284,9 @@ async fn handle_client_query_inner(
283284
.unwrap_or_default();
284285
let mut context = AgoraContext::new(&payload.query, &variables)
285286
.map_err(|err| Error::BadQuery(anyhow!("{err}")))?;
287+
288+
validate_query(&context, SqlFieldBehavior::RejectSql)?;
289+
286290
tracing::info!(
287291
target: CLIENT_REQUEST_TARGET,
288292
query = %payload.query,

graph-gateway/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,5 +5,6 @@ pub mod indexer_client;
55
pub mod indexers;
66
pub mod indexings_blocklist;
77
pub mod reports;
8+
pub mod sql_constraints;
89
pub mod subgraph_studio;
910
pub mod unattestable_errors;
Lines changed: 356 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,356 @@
1+
use anyhow::anyhow;
2+
use cost_model::Context;
3+
use gateway_framework::errors::Error;
4+
use graphql::graphql_parser::query::{OperationDefinition, Query, SelectionSet};
5+
6+
#[derive(Clone, Copy)]
7+
pub enum SqlFieldBehavior {
8+
RejectSql,
9+
AcceptSqlOnly,
10+
}
11+
12+
pub fn validate_query(ctx: &Context<String>, behavior: SqlFieldBehavior) -> Result<(), Error> {
13+
for operation in &ctx.operations {
14+
match operation {
15+
OperationDefinition::SelectionSet(selection_set)
16+
| OperationDefinition::Query(Query { selection_set, .. }) => {
17+
if !selection_set_is_valid(selection_set, behavior) {
18+
use SqlFieldBehavior::*;
19+
match behavior {
20+
RejectSql => return Err(Error::BadQuery(anyhow!("Query contains SQL"))),
21+
AcceptSqlOnly => {
22+
let invalid_fields: Vec<_> = selection_set
23+
.items
24+
.iter()
25+
.filter_map(|selection| {
26+
if let graphql::graphql_parser::query::Selection::Field(field) =
27+
selection
28+
{
29+
return Some(&field.name[..]);
30+
}
31+
None
32+
})
33+
.collect();
34+
return Err(Error::BadQuery(anyhow!(
35+
"Fields [{}] are not SQL",
36+
invalid_fields.join(", ")
37+
)));
38+
}
39+
}
40+
}
41+
}
42+
_ => continue,
43+
}
44+
}
45+
Ok(())
46+
}
47+
48+
fn selection_set_is_valid(
49+
selection_set: &SelectionSet<String>,
50+
behavior: SqlFieldBehavior,
51+
) -> bool {
52+
let validate_field = |field_name: &str| match behavior {
53+
SqlFieldBehavior::RejectSql => field_name != "sql",
54+
SqlFieldBehavior::AcceptSqlOnly => field_name == "sql",
55+
};
56+
57+
selection_set.items.iter().all(|selection| {
58+
if let graphql::graphql_parser::query::Selection::Field(field) = selection {
59+
validate_field(&field.name)
60+
} else {
61+
false
62+
}
63+
})
64+
}
65+
66+
#[cfg(test)]
67+
mod tests {
68+
use super::*;
69+
70+
fn reject_sql() -> SqlFieldBehavior {
71+
SqlFieldBehavior::RejectSql
72+
}
73+
74+
fn accept_sql_only() -> SqlFieldBehavior {
75+
SqlFieldBehavior::AcceptSqlOnly
76+
}
77+
78+
fn create_context(query: &str) -> Context<String> {
79+
let variables = r#"{}"#;
80+
Context::new(query, variables).unwrap()
81+
}
82+
83+
#[test]
84+
fn test_single_selection_set_reject_sql() {
85+
let query = r#"
86+
query {
87+
sql(input: { query: "SELECT * FROM users" }) {
88+
id
89+
name
90+
}
91+
}
92+
"#;
93+
let ctx = create_context(query);
94+
assert!(validate_query(&ctx, reject_sql()).is_err());
95+
}
96+
97+
#[test]
98+
fn test_single_selection_set_without_query_reject_sql() {
99+
let query = r#"
100+
{
101+
sql(input: { query: "SELECT * FROM users" }) {
102+
id
103+
name
104+
}
105+
}
106+
"#;
107+
let ctx = create_context(query);
108+
assert!(validate_query(&ctx, reject_sql()).is_err());
109+
}
110+
111+
#[test]
112+
fn test_no_sql_single_selection_set_reject_sql() {
113+
let query = r#"
114+
query {
115+
users {
116+
id
117+
name
118+
}
119+
}
120+
"#;
121+
let ctx = create_context(query);
122+
assert!(validate_query(&ctx, reject_sql()).is_ok());
123+
}
124+
125+
#[test]
126+
fn test_no_sql_single_selection_set_without_query_reject_sql() {
127+
let query = r#"
128+
{
129+
users {
130+
id
131+
name
132+
}
133+
}
134+
"#;
135+
let ctx = create_context(query);
136+
assert!(validate_query(&ctx, reject_sql()).is_ok());
137+
}
138+
139+
#[test]
140+
fn test_multi_selection_set_reject_sql() {
141+
let query = r#"
142+
query {
143+
sql(input: { query: "SELECT * FROM users" }) {
144+
id
145+
name
146+
}
147+
users {
148+
id
149+
name
150+
}
151+
}
152+
"#;
153+
let ctx = create_context(query);
154+
assert!(validate_query(&ctx, reject_sql()).is_err());
155+
}
156+
157+
#[test]
158+
fn test_no_sql_multi_selection_set_reject_sql() {
159+
let query = r#"
160+
query {
161+
tokens {
162+
id
163+
name
164+
}
165+
users {
166+
id
167+
name
168+
}
169+
}
170+
"#;
171+
let ctx = create_context(query);
172+
assert!(validate_query(&ctx, reject_sql()).is_ok());
173+
}
174+
175+
#[test]
176+
fn test_multi_selection_set_without_query_reject_sql() {
177+
let query = r#"
178+
{
179+
sql(input: { query: "SELECT * FROM users" }) {
180+
id
181+
name
182+
}
183+
users {
184+
id
185+
name
186+
}
187+
}
188+
"#;
189+
let ctx = create_context(query);
190+
assert!(validate_query(&ctx, reject_sql()).is_err());
191+
}
192+
193+
#[test]
194+
fn test_no_sql_multi_selection_set_without_query_reject_sql() {
195+
let query = r#"
196+
{
197+
tokens {
198+
id
199+
name
200+
}
201+
users {
202+
id
203+
name
204+
}
205+
}
206+
"#;
207+
let ctx = create_context(query);
208+
assert!(validate_query(&ctx, reject_sql()).is_ok());
209+
}
210+
211+
#[test]
212+
fn test_single_selection_set_accept_sql_only() {
213+
let query = r#"
214+
query {
215+
sql(input: { query: "SELECT * FROM users" }) {
216+
id
217+
name
218+
}
219+
}
220+
"#;
221+
let ctx = create_context(query);
222+
assert!(validate_query(&ctx, accept_sql_only()).is_ok());
223+
}
224+
225+
#[test]
226+
fn test_single_selection_set_without_query_accept_sql_only() {
227+
let query = r#"
228+
{
229+
sql(input: { query: "SELECT * FROM users" }) {
230+
id
231+
name
232+
}
233+
}
234+
"#;
235+
let ctx = create_context(query);
236+
assert!(validate_query(&ctx, accept_sql_only()).is_ok());
237+
}
238+
239+
#[test]
240+
fn test_no_sql_single_selection_set_accept_sql_only() {
241+
let query = r#"
242+
query {
243+
users {
244+
id
245+
name
246+
}
247+
}
248+
"#;
249+
let ctx = create_context(query);
250+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
251+
}
252+
253+
#[test]
254+
fn test_no_sql_single_selection_set_without_query_accept_sql_only() {
255+
let query = r#"
256+
{
257+
users {
258+
id
259+
name
260+
}
261+
}
262+
"#;
263+
let ctx = create_context(query);
264+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
265+
}
266+
267+
#[test]
268+
fn test_multi_selection_set_accept_sql_only() {
269+
let query = r#"
270+
query {
271+
sql(input: { query: "SELECT * FROM users" }) {
272+
id
273+
name
274+
}
275+
sql(input: { query: "SELECT * FROM tokens" }) {
276+
id
277+
name
278+
}
279+
}
280+
"#;
281+
let ctx = create_context(query);
282+
assert!(validate_query(&ctx, accept_sql_only()).is_ok());
283+
}
284+
285+
#[test]
286+
fn test_with_graphql_multi_selection_set_accept_sql_only() {
287+
let query = r#"
288+
query {
289+
sql(input: { query: "SELECT * FROM users" }) {
290+
id
291+
name
292+
}
293+
users {
294+
id
295+
name
296+
}
297+
}
298+
"#;
299+
let ctx = create_context(query);
300+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
301+
}
302+
303+
#[test]
304+
fn test_no_sql_multi_selection_set_accept_sql_only() {
305+
let query = r#"
306+
query {
307+
tokens {
308+
id
309+
name
310+
}
311+
users {
312+
id
313+
name
314+
}
315+
}
316+
"#;
317+
let ctx = create_context(query);
318+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
319+
}
320+
321+
#[test]
322+
fn test_with_graphql_multi_selection_set_without_query_accept_sql_only() {
323+
let query = r#"
324+
{
325+
sql(input: { query: "SELECT * FROM users" }) {
326+
id
327+
name
328+
}
329+
users {
330+
id
331+
name
332+
}
333+
}
334+
"#;
335+
let ctx = create_context(query);
336+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
337+
}
338+
339+
#[test]
340+
fn test_no_sql_multi_selection_set_without_query_accept_sql_only() {
341+
let query = r#"
342+
{
343+
tokens {
344+
id
345+
name
346+
}
347+
users {
348+
id
349+
name
350+
}
351+
}
352+
"#;
353+
let ctx = create_context(query);
354+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
355+
}
356+
}

0 commit comments

Comments
 (0)