synapse_proxy/transform/
inject.rs1use async_trait::async_trait;
5use serde_json::Value;
6
7use crate::config::InjectSpec;
8use crate::context::ResolvedContext;
9
10use super::{ProxyRequest, RequestTransform, TransformError};
11
12#[derive(Clone)]
13enum Target {
14 Header(String),
15 Body(String),
16}
17
18#[derive(Clone)]
19enum Source {
20 Context(String),
21 Const(Value),
22}
23
24#[derive(Clone)]
25pub struct Inject {
26 target: Target,
27 source: Source,
28}
29
30impl Inject {
31 pub fn from_spec(spec: &InjectSpec) -> anyhow::Result<Self> {
33 let target = match (&spec.header, &spec.body) {
34 (Some(h), None) => Target::Header(h.clone()),
35 (None, Some(b)) => Target::Body(b.clone()),
36 _ => anyhow::bail!("inject requires exactly one of `header` or `body`"),
37 };
38 let source = match (&spec.from_context, &spec.constant) {
39 (Some(k), None) => Source::Context(k.clone()),
40 (None, Some(v)) => Source::Const(v.clone()),
41 _ => anyhow::bail!("inject requires exactly one of `from_context` or `const`"),
42 };
43 Ok(Self { target, source })
44 }
45
46 fn resolve_value(&self, ctx: &ResolvedContext) -> Option<Value> {
47 match &self.source {
48 Source::Const(v) => Some(v.clone()),
49 Source::Context(k) => ctx.get(k).map(|s| Value::String(s.to_string())),
50 }
51 }
52}
53
54pub fn set_body_path(root: &mut Value, path: &str, value: Value) {
56 let mut cur = root;
57 let parts: Vec<&str> = path.split('.').collect();
58 for (i, part) in parts.iter().enumerate() {
59 if !cur.is_object() {
60 *cur = Value::Object(Default::default());
61 }
62 let Some(obj) = cur.as_object_mut() else {
63 return;
64 };
65 if i == parts.len() - 1 {
66 obj.insert((*part).to_string(), value);
67 return;
68 }
69 cur = obj
70 .entry((*part).to_string())
71 .or_insert_with(|| Value::Object(Default::default()));
72 }
73}
74
75#[async_trait]
76impl RequestTransform for Inject {
77 async fn apply(
78 &self,
79 ctx: &ResolvedContext,
80 req: &mut ProxyRequest,
81 ) -> Result<(), TransformError> {
82 let value = self.resolve_value(ctx);
83 match &self.target {
84 Target::Header(name) => match value {
85 Some(v) => {
86 let s = match &v {
87 Value::String(s) => s.clone(),
88 other => other.to_string(),
89 };
90 req.set_header(name, &s);
91 }
92 None => req.remove_header(name), },
94 Target::Body(path) => {
95 if let Some(v) = value {
96 let body = req.body_json_mut()?;
97 set_body_path(body, path, v);
98 }
99 }
101 }
102 Ok(())
103 }
104}
105
106#[cfg(test)]
107mod tests {
108 use super::*;
109 use crate::transform::ProxyRequest;
110 use axum::http::{HeaderMap, Method};
111 use std::collections::HashMap;
112
113 fn ctx() -> ResolvedContext {
114 let s = crate::context::ContextStore::new(HashMap::from([("org".into(), "acme".into())]));
116 s.resolve()
117 }
118 fn req(body: &[u8]) -> ProxyRequest {
119 ProxyRequest::from_parts(
120 Method::POST,
121 "/x".into(),
122 None,
123 HeaderMap::new(),
124 body.to_vec(),
125 )
126 }
127
128 #[tokio::test]
129 async fn injects_context_header_overwriting() {
130 let inj = Inject::from_spec(&InjectSpec {
131 header: Some("X-Tenant-Id".into()),
132 body: None,
133 from_context: Some("org".into()),
134 constant: None,
135 })
136 .unwrap();
137 let mut r = req(b"");
138 r.set_header("x-tenant-id", "attacker");
139 inj.apply(&ctx(), &mut r).await.unwrap();
140 assert_eq!(r.headers.get("X-Tenant-Id").unwrap(), "acme"); }
142
143 #[tokio::test]
144 async fn injects_constant_into_nested_body_path() {
145 let inj = Inject::from_spec(&InjectSpec {
146 header: None,
147 body: Some("params.context.user".into()),
148 from_context: None,
149 constant: Some(serde_json::json!("_default")),
150 })
151 .unwrap();
152 let mut r = req(b"{\"params\":{}}");
153 inj.apply(&ctx(), &mut r).await.unwrap();
154 let v: Value = serde_json::from_slice(&r.into_body_bytes()).unwrap();
155 assert_eq!(v["params"]["context"]["user"], "_default");
156 }
157
158 #[test]
159 fn from_spec_rejects_ambiguous_target() {
160 let bad = InjectSpec {
161 header: Some("h".into()),
162 body: Some("b".into()),
163 from_context: Some("k".into()),
164 constant: None,
165 };
166 assert!(Inject::from_spec(&bad).is_err());
167 }
168
169 #[test]
170 fn set_body_path_single_segment() {
171 let mut v = serde_json::json!({});
172 set_body_path(&mut v, "org", serde_json::json!("acme"));
173 assert_eq!(v["org"], "acme");
174 }
175
176 #[test]
177 fn set_body_path_overwrites_non_object_intermediate() {
178 let mut v = serde_json::json!({ "a": "scalar" });
179 set_body_path(&mut v, "a.b", serde_json::json!(1));
180 assert_eq!(v["a"]["b"], 1); }
182
183 #[tokio::test]
184 async fn absent_context_header_is_stripped_not_passed_through() {
185 let s = crate::context::ContextStore::new(std::collections::HashMap::new());
187 let ctx = s.resolve();
188 let inj = Inject::from_spec(&InjectSpec {
189 header: Some("x-user-id".into()),
190 body: None,
191 from_context: Some("user".into()),
192 constant: None,
193 })
194 .unwrap();
195 let mut r = req(b""); r.set_header("x-user-id", "attacker"); inj.apply(&ctx, &mut r).await.unwrap();
198 assert!(
199 r.headers.get("x-user-id").is_none(),
200 "caller identity must be stripped when context absent"
201 );
202 }
203}