1use std::collections::BTreeMap;
2use std::sync::Arc;
3
4use async_trait::async_trait;
5use chrono::Utc;
6use http::{Method, StatusCode};
7use serde_json::{json, Value};
8use sha2::{Digest, Sha256};
9use tokio::sync::Mutex as AsyncMutex;
10
11use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
12use fakecloud_persistence::SnapshotStore;
13
14use crate::runtime::ContainerRuntime;
15use crate::state::{
16 EventSourceMapping, LambdaFunction, LambdaSnapshot, LambdaState, SharedLambdaState,
17 LAMBDA_SNAPSHOT_SCHEMA_VERSION,
18};
19
20fn invalid_param(msg: impl Into<String>) -> AwsServiceError {
21 AwsServiceError::aws_error(
22 StatusCode::BAD_REQUEST,
23 "InvalidParameterValueException",
24 msg,
25 )
26}
27
28fn check_len(field: &str, v: &str, min: usize, max: usize) -> Result<(), AwsServiceError> {
29 if v.len() < min || v.len() > max {
30 return Err(invalid_param(format!(
31 "{field} length must be in [{min},{max}], got {}",
32 v.len()
33 )));
34 }
35 Ok(())
36}
37
38fn check_optional_len(
39 field: &str,
40 v: Option<&str>,
41 min: usize,
42 max: usize,
43) -> Result<(), AwsServiceError> {
44 if let Some(s) = v {
45 check_len(field, s, min, max)?;
46 }
47 Ok(())
48}
49
50fn check_optional_int_range(
51 field: &str,
52 v: Option<i64>,
53 min: i64,
54 max: i64,
55) -> Result<(), AwsServiceError> {
56 if let Some(n) = v {
57 if n < min || n > max {
58 return Err(invalid_param(format!(
59 "{field} must be in [{min},{max}], got {n}"
60 )));
61 }
62 }
63 Ok(())
64}
65
66const LAMBDA_PUBLISH_TO_VALUES: &[&str] = &["LATEST_PUBLISHED"];
67
68const LAMBDA_RUNTIMES: &[&str] = &[
74 "nodejs",
75 "nodejs4.3",
76 "nodejs4.3-edge",
77 "nodejs6.10",
78 "nodejs8.10",
79 "nodejs10.x",
80 "nodejs12.x",
81 "nodejs14.x",
82 "nodejs16.x",
83 "nodejs18.x",
84 "nodejs20.x",
85 "nodejs22.x",
86 "nodejs24.x",
87 "java8",
88 "java8.al2",
89 "java11",
90 "java17",
91 "java21",
92 "java25",
93 "python2.7",
94 "python3.6",
95 "python3.7",
96 "python3.8",
97 "python3.9",
98 "python3.10",
99 "python3.11",
100 "python3.12",
101 "python3.13",
102 "python3.14",
103 "dotnetcore1.0",
104 "dotnetcore2.0",
105 "dotnetcore2.1",
106 "dotnetcore3.1",
107 "dotnet6",
108 "dotnet8",
109 "dotnet10",
110 "go1.x",
111 "ruby2.5",
112 "ruby2.7",
113 "ruby3.2",
114 "ruby3.3",
115 "ruby3.4",
116 "provided",
117 "provided.al2",
118 "provided.al2023",
119];
120
121fn check_optional_enum(
122 field: &str,
123 v: Option<&str>,
124 allowed: &[&str],
125) -> Result<(), AwsServiceError> {
126 if let Some(s) = v {
127 if !allowed.contains(&s) {
128 return Err(invalid_param(format!(
129 "{field} must be one of the enum values, got '{s}'"
130 )));
131 }
132 }
133 Ok(())
134}
135
136fn prevalidate_lambda(action: &str, req: &AwsRequest) -> Result<(), AwsServiceError> {
137 let body: Value = serde_json::from_slice(&req.body).unwrap_or(Value::Null);
138 match action {
139 "CreateFunction" => {
140 check_optional_len("Description", body["Description"].as_str(), 0, 256)?;
141 check_optional_len("Handler", body["Handler"].as_str(), 0, 128)?;
142 check_optional_int_range("MemorySize", body["MemorySize"].as_i64(), 128, 32768)?;
143 check_optional_int_range("Timeout", body["Timeout"].as_i64(), 1, 5400)?;
144 check_optional_enum("Runtime", body["Runtime"].as_str(), LAMBDA_RUNTIMES)?;
145 }
146 "PublishVersion" => {
147 check_optional_len("Description", body["Description"].as_str(), 0, 256)?;
148 check_optional_enum(
149 "PublishTo",
150 body["PublishTo"].as_str(),
151 LAMBDA_PUBLISH_TO_VALUES,
152 )?;
153 }
154 "UpdateFunctionCode" => {
155 check_optional_enum(
156 "PublishTo",
157 body["PublishTo"].as_str(),
158 LAMBDA_PUBLISH_TO_VALUES,
159 )?;
160 check_optional_len("S3Bucket", body["S3Bucket"].as_str(), 3, 63)?;
161 check_optional_len("S3Key", body["S3Key"].as_str(), 1, 1024)?;
162 check_optional_len("S3ObjectVersion", body["S3ObjectVersion"].as_str(), 1, 1024)?;
163 }
164 "UpdateFunctionConfiguration" => {
165 check_optional_len("Description", body["Description"].as_str(), 0, 256)?;
166 check_optional_len("Handler", body["Handler"].as_str(), 0, 128)?;
167 check_optional_int_range("MemorySize", body["MemorySize"].as_i64(), 128, 32768)?;
168 check_optional_int_range("Timeout", body["Timeout"].as_i64(), 1, 5400)?;
169 check_optional_enum("Runtime", body["Runtime"].as_str(), LAMBDA_RUNTIMES)?;
170 }
171 _ => {}
172 }
173 Ok(())
174}
175
176pub(crate) fn action_takes_function_name(action: &str) -> bool {
181 matches!(
182 action,
183 "GetFunction"
184 | "DeleteFunction"
185 | "Invoke"
186 | "InvokeAsync"
187 | "InvokeWithResponseStream"
188 | "PublishVersion"
189 | "ListVersionsByFunction"
190 | "AddPermission"
191 | "RemovePermission"
192 | "GetPolicy"
193 | "GetFunctionConfiguration"
194 | "UpdateFunctionConfiguration"
195 | "UpdateFunctionCode"
196 | "GetFunctionConcurrency"
197 | "PutFunctionConcurrency"
198 | "DeleteFunctionConcurrency"
199 | "PutProvisionedConcurrencyConfig"
200 | "GetProvisionedConcurrencyConfig"
201 | "DeleteProvisionedConcurrencyConfig"
202 | "ListProvisionedConcurrencyConfigs"
203 | "PutFunctionEventInvokeConfig"
204 | "UpdateFunctionEventInvokeConfig"
205 | "GetFunctionEventInvokeConfig"
206 | "DeleteFunctionEventInvokeConfig"
207 | "ListFunctionEventInvokeConfigs"
208 | "CreateFunctionUrlConfig"
209 | "UpdateFunctionUrlConfig"
210 | "GetFunctionUrlConfig"
211 | "DeleteFunctionUrlConfig"
212 | "ListFunctionUrlConfigs"
213 | "PutFunctionCodeSigningConfig"
214 | "GetFunctionCodeSigningConfig"
215 | "DeleteFunctionCodeSigningConfig"
216 | "GetFunctionScalingConfig"
217 | "PutFunctionScalingConfig"
218 | "PutFunctionRecursionConfig"
219 | "GetFunctionRecursionConfig"
220 | "CreateAlias"
221 | "GetAlias"
222 | "ListAliases"
223 | "UpdateAlias"
224 | "DeleteAlias"
225 | "PutRuntimeManagementConfig"
226 | "GetRuntimeManagementConfig"
227 )
228}
229
230pub(crate) fn iam_action_name_for(op: &str) -> Option<&'static str> {
260 let action = match op {
261 "Invoke" => "InvokeFunction",
263 "InvokeWithResponseStream" => "InvokeFunction",
264 "GetLayerVersionByArn" => "GetLayerVersion",
265
266 "CreateFunction" => "CreateFunction",
268 "ListFunctions" => "ListFunctions",
269 "GetFunction" => "GetFunction",
270 "DeleteFunction" => "DeleteFunction",
271 "InvokeAsync" => "InvokeAsync",
272 "UpdateFunctionCode" => "UpdateFunctionCode",
273 "UpdateFunctionConfiguration" => "UpdateFunctionConfiguration",
274 "GetFunctionConfiguration" => "GetFunctionConfiguration",
275 "PublishVersion" => "PublishVersion",
276 "ListVersionsByFunction" => "ListVersionsByFunction",
277 "GetAccountSettings" => "GetAccountSettings",
278
279 "AddPermission" => "AddPermission",
281 "RemovePermission" => "RemovePermission",
282 "GetPolicy" => "GetPolicy",
283
284 "CreateAlias" => "CreateAlias",
286 "GetAlias" => "GetAlias",
287 "UpdateAlias" => "UpdateAlias",
288 "DeleteAlias" => "DeleteAlias",
289 "ListAliases" => "ListAliases",
290
291 "PutFunctionConcurrency" => "PutFunctionConcurrency",
293 "GetFunctionConcurrency" => "GetFunctionConcurrency",
294 "DeleteFunctionConcurrency" => "DeleteFunctionConcurrency",
295 "PutProvisionedConcurrencyConfig" => "PutProvisionedConcurrencyConfig",
296 "GetProvisionedConcurrencyConfig" => "GetProvisionedConcurrencyConfig",
297 "DeleteProvisionedConcurrencyConfig" => "DeleteProvisionedConcurrencyConfig",
298 "ListProvisionedConcurrencyConfigs" => "ListProvisionedConcurrencyConfigs",
299
300 "PutFunctionEventInvokeConfig" => "PutFunctionEventInvokeConfig",
302 "GetFunctionEventInvokeConfig" => "GetFunctionEventInvokeConfig",
303 "UpdateFunctionEventInvokeConfig" => "UpdateFunctionEventInvokeConfig",
304 "DeleteFunctionEventInvokeConfig" => "DeleteFunctionEventInvokeConfig",
305 "ListFunctionEventInvokeConfigs" => "ListFunctionEventInvokeConfigs",
306
307 "PutRuntimeManagementConfig" => "PutRuntimeManagementConfig",
309 "GetRuntimeManagementConfig" => "GetRuntimeManagementConfig",
310 "PutFunctionScalingConfig" => "PutFunctionScalingConfig",
311 "GetFunctionScalingConfig" => "GetFunctionScalingConfig",
312 "PutFunctionRecursionConfig" => "PutFunctionRecursionConfig",
313 "GetFunctionRecursionConfig" => "GetFunctionRecursionConfig",
314
315 "CreateFunctionUrlConfig" => "CreateFunctionUrlConfig",
317 "GetFunctionUrlConfig" => "GetFunctionUrlConfig",
318 "UpdateFunctionUrlConfig" => "UpdateFunctionUrlConfig",
319 "DeleteFunctionUrlConfig" => "DeleteFunctionUrlConfig",
320 "ListFunctionUrlConfigs" => "ListFunctionUrlConfigs",
321
322 "CreateEventSourceMapping" => "CreateEventSourceMapping",
324 "ListEventSourceMappings" => "ListEventSourceMappings",
325 "GetEventSourceMapping" => "GetEventSourceMapping",
326 "UpdateEventSourceMapping" => "UpdateEventSourceMapping",
327 "DeleteEventSourceMapping" => "DeleteEventSourceMapping",
328
329 "PublishLayerVersion" => "PublishLayerVersion",
331 "ListLayers" => "ListLayers",
332 "ListLayerVersions" => "ListLayerVersions",
333 "GetLayerVersion" => "GetLayerVersion",
334 "DeleteLayerVersion" => "DeleteLayerVersion",
335 "GetLayerVersionPolicy" => "GetLayerVersionPolicy",
336 "AddLayerVersionPermission" => "AddLayerVersionPermission",
337 "RemoveLayerVersionPermission" => "RemoveLayerVersionPermission",
338
339 "CreateCodeSigningConfig" => "CreateCodeSigningConfig",
341 "GetCodeSigningConfig" => "GetCodeSigningConfig",
342 "UpdateCodeSigningConfig" => "UpdateCodeSigningConfig",
343 "DeleteCodeSigningConfig" => "DeleteCodeSigningConfig",
344 "ListCodeSigningConfigs" => "ListCodeSigningConfigs",
345 "PutFunctionCodeSigningConfig" => "PutFunctionCodeSigningConfig",
346 "GetFunctionCodeSigningConfig" => "GetFunctionCodeSigningConfig",
347 "DeleteFunctionCodeSigningConfig" => "DeleteFunctionCodeSigningConfig",
348 "ListFunctionsByCodeSigningConfig" => "ListFunctionsByCodeSigningConfig",
349
350 "TagResource" => "TagResource",
352 "UntagResource" => "UntagResource",
353 "ListTags" => "ListTags",
354
355 "CreateCapacityProvider" => "CreateCapacityProvider",
357 "GetCapacityProvider" => "GetCapacityProvider",
358 "ListCapacityProviders" => "ListCapacityProviders",
359 "UpdateCapacityProvider" => "UpdateCapacityProvider",
360 "DeleteCapacityProvider" => "DeleteCapacityProvider",
361 "ListFunctionVersionsByCapacityProvider" => "ListFunctionVersionsByCapacityProvider",
362
363 "GetDurableExecution" => "GetDurableExecution",
368 "GetDurableExecutionHistory" => "GetDurableExecutionHistory",
369 "GetDurableExecutionState" => "GetDurableExecutionState",
370 "ListDurableExecutionsByFunction" => "ListDurableExecutionsByFunction",
371 "CheckpointDurableExecution" => "CheckpointDurableExecution",
372 "StopDurableExecution" => "StopDurableExecution",
373 "SendDurableExecutionCallbackSuccess" => "SendDurableExecutionCallbackSuccess",
374 "SendDurableExecutionCallbackFailure" => "SendDurableExecutionCallbackFailure",
375 "SendDurableExecutionCallbackHeartbeat" => "SendDurableExecutionCallbackHeartbeat",
376
377 _ => return None,
378 };
379 Some(action)
380}
381
382fn strip_lambda_arn_prefix(input: &str) -> Option<&str> {
388 let rest = input.strip_prefix("arn:")?;
389 let (partition, after) = rest.split_once(':')?;
390 if partition.is_empty() {
391 return None;
392 }
393 after.strip_prefix("lambda:")
394}
395
396pub(crate) fn normalize_function_name(input: &str) -> String {
397 if input.is_empty() {
398 return String::new();
399 }
400
401 let decoded = percent_encoding::percent_decode_str(input)
406 .decode_utf8_lossy()
407 .into_owned();
408 let input = decoded.as_str();
409
410 if let Some(rest) = strip_lambda_arn_prefix(input) {
412 let parts: Vec<&str> = rest.splitn(5, ':').collect();
413 if parts.len() >= 4 && parts[2] == "function" && !parts[3].is_empty() {
415 return parts[3].to_string();
416 }
417 return input.to_string();
418 }
419
420 let parts: Vec<&str> = input.splitn(4, ':').collect();
422 if parts.len() >= 3 && parts[1] == "function" && parts[0].chars().all(|c| c.is_ascii_digit()) {
423 if !parts[2].is_empty() {
424 return parts[2].to_string();
425 }
426 return input.to_string();
427 }
428
429 if input.matches(':').count() == 1 {
435 if let Some((name, _qualifier)) = input.split_once(':') {
436 if !name.is_empty() && name.chars().all(is_function_name_char) {
437 return name.to_string();
438 }
439 }
440 }
441
442 input.to_string()
443}
444
445fn is_function_name_char(c: char) -> bool {
446 c.is_ascii_alphanumeric() || c == '-' || c == '_'
447}
448
449pub(crate) fn paginate_marker<T, F>(
459 mut items: Vec<T>,
460 marker: Option<&str>,
461 max_items: Option<usize>,
462 marker_of: F,
463) -> (Vec<T>, String)
464where
465 F: Fn(&T) -> String,
466{
467 items.sort_by_key(&marker_of);
468
469 let start = match marker {
470 Some(m) if !m.is_empty() => items
471 .iter()
472 .position(|it| marker_of(it) == m)
473 .map(|p| p + 1)
474 .unwrap_or(items.len()),
475 _ => 0,
476 };
477 if start >= items.len() {
478 return (Vec::new(), String::new());
479 }
480
481 let limit = max_items.filter(|&n| n > 0).unwrap_or(usize::MAX);
482 let end = start.saturating_add(limit).min(items.len());
483 let next_marker = if end < items.len() {
484 marker_of(&items[end - 1])
485 } else {
486 String::new()
487 };
488 let page: Vec<T> = items.drain(start..end).collect();
489 (page, next_marker)
490}
491
492pub(crate) fn marker_page_size(req: &AwsRequest) -> Option<usize> {
495 req.query_params
496 .get("MaxItems")
497 .and_then(|s| s.parse::<usize>().ok())
498}
499
500pub(crate) fn qualifier_from_function_ref(input: &str) -> Option<String> {
507 if input.is_empty() {
508 return None;
509 }
510 let decoded = percent_encoding::percent_decode_str(input)
511 .decode_utf8_lossy()
512 .into_owned();
513 let input = decoded.as_str();
514
515 if let Some(rest) = strip_lambda_arn_prefix(input) {
516 let parts: Vec<&str> = rest.splitn(5, ':').collect();
518 if parts.len() == 5 && parts[2] == "function" && !parts[4].is_empty() {
519 return Some(parts[4].to_string());
520 }
521 return None;
522 }
523 let parts: Vec<&str> = input.splitn(4, ':').collect();
525 if parts.len() == 4
526 && parts[1] == "function"
527 && parts[0].chars().all(|c| c.is_ascii_digit())
528 && !parts[3].is_empty()
529 {
530 return Some(parts[3].to_string());
531 }
532 if input.matches(':').count() == 1 {
534 if let Some((name, qualifier)) = input.split_once(':') {
535 if !name.is_empty() && name.chars().all(is_function_name_char) && !qualifier.is_empty()
536 {
537 return Some(qualifier.to_string());
538 }
539 }
540 }
541 None
542}
543
544pub(crate) fn validate_ephemeral_storage(size: i64) -> Result<i64, AwsServiceError> {
549 if !(512..=10240).contains(&size) {
550 return Err(AwsServiceError::aws_error(
551 StatusCode::BAD_REQUEST,
552 "InvalidParameterValueException",
553 format!(
554 "Value {size} at 'ephemeralStorage.size' failed to satisfy constraint: \
555 Member must satisfy constraint: [Member must have value less than or equal to 10240, \
556 Member must have value greater than or equal to 512]"
557 ),
558 ));
559 }
560 Ok(size)
561}
562
563struct CreateFunctionInput {
567 function_name: String,
568 runtime: String,
569 role: String,
570 handler: String,
571 description: String,
572 timeout: i64,
573 memory_size: i64,
574 package_type: String,
575 tags: BTreeMap<String, String>,
576 environment: BTreeMap<String, String>,
577 architectures: Vec<String>,
578 code_zip: Option<Vec<u8>>,
579 code_fallback: Vec<u8>,
580 image_uri: Option<String>,
581 layer_arns: Vec<String>,
582 tracing_mode: Option<String>,
583 kms_key_arn: Option<String>,
584 ephemeral_storage_size: Option<i64>,
585 vpc_config: Option<serde_json::Value>,
586 snap_start: Option<serde_json::Value>,
587 dead_letter_config_arn: Option<String>,
588 file_system_configs: Vec<serde_json::Value>,
589 logging_config: Option<serde_json::Value>,
590 image_config: Option<serde_json::Value>,
591 durable_config: Option<serde_json::Value>,
592}
593
594impl CreateFunctionInput {
595 fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
596 let function_name = body["FunctionName"]
597 .as_str()
598 .ok_or_else(|| {
599 AwsServiceError::aws_error(
600 StatusCode::BAD_REQUEST,
601 "InvalidParameterValueException",
602 "FunctionName is required",
603 )
604 })?
605 .to_string();
606
607 let tags: BTreeMap<String, String> = body["Tags"]
608 .as_object()
609 .map(|m| {
610 m.iter()
611 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
612 .collect()
613 })
614 .unwrap_or_default();
615
616 let environment: BTreeMap<String, String> = body["Environment"]["Variables"]
617 .as_object()
618 .map(|m| {
619 m.iter()
620 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
621 .collect()
622 })
623 .unwrap_or_default();
624
625 let architectures = body["Architectures"]
626 .as_array()
627 .map(|a| {
628 a.iter()
629 .filter_map(|v| v.as_str().map(|s| s.to_string()))
630 .collect()
631 })
632 .unwrap_or_else(|| vec!["x86_64".to_string()]);
633
634 let code_zip: Option<Vec<u8>> = match body["Code"]["ZipFile"].as_str() {
635 Some(b64) => Some(
636 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64).map_err(
637 |_| {
638 AwsServiceError::aws_error(
639 StatusCode::BAD_REQUEST,
640 "InvalidParameterValueException",
641 "Could not decode Code.ZipFile: invalid base64",
642 )
643 },
644 )?,
645 ),
646 None => None,
647 };
648
649 let code_fallback = serde_json::to_vec(&body["Code"]).unwrap_or_default();
650
651 let package_type = body["PackageType"].as_str().unwrap_or("Zip").to_string();
652 let image_uri = if package_type == "Image" {
657 body["Code"]["ImageUri"].as_str().map(String::from)
658 } else {
659 None
660 };
661
662 if package_type == "Image" && image_uri.is_none() {
666 return Err(AwsServiceError::aws_error(
667 StatusCode::BAD_REQUEST,
668 "InvalidParameterValueException",
669 "Code.ImageUri is required when PackageType is Image",
670 ));
671 }
672
673 let layer_arns: Vec<String> = body["Layers"]
674 .as_array()
675 .map(|arr| {
676 arr.iter()
677 .filter_map(|v| v.as_str().map(String::from))
678 .collect()
679 })
680 .unwrap_or_default();
681
682 let tracing_mode = body["TracingConfig"]["Mode"].as_str().map(String::from);
683 let kms_key_arn = body["KMSKeyArn"].as_str().map(String::from);
684 let ephemeral_storage_size = match body["EphemeralStorage"]["Size"].as_i64() {
685 Some(size) => Some(validate_ephemeral_storage(size)?),
686 None => None,
687 };
688 let vpc_config = body["VpcConfig"]
689 .is_object()
690 .then(|| body["VpcConfig"].clone());
691 let snap_start = body["SnapStart"]
692 .is_object()
693 .then(|| body["SnapStart"].clone());
694 let dead_letter_config_arn = body["DeadLetterConfig"]["TargetArn"]
695 .as_str()
696 .map(String::from);
697 let file_system_configs = body["FileSystemConfigs"]
698 .as_array()
699 .cloned()
700 .unwrap_or_default();
701 let logging_config = body["LoggingConfig"]
702 .is_object()
703 .then(|| body["LoggingConfig"].clone());
704 let image_config = body["ImageConfig"]
705 .is_object()
706 .then(|| body["ImageConfig"].clone());
707 let durable_config = body["DurableConfig"]
708 .is_object()
709 .then(|| body["DurableConfig"].clone());
710
711 Ok(Self {
712 function_name,
713 runtime: body["Runtime"].as_str().unwrap_or("python3.12").to_string(),
714 role: body["Role"].as_str().unwrap_or("").to_string(),
715 handler: body["Handler"]
716 .as_str()
717 .unwrap_or("index.handler")
718 .to_string(),
719 description: body["Description"].as_str().unwrap_or("").to_string(),
720 timeout: body["Timeout"].as_i64().unwrap_or(3),
721 memory_size: body["MemorySize"].as_i64().unwrap_or(128),
722 package_type,
723 tags,
724 environment,
725 architectures,
726 code_zip,
727 code_fallback,
728 image_uri,
729 layer_arns,
730 tracing_mode,
731 kms_key_arn,
732 ephemeral_storage_size,
733 vpc_config,
734 snap_start,
735 dead_letter_config_arn,
736 file_system_configs,
737 logging_config,
738 image_config,
739 durable_config,
740 })
741 }
742}
743
744#[derive(Debug, Clone, Copy, PartialEq, Eq)]
746pub enum InvocationType {
747 RequestResponse,
748 Event,
749 DryRun,
750}
751
752impl InvocationType {
753 pub fn from_header(value: Option<&str>) -> Self {
754 match value {
755 Some("Event") => Self::Event,
756 Some("DryRun") => Self::DryRun,
757 _ => Self::RequestResponse,
758 }
759 }
760}
761
762fn route_to_destination(
766 bus: Arc<fakecloud_core::delivery::DeliveryBus>,
767 function_arn: &str,
768 request_payload: &[u8],
769 result: &Result<Vec<u8>, String>,
770 destination_config: Option<&serde_json::Value>,
771) {
772 let Some(cfg) = destination_config else {
773 return;
774 };
775 let (key, condition, response_value): (&str, &str, serde_json::Value) = match result {
776 Ok(bytes) => (
777 "OnSuccess",
778 "Success",
779 serde_json::from_slice(bytes).unwrap_or(serde_json::Value::Null),
780 ),
781 Err(err) => (
782 "OnFailure",
783 "RetriesExhausted",
784 serde_json::json!({ "errorMessage": err }),
785 ),
786 };
787 let Some(dest) = cfg
788 .get(key)
789 .and_then(|v| v.get("Destination"))
790 .and_then(|v| v.as_str())
791 else {
792 return;
793 };
794 let request_payload_v: serde_json::Value =
795 serde_json::from_slice(request_payload).unwrap_or(serde_json::Value::Null);
796 let record = serde_json::json!({
797 "version": "1.0",
798 "timestamp": chrono::Utc::now().to_rfc3339(),
799 "requestContext": {
800 "requestId": uuid::Uuid::new_v4().to_string(),
801 "functionArn": format!("{function_arn}:$LATEST"),
802 "condition": condition,
803 "approximateInvokeCount": 1,
804 },
805 "requestPayload": request_payload_v,
806 "responseContext": {
807 "statusCode": 200,
808 "executedVersion": "$LATEST",
809 },
810 "responsePayload": response_value,
811 });
812 let body = record.to_string();
813 if dest.contains(":sqs:") {
814 bus.send_to_sqs(dest, &body, &std::collections::HashMap::new());
815 } else if dest.contains(":sns:") {
816 bus.publish_to_sns(dest, &body, None);
817 } else if dest.contains(":lambda:") {
818 let dest = dest.to_string();
819 let payload = body.clone();
820 tokio::spawn(async move {
821 let _ = bus.invoke_lambda(&dest, &payload).await;
822 });
823 } else if dest.contains(":events:") || dest.contains(":eventbridge:") {
824 let detail_type = if result.is_ok() {
825 "Lambda Function Invocation Result - Success"
826 } else {
827 "Lambda Function Invocation Result - Failure"
828 };
829 bus.put_event_to_eventbridge("lambda", detail_type, &body, "default");
830 }
831}
832
833pub(crate) struct ConcurrencyGuard {
839 pub(crate) map: Arc<parking_lot::RwLock<BTreeMap<String, i64>>>,
840 pub(crate) key: String,
841}
842
843impl Drop for ConcurrencyGuard {
844 fn drop(&mut self) {
845 let mut m = self.map.write();
846 let n = m.get(&self.key).copied().unwrap_or(0);
847 if n <= 1 {
848 m.remove(&self.key);
849 } else {
850 m.insert(self.key.clone(), n - 1);
851 }
852 }
853}
854
855fn function_config_unchanged_for_publish(
871 prev: &LambdaFunction,
872 live: &LambdaFunction,
873 effective_description: &str,
874) -> bool {
875 prev.code_sha256 == live.code_sha256
876 && prev.code_size == live.code_size
877 && prev.image_uri == live.image_uri
878 && prev.package_type == live.package_type
879 && prev.runtime == live.runtime
880 && prev.role == live.role
881 && prev.handler == live.handler
882 && prev.description == effective_description
883 && prev.timeout == live.timeout
884 && prev.memory_size == live.memory_size
885 && prev.environment == live.environment
886 && prev.architectures == live.architectures
887 && prev.layers.len() == live.layers.len()
888 && prev
889 .layers
890 .iter()
891 .zip(live.layers.iter())
892 .all(|(a, b)| a.arn == b.arn && a.code_size == b.code_size)
893 && prev.tracing_mode == live.tracing_mode
894 && prev.kms_key_arn == live.kms_key_arn
895 && prev.ephemeral_storage_size == live.ephemeral_storage_size
896 && prev.vpc_config == live.vpc_config
897 && prev.dead_letter_config_arn == live.dead_letter_config_arn
898 && prev.file_system_configs == live.file_system_configs
899 && prev.logging_config == live.logging_config
900 && prev.image_config == live.image_config
901 && prev.signing_profile_version_arn == live.signing_profile_version_arn
902 && prev.signing_job_arn == live.signing_job_arn
903 && prev.runtime_version_config == live.runtime_version_config
904 && snap_start_apply_on_eq(prev.snap_start.as_ref(), live.snap_start.as_ref())
905}
906
907fn snap_start_apply_on_eq(prev: Option<&Value>, live: Option<&Value>) -> bool {
916 let prev_apply = prev
917 .and_then(|v| v.get("ApplyOn"))
918 .and_then(|v| v.as_str())
919 .unwrap_or("None");
920 let live_apply = live
921 .and_then(|v| v.get("ApplyOn"))
922 .and_then(|v| v.as_str())
923 .unwrap_or("None");
924 prev_apply == live_apply
925}
926
927pub(crate) fn resolve_qualifier_to_version(
930 state: &LambdaState,
931 function_name: &str,
932 qualifier: Option<&str>,
933) -> Option<String> {
934 let q = qualifier?;
935 if q == "$LATEST" {
936 return None;
937 }
938 if q.chars().all(|c| c.is_ascii_digit()) {
939 return Some(q.to_string());
940 }
941 let alias_key = format!("{function_name}:{q}");
942 let alias = state.aliases.get(&alias_key)?;
943 let primary = alias.function_version.clone();
944 let routing = alias
945 .routing_config
946 .as_ref()
947 .and_then(|rc| rc.get("AdditionalVersionWeights"))
948 .and_then(|m| m.as_object());
949 let Some(weights) = routing else {
950 return Some(primary);
951 };
952 let mut additional: Vec<(String, f64)> = Vec::with_capacity(weights.len());
955 let mut sum: f64 = 0.0;
956 for (ver, w) in weights {
957 let weight = w.as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
958 sum += weight;
959 additional.push((ver.clone(), weight));
960 }
961 let primary_weight = (1.0 - sum).max(0.0);
962 let pick: f64 = {
963 use std::cell::Cell;
968 thread_local! {
969 static RNG: Cell<u64> = const { Cell::new(0x9E37_79B9_7F4A_7C15) };
970 }
971 let now_nanos = std::time::SystemTime::now()
972 .duration_since(std::time::UNIX_EPOCH)
973 .map(|d| d.as_nanos() as u64)
974 .unwrap_or(0);
975 RNG.with(|cell| {
976 let mut s = cell.get() ^ now_nanos;
977 s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
979 let mut z = s;
980 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
981 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
982 z ^= z >> 31;
983 cell.set(s);
984 (z >> 11) as f64 / ((1u64 << 53) as f64)
985 })
986 };
987 let mut acc = primary_weight;
988 if pick < acc {
989 return Some(primary);
990 }
991 for (ver, w) in &additional {
992 acc += w;
993 if pick < acc {
994 return Some(ver.clone());
995 }
996 }
997 Some(primary)
998}
999
1000pub struct LambdaService {
1001 pub(crate) state: SharedLambdaState,
1002 pub(crate) runtime: Option<Arc<ContainerRuntime>>,
1003 snapshot_store: Option<Arc<dyn SnapshotStore>>,
1004 snapshot_lock: Arc<AsyncMutex<()>>,
1005 pub(crate) delivery_bus: Option<Arc<fakecloud_core::delivery::DeliveryBus>>,
1006 pub(crate) role_trust_validator: Option<Arc<dyn fakecloud_core::auth::RoleTrustValidator>>,
1007 pub(crate) s3_delivery: Option<Arc<dyn fakecloud_core::delivery::S3Delivery>>,
1008 pub(crate) inflight_invocations: Arc<parking_lot::RwLock<BTreeMap<String, i64>>>,
1015}
1016
1017mod functions;
1018mod init;
1019mod invoke;
1020mod publish;
1021
1022impl LambdaService {
1023 pub fn new(state: SharedLambdaState) -> Self {
1024 Self {
1025 state,
1026 runtime: None,
1027 snapshot_store: None,
1028 snapshot_lock: Arc::new(AsyncMutex::new(())),
1029 delivery_bus: None,
1030 role_trust_validator: None,
1031 s3_delivery: None,
1032 inflight_invocations: Arc::new(parking_lot::RwLock::new(BTreeMap::new())),
1033 }
1034 }
1035
1036 pub fn with_s3_delivery(mut self, s3: Arc<dyn fakecloud_core::delivery::S3Delivery>) -> Self {
1037 self.s3_delivery = Some(s3);
1038 self
1039 }
1040
1041 pub fn with_runtime(mut self, runtime: Arc<ContainerRuntime>) -> Self {
1042 self.runtime = Some(runtime);
1043 self
1044 }
1045
1046 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
1047 self.snapshot_store = Some(store);
1048 self
1049 }
1050
1051 pub fn with_delivery_bus(mut self, bus: Arc<fakecloud_core::delivery::DeliveryBus>) -> Self {
1052 self.delivery_bus = Some(bus);
1053 self
1054 }
1055
1056 pub fn with_role_trust_validator(
1057 mut self,
1058 validator: Arc<dyn fakecloud_core::auth::RoleTrustValidator>,
1059 ) -> Self {
1060 self.role_trust_validator = Some(validator);
1061 self
1062 }
1063
1064 async fn save_snapshot(&self) {
1065 save_lambda_snapshot(
1066 &self.state,
1067 self.snapshot_store.clone(),
1068 &self.snapshot_lock,
1069 )
1070 .await;
1071 }
1072
1073 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
1078 let store = self.snapshot_store.clone()?;
1079 let state = self.state.clone();
1080 let lock = self.snapshot_lock.clone();
1081 Some(Arc::new(move || {
1082 let state = state.clone();
1083 let store = store.clone();
1084 let lock = lock.clone();
1085 Box::pin(async move {
1086 save_lambda_snapshot(&state, Some(store), &lock).await;
1087 })
1088 }))
1089 }
1090}
1091
1092pub async fn save_lambda_snapshot(
1098 state: &SharedLambdaState,
1099 store: Option<Arc<dyn SnapshotStore>>,
1100 lock: &AsyncMutex<()>,
1101) {
1102 let Some(store) = store else {
1103 return;
1104 };
1105 let _guard = lock.lock().await;
1106 let snapshot = LambdaSnapshot {
1107 schema_version: LAMBDA_SNAPSHOT_SCHEMA_VERSION,
1108 accounts: Some(state.read().clone()),
1109 state: None,
1110 };
1111 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
1112 let bytes = serde_json::to_vec(&snapshot)
1113 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
1114 store.save(&bytes)
1115 })
1116 .await;
1117 match join {
1118 Ok(Ok(())) => {}
1119 Ok(Err(err)) => tracing::error!(%err, "failed to write lambda snapshot"),
1120 Err(err) => tracing::error!(%err, "lambda snapshot task panicked"),
1121 }
1122}
1123
1124#[async_trait]
1125impl AwsService for LambdaService {
1126 fn service_name(&self) -> &str {
1127 "lambda"
1128 }
1129
1130 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1131 let (action, resource_name) = Self::resolve_action(&req).ok_or_else(|| {
1132 const KNOWN_COLLECTIONS: &[&str] = &[
1143 "functions",
1144 "layers",
1145 "layers-by-arn",
1146 "event-source-mappings",
1147 "tags",
1148 "account-settings",
1149 "code-signing-configs",
1150 ];
1151 let is_known_collection = req
1152 .path_segments
1153 .get(1)
1154 .map(|s| KNOWN_COLLECTIONS.contains(&s.as_str()))
1155 .unwrap_or(false);
1156 if is_known_collection {
1157 AwsServiceError::aws_error(
1158 StatusCode::BAD_REQUEST,
1159 "InvalidParameterValueException",
1160 format!(
1161 "Could not route request {} {} — missing or invalid identifier",
1162 req.method, req.raw_path
1163 ),
1164 )
1165 } else {
1166 AwsServiceError::aws_error(
1167 StatusCode::NOT_FOUND,
1168 "UnknownOperationException",
1169 format!("Unknown operation: {} {}", req.method, req.raw_path),
1170 )
1171 }
1172 })?;
1173
1174 let arn_embedded_qualifier = resource_name
1182 .as_deref()
1183 .and_then(qualifier_from_function_ref);
1184 let resource_name = if action_takes_function_name(action) {
1185 if let Some(raw) = resource_name.as_ref() {
1192 let decoded = crate::extras::percent_decode_for_length(raw);
1197 let len = decoded.chars().count();
1198 let limit = if decoded.starts_with("arn:") {
1209 200
1210 } else {
1211 140
1212 };
1213 if decoded.is_empty() || len > limit {
1214 let (code, msg) = if action == "InvokeAsync" {
1215 (
1216 "ResourceNotFoundException",
1217 format!("Function not found: {}", raw),
1218 )
1219 } else {
1220 (
1221 "InvalidParameterValueException",
1222 format!(
1223 "1 validation error detected: Value '{}' at 'functionName' failed to \
1224 satisfy constraint: Member must have length less than or equal to 140",
1225 raw
1226 ),
1227 )
1228 };
1229 return Err(AwsServiceError::aws_error(
1230 if action == "InvokeAsync" {
1231 StatusCode::NOT_FOUND
1232 } else {
1233 StatusCode::BAD_REQUEST
1234 },
1235 code,
1236 msg,
1237 ));
1238 }
1239 }
1240 resource_name.map(|s| normalize_function_name(&s))
1241 } else {
1242 resource_name
1243 };
1244
1245 if let Some(raw) = req.query_params.get("MaxItems") {
1251 let n = raw.parse::<i64>().map_err(|_| {
1255 AwsServiceError::aws_error(
1256 StatusCode::BAD_REQUEST,
1257 "InvalidParameterValueException",
1258 format!("MaxItems must be a number (got '{raw}')"),
1259 )
1260 })?;
1261 let max = match action {
1265 "ListLayers"
1266 | "ListLayerVersions"
1267 | "ListFunctionUrlConfigs"
1268 | "ListProvisionedConcurrencyConfigs"
1269 | "ListFunctionEventInvokeConfigs" => 50,
1270 _ => 10000,
1271 };
1272 if !(1..=max).contains(&n) {
1273 return Err(AwsServiceError::aws_error(
1274 StatusCode::BAD_REQUEST,
1275 "InvalidParameterValueException",
1276 format!("MaxItems must be between 1 and {} (got {})", max, n),
1277 ));
1278 }
1279 }
1280
1281 if let Some(q) = req.query_params.get("Qualifier") {
1286 let len = q.chars().count();
1287 if q.is_empty() || len > 128 {
1288 return Err(AwsServiceError::aws_error(
1289 StatusCode::BAD_REQUEST,
1290 "InvalidParameterValueException",
1291 format!("Qualifier must be 1..128 characters (got length {})", len),
1292 ));
1293 }
1294 }
1295 if let Some(fv) = req.query_params.get("FunctionVersion") {
1298 let len = fv.chars().count();
1299 if fv.is_empty() || len > 1024 {
1300 return Err(AwsServiceError::aws_error(
1301 StatusCode::BAD_REQUEST,
1302 "InvalidParameterValueException",
1303 format!(
1304 "FunctionVersion must be 1..1024 characters (got length {})",
1305 len
1306 ),
1307 ));
1308 }
1309 }
1310
1311 let mutates = matches!(
1312 action,
1313 "CreateFunction"
1314 | "DeleteFunction"
1315 | "PublishVersion"
1316 | "AddPermission"
1317 | "RemovePermission"
1318 | "CreateEventSourceMapping"
1319 | "DeleteEventSourceMapping"
1320 | "UpdateEventSourceMapping"
1321 | "UpdateFunctionCode"
1322 | "UpdateFunctionConfiguration"
1323 | "CreateAlias"
1324 | "DeleteAlias"
1325 | "UpdateAlias"
1326 | "PublishLayerVersion"
1327 | "DeleteLayerVersion"
1328 | "AddLayerVersionPermission"
1329 | "RemoveLayerVersionPermission"
1330 | "CreateFunctionUrlConfig"
1331 | "DeleteFunctionUrlConfig"
1332 | "UpdateFunctionUrlConfig"
1333 | "PutFunctionConcurrency"
1334 | "DeleteFunctionConcurrency"
1335 | "PutProvisionedConcurrencyConfig"
1336 | "DeleteProvisionedConcurrencyConfig"
1337 | "CreateCodeSigningConfig"
1338 | "UpdateCodeSigningConfig"
1339 | "DeleteCodeSigningConfig"
1340 | "PutFunctionCodeSigningConfig"
1341 | "DeleteFunctionCodeSigningConfig"
1342 | "PutFunctionEventInvokeConfig"
1343 | "UpdateFunctionEventInvokeConfig"
1344 | "DeleteFunctionEventInvokeConfig"
1345 | "PutRuntimeManagementConfig"
1346 | "PutFunctionScalingConfig"
1347 | "PutFunctionRecursionConfig"
1348 | "TagResource"
1349 | "UntagResource"
1350 | "InvokeAsync"
1351 | "InvokeWithResponseStream"
1352 );
1353
1354 let aid = &req.account_id;
1355 prevalidate_lambda(action, &req)?;
1360 let result = match action {
1361 "CreateFunction" => self.create_function(&req),
1362 "ListFunctions" => self.list_functions(
1363 aid,
1364 req.query_params.get("FunctionVersion").map(String::as_str),
1365 req.query_params.get("Marker").map(String::as_str),
1366 marker_page_size(&req),
1367 ),
1368 "GetFunction" => self.get_function(
1369 &req,
1370 resource_name.as_deref().unwrap_or(""),
1371 aid,
1372 req.region.as_str(),
1373 req.query_params.get("Qualifier").map(String::as_str),
1374 ),
1375 "DeleteFunction" => self.delete_function(
1376 resource_name.as_deref().unwrap_or(""),
1377 aid,
1378 req.query_params.get("Qualifier").map(String::as_str),
1379 ),
1380 "Invoke" => {
1381 let invocation_type = InvocationType::from_header(
1382 req.headers
1383 .get("x-amz-invocation-type")
1384 .and_then(|v| v.to_str().ok()),
1385 );
1386 let log_tail = req
1387 .headers
1388 .get("x-amz-log-type")
1389 .and_then(|v| v.to_str().ok())
1390 == Some("Tail");
1391 let qualifier = req
1394 .query_params
1395 .get("Qualifier")
1396 .map(String::as_str)
1397 .or(arn_embedded_qualifier.as_deref());
1398 self.invoke(
1399 resource_name.as_deref().unwrap_or(""),
1400 &req.body,
1401 aid,
1402 invocation_type,
1403 qualifier,
1404 log_tail,
1405 )
1406 .await
1407 }
1408 "InvokeAsync" => {
1409 let name = resource_name.as_deref().unwrap_or("");
1415 let accounts = self.state.read();
1416 let exists = accounts
1417 .get(aid)
1418 .map(|s| s.functions.contains_key(name))
1419 .unwrap_or(false);
1420 if !exists {
1421 Err(AwsServiceError::aws_error(
1422 StatusCode::NOT_FOUND,
1423 "ResourceNotFoundException",
1424 format!("Function not found: {}", name),
1425 ))
1426 } else {
1427 Ok(AwsResponse::json(
1428 StatusCode::ACCEPTED,
1429 json!({ "Status": 202 }).to_string(),
1430 ))
1431 }
1432 }
1433 "PublishVersion" => {
1434 self.publish_version(resource_name.as_deref().unwrap_or(""), aid, &req)
1435 }
1436 "AddPermission" => self.add_permission(resource_name.as_deref().unwrap_or(""), &req),
1437 "GetPolicy" => self.get_policy(
1438 resource_name.as_deref().unwrap_or(""),
1439 aid,
1440 req.query_params.get("Qualifier").map(String::as_str),
1441 ),
1442 "RemovePermission" => {
1443 let sid = req.path_segments.get(4).cloned().unwrap_or_default();
1445 self.remove_permission(
1446 resource_name.as_deref().unwrap_or(""),
1447 &sid,
1448 aid,
1449 req.query_params.get("Qualifier").map(String::as_str),
1450 )
1451 }
1452 "CreateEventSourceMapping" => self.create_event_source_mapping(&req),
1453 "ListEventSourceMappings" => {
1454 if let Some(fn_name) = req.query_params.get("FunctionName") {
1458 let len = fn_name.chars().count();
1459 if fn_name.is_empty() || len > 140 {
1460 return Err(AwsServiceError::aws_error(
1461 StatusCode::BAD_REQUEST,
1462 "InvalidParameterValueException",
1463 "FunctionName must be 1..140 characters",
1464 ));
1465 }
1466 }
1467 self.list_event_source_mappings(aid, &req)
1468 }
1469 "GetEventSourceMapping" => {
1470 self.get_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
1471 }
1472 "DeleteEventSourceMapping" => {
1473 self.delete_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
1474 }
1475 "CreateCapacityProvider" => {
1476 crate::workflows::create_capacity_provider(&self.state, &req, &req.json_body())
1477 }
1478 "GetCapacityProvider" => crate::workflows::get_capacity_provider(
1479 &self.state,
1480 &req,
1481 resource_name.as_deref().unwrap_or(""),
1482 ),
1483 "ListCapacityProviders" => crate::workflows::list_capacity_providers(&self.state, &req),
1484 "UpdateCapacityProvider" => crate::workflows::update_capacity_provider(
1485 &self.state,
1486 &req,
1487 resource_name.as_deref().unwrap_or(""),
1488 &req.json_body(),
1489 ),
1490 "DeleteCapacityProvider" => crate::workflows::delete_capacity_provider(
1491 &self.state,
1492 &req,
1493 resource_name.as_deref().unwrap_or(""),
1494 ),
1495 "ListFunctionVersionsByCapacityProvider" => {
1496 crate::workflows::list_function_versions_by_capacity_provider(
1497 &self.state,
1498 &req,
1499 resource_name.as_deref().unwrap_or(""),
1500 )
1501 }
1502 "GetDurableExecution" => crate::workflows::get_durable_execution(
1503 &self.state,
1504 &req,
1505 resource_name.as_deref().unwrap_or(""),
1506 ),
1507 "GetDurableExecutionHistory" => crate::workflows::get_durable_execution_history(
1508 &self.state,
1509 &req,
1510 resource_name.as_deref().unwrap_or(""),
1511 ),
1512 "GetDurableExecutionState" => crate::workflows::get_durable_execution_state(
1513 &self.state,
1514 &req,
1515 resource_name.as_deref().unwrap_or(""),
1516 ),
1517 "ListDurableExecutionsByFunction" => {
1518 crate::workflows::list_durable_executions_by_function(
1519 &self.state,
1520 &req,
1521 resource_name.as_deref().unwrap_or(""),
1522 )
1523 }
1524 "CheckpointDurableExecution" => crate::workflows::checkpoint_durable_execution(
1525 &self.state,
1526 &req,
1527 resource_name.as_deref().unwrap_or(""),
1528 &req.json_body(),
1529 ),
1530 "StopDurableExecution" => crate::workflows::stop_durable_execution(
1531 &self.state,
1532 &req,
1533 resource_name.as_deref().unwrap_or(""),
1534 ),
1535 "SendDurableExecutionCallbackSuccess" => crate::workflows::send_callback_success(
1536 &self.state,
1537 &req,
1538 resource_name.as_deref().unwrap_or(""),
1539 ),
1540 "SendDurableExecutionCallbackFailure" => crate::workflows::send_callback_failure(
1541 &self.state,
1542 &req,
1543 resource_name.as_deref().unwrap_or(""),
1544 ),
1545 "SendDurableExecutionCallbackHeartbeat" => crate::workflows::send_callback_heartbeat(
1546 &self.state,
1547 &req,
1548 resource_name.as_deref().unwrap_or(""),
1549 ),
1550 other => {
1551 self.handle_extra(other, resource_name.as_deref(), &req)
1552 .await
1553 }
1554 };
1555 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
1556 self.save_snapshot().await;
1557 }
1558 result
1559 }
1560
1561 fn supported_actions(&self) -> &[&str] {
1562 &[
1563 "CreateFunction",
1564 "GetFunction",
1565 "DeleteFunction",
1566 "ListFunctions",
1567 "Invoke",
1568 "InvokeAsync",
1569 "InvokeWithResponseStream",
1570 "PublishVersion",
1571 "ListVersionsByFunction",
1572 "AddPermission",
1573 "RemovePermission",
1574 "GetPolicy",
1575 "CreateEventSourceMapping",
1576 "ListEventSourceMappings",
1577 "GetEventSourceMapping",
1578 "UpdateEventSourceMapping",
1579 "DeleteEventSourceMapping",
1580 "GetFunctionConfiguration",
1581 "UpdateFunctionConfiguration",
1582 "UpdateFunctionCode",
1583 "GetAccountSettings",
1584 "CreateAlias",
1585 "GetAlias",
1586 "ListAliases",
1587 "UpdateAlias",
1588 "DeleteAlias",
1589 "PublishLayerVersion",
1590 "GetLayerVersion",
1591 "GetLayerVersionByArn",
1592 "DeleteLayerVersion",
1593 "ListLayerVersions",
1594 "ListLayers",
1595 "GetLayerVersionPolicy",
1596 "AddLayerVersionPermission",
1597 "RemoveLayerVersionPermission",
1598 "CreateFunctionUrlConfig",
1599 "GetFunctionUrlConfig",
1600 "UpdateFunctionUrlConfig",
1601 "DeleteFunctionUrlConfig",
1602 "ListFunctionUrlConfigs",
1603 "PutFunctionConcurrency",
1604 "GetFunctionConcurrency",
1605 "DeleteFunctionConcurrency",
1606 "PutProvisionedConcurrencyConfig",
1607 "GetProvisionedConcurrencyConfig",
1608 "DeleteProvisionedConcurrencyConfig",
1609 "ListProvisionedConcurrencyConfigs",
1610 "CreateCodeSigningConfig",
1611 "GetCodeSigningConfig",
1612 "UpdateCodeSigningConfig",
1613 "DeleteCodeSigningConfig",
1614 "ListCodeSigningConfigs",
1615 "PutFunctionCodeSigningConfig",
1616 "GetFunctionCodeSigningConfig",
1617 "DeleteFunctionCodeSigningConfig",
1618 "ListFunctionsByCodeSigningConfig",
1619 "PutFunctionEventInvokeConfig",
1620 "GetFunctionEventInvokeConfig",
1621 "UpdateFunctionEventInvokeConfig",
1622 "DeleteFunctionEventInvokeConfig",
1623 "ListFunctionEventInvokeConfigs",
1624 "PutRuntimeManagementConfig",
1625 "GetRuntimeManagementConfig",
1626 "PutFunctionScalingConfig",
1627 "GetFunctionScalingConfig",
1628 "PutFunctionRecursionConfig",
1629 "GetFunctionRecursionConfig",
1630 "TagResource",
1631 "UntagResource",
1632 "ListTags",
1633 "CreateCapacityProvider",
1634 "GetCapacityProvider",
1635 "ListCapacityProviders",
1636 "UpdateCapacityProvider",
1637 "DeleteCapacityProvider",
1638 "ListFunctionVersionsByCapacityProvider",
1639 "GetDurableExecution",
1640 "GetDurableExecutionHistory",
1641 "GetDurableExecutionState",
1642 "ListDurableExecutionsByFunction",
1643 "CheckpointDurableExecution",
1644 "StopDurableExecution",
1645 "SendDurableExecutionCallbackSuccess",
1646 "SendDurableExecutionCallbackFailure",
1647 "SendDurableExecutionCallbackHeartbeat",
1648 ]
1649 }
1650
1651 fn iam_enforceable(&self) -> bool {
1652 true
1653 }
1654
1655 fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
1659 let (action_str, resource_name) = Self::resolve_action(request)?;
1664 let action = iam_action_name_for(action_str)?;
1672 let accounts = self.state.read();
1673 let empty = LambdaState::new(&request.account_id, &request.region);
1674 let state = accounts.get(&request.account_id).unwrap_or(&empty);
1675 let resource = match action_str {
1676 "GetFunction"
1681 | "DeleteFunction"
1682 | "Invoke"
1683 | "InvokeAsync"
1684 | "InvokeWithResponseStream"
1685 | "PublishVersion"
1686 | "ListVersionsByFunction"
1687 | "AddPermission"
1688 | "RemovePermission"
1689 | "GetPolicy"
1690 | "GetFunctionConfiguration"
1691 | "UpdateFunctionConfiguration"
1692 | "UpdateFunctionCode"
1693 | "CreateAlias"
1694 | "GetAlias"
1695 | "UpdateAlias"
1696 | "DeleteAlias"
1697 | "ListAliases"
1698 | "PutFunctionConcurrency"
1699 | "GetFunctionConcurrency"
1700 | "DeleteFunctionConcurrency"
1701 | "PutProvisionedConcurrencyConfig"
1702 | "GetProvisionedConcurrencyConfig"
1703 | "DeleteProvisionedConcurrencyConfig"
1704 | "ListProvisionedConcurrencyConfigs"
1705 | "PutFunctionEventInvokeConfig"
1706 | "GetFunctionEventInvokeConfig"
1707 | "UpdateFunctionEventInvokeConfig"
1708 | "DeleteFunctionEventInvokeConfig"
1709 | "ListFunctionEventInvokeConfigs"
1710 | "PutRuntimeManagementConfig"
1711 | "GetRuntimeManagementConfig"
1712 | "PutFunctionScalingConfig"
1713 | "GetFunctionScalingConfig"
1714 | "PutFunctionRecursionConfig"
1715 | "GetFunctionRecursionConfig"
1716 | "PutFunctionCodeSigningConfig"
1717 | "GetFunctionCodeSigningConfig"
1718 | "DeleteFunctionCodeSigningConfig"
1719 | "CreateFunctionUrlConfig"
1720 | "GetFunctionUrlConfig"
1721 | "UpdateFunctionUrlConfig"
1722 | "DeleteFunctionUrlConfig"
1723 | "ListFunctionUrlConfigs"
1724 | "ListDurableExecutionsByFunction" => {
1725 let raw = resource_name.unwrap_or_default();
1726 if raw.is_empty() {
1727 "*".to_string()
1728 } else {
1729 let name = normalize_function_name(&raw);
1735 format!(
1736 "arn:aws:lambda:{}:{}:function:{}",
1737 state.region, state.account_id, name
1738 )
1739 }
1740 }
1741 "CreateFunction" => {
1742 serde_json::from_slice::<Value>(&request.body)
1747 .ok()
1748 .and_then(|v| {
1749 v.get("FunctionName").and_then(|f| f.as_str()).map(|n| {
1750 format!(
1751 "arn:aws:lambda:{}:{}:function:{}",
1752 state.region, state.account_id, n
1753 )
1754 })
1755 })
1756 .unwrap_or_else(|| "*".to_string())
1757 }
1758 _ => "*".to_string(),
1759 };
1760 Some(fakecloud_core::auth::IamAction {
1761 service: "lambda",
1762 action,
1763 resource,
1764 })
1765 }
1766
1767 fn iam_condition_keys_for(
1768 &self,
1769 request: &AwsRequest,
1770 action: &fakecloud_core::auth::IamAction,
1771 ) -> std::collections::BTreeMap<String, Vec<String>> {
1772 let mut out = std::collections::BTreeMap::new();
1773 if action.action == "AddPermission" {
1774 if action.resource != "*" {
1775 out.insert(
1776 "lambda:functionarn".to_string(),
1777 vec![action.resource.clone()],
1778 );
1779 }
1780 if let Ok(body) = serde_json::from_slice::<Value>(&request.body) {
1781 if let Some(principal) = body.get("Principal").and_then(|p| p.as_str()) {
1782 out.insert("lambda:principal".to_string(), vec![principal.to_string()]);
1783 }
1784 }
1785 }
1786 out
1787 }
1788}
1789
1790#[path = "../service_event_sources.rs"]
1791mod service_event_sources;
1792#[path = "../service_permissions.rs"]
1793mod service_permissions;
1794
1795#[cfg(test)]
1796#[path = "../service_tests.rs"]
1797mod tests;