Skip to content

Commit fbf4364

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 fbf4364

File tree

3 files changed

+357
-0
lines changed

3 files changed

+357
-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: 352 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,352 @@
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 = 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::<Vec<_>>();
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 field_is_valid = |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+
matches!(selection, graphql::graphql_parser::query::Selection::Field(field) if field_is_valid(&field.name))
59+
})
60+
}
61+
62+
#[cfg(test)]
63+
mod tests {
64+
use super::*;
65+
66+
fn reject_sql() -> SqlFieldBehavior {
67+
SqlFieldBehavior::RejectSql
68+
}
69+
70+
fn accept_sql_only() -> SqlFieldBehavior {
71+
SqlFieldBehavior::AcceptSqlOnly
72+
}
73+
74+
fn create_context(query: &str) -> Context<String> {
75+
let variables = r#"{}"#;
76+
Context::new(query, variables).unwrap()
77+
}
78+
79+
#[test]
80+
fn test_single_selection_set_reject_sql() {
81+
let query = r#"
82+
query {
83+
sql(input: { query: "SELECT * FROM users" }) {
84+
id
85+
name
86+
}
87+
}
88+
"#;
89+
let ctx = create_context(query);
90+
assert!(validate_query(&ctx, reject_sql()).is_err());
91+
}
92+
93+
#[test]
94+
fn test_single_selection_set_without_query_reject_sql() {
95+
let query = r#"
96+
{
97+
sql(input: { query: "SELECT * FROM users" }) {
98+
id
99+
name
100+
}
101+
}
102+
"#;
103+
let ctx = create_context(query);
104+
assert!(validate_query(&ctx, reject_sql()).is_err());
105+
}
106+
107+
#[test]
108+
fn test_no_sql_single_selection_set_reject_sql() {
109+
let query = r#"
110+
query {
111+
users {
112+
id
113+
name
114+
}
115+
}
116+
"#;
117+
let ctx = create_context(query);
118+
assert!(validate_query(&ctx, reject_sql()).is_ok());
119+
}
120+
121+
#[test]
122+
fn test_no_sql_single_selection_set_without_query_reject_sql() {
123+
let query = r#"
124+
{
125+
users {
126+
id
127+
name
128+
}
129+
}
130+
"#;
131+
let ctx = create_context(query);
132+
assert!(validate_query(&ctx, reject_sql()).is_ok());
133+
}
134+
135+
#[test]
136+
fn test_multi_selection_set_reject_sql() {
137+
let query = r#"
138+
query {
139+
sql(input: { query: "SELECT * FROM users" }) {
140+
id
141+
name
142+
}
143+
users {
144+
id
145+
name
146+
}
147+
}
148+
"#;
149+
let ctx = create_context(query);
150+
assert!(validate_query(&ctx, reject_sql()).is_err());
151+
}
152+
153+
#[test]
154+
fn test_no_sql_multi_selection_set_reject_sql() {
155+
let query = r#"
156+
query {
157+
tokens {
158+
id
159+
name
160+
}
161+
users {
162+
id
163+
name
164+
}
165+
}
166+
"#;
167+
let ctx = create_context(query);
168+
assert!(validate_query(&ctx, reject_sql()).is_ok());
169+
}
170+
171+
#[test]
172+
fn test_multi_selection_set_without_query_reject_sql() {
173+
let query = r#"
174+
{
175+
sql(input: { query: "SELECT * FROM users" }) {
176+
id
177+
name
178+
}
179+
users {
180+
id
181+
name
182+
}
183+
}
184+
"#;
185+
let ctx = create_context(query);
186+
assert!(validate_query(&ctx, reject_sql()).is_err());
187+
}
188+
189+
#[test]
190+
fn test_no_sql_multi_selection_set_without_query_reject_sql() {
191+
let query = r#"
192+
{
193+
tokens {
194+
id
195+
name
196+
}
197+
users {
198+
id
199+
name
200+
}
201+
}
202+
"#;
203+
let ctx = create_context(query);
204+
assert!(validate_query(&ctx, reject_sql()).is_ok());
205+
}
206+
207+
#[test]
208+
fn test_single_selection_set_accept_sql_only() {
209+
let query = r#"
210+
query {
211+
sql(input: { query: "SELECT * FROM users" }) {
212+
id
213+
name
214+
}
215+
}
216+
"#;
217+
let ctx = create_context(query);
218+
assert!(validate_query(&ctx, accept_sql_only()).is_ok());
219+
}
220+
221+
#[test]
222+
fn test_single_selection_set_without_query_accept_sql_only() {
223+
let query = r#"
224+
{
225+
sql(input: { query: "SELECT * FROM users" }) {
226+
id
227+
name
228+
}
229+
}
230+
"#;
231+
let ctx = create_context(query);
232+
assert!(validate_query(&ctx, accept_sql_only()).is_ok());
233+
}
234+
235+
#[test]
236+
fn test_no_sql_single_selection_set_accept_sql_only() {
237+
let query = r#"
238+
query {
239+
users {
240+
id
241+
name
242+
}
243+
}
244+
"#;
245+
let ctx = create_context(query);
246+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
247+
}
248+
249+
#[test]
250+
fn test_no_sql_single_selection_set_without_query_accept_sql_only() {
251+
let query = r#"
252+
{
253+
users {
254+
id
255+
name
256+
}
257+
}
258+
"#;
259+
let ctx = create_context(query);
260+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
261+
}
262+
263+
#[test]
264+
fn test_multi_selection_set_accept_sql_only() {
265+
let query = r#"
266+
query {
267+
sql(input: { query: "SELECT * FROM users" }) {
268+
id
269+
name
270+
}
271+
sql(input: { query: "SELECT * FROM tokens" }) {
272+
id
273+
name
274+
}
275+
}
276+
"#;
277+
let ctx = create_context(query);
278+
assert!(validate_query(&ctx, accept_sql_only()).is_ok());
279+
}
280+
281+
#[test]
282+
fn test_with_graphql_multi_selection_set_accept_sql_only() {
283+
let query = r#"
284+
query {
285+
sql(input: { query: "SELECT * FROM users" }) {
286+
id
287+
name
288+
}
289+
users {
290+
id
291+
name
292+
}
293+
}
294+
"#;
295+
let ctx = create_context(query);
296+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
297+
}
298+
299+
#[test]
300+
fn test_no_sql_multi_selection_set_accept_sql_only() {
301+
let query = r#"
302+
query {
303+
tokens {
304+
id
305+
name
306+
}
307+
users {
308+
id
309+
name
310+
}
311+
}
312+
"#;
313+
let ctx = create_context(query);
314+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
315+
}
316+
317+
#[test]
318+
fn test_with_graphql_multi_selection_set_without_query_accept_sql_only() {
319+
let query = r#"
320+
{
321+
sql(input: { query: "SELECT * FROM users" }) {
322+
id
323+
name
324+
}
325+
users {
326+
id
327+
name
328+
}
329+
}
330+
"#;
331+
let ctx = create_context(query);
332+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
333+
}
334+
335+
#[test]
336+
fn test_no_sql_multi_selection_set_without_query_accept_sql_only() {
337+
let query = r#"
338+
{
339+
tokens {
340+
id
341+
name
342+
}
343+
users {
344+
id
345+
name
346+
}
347+
}
348+
"#;
349+
let ctx = create_context(query);
350+
assert!(validate_query(&ctx, accept_sql_only()).is_err());
351+
}
352+
}

0 commit comments

Comments
 (0)