From 3b3653c5f20122462f9b31795ecc4374197c1556 Mon Sep 17 00:00:00 2001 From: tmokmss Date: Mon, 15 Jul 2024 15:16:04 +0900 Subject: [PATCH 1/2] add authorization_source feature --- README.md | 3 +++ src/lib.rs | 13 +++++++++++ tests/integ_tests/main.rs | 45 +++++++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+) diff --git a/README.md b/README.md index f9ca94e..533ee19 100644 --- a/README.md +++ b/README.md @@ -104,6 +104,7 @@ The readiness check port/path and traffic port can be configured using environme | AWS_LWA_ENABLE_COMPRESSION | enable gzip compression for response body | "false" | | AWS_LWA_INVOKE_MODE | Lambda function invoke mode: "buffered" or "response_stream", default is "buffered" | "buffered" | | AWS_LWA_PASS_THROUGH_PATH | the path for receiving event payloads that are passed through from non-http triggers | "/events" | +| AWS_LWA_AUTHORIZATION_SOURCE | a header name to be replaced to `Authorization` | None | > **Note:** > We use "AWS_LWA_" prefix to namespacing all environment variables used by Lambda Web Adapter. The original ones will be supported until we reach version 1.0. @@ -134,6 +135,8 @@ Please check out [FastAPI with Response Streaming](examples/fastapi-response-str **AWS_LWA_PASS_THROUGH_PATH** - Path to receive events payloads passed through from non-http event triggers. The default is "/events". +**AWS_LWA_AUTHORIZATION_SOURCE** - When set, Lambda Web Adapter replaces the specified header name to `Authorization` before proxying a request. This is useful when you use Lambda function URL with [IAM auth type](https://docs.aws.amazon.com/lambda/latest/dg/urls-auth.html), which reserves Authorization header for IAM authentication, but you want to still use Authorization header for your backend apps. This feature is disabled by default. + ## Request Context **Request Context** is metadata API Gateway sends to Lambda for a request. It usually contains requestId, requestTime, apiId, identity, and authorizer. Identity and authorizer are useful to get client identity for authorization. API Gateway Developer Guide contains more details [here](https://docs.aws.amazon.com/apigateway/latest/developerguide/set-up-lambda-proxy-integrations.html#api-gateway-simple-proxy-for-lambda-input-format). diff --git a/src/lib.rs b/src/lib.rs index 3cb2b4f..dde736e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,6 +77,7 @@ pub struct AdapterOptions { pub async_init: bool, pub compression: bool, pub invoke_mode: LambdaInvokeMode, + pub authorization_source: String, } impl Default for AdapterOptions { @@ -114,6 +115,10 @@ impl Default for AdapterOptions { .unwrap_or("buffered".to_string()) .as_str() .into(), + authorization_source: env::var("AWS_LWA_AUTHORIZATION_SOURCE") + .unwrap_or("".to_string()) + .as_str() + .into(), } } } @@ -131,6 +136,7 @@ pub struct Adapter { path_through_path: String, compression: bool, invoke_mode: LambdaInvokeMode, + authorization_source: String, } impl Adapter { @@ -167,6 +173,7 @@ impl Adapter { ready_at_init: Arc::new(AtomicBool::new(false)), compression: options.compression, invoke_mode: options.invoke_mode, + authorization_source: options.authorization_source.clone(), } } } @@ -312,6 +319,12 @@ impl Adapter { HeaderName::from_static("x-amzn-lambda-context"), HeaderValue::from_bytes(serde_json::to_string(&lambda_context)?.as_bytes())?, ); + + if !self.authorization_source.is_empty() && req_headers.contains_key(&self.authorization_source) { + let original = req_headers.remove(&self.authorization_source).unwrap(); + req_headers.insert("authorization", original); + } + let mut app_url = self.domain.clone(); app_url.set_path(path); app_url.set_query(parts.uri.query()); diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index 77aa6bc..2b49ea2 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -41,6 +41,7 @@ fn test_adapter_options_from_env() { env::set_var("AWS_LWA_TLS_SERVER_NAME", "api.example.com"); env::remove_var("AWS_LWA_TLS_CERT_FILE"); env::set_var("AWS_LWA_INVOKE_MODE", "buffered"); + env::set_var("AWS_LWA_AUTHORIZATION_SOURCE", "auth-token"); // Initialize adapter with env options let options = AdapterOptions::default(); @@ -55,6 +56,7 @@ fn test_adapter_options_from_env() { assert!(options.async_init); assert!(options.compression); assert_eq!(LambdaInvokeMode::Buffered, options.invoke_mode); + assert_eq!("auth-token", options.authorization_source); } #[test] @@ -69,6 +71,7 @@ fn test_adapter_options_from_namespaced_env() { env::set_var("AWS_LWA_ASYNC_INIT", "true"); env::set_var("AWS_LWA_ENABLE_COMPRESSION", "true"); env::set_var("AWS_LWA_INVOKE_MODE", "response_stream"); + env::set_var("AWS_LWA_AUTHORIZATION_SOURCE", "auth-token"); // Initialize adapter with env options let options = AdapterOptions::default(); @@ -84,6 +87,7 @@ fn test_adapter_options_from_namespaced_env() { assert!(options.async_init); assert!(options.compression); assert_eq!(LambdaInvokeMode::ResponseStream, options.invoke_mode); + assert_eq!("auth-token", options.authorization_source); } #[test] @@ -607,6 +611,47 @@ async fn test_http_content_encoding_suffix() { assert_eq!(json_data.to_owned(), body_to_string(response).await); } +#[tokio::test] +async fn test_http_authorization_source() { + // Start app server + let app_server = MockServer::start(); + let hello = app_server.mock(|when, then| { + when.method(GET).path("/hello").header_exists("Authorization"); + then.status(200).body("Hello World"); + }); + + // Initialize adapter + let mut adapter = Adapter::new(&AdapterOptions { + host: app_server.host(), + port: app_server.port().to_string(), + readiness_check_port: app_server.port().to_string(), + readiness_check_path: "/healthcheck".to_string(), + authorization_source: "auth-token".to_string(), + ..Default::default() + }); + + // // Call the adapter service with basic request + let req = LambdaEventBuilder::new() + .with_path("/hello") + .with_header("auth-token", "Bearer token") + .build(); + + // We convert to Request object because it allows us to add + // the lambda Context + let mut request = Request::from(req); + add_lambda_context_to_request(&mut request); + + let response = adapter.call(request).await.expect("Request failed"); + + // Assert endpoint was called once + hello.assert(); + + // and response has expected content + assert_eq!(200, response.status()); + assert_eq!(response.headers().get("content-length").unwrap(), "11"); + assert_eq!("Hello World", body_to_string(response).await); +} + #[tokio::test] async fn test_http_context_multi_headers() { // Start app server From cd3d5fcb5e2bbfada187c353fc98629102016456 Mon Sep 17 00:00:00 2001 From: tmokmss Date: Mon, 15 Jul 2024 15:46:39 +0900 Subject: [PATCH 2/2] use option --- src/lib.rs | 17 ++++++++--------- tests/integ_tests/main.rs | 6 +++--- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index dde736e..5a73c7c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -77,7 +77,7 @@ pub struct AdapterOptions { pub async_init: bool, pub compression: bool, pub invoke_mode: LambdaInvokeMode, - pub authorization_source: String, + pub authorization_source: Option, } impl Default for AdapterOptions { @@ -115,10 +115,7 @@ impl Default for AdapterOptions { .unwrap_or("buffered".to_string()) .as_str() .into(), - authorization_source: env::var("AWS_LWA_AUTHORIZATION_SOURCE") - .unwrap_or("".to_string()) - .as_str() - .into(), + authorization_source: env::var("AWS_LWA_AUTHORIZATION_SOURCE").ok(), } } } @@ -136,7 +133,7 @@ pub struct Adapter { path_through_path: String, compression: bool, invoke_mode: LambdaInvokeMode, - authorization_source: String, + authorization_source: Option, } impl Adapter { @@ -320,9 +317,11 @@ impl Adapter { HeaderValue::from_bytes(serde_json::to_string(&lambda_context)?.as_bytes())?, ); - if !self.authorization_source.is_empty() && req_headers.contains_key(&self.authorization_source) { - let original = req_headers.remove(&self.authorization_source).unwrap(); - req_headers.insert("authorization", original); + if let Some(authorization_source) = self.authorization_source.as_deref() { + if req_headers.contains_key(authorization_source) { + let original = req_headers.remove(authorization_source).unwrap(); + req_headers.insert("authorization", original); + } } let mut app_url = self.domain.clone(); diff --git a/tests/integ_tests/main.rs b/tests/integ_tests/main.rs index 2b49ea2..46fd7c6 100644 --- a/tests/integ_tests/main.rs +++ b/tests/integ_tests/main.rs @@ -56,7 +56,7 @@ fn test_adapter_options_from_env() { assert!(options.async_init); assert!(options.compression); assert_eq!(LambdaInvokeMode::Buffered, options.invoke_mode); - assert_eq!("auth-token", options.authorization_source); + assert_eq!(Some("auth-token".into()), options.authorization_source); } #[test] @@ -87,7 +87,7 @@ fn test_adapter_options_from_namespaced_env() { assert!(options.async_init); assert!(options.compression); assert_eq!(LambdaInvokeMode::ResponseStream, options.invoke_mode); - assert_eq!("auth-token", options.authorization_source); + assert_eq!(Some("auth-token".into()), options.authorization_source); } #[test] @@ -626,7 +626,7 @@ async fn test_http_authorization_source() { port: app_server.port().to_string(), readiness_check_port: app_server.port().to_string(), readiness_check_path: "/healthcheck".to_string(), - authorization_source: "auth-token".to_string(), + authorization_source: Some("auth-token".to_string()), ..Default::default() });