Skip to content

Commit

Permalink
feat: limit functions exposed in indexing status API
Browse files Browse the repository at this point in the history
  • Loading branch information
hopeyen committed Sep 27, 2023
1 parent 513d1ad commit 18882a1
Show file tree
Hide file tree
Showing 6 changed files with 137 additions and 4 deletions.
1 change: 1 addition & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions common/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ faux = { version = "0.1.10", optional = true }
keccak-hash = "0.10.0"
lazy_static = "1.4.0"
log = "0.4.20"
regex = "1.7.1"
reqwest = "0.11.20"
secp256k1 = { version = "0.27.0", features = ["recovery"] }
serde = { version = "1.0.188", features = ["derive"] }
Expand Down
93 changes: 93 additions & 0 deletions common/src/graphql.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
// Copyright 2023-, GraphOps and Semiotic Labs.
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashSet;

use regex::Regex;

/// There is no convenient function for filtering GraphQL executable documents
/// For sake of simplicity, use regex to filter graphql query string
/// Return original string if the query is okay, otherwise error out with
/// unsupported fields
pub fn filter_supported_fields(
query: &str,
supported_root_fields: &HashSet<&str>,
) -> Result<String, Vec<String>> {
// Create a regex pattern to match the fields not in the supported fields
let re = Regex::new(r"\b(\w+)\s*\{").unwrap();
let mut unsupported_fields = Vec::new();

for cap in re.captures_iter(query) {
if let Some(match_) = cap.get(1) {
let field = match_.as_str();
if !supported_root_fields.contains(field) {
unsupported_fields.push(field.to_string());
}
}
}

if !unsupported_fields.is_empty() {
return Err(unsupported_fields);
}

Ok(query.to_string())
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_filter_supported_fields_with_valid_fields() {
let supported_fields = vec![
"indexingStatuses",
"publicProofsOfIndexing",
"entityChangesInBlock",
]
.into_iter()
.collect::<HashSet<&str>>();

let query_string = "{
indexingStatuses {
subgraph
health
}
publicProofsOfIndexing {
number
}
}";

assert_eq!(
filter_supported_fields(query_string, &supported_fields).unwrap(),
query_string.to_string()
);
}

#[test]
fn test_filter_supported_fields_with_unsupported_fields() {
let supported_fields = vec![
"indexingStatuses",
"publicProofsOfIndexing",
"entityChangesInBlock",
]
.into_iter()
.collect::<HashSet<&str>>();

let query_string = "{
someField {
subfield1
subfield2
}
indexingStatuses {
subgraph
health
}
}";

let filtered = filter_supported_fields(query_string, &supported_fields);
assert!(filtered.is_err(),);
let errors = filtered.err().unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors.first().unwrap(), &String::from("someField"));
}
}
1 change: 1 addition & 0 deletions common/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pub mod allocations;
pub mod attestations;
pub mod graphql;
pub mod network_subgraph;
pub mod signature_verification;
pub mod types;
Expand Down
4 changes: 4 additions & 0 deletions service/src/query_processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pub enum QueryError {
IndexingError,
#[error("Bad or invalid entity data found in the subgraph: {}", .0.to_string())]
BadData(anyhow::Error),
#[error("Invalid GraphQL query string: {0}")]
InvalidFormat(String),
#[error("Cannot query field: {:#?}", .0)]
UnsupportedFields(Vec<String>),
#[error("Unknown error: {0}")]
Other(anyhow::Error),
}
Expand Down
41 changes: 37 additions & 4 deletions service/src/server/routes/status.rs
Original file line number Diff line number Diff line change
@@ -1,15 +1,20 @@
// Copyright 2023-, GraphOps and Semiotic Labs.
// SPDX-License-Identifier: Apache-2.0

use std::collections::HashSet;

use axum::{
http::{Request, StatusCode},
response::IntoResponse,
Extension, Json,
};

use hyper::body::Bytes;

use reqwest::{header, Client};

use crate::server::ServerOptions;
use indexer_common::graphql::filter_supported_fields;

use super::bad_request_response;

Expand All @@ -18,13 +23,41 @@ pub async fn status_queries(
Extension(server): Extension<ServerOptions>,
req: Request<axum::body::Body>,
) -> impl IntoResponse {
let req_body = req.into_body();
// TODO: Extract the incoming GraphQL operation and filter root fields
// Pass the modified operation to the actual endpoint
fn status_supported_fields() -> HashSet<&'static str> {
HashSet::from([
"indexingStatuses",
"publicProofsOfIndexing",
"entityChangesInBlock",
"blockData",
"cachedEthereumCalls",
"subgraphFeatures",
"apiVersions",
])
}

let body_bytes = hyper::body::to_bytes(req.into_body()).await.unwrap();
// Read the requested query string
let query_string = match String::from_utf8(body_bytes.to_vec()) {
Ok(s) => s,
Err(e) => return bad_request_response(&e.to_string()),
};

// filter supported root fields
let query_string = match filter_supported_fields(&query_string, &status_supported_fields()) {
Ok(query) => query,
Err(unsupported_fields) => {
return (
StatusCode::BAD_REQUEST,
format!("Cannot query field: {:#?}", unsupported_fields),
)
.into_response();
}
};

// Pass the modified operation to the actual endpoint
let request = Client::new()
.post(&server.graph_node_status_endpoint)
.body(req_body)
.body(Bytes::from(query_string))
.header(header::CONTENT_TYPE, "application/json");

let response: reqwest::Response = match request.send().await {
Expand Down

0 comments on commit 18882a1

Please sign in to comment.