turul_a2a_aws_lambda/
adapter.rs1use axum::body::Body;
4use http_body_util::BodyExt;
5
6pub const AUTHORIZER_HEADER_PREFIX: &str = "x-authorizer-";
9
10pub fn lambda_to_axum_request(
16 event: lambda_http::Request,
17) -> Result<http::Request<Body>, lambda_http::Error> {
18 let (mut parts, body) = event.into_parts();
19
20 let keys_to_remove: Vec<http::header::HeaderName> = parts
22 .headers
23 .keys()
24 .filter(|k| k.as_str().starts_with(AUTHORIZER_HEADER_PREFIX))
25 .cloned()
26 .collect();
27 for key in keys_to_remove {
28 parts.headers.remove(&key);
29 }
30
31 if let Some(ctx) = parts.extensions.get::<lambda_http::request::RequestContext>() {
36 match ctx {
37 lambda_http::request::RequestContext::ApiGatewayV2(apigw) => {
38 if let Some(ref authorizer) = apigw.authorizer {
39 if let Some(ref jwt) = authorizer.jwt {
41 for (key, value) in &jwt.claims {
42 inject_authorizer_header(&mut parts.headers, key, value);
43 }
44 }
45 }
49 }
50 lambda_http::request::RequestContext::ApiGatewayV1(apigw) => {
51 for (key, value) in &apigw.authorizer.fields {
53 if let serde_json::Value::String(s) = value {
54 inject_authorizer_header(&mut parts.headers, key, s);
55 }
56 }
57 }
58 _ => {} }
60 }
61
62 let body_bytes: bytes::Bytes = match body {
64 lambda_http::Body::Empty => bytes::Bytes::new(),
65 lambda_http::Body::Text(s) => bytes::Bytes::from(s),
66 lambda_http::Body::Binary(b) => bytes::Bytes::from(b),
67 _ => bytes::Bytes::new(),
68 };
69
70 let req = http::Request::from_parts(parts, Body::from(body_bytes));
71 Ok(req)
72}
73
74fn inject_authorizer_header(headers: &mut http::HeaderMap, key: &str, value: &str) {
75 let header_name = format!("{AUTHORIZER_HEADER_PREFIX}{}", key.to_lowercase());
76 if let Ok(name) = http::header::HeaderName::from_bytes(header_name.as_bytes()) {
77 if let Ok(val) = http::header::HeaderValue::from_str(value) {
78 headers.insert(name, val);
79 }
80 }
81}
82
83pub async fn axum_to_lambda_response(
85 resp: http::Response<Body>,
86) -> Result<lambda_http::Response<lambda_http::Body>, lambda_http::Error> {
87 let (parts, body) = resp.into_parts();
88 let body_bytes = body.collect().await
89 .map_err(|e| lambda_http::Error::from(format!("Body collect error: {e}")))?
90 .to_bytes();
91
92 let lambda_body = if body_bytes.is_empty() {
93 lambda_http::Body::Empty
94 } else {
95 lambda_http::Body::Text(String::from_utf8_lossy(&body_bytes).into_owned())
96 };
97
98 let resp = http::Response::from_parts(parts, lambda_body);
99 Ok(resp)
100}
101
102#[cfg(test)]
103mod tests {
104 use super::*;
105
106 #[test]
107 fn strips_client_supplied_authorizer_headers() {
108 let req: lambda_http::Request = http::Request::builder()
109 .method("POST")
110 .uri("/message:send")
111 .header("content-type", "application/json")
112 .header("a2a-version", "1.0")
113 .header("x-authorizer-userid", "forged-admin")
114 .header("x-authorizer-role", "superuser")
115 .header("x-real-header", "keep-this")
116 .body(lambda_http::Body::Text("{}".into()))
117 .unwrap();
118
119 let axum_req = lambda_to_axum_request(req).unwrap();
120
121 assert!(
123 axum_req.headers().get("x-authorizer-userid").is_none(),
124 "Forged x-authorizer-userid must be stripped"
125 );
126 assert!(
127 axum_req.headers().get("x-authorizer-role").is_none(),
128 "Forged x-authorizer-role must be stripped"
129 );
130
131 assert_eq!(
133 axum_req.headers().get("x-real-header").unwrap(),
134 "keep-this"
135 );
136 assert_eq!(
137 axum_req.headers().get("content-type").unwrap(),
138 "application/json"
139 );
140 }
141
142 #[test]
143 fn inject_authorizer_header_works() {
144 let mut headers = http::HeaderMap::new();
145 inject_authorizer_header(&mut headers, "userId", "user-123");
146 assert_eq!(
147 headers.get("x-authorizer-userid").unwrap(),
148 "user-123"
149 );
150 }
151}