diff --git a/Makefile b/Makefile index 477f0ffe..d27d0e47 100644 --- a/Makefile +++ b/Makefile @@ -107,6 +107,7 @@ check-event-features: cargo test --package aws_lambda_events --no-default-features --features sns cargo test --package aws_lambda_events --no-default-features --features sqs cargo test --package aws_lambda_events --no-default-features --features streams + cargo test --package aws_lambda_events --no-default-features --features vpc_lattice fmt: cargo +nightly fmt --all diff --git a/lambda-events/Cargo.toml b/lambda-events/Cargo.toml index ae2d948a..2cc0c5f9 100644 --- a/lambda-events/Cargo.toml +++ b/lambda-events/Cargo.toml @@ -78,6 +78,7 @@ default = [ "streams", "documentdb", "eventbridge", + "vpc_lattice" ] activemq = [] @@ -124,6 +125,7 @@ sqs = ["serde_with"] streams = [] documentdb = [] eventbridge = ["chrono", "serde_with"] +vpc_lattice = ["bytes", "http", "http-body", "http-serde", "iam", "query_map"] catch-all-fields = [] diff --git a/lambda-events/src/custom_serde/headers.rs b/lambda-events/src/custom_serde/headers.rs index ea52c88e..b343180d 100644 --- a/lambda-events/src/custom_serde/headers.rs +++ b/lambda-events/src/custom_serde/headers.rs @@ -34,6 +34,24 @@ where map.end() } +/// Serialize a HeaderMap with multiple values per header combined as comma-separated strings. +/// Used by VPC Lattice V1 which expects multi-value headers as "value1, value2". +#[cfg(feature = "vpc_lattice")] +pub(crate) fn serialize_comma_separated_headers(headers: &HeaderMap, serializer: S) -> Result +where + S: Serializer, +{ + let mut map = serializer.serialize_map(Some(headers.keys_len()))?; + for key in headers.keys() { + let values: Vec<&str> = headers.get_all(key).iter().filter_map(|v| v.to_str().ok()).collect(); + if !values.is_empty() { + let combined_value = values.join(", "); + map.serialize_entry(key.as_str(), &combined_value)?; + } + } + map.end() +} + #[derive(serde::Deserialize)] #[serde(untagged)] enum OneOrMore<'a> { @@ -44,6 +62,7 @@ enum OneOrMore<'a> { struct HeaderMapVisitor { is_human_readable: bool, + split_comma_separated: bool, } impl<'de> Visitor<'de> for HeaderMapVisitor { @@ -97,17 +116,25 @@ impl<'de> Visitor<'de> for HeaderMapVisitor { .map_err(|_| de::Error::invalid_value(Unexpected::Str(&key), &self))?; match val { OneOrMore::One(val) => { - let val = val - .parse() - .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; - map.insert(key, val); - } - OneOrMore::Strings(arr) => { - for val in arr { + if self.split_comma_separated && val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { let val = val .parse() .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; - map.append(&key, val); + map.insert(key, val); + } + } + OneOrMore::Strings(arr) => { + for val in arr { + if self.split_comma_separated && val.contains(',') { + split_and_append_header(&mut map, &key, &val, &self)?; + } else { + let val = val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(&val), &self))?; + map.append(&key, val); + } } } OneOrMore::Bytes(arr) => { @@ -124,13 +151,51 @@ impl<'de> Visitor<'de> for HeaderMapVisitor { } } +fn split_and_append_header( + map: &mut HeaderMap, + key: &HeaderName, + value: &str, + visitor: &HeaderMapVisitor, +) -> Result<(), E> +where + E: DeError, +{ + for split_val in value.split(',') { + let trimmed_val = split_val.trim(); + if !trimmed_val.is_empty() { + let header_val = trimmed_val + .parse() + .map_err(|_| de::Error::invalid_value(Unexpected::Str(trimmed_val), visitor))?; + map.append(key, header_val); + } + } + Ok(()) +} + /// Implementation detail. pub(crate) fn deserialize_headers<'de, D>(de: D) -> Result where D: Deserializer<'de>, { let is_human_readable = de.is_human_readable(); - de.deserialize_option(HeaderMapVisitor { is_human_readable }) + de.deserialize_option(HeaderMapVisitor { + is_human_readable, + split_comma_separated: false, + }) +} + +/// Deserialize headers, splitting comma-separated values into multiple header values. +/// Used by VPC Lattice V1 which sends multi-value headers as "value1, value2". +#[cfg(feature = "vpc_lattice")] +pub(crate) fn deserialize_comma_separated_headers<'de, D>(de: D) -> Result +where + D: Deserializer<'de>, +{ + let is_human_readable = de.is_human_readable(); + de.deserialize_option(HeaderMapVisitor { + is_human_readable, + split_comma_separated: true, + }) } #[cfg(test)] diff --git a/lambda-events/src/custom_serde/mod.rs b/lambda-events/src/custom_serde/mod.rs index 02b50c78..a996e91f 100644 --- a/lambda-events/src/custom_serde/mod.rs +++ b/lambda-events/src/custom_serde/mod.rs @@ -16,7 +16,8 @@ pub type CodeBuildNumber = f32; feature = "apigw", feature = "s3", feature = "iot", - feature = "lambda_function_urls" + feature = "lambda_function_urls", + feature = "vpc_lattice" ))] mod headers; #[cfg(any( @@ -24,14 +25,15 @@ mod headers; feature = "apigw", feature = "s3", feature = "iot", - feature = "lambda_function_urls" + feature = "lambda_function_urls", + feature = "vpc_lattice" ))] pub(crate) use self::headers::*; #[cfg(feature = "dynamodb")] pub(crate) mod float_unix_epoch; -#[cfg(any(feature = "alb", feature = "apigw"))] +#[cfg(any(feature = "alb", feature = "apigw", feature = "vpc_lattice"))] pub(crate) mod http_method; #[cfg(feature = "alb")] @@ -89,6 +91,7 @@ where feature = "code_commit", feature = "cognito", feature = "sns", + feature = "vpc_lattice", test ))] pub(crate) fn deserialize_nullish_boolean<'de, D>(deserializer: D) -> Result diff --git a/lambda-events/src/event/mod.rs b/lambda-events/src/event/mod.rs index 275450fd..44ead0bb 100644 --- a/lambda-events/src/event/mod.rs +++ b/lambda-events/src/event/mod.rs @@ -201,3 +201,8 @@ pub mod documentdb; #[cfg(feature = "eventbridge")] #[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] pub mod eventbridge; + +/// AWS Lambda event definitions for VPC Lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub mod vpc_lattice; diff --git a/lambda-events/src/event/vpc_lattice/common.rs b/lambda-events/src/event/vpc_lattice/common.rs new file mode 100644 index 00000000..f56643c1 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/common.rs @@ -0,0 +1,62 @@ +use crate::{ + custom_serde::{deserialize_headers, serialize_headers}, + encodings::Body, +}; +use http::HeaderMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeResponse` configures the response to be returned +/// by VPC Lattice (both V1 and V2) for the request +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeResponse { + // https://docs.aws.amazon.com/vpc-lattice/latest/ug/lambda-functions.html#respond-to-service + /// Whether the body is base64 encoded + #[serde(default)] + pub is_base64_encoded: bool, + + /// The HTTP status code for the request + // i64 for consistency with other event types (e.g. AlbTargetGroupResponse, ApiGatewayProxyResponse) + pub status_code: i64, + + /// The HTTP status description (optional) + #[serde(default)] + pub status_description: Option, + + /// The Http headers to return + #[serde(deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_headers")] + #[serde(skip_serializing_if = "HeaderMap::is_empty")] + #[serde(default)] + pub headers: HeaderMap, + + /// The response body + #[serde(default)] + pub body: Option, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_response() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-response.json"); + let parsed: VpcLatticeResponse = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeResponse = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/event/vpc_lattice/mod.rs b/lambda-events/src/event/vpc_lattice/mod.rs new file mode 100644 index 00000000..342637bb --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/mod.rs @@ -0,0 +1,6 @@ +mod common; +mod v1; +mod v2; + +// re-export types +pub use self::{common::*, v1::*, v2::*}; diff --git a/lambda-events/src/event/vpc_lattice/v1.rs b/lambda-events/src/event/vpc_lattice/v1.rs new file mode 100644 index 00000000..66743df2 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/v1.rs @@ -0,0 +1,90 @@ +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +use crate::custom_serde::{ + deserialize_comma_separated_headers, deserialize_nullish_boolean, http_method, serialize_comma_separated_headers, +}; + +/// `VpcLatticeRequestV1` contains data coming from VPC Lattice service (V1 format) +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +// we note that V1 requests are snake cased UNLIKE v2 which are camel cased +#[serde(rename_all = "snake_case")] +pub struct VpcLatticeRequestV1 { + /// The url path for the request + #[serde(default)] + pub raw_path: Option, + + /// The HTTP method of the request + #[serde(with = "http_method")] + pub method: Method, + + /// HTTP headers of the request (V1 uses comma-separated strings for multi-values) + #[serde(deserialize_with = "deserialize_comma_separated_headers", default)] + #[serde(serialize_with = "serialize_comma_separated_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (V1 uses the last value passed for multi-values + /// so no special serializer is needed) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + pub is_base64_encoded: bool, + + /// Catchall to catch any additional fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("/api/product", parsed.raw_path.unwrap()); + assert_eq!("POST", parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().next().unwrap() + ); + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!("{\"id\": 5, \"description\": \"TV\"}", parsed.body.unwrap()); + assert!(!parsed.is_base64_encoded); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_headers_multi_values() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("abcd", parsed.headers.get_all("multi").iter().next().unwrap()); + assert_eq!("DEF", parsed.headers.get_all("multi").iter().nth(1).unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v1_deserialize_query_string_map() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v1-request.json"); + let parsed: VpcLatticeRequestV1 = serde_json::from_slice(data).unwrap(); + + assert_eq!("electronics", parsed.query_string_parameters.first("category").unwrap()); + assert_eq!("tv", parsed.query_string_parameters.first("tags").unwrap()); + } +} diff --git a/lambda-events/src/event/vpc_lattice/v2.rs b/lambda-events/src/event/vpc_lattice/v2.rs new file mode 100644 index 00000000..0b89d5f4 --- /dev/null +++ b/lambda-events/src/event/vpc_lattice/v2.rs @@ -0,0 +1,240 @@ +use crate::custom_serde::{ + deserialize_headers, deserialize_nullish_boolean, http_method, serialize_multi_value_headers, +}; +use http::{HeaderMap, Method}; +use query_map::QueryMap; +use serde::{Deserialize, Serialize}; +#[cfg(feature = "catch-all-fields")] +use serde_json::Value; + +/// `VpcLatticeRequestV2` contains data coming from VPC Lattice service (V2 format) +/// see: +/// for field definitions. +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2 { + /// The version of the event structure (always "2.0" for V2) + #[serde(default)] + pub version: Option, + + /// The url path for the request + #[serde(default)] + pub path: Option, + + /// The HTTP method of the request + #[serde(with = "http_method")] + pub method: Method, + + /// HTTP headers of the request (VPC Lattice V2 uses arrays for multi-values) + #[serde(default, deserialize_with = "deserialize_headers")] + #[serde(serialize_with = "serialize_multi_value_headers")] + pub headers: HeaderMap, + + /// HTTP query string parameters (VPC Lattice V2 uses arrays for multi-values) + #[serde(default)] + pub query_string_parameters: QueryMap, + + /// The request body + #[serde(default)] + pub body: Option, + + /// Whether the body is base64 encoded + #[serde(default, deserialize_with = "deserialize_nullish_boolean")] + pub is_base64_encoded: bool, + + /// VPC Lattice specific request context + #[serde(bound = "")] + pub request_context: VpcLatticeRequestV2Context, + + /// Catchall to catch any additional fields that were present but not explicitly defined by this struct. + /// Enabled with Cargo feature `catch-all-fields`. + /// If `catch-all-fields` is disabled, any additional fields that are present will be ignored. + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// VPC Lattice specific request context +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Context { + /// ARN of the service network that delivers the request + #[serde(default)] + pub service_network_arn: Option, + + /// ARN of the service that receives the request + #[serde(default)] + pub service_arn: Option, + + /// ARN of the target group that receives the request + #[serde(default)] + pub target_group_arn: Option, + + /// Identity information for the request + #[serde(default)] + pub identity: Option, + + /// AWS region where the request is processed + #[serde(default)] + pub region: Option, + + /// Time of the request in microseconds since epoch + #[serde(default)] + pub time_epoch: Option, + + /// Catchall for additional context fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +/// Identity information in VPC Lattice request context +#[non_exhaustive] +#[derive(Clone, Debug, Default, Deserialize, PartialEq, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct VpcLatticeRequestV2Identity { + /// ARN of the VPC where the request originated + #[serde(default)] + pub source_vpc_arn: Option, + + /// Type of authentication (e.g., "AWS_IAM") + #[serde(rename = "type")] + #[serde(default)] + pub identity_type: Option, + + /// The authenticated principal + #[serde(default)] + pub principal: Option, + + /// Organization ID of the authenticated principal + #[serde(rename = "principalOrgID")] + #[serde(default)] + pub principal_org_id: Option, + + /// Name of the authenticated session + #[serde(default)] + pub session_name: Option, + + /// X.509 certificate fields (for Roles Anywhere) + #[serde(rename = "x509IssuerOu")] + #[serde(default)] + pub x509_issuer_ou: Option, + #[serde(rename = "x509SanDns")] + #[serde(default)] + pub x509_san_dns: Option, + #[serde(rename = "x509SanNameCn")] + #[serde(default)] + pub x509_san_name_cn: Option, + #[serde(rename = "x509SanUri")] + #[serde(default)] + pub x509_san_uri: Option, + #[serde(rename = "x509SubjectCn")] + #[serde(default)] + pub x509_subject_cn: Option, + + /// Catchall for additional identity fields + #[cfg(feature = "catch-all-fields")] + #[cfg_attr(docsrs, doc(cfg(feature = "catch-all-fields")))] + #[serde(flatten)] + pub other: serde_json::Map, +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_deserialize() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + + assert_eq!("/health", parsed.path.unwrap()); + assert_eq!("GET", parsed.method); + assert_eq!( + "curl/7.68.0", + parsed.headers.get_all("user-agent").iter().next().unwrap() + ); + + // headers including testing multi-values + let header_multi = parsed.headers.get_all("multi"); + assert_eq!("x", header_multi.iter().next().unwrap()); + assert_eq!("y", header_multi.iter().nth(1).unwrap()); + + // query string including testing multi-values + assert_eq!("prod", parsed.query_string_parameters.first("state").unwrap()); + let query_multi = parsed.query_string_parameters.all("multi").unwrap(); + assert_eq!(&"a", query_multi.first().unwrap()); + assert_eq!(&"DEF", query_multi.get(1).unwrap()); + assert_eq!(&"g", query_multi.get(2).unwrap()); + + assert!(parsed.body.is_none()); + assert!(!parsed.is_base64_encoded); + + // nested structure testing + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + parsed.request_context.service_arn.unwrap() + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + parsed.request_context.service_network_arn.unwrap() + ); + assert_eq!( + "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + parsed.request_context.target_group_arn.unwrap() + ); + assert_eq!("ap-southeast-2", parsed.request_context.region.unwrap()); + assert_eq!("1724875399456789", parsed.request_context.time_epoch.unwrap()); + + let context = parsed.request_context.identity.as_ref().unwrap(); + + // identity + assert_eq!( + "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + context.clone().source_vpc_arn.unwrap() + ); + assert_eq!("AWS_IAM", context.clone().identity_type.unwrap()); + assert_eq!( + "arn:aws:iam::123456789012:role/service-role/HealthChecker", + context.clone().principal.unwrap() + ); + assert_eq!("o-50dc6c495c0c9188", context.clone().principal_org_id.unwrap()); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip() { + // our basic example has instances of multi-value headers and multi-value parameters + // so this test covers both those serialization edge cases + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_base64_body() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-base64.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } + + #[test] + #[cfg(feature = "vpc_lattice")] + fn example_vpc_lattice_v2_serde_round_trip_role_anywhere() { + let data = include_bytes!("../../fixtures/example-vpc-lattice-v2-request-roles-anywhere.json"); + let parsed: VpcLatticeRequestV2 = serde_json::from_slice(data).unwrap(); + let output: String = serde_json::to_string(&parsed).unwrap(); + let reparsed: VpcLatticeRequestV2 = serde_json::from_slice(output.as_bytes()).unwrap(); + assert_eq!(parsed, reparsed); + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-response.json b/lambda-events/src/fixtures/example-vpc-lattice-response.json new file mode 100644 index 00000000..70a95cbf --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-response.json @@ -0,0 +1,11 @@ +{ + "statusCode": 200, + "statusDescription": "200 OK", + "headers": { + "content-type": "application/json", + "x-request-id": "req-123456789", + "cache-control": "no-cache" + }, + "body": "{\"orderId\": \"order-789\", \"status\": \"created\", \"message\": \"Order successfully created\"}", + "isBase64Encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json new file mode 100644 index 00000000..bda2de8f --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v1-request.json @@ -0,0 +1,19 @@ +{ + "raw_path": "/api/product", + "method": "POST", + "headers": { + "accept": "*/*", + "user-agent": "curl/7.68.0", + "x-forwarded-for": "10.0.2.100", + "authorization": "Bearer abc123def456", + "multi": "abcd, DEF" + }, + "query_string_parameters": { + "category": "electronics", + "sort": "price", + "limit": "10", + "tags": "tv" + }, + "body": "{\"id\": 5, \"description\": \"TV\"}", + "is_base64_encoded": false +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json new file mode 100644 index 00000000..8cf238b6 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-base64.json @@ -0,0 +1,29 @@ +{ + "version": "2.0", + "path": "/api/files/upload", + "method": "POST", + "headers": { + "content-type": ["multipart/form-data; boundary=----WebKitFormBoundary7MA4YWxkTrZu0gW"], + "content-encoding": ["gzip"], + "content-length": ["1024"], + "x-forwarded-for": ["10.0.1.45"] + }, + "queryStringParameters": { + "uploadType": ["profile-image"] + }, + "body": "H4sIAAAAAAAAA+3BMQEAAADCoPVPbQwfoAAAAAAAAAAAAAAAAAAAAIC3AYbSVKsAQAAA", + "isBase64Encoded": true, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:user/john.developer", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875299234567" + } +} \ No newline at end of file diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json new file mode 100644 index 00000000..f4d1dbb4 --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request-roles-anywhere.json @@ -0,0 +1,37 @@ +{ + "version": "2.0", + "path": "/api/external/data-sync", + "method": "POST", + "headers": { + "content-type": ["application/json"], + "user-agent": ["ExternalDataSync/2.1.0"], + "x-forwarded-for": ["203.45.67.89"], + "x-client-cert": ["present"], + "authorization": ["X509-Cert"] + }, + "queryStringParameters": { + "sync-type": ["incremental"], + "validate": ["true"] + }, + "body": "{\"timestamp\": \"2025-08-28T14:30:00Z\", \"records\": [{\"id\": \"ext-001\", \"data\": \"sample-data\"}], \"source\": \"external-partner-system\"}", + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/ExternalPartnerAccessRole", + "principalOrgID": "o-50dc6c495c0c9188", + "sessionName": "external-data-sync-session", + "x509IssuerOu": "Engineering Department", + "x509SanDns": "partner-system.external-company.com", + "x509SanNameCn": "Data Sync Service", + "x509SanUri": "https://partner-system.external-company.com/sync", + "x509SubjectCn": "external-partner-data-sync.external-company.com" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875199177430" + } +} diff --git a/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json new file mode 100644 index 00000000..6901a37b --- /dev/null +++ b/lambda-events/src/fixtures/example-vpc-lattice-v2-request.json @@ -0,0 +1,30 @@ +{ + "version": "2.0", + "path": "/health", + "method": "GET", + "headers": { + "accept": ["*/*"], + "user-agent": ["curl/7.68.0"], + "x-forwarded-for": ["10.0.2.100"], + "multi": ["x", "y"] + }, + "queryStringParameters": { + "state": ["prod"], + "multi": ["a", "DEF", "g"] + }, + "body": null, + "isBase64Encoded": false, + "requestContext": { + "serviceNetworkArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:servicenetwork/sn-0bf3f2882e9cc805a", + "serviceArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:service/svc-0a40eebed65f8d69c", + "targetGroupArn": "arn:aws:vpc-lattice:ap-southeast-2:123456789012:targetgroup/tg-6d0ecf831eec9f09", + "identity": { + "sourceVpcArn": "arn:aws:ec2:ap-southeast-2:123456789012:vpc/vpc-0b8276c84697e7339", + "type": "AWS_IAM", + "principal": "arn:aws:iam::123456789012:role/service-role/HealthChecker", + "principalOrgID": "o-50dc6c495c0c9188" + }, + "region": "ap-southeast-2", + "timeEpoch": "1724875399456789" + } +} \ No newline at end of file diff --git a/lambda-events/src/lib.rs b/lambda-events/src/lib.rs index d35dbd76..38067099 100644 --- a/lambda-events/src/lib.rs +++ b/lambda-events/src/lib.rs @@ -226,3 +226,8 @@ pub use event::documentdb; #[cfg(feature = "eventbridge")] #[cfg_attr(docsrs, doc(cfg(feature = "eventbridge")))] pub use event::eventbridge; + +/// AWS Lambda event definitions for VPC lattice. +#[cfg(feature = "vpc_lattice")] +#[cfg_attr(docsrs, doc(cfg(feature = "vpc_lattice")))] +pub use event::vpc_lattice;