Skip to content

Commit 066a04c

Browse files
committed
feat(router): HMAC based Subgraph Auth
1 parent 7ebe2d9 commit 066a04c

File tree

10 files changed

+238
-24
lines changed

10 files changed

+238
-24
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/README.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
|[**csrf**](#csrf)|`object`|Configuration for CSRF prevention.<br/>Default: `{"enabled":false,"required_headers":[]}`<br/>||
99
|[**graphiql**](#graphiql)|`object`|Configuration for the GraphiQL interface.<br/>Default: `{"enabled":true}`<br/>||
1010
|[**headers**](#headers)|`object`|Configuration for the headers.<br/>Default: `{}`<br/>||
11+
|[**hmac\_signature**](#hmac_signature)|`object`||yes|
1112
|[**http**](#http)|`object`|Configuration for the HTTP server/listener.<br/>Default: `{"host":"0.0.0.0","port":4000}`<br/>||
1213
|[**jwt**](#jwt)|`object`|Configuration for JWT authentication plugin.<br/>|yes|
1314
|[**log**](#log)|`object`|The router logger configuration.<br/>Default: `{"filter":null,"format":"json","level":"info"}`<br/>||
@@ -57,6 +58,8 @@ headers:
5758
default: unknown
5859
named: x-tenant-id
5960
rename: x-acct-tenant
61+
hmac_signature:
62+
extension_name: hmac_signature
6063
http:
6164
host: 0.0.0.0
6265
port: 4000
@@ -1341,6 +1344,24 @@ For more information on the available functions and syntax, see the
13411344
|**expression**|`string`||yes|
13421345

13431346

1347+
<a name="hmac_signature"></a>
1348+
## hmac\_signature: object
1349+
1350+
**Properties**
1351+
1352+
|Name|Type|Description|Required|
1353+
|----|----|-----------|--------|
1354+
|**enabled**|||yes|
1355+
|**extension\_name**|`string`|Default: `"hmac_signature"`<br/>|no|
1356+
|**secret**|`string`||yes|
1357+
1358+
**Example**
1359+
1360+
```yaml
1361+
extension_name: hmac_signature
1362+
1363+
```
1364+
13441365
<a name="http"></a>
13451366
## http: object
13461367

lib/executor/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,9 @@ ryu = "1.0.20"
5050
indexmap = "2.10.0"
5151
bumpalo = "3.19.0"
5252
once_cell = "1.21.3"
53+
hmac = "0.12.1"
54+
sha2 = "0.10.9"
55+
hex = "0.4.3"
5356

5457
[dev-dependencies]
5558
subgraphs = { path = "../../bench/subgraphs" }

lib/executor/src/execution/plan.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -708,6 +708,7 @@ impl<'exec, 'req> Executor<'exec, 'req> {
708708
representations,
709709
headers: headers_map,
710710
extensions: None,
711+
client_request: self.client_request,
711712
};
712713

713714
if let Some(jwt_forwarding_plan) = &self.jwt_forwarding_plan {
@@ -722,7 +723,7 @@ impl<'exec, 'req> Executor<'exec, 'req> {
722723
subgraph_name: node.service_name.clone(),
723724
response: self
724725
.executors
725-
.execute(&node.service_name, subgraph_request, self.client_request)
726+
.execute(&node.service_name, subgraph_request)
726727
.await
727728
.into(),
728729
}))

lib/executor/src/executors/common.rs

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@ use bytes::Bytes;
55
use http::HeaderMap;
66
use sonic_rs::Value;
77

8+
use crate::execution::client_request_details::ClientRequestDetails;
9+
810
#[async_trait]
911
pub trait SubgraphExecutor {
10-
async fn execute<'a>(
12+
async fn execute<'exec, 'req>(
1113
&self,
12-
execution_request: HttpExecutionRequest<'a>,
14+
execution_request: HttpExecutionRequest<'exec, 'req>,
1315
) -> HttpExecutionResponse;
1416

1517
fn to_boxed_arc<'a>(self) -> Arc<Box<dyn SubgraphExecutor + Send + Sync + 'a>>
@@ -26,18 +28,19 @@ pub type SubgraphExecutorBoxedArc = Arc<Box<SubgraphExecutorType>>;
2628

2729
pub type SubgraphRequestExtensions = HashMap<String, Value>;
2830

29-
pub struct HttpExecutionRequest<'a> {
30-
pub query: &'a str,
31+
pub struct HttpExecutionRequest<'exec, 'req> {
32+
pub query: &'exec str,
3133
pub dedupe: bool,
32-
pub operation_name: Option<&'a str>,
34+
pub operation_name: Option<&'exec str>,
3335
// TODO: variables could be stringified before even executing the request
34-
pub variables: Option<HashMap<&'a str, &'a sonic_rs::Value>>,
36+
pub variables: Option<HashMap<&'exec str, &'exec sonic_rs::Value>>,
3537
pub headers: HeaderMap,
3638
pub representations: Option<Vec<u8>>,
3739
pub extensions: Option<SubgraphRequestExtensions>,
40+
pub client_request: &'exec ClientRequestDetails<'exec, 'req>,
3841
}
3942

40-
impl HttpExecutionRequest<'_> {
43+
impl HttpExecutionRequest<'_, '_> {
4144
pub fn add_request_extensions_field(&mut self, key: String, value: Value) {
4245
self.extensions
4346
.get_or_insert_with(HashMap::new)

lib/executor/src/executors/error.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,12 @@ pub enum SubgraphExecutorError {
2020
RequestFailure(String, String),
2121
#[error("Failed to serialize variable \"{0}\": {1}")]
2222
VariablesSerializationFailure(String, String),
23+
#[error("Failed to serialize extension \"{0}\": {1}")]
24+
ExtensionSerializationFailure(String, String),
25+
#[error("Failed to build HMAC expression for subgraph '{0}'. Please check your VRL expression for syntax errors. Diagnostic: {1}")]
26+
HMACExpressionBuild(String, String),
27+
#[error("HMAC signature error: {0}")]
28+
HMACSignatureError(String),
2329
}
2430

2531
impl From<SubgraphExecutorError> for GraphQLError {
@@ -61,6 +67,13 @@ impl SubgraphExecutorError {
6167
SubgraphExecutorError::VariablesSerializationFailure(_, _) => {
6268
"SUBGRAPH_VARIABLES_SERIALIZATION_FAILURE"
6369
}
70+
SubgraphExecutorError::ExtensionSerializationFailure(_, _) => {
71+
"SUBGRAPH_EXTENSION_SERIALIZATION_FAILURE"
72+
}
73+
SubgraphExecutorError::HMACSignatureError(_) => "SUBGRAPH_HMAC_SIGNATURE_ERROR",
74+
SubgraphExecutorError::HMACExpressionBuild(_, _) => {
75+
"SUBGRAPH_HMAC_EXPRESSION_BUILD_FAILURE"
76+
}
6477
}
6578
}
6679
}

lib/executor/src/executors/http.rs

Lines changed: 104 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,28 @@
1+
use std::collections::BTreeMap;
12
use std::sync::Arc;
23

34
use crate::executors::common::HttpExecutionResponse;
45
use crate::executors::dedupe::{request_fingerprint, ABuildHasher, SharedResponse};
6+
use crate::utils::expression::execute_expression_with_value;
57
use dashmap::DashMap;
68
use hive_router_config::HiveRouterConfig;
79
use tokio::sync::OnceCell;
810

911
use async_trait::async_trait;
1012

1113
use bytes::{BufMut, Bytes, BytesMut};
14+
use hmac::{Hmac, Mac};
1215
use http::HeaderMap;
1316
use http::HeaderValue;
1417
use http_body_util::BodyExt;
1518
use http_body_util::Full;
1619
use hyper::Version;
1720
use hyper_tls::HttpsConnector;
1821
use hyper_util::client::legacy::{connect::HttpConnector, Client};
22+
use sha2::Sha256;
1923
use tokio::sync::Semaphore;
2024
use tracing::debug;
25+
use vrl::compiler::Program as VrlProgram;
2126

2227
use crate::executors::common::HttpExecutionRequest;
2328
use crate::executors::error::SubgraphExecutorError;
@@ -27,6 +32,7 @@ use crate::utils::consts::COLON;
2732
use crate::utils::consts::COMMA;
2833
use crate::utils::consts::QUOTE;
2934
use crate::{executors::common::SubgraphExecutor, json_writer::write_and_escape_string};
35+
use vrl::core::Value as VrlValue;
3036

3137
#[derive(Debug)]
3238
pub struct HTTPSubgraphExecutor {
@@ -37,13 +43,23 @@ pub struct HTTPSubgraphExecutor {
3743
pub semaphore: Arc<Semaphore>,
3844
pub config: Arc<HiveRouterConfig>,
3945
pub in_flight_requests: Arc<DashMap<u64, Arc<OnceCell<SharedResponse>>, ABuildHasher>>,
46+
pub should_sign_hmac: BooleanOrProgram,
4047
}
4148

4249
const FIRST_VARIABLE_STR: &[u8] = b",\"variables\":{";
4350
const FIRST_QUOTE_STR: &[u8] = b"{\"query\":";
51+
const FIRST_EXTENSION_STR: &[u8] = b",\"extensions\":{";
4452

4553
pub type HttpClient = Client<HttpsConnector<HttpConnector>, Full<Bytes>>;
4654

55+
type HmacSha256 = Hmac<Sha256>;
56+
57+
#[derive(Debug)]
58+
pub enum BooleanOrProgram {
59+
Boolean(bool),
60+
Program(Box<VrlProgram>),
61+
}
62+
4763
impl HTTPSubgraphExecutor {
4864
pub fn new(
4965
subgraph_name: String,
@@ -52,6 +68,7 @@ impl HTTPSubgraphExecutor {
5268
semaphore: Arc<Semaphore>,
5369
config: Arc<HiveRouterConfig>,
5470
in_flight_requests: Arc<DashMap<u64, Arc<OnceCell<SharedResponse>>, ABuildHasher>>,
71+
should_sign_hmac: BooleanOrProgram,
5572
) -> Self {
5673
let mut header_map = HeaderMap::new();
5774
header_map.insert(
@@ -71,12 +88,13 @@ impl HTTPSubgraphExecutor {
7188
semaphore,
7289
config,
7390
in_flight_requests,
91+
should_sign_hmac,
7492
}
7593
}
7694

77-
fn build_request_body<'a>(
95+
fn build_request_body<'exec, 'req>(
7896
&self,
79-
execution_request: &HttpExecutionRequest<'a>,
97+
execution_request: &HttpExecutionRequest<'exec, 'req>,
8098
) -> Result<Vec<u8>, SubgraphExecutorError> {
8199
let mut body = Vec::with_capacity(4096);
82100
body.put(FIRST_QUOTE_STR);
@@ -118,13 +136,89 @@ impl HTTPSubgraphExecutor {
118136
body.put(CLOSE_BRACE);
119137
}
120138

121-
if let Some(extensions) = &execution_request.extensions {
122-
if !extensions.is_empty() {
123-
let as_value = sonic_rs::to_value(extensions).unwrap();
139+
let should_sign_hmac = match &self.should_sign_hmac {
140+
BooleanOrProgram::Boolean(b) => *b,
141+
BooleanOrProgram::Program(expr) => {
142+
// .subgraph
143+
let subgraph_value = VrlValue::Object(BTreeMap::from([(
144+
"name".into(),
145+
VrlValue::Bytes(Bytes::from(self.subgraph_name.to_owned())),
146+
)]));
147+
// .request
148+
let request_value: VrlValue = execution_request.client_request.into();
149+
let target_value = VrlValue::Object(BTreeMap::from([
150+
("subgraph".into(), subgraph_value),
151+
("request".into(), request_value),
152+
]));
153+
let result = execute_expression_with_value(expr, target_value);
154+
match result {
155+
Ok(VrlValue::Boolean(b)) => b,
156+
Ok(_) => {
157+
return Err(SubgraphExecutorError::HMACSignatureError(
158+
"HMAC signature expression did not evaluate to a boolean".to_string(),
159+
));
160+
}
161+
Err(e) => {
162+
return Err(SubgraphExecutorError::HMACSignatureError(format!(
163+
"HMAC signature expression evaluation error: {}",
164+
e
165+
)));
166+
}
167+
}
168+
}
169+
};
124170

125-
body.put(COMMA);
126-
body.put("\"extensions\":".as_bytes());
127-
body.extend_from_slice(as_value.to_string().as_bytes());
171+
let hmac_signature_ext = if should_sign_hmac {
172+
let mut mac = HmacSha256::new_from_slice(self.config.hmac_signature.secret.as_bytes())
173+
.map_err(|e| {
174+
SubgraphExecutorError::HMACSignatureError(format!(
175+
"Failed to create HMAC instance: {}",
176+
e
177+
))
178+
})?;
179+
let mut body_without_extensions = body.clone();
180+
body_without_extensions.put(CLOSE_BRACE);
181+
mac.update(&body_without_extensions);
182+
let result = mac.finalize();
183+
let result_bytes = result.into_bytes();
184+
Some(result_bytes)
185+
} else {
186+
None
187+
};
188+
189+
if let Some(extensions) = &execution_request.extensions {
190+
let mut first = true;
191+
if let Some(hmac_bytes) = hmac_signature_ext {
192+
if first {
193+
body.put(FIRST_EXTENSION_STR);
194+
first = false;
195+
} else {
196+
body.put(COMMA);
197+
}
198+
body.put(self.config.hmac_signature.extension_name.as_bytes());
199+
let hmac_hex = hex::encode(hmac_bytes);
200+
body.put(QUOTE);
201+
body.put(hmac_hex.as_bytes());
202+
body.put(QUOTE);
203+
}
204+
for (extension_name, extension_value) in extensions {
205+
if first {
206+
body.put(FIRST_EXTENSION_STR);
207+
first = false;
208+
} else {
209+
body.put(COMMA);
210+
}
211+
body.put(QUOTE);
212+
body.put(extension_name.as_bytes());
213+
body.put(QUOTE);
214+
body.put(COLON);
215+
let value_str = sonic_rs::to_string(extension_value).map_err(|err| {
216+
SubgraphExecutorError::ExtensionSerializationFailure(
217+
extension_name.to_string(),
218+
err.to_string(),
219+
)
220+
})?;
221+
body.put(value_str.as_bytes());
128222
}
129223
}
130224

@@ -210,9 +304,9 @@ impl HTTPSubgraphExecutor {
210304
#[async_trait]
211305
impl SubgraphExecutor for HTTPSubgraphExecutor {
212306
#[tracing::instrument(skip_all, fields(subgraph_name = self.subgraph_name))]
213-
async fn execute<'a>(
307+
async fn execute<'exec, 'req>(
214308
&self,
215-
execution_request: HttpExecutionRequest<'a>,
309+
execution_request: HttpExecutionRequest<'exec, 'req>,
216310
) -> HttpExecutionResponse {
217311
let body = match self.build_request_body(&execution_request) {
218312
Ok(body) => body,

0 commit comments

Comments
 (0)