Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: limit functions exposed in indexing status API #55

Merged
merged 1 commit into from
Sep 28, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 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 @@ -15,6 +15,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
1 change: 1 addition & 0 deletions service/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ ethereum-types = "0.14.1"
sqlx = { version = "0.7.1", features = ["postgres", "runtime-tokio", "bigdecimal", "rust_decimal", "time"] }
alloy-primitives = { version = "0.3.3", features = ["serde"] }
alloy-sol-types = "0.3.2"
lazy_static = "1.4.0"

[dev-dependencies]
faux = "0.1.10"
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
42 changes: 38 additions & 4 deletions service/src/server/routes/status.rs
Original file line number Diff line number Diff line change
@@ -1,30 +1,64 @@
// 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;

lazy_static::lazy_static! {
static ref SUPPORTED_ROOT_FIELDS: HashSet<&'static str> =
vec![
"indexingStatuses",
"publicProofsOfIndexing",
"entityChangesInBlock",
"blockData",
"cachedEthereumCalls",
"subgraphFeatures",
"apiVersions",
].into_iter().collect();
}

// Custom middleware function to process the request before reaching the main handler
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
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, &SUPPORTED_ROOT_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