diff --git a/Cargo.lock b/Cargo.lock
index 9c47867cf..b19e48ebf 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2114,6 +2114,7 @@ dependencies = [
"async-trait",
"bumpalo",
"bytes",
+ "chrono",
"criterion",
"dashmap",
"futures",
@@ -2132,6 +2133,10 @@ dependencies = [
"ntex-http",
"ordered-float",
"regex-automata",
+ "reqsign-aws-v4",
+ "reqsign-core",
+ "reqsign-file-read-tokio",
+ "reqsign-http-send-reqwest",
"ryu",
"serde",
"sonic-rs",
@@ -2604,6 +2609,47 @@ version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c"
+[[package]]
+name = "jiff"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "be1f93b8b1eb69c77f24bbb0afdf66f54b632ee39af40ca21c4365a1d7347e49"
+dependencies = [
+ "jiff-static",
+ "jiff-tzdb-platform",
+ "log",
+ "portable-atomic",
+ "portable-atomic-util",
+ "serde",
+ "windows-sys 0.59.0",
+]
+
+[[package]]
+name = "jiff-static"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "03343451ff899767262ec32146f6d559dd759fdadf42ff0e227c7c48f72594b4"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.108",
+]
+
+[[package]]
+name = "jiff-tzdb"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c1283705eb0a21404d2bfd6eef2a7593d240bc42a0bdb39db0ad6fa2ec026524"
+
+[[package]]
+name = "jiff-tzdb-platform"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "875a5a69ac2bab1a891711cf5eccbec1ce0341ea805560dcd90b7a2e925132e8"
+dependencies = [
+ "jiff-tzdb",
+]
+
[[package]]
name = "jobserver"
version = "0.1.34"
@@ -3974,6 +4020,15 @@ version = "1.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f84267b20a16ea918e43c6a88433c2d54fa145c92a811b5b047ccbe153674483"
+[[package]]
+name = "portable-atomic-util"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8a2f0d8d040d7848a709caf78912debcc3f33ee4b3cac47d73d1e1069e83507"
+dependencies = [
+ "portable-atomic",
+]
+
[[package]]
name = "potential_utf"
version = "0.1.4"
@@ -4130,6 +4185,16 @@ dependencies = [
"tracing-tree",
]
+[[package]]
+name = "quick-xml"
+version = "0.38.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "42a232e7487fc2ef313d96dde7948e7a3c05101870d8985e4fd8d26aedd27b89"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
[[package]]
name = "quinn"
version = "0.11.9"
@@ -4395,6 +4460,79 @@ version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cadadef317c2f20755a64d7fdc48f9e7178ee6b0e1f7fce33fa60f1d68a276e6"
+[[package]]
+name = "reqsign-aws-v4"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4510c2a3e42b653cf788d560a3d54b0ae4cc315a62aaba773554f18319c0db0b"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bytes",
+ "form_urlencoded",
+ "http",
+ "log",
+ "percent-encoding",
+ "quick-xml",
+ "reqsign-core",
+ "rust-ini",
+ "serde",
+ "serde_json",
+ "serde_urlencoded",
+ "sha1",
+]
+
+[[package]]
+name = "reqsign-core"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "39da118ccf3bdb067ac6cc40136fec99bc5ba418cbd388dc88e4ce0e5d0b1423"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "base64 0.22.1",
+ "bytes",
+ "form_urlencoded",
+ "hex",
+ "hmac",
+ "http",
+ "jiff",
+ "log",
+ "percent-encoding",
+ "sha1",
+ "sha2",
+ "windows-sys 0.61.2",
+]
+
+[[package]]
+name = "reqsign-file-read-tokio"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "669ea66036266a9ac371d2e63cc7d345e69994da0168b4e6f3487fe21e126f76"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "reqsign-core",
+ "tokio",
+]
+
+[[package]]
+name = "reqsign-http-send-reqwest"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46186bce769674f9200ad01af6f2ca42de3e819ddc002fff1edae135bfb6cd9c"
+dependencies = [
+ "anyhow",
+ "async-trait",
+ "bytes",
+ "futures-channel",
+ "http",
+ "http-body-util",
+ "reqsign-core",
+ "reqwest",
+ "wasm-bindgen-futures",
+]
+
[[package]]
name = "reqwest"
version = "0.12.24"
diff --git a/docs/README.md b/docs/README.md
index fb474b9d2..6cf190532 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -4,6 +4,7 @@
|Name|Type|Description|Required|
|----|----|-----------|--------|
+|[**aws\_sig\_v4**](#aws_sig_v4)|`object`|Configuration for AWS SigV4 signing of requests to subgraphs.
|yes|
|[**cors**](#cors)|`object`|Configuration for CORS (Cross-Origin Resource Sharing).
Default: `{"allow_any_origin":false,"allow_credentials":false,"enabled":false,"policies":[]}`
|yes|
|[**csrf**](#csrf)|`object`|Configuration for CSRF prevention.
Default: `{"enabled":false,"required_headers":[]}`
||
|[**graphiql**](#graphiql)|`object`|Configuration for the GraphiQL interface.
Default: `{"enabled":true}`
||
@@ -21,6 +22,8 @@
**Example**
```yaml
+aws_sig_v4:
+ subgraphs: {}
cors:
allow_any_origin: false
allow_credentials: false
@@ -113,6 +116,36 @@ traffic_shaping:
```
+
+## aws\_sig\_v4: object
+
+Configuration for AWS SigV4 signing of requests to subgraphs.
+
+
+**Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**all**|||yes|
+|[**subgraphs**](#aws_sig_v4subgraphs)|`object`||no|
+
+**Additional Properties:** not allowed
+**Example**
+
+```yaml
+subgraphs: {}
+
+```
+
+
+### aws\_sig\_v4\.subgraphs: object
+
+**Additional Properties**
+
+|Name|Type|Description|Required|
+|----|----|-----------|--------|
+|**Additional Properties**||||
+
## cors: object
diff --git a/lib/executor/Cargo.toml b/lib/executor/Cargo.toml
index 27f7af1bf..55c689f95 100644
--- a/lib/executor/Cargo.toml
+++ b/lib/executor/Cargo.toml
@@ -49,12 +49,17 @@ itoa = "1.0.15"
ryu = "1.0.20"
indexmap = "2.10.0"
bumpalo = "3.19.0"
+reqsign-aws-v4 = "2.0.1"
+reqsign-core = "2.0.1"
+reqsign-file-read-tokio = "2.0.1"
+reqsign-http-send-reqwest = "2.0.1"
[dev-dependencies]
subgraphs = { path = "../../bench/subgraphs" }
criterion = { workspace = true }
tokio = { workspace = true }
insta = { workspace = true }
+chrono = "0.4.42"
[[bench]]
name = "executor_benches"
diff --git a/lib/executor/src/execution/awssigv4.rs b/lib/executor/src/execution/awssigv4.rs
new file mode 100644
index 000000000..ad07711e5
--- /dev/null
+++ b/lib/executor/src/execution/awssigv4.rs
@@ -0,0 +1,145 @@
+use hive_router_config::aws_sig_v4::AwsSigV4SubgraphConfig;
+use reqsign_aws_v4::{
+ Credential, DefaultCredentialProvider, DefaultCredentialProviderBuilder, RequestSigner,
+ StaticCredentialProvider,
+};
+use reqsign_core::{Context, OsEnv, ProvideCredentialChain, Signer};
+use reqsign_file_read_tokio::TokioFileRead;
+use reqsign_http_send_reqwest::ReqwestHttpSend;
+
+pub fn create_awssigv4_signer(config: &AwsSigV4SubgraphConfig) -> Option> {
+ let ctx = Context::new()
+ .with_file_read(TokioFileRead)
+ .with_http_send(ReqwestHttpSend::default())
+ .with_env(OsEnv);
+ let mut loader = ProvideCredentialChain::new();
+ match config {
+ AwsSigV4SubgraphConfig::Disabled => {
+ return None;
+ }
+ AwsSigV4SubgraphConfig::DefaultChain {
+ default_chain: default_chain_config,
+ } => {
+ loader = loader.push(DefaultCredentialProvider::new());
+ let mut default_chain_builder = DefaultCredentialProviderBuilder::new();
+ if let Some(profile_name) = &default_chain_config.profile_name {
+ default_chain_builder = default_chain_builder
+ .configure_profile(|p| p.with_credentials_file(profile_name));
+ }
+ if let Some(assume_role_config) = &default_chain_config.assume_role {
+ default_chain_builder =
+ default_chain_builder.configure_assume_role(|mut assume_role| {
+ assume_role = assume_role
+ .with_role_arn(&assume_role_config.role_arn)
+ .with_region(default_chain_config.region.to_string());
+ if let Some(session_name) = &assume_role_config.session_name {
+ assume_role =
+ assume_role.with_role_session_name(session_name.to_string());
+ }
+ assume_role
+ });
+ let default_chain = default_chain_builder.build();
+ loader = loader.push(default_chain);
+ }
+ }
+ AwsSigV4SubgraphConfig::HardCoded { hardcoded } => {
+ let mut provider = StaticCredentialProvider::new(
+ &hardcoded.access_key_id,
+ &hardcoded.secret_access_key,
+ );
+ if let Some(session_token) = &hardcoded.session_token {
+ provider = provider.with_session_token(session_token);
+ }
+ loader = loader.push(provider);
+ }
+ }
+ let service: &str = match config {
+ AwsSigV4SubgraphConfig::DefaultChain { default_chain } => &default_chain.service,
+ AwsSigV4SubgraphConfig::HardCoded { hardcoded } => &hardcoded.service_name,
+ AwsSigV4SubgraphConfig::Disabled => unreachable!(),
+ };
+ let region: &str = match config {
+ AwsSigV4SubgraphConfig::DefaultChain { default_chain } => &default_chain.region,
+ AwsSigV4SubgraphConfig::HardCoded { hardcoded } => &hardcoded.region,
+ AwsSigV4SubgraphConfig::Disabled => unreachable!(),
+ };
+ let builder = RequestSigner::new(service, region);
+
+ Some(Signer::new(ctx, loader, builder))
+}
+
+#[cfg(test)]
+mod tests {
+ use crate::execution::awssigv4::create_awssigv4_signer;
+ use bytes::Bytes;
+ use chrono::Utc;
+ use hive_router_config::aws_sig_v4::{AwsSigV4SubgraphConfig, HardCodedConfig};
+ use http_body_util::Full;
+ use hyper::body::Body;
+
+ #[tokio::test]
+ async fn signs_the_request_correctly() {
+ let access_key_id = "AKIAIOSFODNN7EXAMPLE";
+ let secret_access_key = "wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLEKEY";
+ let region = "eu-central-1";
+ let service_name = "s3";
+ let config = AwsSigV4SubgraphConfig::HardCoded {
+ hardcoded: HardCodedConfig {
+ access_key_id: access_key_id.to_string(),
+ secret_access_key: secret_access_key.to_string(),
+ region: region.to_string(),
+ service_name: service_name.to_string(),
+ session_token: None,
+ },
+ };
+ let signer = create_awssigv4_signer(&config).expect("Expected to return a signer");
+ let body = Full::new(Bytes::from("query { hello }"));
+ let content_length = body.size_hint().exact().unwrap();
+ let endpoint = format!(
+ "http://sigv4examplegraphqlbucket.{}-{}.amazonaws.com",
+ service_name, region
+ );
+ let req: http::Request> = http::Request::builder()
+ .method("POST")
+ .uri(endpoint)
+ .header("Accept", "application/json")
+ .header("Content-Length", content_length)
+ .header("Content-Type", "application/json")
+ .body(body)
+ .unwrap();
+
+ let (mut parts, body) = req.into_parts();
+
+ signer
+ .sign(&mut parts, None)
+ .await
+ .expect("Expected to sign correctly");
+
+ let req = http::Request::from_parts(parts, body);
+
+ let authorization_header = req
+ .headers()
+ .get("Authorization")
+ .expect("Expected to have Authorization header")
+ .to_str()
+ .expect("Expected to convert to str");
+
+ let date_stamp = Utc::now().format("%Y%m%d");
+
+ let mut expected_auth_header_prefix = "AWS4-HMAC-SHA256 ".to_string();
+ expected_auth_header_prefix.push_str(&format!(
+ "Credential={}/{}/{}/{}/aws4_request, ",
+ access_key_id, date_stamp, region, service_name
+ ));
+ expected_auth_header_prefix.push_str(
+ "SignedHeaders=accept;content-length;content-type;host;x-amz-content-sha256;x-amz-date, Signature=",
+ );
+
+ assert!(
+ authorization_header.starts_with(&expected_auth_header_prefix),
+ "Expected authorization header to start with '{}', but got '{}'",
+ expected_auth_header_prefix,
+ authorization_header
+ );
+ }
+}
diff --git a/lib/executor/src/execution/mod.rs b/lib/executor/src/execution/mod.rs
index 52dc59506..26a28c572 100644
--- a/lib/executor/src/execution/mod.rs
+++ b/lib/executor/src/execution/mod.rs
@@ -1,3 +1,4 @@
+pub mod awssigv4;
pub mod client_request_details;
pub mod error;
pub mod jwt_forward;
diff --git a/lib/executor/src/executors/error.rs b/lib/executor/src/executors/error.rs
index 2234f524c..ecc6d5df0 100644
--- a/lib/executor/src/executors/error.rs
+++ b/lib/executor/src/executors/error.rs
@@ -20,6 +20,8 @@ pub enum SubgraphExecutorError {
RequestFailure(String, String),
#[error("Failed to serialize variable \"{0}\": {1}")]
VariablesSerializationFailure(String, String),
+ #[error("Failed to sign request with AWSSigV4 for subgraph \"{0}\": {1}")]
+ AwsSigV4SigningFailure(String, String),
}
impl From for GraphQLError {
@@ -76,6 +78,9 @@ impl SubgraphExecutorError {
SubgraphExecutorError::VariablesSerializationFailure(_, _) => {
"SUBGRAPH_VARIABLES_SERIALIZATION_FAILURE"
}
+ SubgraphExecutorError::AwsSigV4SigningFailure(_, _) => {
+ "SUBGRAPH_AWS_SIGV4_SIGNING_FAILURE"
+ }
}
}
}
diff --git a/lib/executor/src/executors/http.rs b/lib/executor/src/executors/http.rs
index 29b392567..2d45929f4 100644
--- a/lib/executor/src/executors/http.rs
+++ b/lib/executor/src/executors/http.rs
@@ -2,12 +2,13 @@ use std::sync::Arc;
use crate::executors::common::HttpExecutionResponse;
use crate::executors::dedupe::{request_fingerprint, ABuildHasher, SharedResponse};
+use async_trait::async_trait;
use dashmap::DashMap;
use hive_router_config::HiveRouterConfig;
+use reqsign_aws_v4::Credential;
+use reqsign_core::Signer;
use tokio::sync::OnceCell;
-use async_trait::async_trait;
-
use bytes::{BufMut, Bytes, BytesMut};
use http::HeaderMap;
use http::HeaderValue;
@@ -37,6 +38,7 @@ pub struct HTTPSubgraphExecutor {
pub semaphore: Arc,
pub config: Arc,
pub in_flight_requests: Arc>, ABuildHasher>>,
+ pub aws_sigv4_signer: Option>,
}
const FIRST_VARIABLE_STR: &[u8] = b",\"variables\":{";
@@ -52,6 +54,7 @@ impl HTTPSubgraphExecutor {
semaphore: Arc,
config: Arc,
in_flight_requests: Arc>, ABuildHasher>>,
+ aws_sigv4_signer: Option>,
) -> Self {
let mut header_map = HeaderMap::new();
header_map.insert(
@@ -71,6 +74,7 @@ impl HTTPSubgraphExecutor {
semaphore,
config,
in_flight_requests,
+ aws_sigv4_signer,
}
}
@@ -138,7 +142,7 @@ impl HTTPSubgraphExecutor {
body: Vec,
headers: HeaderMap,
) -> Result {
- let mut req = hyper::Request::builder()
+ let mut req: http::Request> = hyper::Request::builder()
.method(http::Method::POST)
.uri(&self.endpoint)
.version(Version::HTTP_11)
@@ -151,6 +155,17 @@ impl HTTPSubgraphExecutor {
debug!("making http request to {}", self.endpoint.to_string());
+ if let Some(aws_sigv4_signer) = &self.aws_sigv4_signer {
+ let (mut parts, body) = req.into_parts();
+ aws_sigv4_signer.sign(&mut parts, None).await.map_err(|e| {
+ SubgraphExecutorError::AwsSigV4SigningFailure(
+ self.endpoint.to_string(),
+ e.to_string(),
+ )
+ })?;
+ req = http::Request::from_parts(parts, body);
+ }
+
let res = self.http_client.request(req).await.map_err(|e| {
SubgraphExecutorError::RequestFailure(self.endpoint.to_string(), e.to_string())
})?;
diff --git a/lib/executor/src/executors/map.rs b/lib/executor/src/executors/map.rs
index a3c297ad1..9fc1746b3 100644
--- a/lib/executor/src/executors/map.rs
+++ b/lib/executor/src/executors/map.rs
@@ -27,7 +27,7 @@ use vrl::{
};
use crate::{
- execution::client_request_details::ClientRequestDetails,
+ execution::{awssigv4::create_awssigv4_signer, client_request_details::ClientRequestDetails},
executors::{
common::{
HttpExecutionRequest, HttpExecutionResponse, SubgraphExecutor, SubgraphExecutorBoxedArc,
@@ -317,6 +317,15 @@ impl SubgraphExecutorMap {
.or_insert_with(|| Arc::new(Semaphore::new(self.max_connections_per_host)))
.clone();
+ let aws_sigv4_subgraph_config = self
+ .config
+ .aws_sig_v4
+ .subgraphs
+ .get(subgraph_name)
+ .unwrap_or(&self.config.aws_sig_v4.all);
+
+ let aws_sigv4_signer = create_awssigv4_signer(aws_sigv4_subgraph_config);
+
let executor = HTTPSubgraphExecutor::new(
subgraph_name.to_string(),
endpoint_uri,
@@ -324,6 +333,7 @@ impl SubgraphExecutorMap {
semaphore,
self.config.clone(),
self.in_flight_requests.clone(),
+ aws_sigv4_signer,
);
self.executors_by_subgraph
diff --git a/lib/router-config/src/aws_sig_v4.rs b/lib/router-config/src/aws_sig_v4.rs
new file mode 100644
index 000000000..a1d095f18
--- /dev/null
+++ b/lib/router-config/src/aws_sig_v4.rs
@@ -0,0 +1,77 @@
+use std::collections::HashMap;
+
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+#[serde(deny_unknown_fields)]
+pub struct AwsSigV4Config {
+ // configuration that will apply to all subgraphs
+ pub all: AwsSigV4SubgraphConfig,
+
+ // per-subgraph configuration overrides
+ #[serde(default, skip_serializing_if = "HashMap::is_empty")]
+ pub subgraphs: HashMap,
+}
+
+impl Default for AwsSigV4Config {
+ fn default() -> Self {
+ Self {
+ all: default_all_config(),
+ subgraphs: HashMap::new(),
+ }
+ }
+}
+
+fn default_all_config() -> AwsSigV4SubgraphConfig {
+ AwsSigV4SubgraphConfig::Disabled
+}
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+#[serde(untagged)]
+pub enum AwsSigV4SubgraphConfig {
+ Disabled,
+ DefaultChain { default_chain: DefaultChainConfig },
+ // Not recommended, prefer using default_chain as shown above
+ HardCoded { hardcoded: HardCodedConfig },
+}
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+pub struct DefaultChainConfig {
+ // https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/iam-roles-for-amazon-ec2.html#ec2-instance-profile
+ pub profile_name: Option,
+
+ // https://docs.aws.amazon.com/general/latest/gr/rande.html
+ pub region: String,
+
+ // https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_aws-services-that-work-with-iam.html
+ pub service: String,
+
+ pub assume_role: Option,
+}
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+pub struct HardCodedConfig {
+ pub access_key_id: String,
+ pub secret_access_key: String,
+ pub region: String,
+ pub service_name: String,
+ pub session_token: Option,
+}
+
+impl AwsSigV4Config {
+ pub fn is_disabled(&self) -> bool {
+ matches!(self.all, AwsSigV4SubgraphConfig::Disabled)
+ && (self.subgraphs.is_empty()
+ || self
+ .subgraphs
+ .values()
+ .all(|cfg| matches!(cfg, AwsSigV4SubgraphConfig::Disabled)))
+ }
+}
+
+#[derive(Debug, Deserialize, Serialize, JsonSchema)]
+pub struct AssumeRoleConfig {
+ pub role_arn: String,
+ pub session_name: Option,
+}
diff --git a/lib/router-config/src/lib.rs b/lib/router-config/src/lib.rs
index 537244c9e..0476f7e9d 100644
--- a/lib/router-config/src/lib.rs
+++ b/lib/router-config/src/lib.rs
@@ -1,3 +1,4 @@
+pub mod aws_sig_v4;
pub mod cors;
pub mod csrf;
mod env_overrides;
@@ -92,6 +93,13 @@ pub struct HiveRouterConfig {
/// Configuration for overriding labels.
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub override_labels: OverrideLabelsConfig,
+
+ /// Configuration for AWS SigV4 signing of requests to subgraphs.
+ #[serde(
+ default,
+ skip_serializing_if = "aws_sig_v4::AwsSigV4Config::is_disabled"
+ )]
+ pub aws_sig_v4: aws_sig_v4::AwsSigV4Config,
}
#[derive(Debug, thiserror::Error)]