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 "PublishVersion" => {
140 check_optional_len("Description", body["Description"].as_str(), 0, 256)?;
141 check_optional_enum(
142 "PublishTo",
143 body["PublishTo"].as_str(),
144 LAMBDA_PUBLISH_TO_VALUES,
145 )?;
146 }
147 "UpdateFunctionCode" => {
148 check_optional_enum(
149 "PublishTo",
150 body["PublishTo"].as_str(),
151 LAMBDA_PUBLISH_TO_VALUES,
152 )?;
153 check_optional_len("S3Bucket", body["S3Bucket"].as_str(), 3, 63)?;
154 check_optional_len("S3Key", body["S3Key"].as_str(), 1, 1024)?;
155 check_optional_len("S3ObjectVersion", body["S3ObjectVersion"].as_str(), 1, 1024)?;
156 }
157 "UpdateFunctionConfiguration" => {
158 check_optional_len("Description", body["Description"].as_str(), 0, 256)?;
159 check_optional_len("Handler", body["Handler"].as_str(), 0, 128)?;
160 check_optional_int_range("MemorySize", body["MemorySize"].as_i64(), 128, 32768)?;
161 check_optional_int_range("Timeout", body["Timeout"].as_i64(), 1, i64::MAX)?;
162 check_optional_enum("Runtime", body["Runtime"].as_str(), LAMBDA_RUNTIMES)?;
163 }
164 _ => {}
165 }
166 Ok(())
167}
168
169pub(crate) fn action_takes_function_name(action: &str) -> bool {
174 matches!(
175 action,
176 "GetFunction"
177 | "DeleteFunction"
178 | "Invoke"
179 | "InvokeAsync"
180 | "InvokeWithResponseStream"
181 | "PublishVersion"
182 | "ListVersionsByFunction"
183 | "AddPermission"
184 | "RemovePermission"
185 | "GetPolicy"
186 | "GetFunctionConfiguration"
187 | "UpdateFunctionConfiguration"
188 | "UpdateFunctionCode"
189 | "GetFunctionConcurrency"
190 | "PutFunctionConcurrency"
191 | "DeleteFunctionConcurrency"
192 | "PutProvisionedConcurrencyConfig"
193 | "GetProvisionedConcurrencyConfig"
194 | "DeleteProvisionedConcurrencyConfig"
195 | "ListProvisionedConcurrencyConfigs"
196 | "PutFunctionEventInvokeConfig"
197 | "UpdateFunctionEventInvokeConfig"
198 | "GetFunctionEventInvokeConfig"
199 | "DeleteFunctionEventInvokeConfig"
200 | "ListFunctionEventInvokeConfigs"
201 | "CreateFunctionUrlConfig"
202 | "UpdateFunctionUrlConfig"
203 | "GetFunctionUrlConfig"
204 | "DeleteFunctionUrlConfig"
205 | "ListFunctionUrlConfigs"
206 | "PutFunctionCodeSigningConfig"
207 | "GetFunctionCodeSigningConfig"
208 | "DeleteFunctionCodeSigningConfig"
209 | "GetFunctionScalingConfig"
210 | "PutFunctionScalingConfig"
211 | "PutFunctionRecursionConfig"
212 | "GetFunctionRecursionConfig"
213 | "CreateAlias"
214 | "GetAlias"
215 | "ListAliases"
216 | "UpdateAlias"
217 | "DeleteAlias"
218 | "PutRuntimeManagementConfig"
219 | "GetRuntimeManagementConfig"
220 )
221}
222
223pub(crate) fn iam_action_name_for(op: &str) -> Option<&'static str> {
253 let action = match op {
254 "Invoke" => "InvokeFunction",
256 "InvokeWithResponseStream" => "InvokeFunction",
257 "GetLayerVersionByArn" => "GetLayerVersion",
258
259 "CreateFunction" => "CreateFunction",
261 "ListFunctions" => "ListFunctions",
262 "GetFunction" => "GetFunction",
263 "DeleteFunction" => "DeleteFunction",
264 "InvokeAsync" => "InvokeAsync",
265 "UpdateFunctionCode" => "UpdateFunctionCode",
266 "UpdateFunctionConfiguration" => "UpdateFunctionConfiguration",
267 "GetFunctionConfiguration" => "GetFunctionConfiguration",
268 "PublishVersion" => "PublishVersion",
269 "ListVersionsByFunction" => "ListVersionsByFunction",
270 "GetAccountSettings" => "GetAccountSettings",
271
272 "AddPermission" => "AddPermission",
274 "RemovePermission" => "RemovePermission",
275 "GetPolicy" => "GetPolicy",
276
277 "CreateAlias" => "CreateAlias",
279 "GetAlias" => "GetAlias",
280 "UpdateAlias" => "UpdateAlias",
281 "DeleteAlias" => "DeleteAlias",
282 "ListAliases" => "ListAliases",
283
284 "PutFunctionConcurrency" => "PutFunctionConcurrency",
286 "GetFunctionConcurrency" => "GetFunctionConcurrency",
287 "DeleteFunctionConcurrency" => "DeleteFunctionConcurrency",
288 "PutProvisionedConcurrencyConfig" => "PutProvisionedConcurrencyConfig",
289 "GetProvisionedConcurrencyConfig" => "GetProvisionedConcurrencyConfig",
290 "DeleteProvisionedConcurrencyConfig" => "DeleteProvisionedConcurrencyConfig",
291 "ListProvisionedConcurrencyConfigs" => "ListProvisionedConcurrencyConfigs",
292
293 "PutFunctionEventInvokeConfig" => "PutFunctionEventInvokeConfig",
295 "GetFunctionEventInvokeConfig" => "GetFunctionEventInvokeConfig",
296 "UpdateFunctionEventInvokeConfig" => "UpdateFunctionEventInvokeConfig",
297 "DeleteFunctionEventInvokeConfig" => "DeleteFunctionEventInvokeConfig",
298 "ListFunctionEventInvokeConfigs" => "ListFunctionEventInvokeConfigs",
299
300 "PutRuntimeManagementConfig" => "PutRuntimeManagementConfig",
302 "GetRuntimeManagementConfig" => "GetRuntimeManagementConfig",
303 "PutFunctionScalingConfig" => "PutFunctionScalingConfig",
304 "GetFunctionScalingConfig" => "GetFunctionScalingConfig",
305 "PutFunctionRecursionConfig" => "PutFunctionRecursionConfig",
306 "GetFunctionRecursionConfig" => "GetFunctionRecursionConfig",
307
308 "CreateFunctionUrlConfig" => "CreateFunctionUrlConfig",
310 "GetFunctionUrlConfig" => "GetFunctionUrlConfig",
311 "UpdateFunctionUrlConfig" => "UpdateFunctionUrlConfig",
312 "DeleteFunctionUrlConfig" => "DeleteFunctionUrlConfig",
313 "ListFunctionUrlConfigs" => "ListFunctionUrlConfigs",
314
315 "CreateEventSourceMapping" => "CreateEventSourceMapping",
317 "ListEventSourceMappings" => "ListEventSourceMappings",
318 "GetEventSourceMapping" => "GetEventSourceMapping",
319 "UpdateEventSourceMapping" => "UpdateEventSourceMapping",
320 "DeleteEventSourceMapping" => "DeleteEventSourceMapping",
321
322 "PublishLayerVersion" => "PublishLayerVersion",
324 "ListLayers" => "ListLayers",
325 "ListLayerVersions" => "ListLayerVersions",
326 "GetLayerVersion" => "GetLayerVersion",
327 "DeleteLayerVersion" => "DeleteLayerVersion",
328 "GetLayerVersionPolicy" => "GetLayerVersionPolicy",
329 "AddLayerVersionPermission" => "AddLayerVersionPermission",
330 "RemoveLayerVersionPermission" => "RemoveLayerVersionPermission",
331
332 "CreateCodeSigningConfig" => "CreateCodeSigningConfig",
334 "GetCodeSigningConfig" => "GetCodeSigningConfig",
335 "UpdateCodeSigningConfig" => "UpdateCodeSigningConfig",
336 "DeleteCodeSigningConfig" => "DeleteCodeSigningConfig",
337 "ListCodeSigningConfigs" => "ListCodeSigningConfigs",
338 "PutFunctionCodeSigningConfig" => "PutFunctionCodeSigningConfig",
339 "GetFunctionCodeSigningConfig" => "GetFunctionCodeSigningConfig",
340 "DeleteFunctionCodeSigningConfig" => "DeleteFunctionCodeSigningConfig",
341 "ListFunctionsByCodeSigningConfig" => "ListFunctionsByCodeSigningConfig",
342
343 "TagResource" => "TagResource",
345 "UntagResource" => "UntagResource",
346 "ListTags" => "ListTags",
347
348 "CreateCapacityProvider" => "CreateCapacityProvider",
350 "GetCapacityProvider" => "GetCapacityProvider",
351 "ListCapacityProviders" => "ListCapacityProviders",
352 "UpdateCapacityProvider" => "UpdateCapacityProvider",
353 "DeleteCapacityProvider" => "DeleteCapacityProvider",
354 "ListFunctionVersionsByCapacityProvider" => "ListFunctionVersionsByCapacityProvider",
355
356 "GetDurableExecution" => "GetDurableExecution",
361 "GetDurableExecutionHistory" => "GetDurableExecutionHistory",
362 "GetDurableExecutionState" => "GetDurableExecutionState",
363 "ListDurableExecutionsByFunction" => "ListDurableExecutionsByFunction",
364 "CheckpointDurableExecution" => "CheckpointDurableExecution",
365 "StopDurableExecution" => "StopDurableExecution",
366 "SendDurableExecutionCallbackSuccess" => "SendDurableExecutionCallbackSuccess",
367 "SendDurableExecutionCallbackFailure" => "SendDurableExecutionCallbackFailure",
368 "SendDurableExecutionCallbackHeartbeat" => "SendDurableExecutionCallbackHeartbeat",
369
370 _ => return None,
371 };
372 Some(action)
373}
374
375pub(crate) fn normalize_function_name(input: &str) -> String {
376 if input.is_empty() {
377 return String::new();
378 }
379
380 let decoded = percent_encoding::percent_decode_str(input)
385 .decode_utf8_lossy()
386 .into_owned();
387 let input = decoded.as_str();
388
389 if let Some(rest) = input.strip_prefix("arn:aws:lambda:") {
391 let parts: Vec<&str> = rest.splitn(5, ':').collect();
392 if parts.len() >= 4 && parts[2] == "function" && !parts[3].is_empty() {
394 return parts[3].to_string();
395 }
396 return input.to_string();
397 }
398
399 let parts: Vec<&str> = input.splitn(4, ':').collect();
401 if parts.len() >= 3 && parts[1] == "function" && parts[0].chars().all(|c| c.is_ascii_digit()) {
402 if !parts[2].is_empty() {
403 return parts[2].to_string();
404 }
405 return input.to_string();
406 }
407
408 if input.matches(':').count() == 1 {
414 if let Some((name, _qualifier)) = input.split_once(':') {
415 if !name.is_empty() && name.chars().all(is_function_name_char) {
416 return name.to_string();
417 }
418 }
419 }
420
421 input.to_string()
422}
423
424fn is_function_name_char(c: char) -> bool {
425 c.is_ascii_alphanumeric() || c == '-' || c == '_'
426}
427
428pub(crate) fn validate_ephemeral_storage(size: i64) -> Result<i64, AwsServiceError> {
433 if !(512..=10240).contains(&size) {
434 return Err(AwsServiceError::aws_error(
435 StatusCode::BAD_REQUEST,
436 "InvalidParameterValueException",
437 format!(
438 "Value {size} at 'ephemeralStorage.size' failed to satisfy constraint: \
439 Member must satisfy constraint: [Member must have value less than or equal to 10240, \
440 Member must have value greater than or equal to 512]"
441 ),
442 ));
443 }
444 Ok(size)
445}
446
447struct CreateFunctionInput {
451 function_name: String,
452 runtime: String,
453 role: String,
454 handler: String,
455 description: String,
456 timeout: i64,
457 memory_size: i64,
458 package_type: String,
459 tags: BTreeMap<String, String>,
460 environment: BTreeMap<String, String>,
461 architectures: Vec<String>,
462 code_zip: Option<Vec<u8>>,
463 code_fallback: Vec<u8>,
464 image_uri: Option<String>,
465 layer_arns: Vec<String>,
466 tracing_mode: Option<String>,
467 kms_key_arn: Option<String>,
468 ephemeral_storage_size: Option<i64>,
469 vpc_config: Option<serde_json::Value>,
470 snap_start: Option<serde_json::Value>,
471 dead_letter_config_arn: Option<String>,
472 file_system_configs: Vec<serde_json::Value>,
473 logging_config: Option<serde_json::Value>,
474 image_config: Option<serde_json::Value>,
475 durable_config: Option<serde_json::Value>,
476}
477
478impl CreateFunctionInput {
479 fn from_body(body: &Value) -> Result<Self, AwsServiceError> {
480 let function_name = body["FunctionName"]
481 .as_str()
482 .ok_or_else(|| {
483 AwsServiceError::aws_error(
484 StatusCode::BAD_REQUEST,
485 "InvalidParameterValueException",
486 "FunctionName is required",
487 )
488 })?
489 .to_string();
490
491 let tags: BTreeMap<String, String> = body["Tags"]
492 .as_object()
493 .map(|m| {
494 m.iter()
495 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
496 .collect()
497 })
498 .unwrap_or_default();
499
500 let environment: BTreeMap<String, String> = body["Environment"]["Variables"]
501 .as_object()
502 .map(|m| {
503 m.iter()
504 .filter_map(|(k, v)| v.as_str().map(|s| (k.clone(), s.to_string())))
505 .collect()
506 })
507 .unwrap_or_default();
508
509 let architectures = body["Architectures"]
510 .as_array()
511 .map(|a| {
512 a.iter()
513 .filter_map(|v| v.as_str().map(|s| s.to_string()))
514 .collect()
515 })
516 .unwrap_or_else(|| vec!["x86_64".to_string()]);
517
518 let code_zip: Option<Vec<u8>> = match body["Code"]["ZipFile"].as_str() {
519 Some(b64) => Some(
520 base64::Engine::decode(&base64::engine::general_purpose::STANDARD, b64).map_err(
521 |_| {
522 AwsServiceError::aws_error(
523 StatusCode::BAD_REQUEST,
524 "InvalidParameterValueException",
525 "Could not decode Code.ZipFile: invalid base64",
526 )
527 },
528 )?,
529 ),
530 None => None,
531 };
532
533 let code_fallback = serde_json::to_vec(&body["Code"]).unwrap_or_default();
534
535 let package_type = body["PackageType"].as_str().unwrap_or("Zip").to_string();
536 let image_uri = if package_type == "Image" {
541 body["Code"]["ImageUri"].as_str().map(String::from)
542 } else {
543 None
544 };
545
546 if package_type == "Image" && image_uri.is_none() {
550 return Err(AwsServiceError::aws_error(
551 StatusCode::BAD_REQUEST,
552 "InvalidParameterValueException",
553 "Code.ImageUri is required when PackageType is Image",
554 ));
555 }
556
557 let layer_arns: Vec<String> = body["Layers"]
558 .as_array()
559 .map(|arr| {
560 arr.iter()
561 .filter_map(|v| v.as_str().map(String::from))
562 .collect()
563 })
564 .unwrap_or_default();
565
566 let tracing_mode = body["TracingConfig"]["Mode"].as_str().map(String::from);
567 let kms_key_arn = body["KMSKeyArn"].as_str().map(String::from);
568 let ephemeral_storage_size = match body["EphemeralStorage"]["Size"].as_i64() {
569 Some(size) => Some(validate_ephemeral_storage(size)?),
570 None => None,
571 };
572 let vpc_config = body["VpcConfig"]
573 .is_object()
574 .then(|| body["VpcConfig"].clone());
575 let snap_start = body["SnapStart"]
576 .is_object()
577 .then(|| body["SnapStart"].clone());
578 let dead_letter_config_arn = body["DeadLetterConfig"]["TargetArn"]
579 .as_str()
580 .map(String::from);
581 let file_system_configs = body["FileSystemConfigs"]
582 .as_array()
583 .cloned()
584 .unwrap_or_default();
585 let logging_config = body["LoggingConfig"]
586 .is_object()
587 .then(|| body["LoggingConfig"].clone());
588 let image_config = body["ImageConfig"]
589 .is_object()
590 .then(|| body["ImageConfig"].clone());
591 let durable_config = body["DurableConfig"]
592 .is_object()
593 .then(|| body["DurableConfig"].clone());
594
595 Ok(Self {
596 function_name,
597 runtime: body["Runtime"].as_str().unwrap_or("python3.12").to_string(),
598 role: body["Role"].as_str().unwrap_or("").to_string(),
599 handler: body["Handler"]
600 .as_str()
601 .unwrap_or("index.handler")
602 .to_string(),
603 description: body["Description"].as_str().unwrap_or("").to_string(),
604 timeout: body["Timeout"].as_i64().unwrap_or(3),
605 memory_size: body["MemorySize"].as_i64().unwrap_or(128),
606 package_type,
607 tags,
608 environment,
609 architectures,
610 code_zip,
611 code_fallback,
612 image_uri,
613 layer_arns,
614 tracing_mode,
615 kms_key_arn,
616 ephemeral_storage_size,
617 vpc_config,
618 snap_start,
619 dead_letter_config_arn,
620 file_system_configs,
621 logging_config,
622 image_config,
623 durable_config,
624 })
625 }
626}
627
628#[derive(Debug, Clone, Copy, PartialEq, Eq)]
630pub enum InvocationType {
631 RequestResponse,
632 Event,
633 DryRun,
634}
635
636impl InvocationType {
637 pub fn from_header(value: Option<&str>) -> Self {
638 match value {
639 Some("Event") => Self::Event,
640 Some("DryRun") => Self::DryRun,
641 _ => Self::RequestResponse,
642 }
643 }
644}
645
646fn route_to_destination(
650 bus: Arc<fakecloud_core::delivery::DeliveryBus>,
651 function_arn: &str,
652 request_payload: &[u8],
653 result: &Result<Vec<u8>, String>,
654 destination_config: Option<&serde_json::Value>,
655) {
656 let Some(cfg) = destination_config else {
657 return;
658 };
659 let (key, condition, response_value): (&str, &str, serde_json::Value) = match result {
660 Ok(bytes) => (
661 "OnSuccess",
662 "Success",
663 serde_json::from_slice(bytes).unwrap_or(serde_json::Value::Null),
664 ),
665 Err(err) => (
666 "OnFailure",
667 "RetriesExhausted",
668 serde_json::json!({ "errorMessage": err }),
669 ),
670 };
671 let Some(dest) = cfg
672 .get(key)
673 .and_then(|v| v.get("Destination"))
674 .and_then(|v| v.as_str())
675 else {
676 return;
677 };
678 let request_payload_v: serde_json::Value =
679 serde_json::from_slice(request_payload).unwrap_or(serde_json::Value::Null);
680 let record = serde_json::json!({
681 "version": "1.0",
682 "timestamp": chrono::Utc::now().to_rfc3339(),
683 "requestContext": {
684 "requestId": uuid::Uuid::new_v4().to_string(),
685 "functionArn": format!("{function_arn}:$LATEST"),
686 "condition": condition,
687 "approximateInvokeCount": 1,
688 },
689 "requestPayload": request_payload_v,
690 "responseContext": {
691 "statusCode": 200,
692 "executedVersion": "$LATEST",
693 },
694 "responsePayload": response_value,
695 });
696 let body = record.to_string();
697 if dest.contains(":sqs:") {
698 bus.send_to_sqs(dest, &body, &std::collections::HashMap::new());
699 } else if dest.contains(":sns:") {
700 bus.publish_to_sns(dest, &body, None);
701 } else if dest.contains(":lambda:") {
702 let dest = dest.to_string();
703 let payload = body.clone();
704 tokio::spawn(async move {
705 let _ = bus.invoke_lambda(&dest, &payload).await;
706 });
707 } else if dest.contains(":events:") || dest.contains(":eventbridge:") {
708 let detail_type = if result.is_ok() {
709 "Lambda Function Invocation Result - Success"
710 } else {
711 "Lambda Function Invocation Result - Failure"
712 };
713 bus.put_event_to_eventbridge("lambda", detail_type, &body, "default");
714 }
715}
716
717pub(crate) struct ConcurrencyGuard {
723 pub(crate) map: Arc<parking_lot::RwLock<BTreeMap<String, i64>>>,
724 pub(crate) key: String,
725}
726
727impl Drop for ConcurrencyGuard {
728 fn drop(&mut self) {
729 let mut m = self.map.write();
730 let n = m.get(&self.key).copied().unwrap_or(0);
731 if n <= 1 {
732 m.remove(&self.key);
733 } else {
734 m.insert(self.key.clone(), n - 1);
735 }
736 }
737}
738
739fn function_config_unchanged_for_publish(
755 prev: &LambdaFunction,
756 live: &LambdaFunction,
757 effective_description: &str,
758) -> bool {
759 prev.code_sha256 == live.code_sha256
760 && prev.code_size == live.code_size
761 && prev.image_uri == live.image_uri
762 && prev.package_type == live.package_type
763 && prev.runtime == live.runtime
764 && prev.role == live.role
765 && prev.handler == live.handler
766 && prev.description == effective_description
767 && prev.timeout == live.timeout
768 && prev.memory_size == live.memory_size
769 && prev.environment == live.environment
770 && prev.architectures == live.architectures
771 && prev.layers.len() == live.layers.len()
772 && prev
773 .layers
774 .iter()
775 .zip(live.layers.iter())
776 .all(|(a, b)| a.arn == b.arn && a.code_size == b.code_size)
777 && prev.tracing_mode == live.tracing_mode
778 && prev.kms_key_arn == live.kms_key_arn
779 && prev.ephemeral_storage_size == live.ephemeral_storage_size
780 && prev.vpc_config == live.vpc_config
781 && prev.dead_letter_config_arn == live.dead_letter_config_arn
782 && prev.file_system_configs == live.file_system_configs
783 && prev.logging_config == live.logging_config
784 && prev.image_config == live.image_config
785 && prev.signing_profile_version_arn == live.signing_profile_version_arn
786 && prev.signing_job_arn == live.signing_job_arn
787 && prev.runtime_version_config == live.runtime_version_config
788 && snap_start_apply_on_eq(prev.snap_start.as_ref(), live.snap_start.as_ref())
789}
790
791fn snap_start_apply_on_eq(prev: Option<&Value>, live: Option<&Value>) -> bool {
800 let prev_apply = prev
801 .and_then(|v| v.get("ApplyOn"))
802 .and_then(|v| v.as_str())
803 .unwrap_or("None");
804 let live_apply = live
805 .and_then(|v| v.get("ApplyOn"))
806 .and_then(|v| v.as_str())
807 .unwrap_or("None");
808 prev_apply == live_apply
809}
810
811pub(crate) fn resolve_qualifier_to_version(
814 state: &LambdaState,
815 function_name: &str,
816 qualifier: Option<&str>,
817) -> Option<String> {
818 let q = qualifier?;
819 if q == "$LATEST" {
820 return None;
821 }
822 if q.chars().all(|c| c.is_ascii_digit()) {
823 return Some(q.to_string());
824 }
825 let alias_key = format!("{function_name}:{q}");
826 let alias = state.aliases.get(&alias_key)?;
827 let primary = alias.function_version.clone();
828 let routing = alias
829 .routing_config
830 .as_ref()
831 .and_then(|rc| rc.get("AdditionalVersionWeights"))
832 .and_then(|m| m.as_object());
833 let Some(weights) = routing else {
834 return Some(primary);
835 };
836 let mut additional: Vec<(String, f64)> = Vec::with_capacity(weights.len());
839 let mut sum: f64 = 0.0;
840 for (ver, w) in weights {
841 let weight = w.as_f64().unwrap_or(0.0).clamp(0.0, 1.0);
842 sum += weight;
843 additional.push((ver.clone(), weight));
844 }
845 let primary_weight = (1.0 - sum).max(0.0);
846 let pick: f64 = {
847 use std::cell::Cell;
852 thread_local! {
853 static RNG: Cell<u64> = const { Cell::new(0x9E37_79B9_7F4A_7C15) };
854 }
855 let now_nanos = std::time::SystemTime::now()
856 .duration_since(std::time::UNIX_EPOCH)
857 .map(|d| d.as_nanos() as u64)
858 .unwrap_or(0);
859 RNG.with(|cell| {
860 let mut s = cell.get() ^ now_nanos;
861 s = s.wrapping_add(0x9E37_79B9_7F4A_7C15);
863 let mut z = s;
864 z = (z ^ (z >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
865 z = (z ^ (z >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
866 z ^= z >> 31;
867 cell.set(s);
868 (z >> 11) as f64 / ((1u64 << 53) as f64)
869 })
870 };
871 let mut acc = primary_weight;
872 if pick < acc {
873 return Some(primary);
874 }
875 for (ver, w) in &additional {
876 acc += w;
877 if pick < acc {
878 return Some(ver.clone());
879 }
880 }
881 Some(primary)
882}
883
884pub struct LambdaService {
885 pub(crate) state: SharedLambdaState,
886 pub(crate) runtime: Option<Arc<ContainerRuntime>>,
887 snapshot_store: Option<Arc<dyn SnapshotStore>>,
888 snapshot_lock: Arc<AsyncMutex<()>>,
889 pub(crate) delivery_bus: Option<Arc<fakecloud_core::delivery::DeliveryBus>>,
890 pub(crate) role_trust_validator: Option<Arc<dyn fakecloud_core::auth::RoleTrustValidator>>,
891 pub(crate) s3_delivery: Option<Arc<dyn fakecloud_core::delivery::S3Delivery>>,
892 pub(crate) inflight_invocations: Arc<parking_lot::RwLock<BTreeMap<String, i64>>>,
899}
900
901mod functions;
902mod init;
903mod invoke;
904mod publish;
905
906impl LambdaService {
907 pub fn new(state: SharedLambdaState) -> Self {
908 Self {
909 state,
910 runtime: None,
911 snapshot_store: None,
912 snapshot_lock: Arc::new(AsyncMutex::new(())),
913 delivery_bus: None,
914 role_trust_validator: None,
915 s3_delivery: None,
916 inflight_invocations: Arc::new(parking_lot::RwLock::new(BTreeMap::new())),
917 }
918 }
919
920 pub fn with_s3_delivery(mut self, s3: Arc<dyn fakecloud_core::delivery::S3Delivery>) -> Self {
921 self.s3_delivery = Some(s3);
922 self
923 }
924
925 pub fn with_runtime(mut self, runtime: Arc<ContainerRuntime>) -> Self {
926 self.runtime = Some(runtime);
927 self
928 }
929
930 pub fn with_snapshot_store(mut self, store: Arc<dyn SnapshotStore>) -> Self {
931 self.snapshot_store = Some(store);
932 self
933 }
934
935 pub fn with_delivery_bus(mut self, bus: Arc<fakecloud_core::delivery::DeliveryBus>) -> Self {
936 self.delivery_bus = Some(bus);
937 self
938 }
939
940 pub fn with_role_trust_validator(
941 mut self,
942 validator: Arc<dyn fakecloud_core::auth::RoleTrustValidator>,
943 ) -> Self {
944 self.role_trust_validator = Some(validator);
945 self
946 }
947
948 async fn save_snapshot(&self) {
949 save_lambda_snapshot(
950 &self.state,
951 self.snapshot_store.clone(),
952 &self.snapshot_lock,
953 )
954 .await;
955 }
956
957 pub fn snapshot_hook(&self) -> Option<fakecloud_persistence::SnapshotHook> {
962 let store = self.snapshot_store.clone()?;
963 let state = self.state.clone();
964 let lock = self.snapshot_lock.clone();
965 Some(Arc::new(move || {
966 let state = state.clone();
967 let store = store.clone();
968 let lock = lock.clone();
969 Box::pin(async move {
970 save_lambda_snapshot(&state, Some(store), &lock).await;
971 })
972 }))
973 }
974}
975
976pub async fn save_lambda_snapshot(
982 state: &SharedLambdaState,
983 store: Option<Arc<dyn SnapshotStore>>,
984 lock: &AsyncMutex<()>,
985) {
986 let Some(store) = store else {
987 return;
988 };
989 let _guard = lock.lock().await;
990 let snapshot = LambdaSnapshot {
991 schema_version: LAMBDA_SNAPSHOT_SCHEMA_VERSION,
992 accounts: Some(state.read().clone()),
993 state: None,
994 };
995 let join = tokio::task::spawn_blocking(move || -> std::io::Result<()> {
996 let bytes = serde_json::to_vec(&snapshot)
997 .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e.to_string()))?;
998 store.save(&bytes)
999 })
1000 .await;
1001 match join {
1002 Ok(Ok(())) => {}
1003 Ok(Err(err)) => tracing::error!(%err, "failed to write lambda snapshot"),
1004 Err(err) => tracing::error!(%err, "lambda snapshot task panicked"),
1005 }
1006}
1007
1008#[async_trait]
1009impl AwsService for LambdaService {
1010 fn service_name(&self) -> &str {
1011 "lambda"
1012 }
1013
1014 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
1015 let (action, resource_name) = Self::resolve_action(&req).ok_or_else(|| {
1016 const KNOWN_COLLECTIONS: &[&str] = &[
1027 "functions",
1028 "layers",
1029 "layers-by-arn",
1030 "event-source-mappings",
1031 "tags",
1032 "account-settings",
1033 "code-signing-configs",
1034 ];
1035 let is_known_collection = req
1036 .path_segments
1037 .get(1)
1038 .map(|s| KNOWN_COLLECTIONS.contains(&s.as_str()))
1039 .unwrap_or(false);
1040 if is_known_collection {
1041 AwsServiceError::aws_error(
1042 StatusCode::BAD_REQUEST,
1043 "InvalidParameterValueException",
1044 format!(
1045 "Could not route request {} {} — missing or invalid identifier",
1046 req.method, req.raw_path
1047 ),
1048 )
1049 } else {
1050 AwsServiceError::aws_error(
1051 StatusCode::NOT_FOUND,
1052 "UnknownOperationException",
1053 format!("Unknown operation: {} {}", req.method, req.raw_path),
1054 )
1055 }
1056 })?;
1057
1058 let resource_name = if action_takes_function_name(action) {
1063 if let Some(raw) = resource_name.as_ref() {
1070 let decoded = crate::extras::percent_decode_for_length(raw);
1075 let len = decoded.chars().count();
1076 let limit = if decoded.starts_with("arn:") {
1087 200
1088 } else {
1089 140
1090 };
1091 if decoded.is_empty() || len > limit {
1092 let (code, msg) = if action == "InvokeAsync" {
1093 (
1094 "ResourceNotFoundException",
1095 format!("Function not found: {}", raw),
1096 )
1097 } else {
1098 (
1099 "InvalidParameterValueException",
1100 format!(
1101 "1 validation error detected: Value '{}' at 'functionName' failed to \
1102 satisfy constraint: Member must have length less than or equal to 140",
1103 raw
1104 ),
1105 )
1106 };
1107 return Err(AwsServiceError::aws_error(
1108 if action == "InvokeAsync" {
1109 StatusCode::NOT_FOUND
1110 } else {
1111 StatusCode::BAD_REQUEST
1112 },
1113 code,
1114 msg,
1115 ));
1116 }
1117 }
1118 resource_name.map(|s| normalize_function_name(&s))
1119 } else {
1120 resource_name
1121 };
1122
1123 if let Some(raw) = req.query_params.get("MaxItems") {
1129 let n = raw.parse::<i64>().map_err(|_| {
1133 AwsServiceError::aws_error(
1134 StatusCode::BAD_REQUEST,
1135 "InvalidParameterValueException",
1136 format!("MaxItems must be a number (got '{raw}')"),
1137 )
1138 })?;
1139 let max = match action {
1140 "ListLayers"
1141 | "ListLayerVersions"
1142 | "ListFunctionUrlConfigs"
1143 | "ListProvisionedConcurrencyConfigs"
1144 | "ListFunctionEventInvokeConfigs"
1145 | "ListAliases" => 50,
1146 _ => 10000,
1147 };
1148 if !(1..=max).contains(&n) {
1149 return Err(AwsServiceError::aws_error(
1150 StatusCode::BAD_REQUEST,
1151 "InvalidParameterValueException",
1152 format!("MaxItems must be between 1 and {} (got {})", max, n),
1153 ));
1154 }
1155 }
1156
1157 if let Some(q) = req.query_params.get("Qualifier") {
1162 let len = q.chars().count();
1163 if q.is_empty() || len > 128 {
1164 return Err(AwsServiceError::aws_error(
1165 StatusCode::BAD_REQUEST,
1166 "InvalidParameterValueException",
1167 format!("Qualifier must be 1..128 characters (got length {})", len),
1168 ));
1169 }
1170 }
1171 if let Some(fv) = req.query_params.get("FunctionVersion") {
1174 let len = fv.chars().count();
1175 if fv.is_empty() || len > 1024 {
1176 return Err(AwsServiceError::aws_error(
1177 StatusCode::BAD_REQUEST,
1178 "InvalidParameterValueException",
1179 format!(
1180 "FunctionVersion must be 1..1024 characters (got length {})",
1181 len
1182 ),
1183 ));
1184 }
1185 }
1186
1187 let mutates = matches!(
1188 action,
1189 "CreateFunction"
1190 | "DeleteFunction"
1191 | "PublishVersion"
1192 | "AddPermission"
1193 | "RemovePermission"
1194 | "CreateEventSourceMapping"
1195 | "DeleteEventSourceMapping"
1196 | "UpdateEventSourceMapping"
1197 | "UpdateFunctionCode"
1198 | "UpdateFunctionConfiguration"
1199 | "CreateAlias"
1200 | "DeleteAlias"
1201 | "UpdateAlias"
1202 | "PublishLayerVersion"
1203 | "DeleteLayerVersion"
1204 | "AddLayerVersionPermission"
1205 | "RemoveLayerVersionPermission"
1206 | "CreateFunctionUrlConfig"
1207 | "DeleteFunctionUrlConfig"
1208 | "UpdateFunctionUrlConfig"
1209 | "PutFunctionConcurrency"
1210 | "DeleteFunctionConcurrency"
1211 | "PutProvisionedConcurrencyConfig"
1212 | "DeleteProvisionedConcurrencyConfig"
1213 | "CreateCodeSigningConfig"
1214 | "UpdateCodeSigningConfig"
1215 | "DeleteCodeSigningConfig"
1216 | "PutFunctionCodeSigningConfig"
1217 | "DeleteFunctionCodeSigningConfig"
1218 | "PutFunctionEventInvokeConfig"
1219 | "UpdateFunctionEventInvokeConfig"
1220 | "DeleteFunctionEventInvokeConfig"
1221 | "PutRuntimeManagementConfig"
1222 | "PutFunctionScalingConfig"
1223 | "PutFunctionRecursionConfig"
1224 | "TagResource"
1225 | "UntagResource"
1226 | "InvokeAsync"
1227 | "InvokeWithResponseStream"
1228 );
1229
1230 let aid = &req.account_id;
1231 prevalidate_lambda(action, &req)?;
1236 let result = match action {
1237 "CreateFunction" => self.create_function(&req),
1238 "ListFunctions" => self.list_functions(
1239 aid,
1240 req.query_params.get("FunctionVersion").map(String::as_str),
1241 ),
1242 "GetFunction" => self.get_function(
1243 &req,
1244 resource_name.as_deref().unwrap_or(""),
1245 aid,
1246 req.region.as_str(),
1247 req.query_params.get("Qualifier").map(String::as_str),
1248 ),
1249 "DeleteFunction" => self.delete_function(
1250 resource_name.as_deref().unwrap_or(""),
1251 aid,
1252 req.query_params.get("Qualifier").map(String::as_str),
1253 ),
1254 "Invoke" => {
1255 let invocation_type = InvocationType::from_header(
1256 req.headers
1257 .get("x-amz-invocation-type")
1258 .and_then(|v| v.to_str().ok()),
1259 );
1260 let qualifier = req.query_params.get("Qualifier").map(String::as_str);
1261 self.invoke(
1262 resource_name.as_deref().unwrap_or(""),
1263 &req.body,
1264 aid,
1265 invocation_type,
1266 qualifier,
1267 )
1268 .await
1269 }
1270 "InvokeAsync" => {
1271 let name = resource_name.as_deref().unwrap_or("");
1277 let accounts = self.state.read();
1278 let exists = accounts
1279 .get(aid)
1280 .map(|s| s.functions.contains_key(name))
1281 .unwrap_or(false);
1282 if !exists {
1283 Err(AwsServiceError::aws_error(
1284 StatusCode::NOT_FOUND,
1285 "ResourceNotFoundException",
1286 format!("Function not found: {}", name),
1287 ))
1288 } else {
1289 Ok(AwsResponse::json(
1290 StatusCode::ACCEPTED,
1291 json!({ "Status": 202 }).to_string(),
1292 ))
1293 }
1294 }
1295 "PublishVersion" => {
1296 self.publish_version(resource_name.as_deref().unwrap_or(""), aid, &req)
1297 }
1298 "AddPermission" => self.add_permission(resource_name.as_deref().unwrap_or(""), &req),
1299 "GetPolicy" => self.get_policy(
1300 resource_name.as_deref().unwrap_or(""),
1301 aid,
1302 req.query_params.get("Qualifier").map(String::as_str),
1303 ),
1304 "RemovePermission" => {
1305 let sid = req.path_segments.get(4).cloned().unwrap_or_default();
1307 self.remove_permission(
1308 resource_name.as_deref().unwrap_or(""),
1309 &sid,
1310 aid,
1311 req.query_params.get("Qualifier").map(String::as_str),
1312 )
1313 }
1314 "CreateEventSourceMapping" => self.create_event_source_mapping(&req),
1315 "ListEventSourceMappings" => {
1316 if let Some(fn_name) = req.query_params.get("FunctionName") {
1320 let len = fn_name.chars().count();
1321 if fn_name.is_empty() || len > 140 {
1322 return Err(AwsServiceError::aws_error(
1323 StatusCode::BAD_REQUEST,
1324 "InvalidParameterValueException",
1325 "FunctionName must be 1..140 characters",
1326 ));
1327 }
1328 }
1329 self.list_event_source_mappings(aid)
1330 }
1331 "GetEventSourceMapping" => {
1332 self.get_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
1333 }
1334 "DeleteEventSourceMapping" => {
1335 self.delete_event_source_mapping(resource_name.as_deref().unwrap_or(""), aid)
1336 }
1337 "CreateCapacityProvider" => {
1338 crate::workflows::create_capacity_provider(&self.state, &req, &req.json_body())
1339 }
1340 "GetCapacityProvider" => crate::workflows::get_capacity_provider(
1341 &self.state,
1342 &req,
1343 resource_name.as_deref().unwrap_or(""),
1344 ),
1345 "ListCapacityProviders" => crate::workflows::list_capacity_providers(&self.state, &req),
1346 "UpdateCapacityProvider" => crate::workflows::update_capacity_provider(
1347 &self.state,
1348 &req,
1349 resource_name.as_deref().unwrap_or(""),
1350 &req.json_body(),
1351 ),
1352 "DeleteCapacityProvider" => crate::workflows::delete_capacity_provider(
1353 &self.state,
1354 &req,
1355 resource_name.as_deref().unwrap_or(""),
1356 ),
1357 "ListFunctionVersionsByCapacityProvider" => {
1358 crate::workflows::list_function_versions_by_capacity_provider(
1359 &self.state,
1360 &req,
1361 resource_name.as_deref().unwrap_or(""),
1362 )
1363 }
1364 "GetDurableExecution" => crate::workflows::get_durable_execution(
1365 &self.state,
1366 &req,
1367 resource_name.as_deref().unwrap_or(""),
1368 ),
1369 "GetDurableExecutionHistory" => crate::workflows::get_durable_execution_history(
1370 &self.state,
1371 &req,
1372 resource_name.as_deref().unwrap_or(""),
1373 ),
1374 "GetDurableExecutionState" => crate::workflows::get_durable_execution_state(
1375 &self.state,
1376 &req,
1377 resource_name.as_deref().unwrap_or(""),
1378 ),
1379 "ListDurableExecutionsByFunction" => {
1380 crate::workflows::list_durable_executions_by_function(
1381 &self.state,
1382 &req,
1383 resource_name.as_deref().unwrap_or(""),
1384 )
1385 }
1386 "CheckpointDurableExecution" => crate::workflows::checkpoint_durable_execution(
1387 &self.state,
1388 &req,
1389 resource_name.as_deref().unwrap_or(""),
1390 &req.json_body(),
1391 ),
1392 "StopDurableExecution" => crate::workflows::stop_durable_execution(
1393 &self.state,
1394 &req,
1395 resource_name.as_deref().unwrap_or(""),
1396 ),
1397 "SendDurableExecutionCallbackSuccess" => crate::workflows::send_callback_success(
1398 &self.state,
1399 &req,
1400 resource_name.as_deref().unwrap_or(""),
1401 ),
1402 "SendDurableExecutionCallbackFailure" => crate::workflows::send_callback_failure(
1403 &self.state,
1404 &req,
1405 resource_name.as_deref().unwrap_or(""),
1406 ),
1407 "SendDurableExecutionCallbackHeartbeat" => crate::workflows::send_callback_heartbeat(
1408 &self.state,
1409 &req,
1410 resource_name.as_deref().unwrap_or(""),
1411 ),
1412 other => {
1413 self.handle_extra(other, resource_name.as_deref(), &req)
1414 .await
1415 }
1416 };
1417 if mutates && matches!(result.as_ref(), Ok(resp) if resp.status.is_success()) {
1418 self.save_snapshot().await;
1419 }
1420 result
1421 }
1422
1423 fn supported_actions(&self) -> &[&str] {
1424 &[
1425 "CreateFunction",
1426 "GetFunction",
1427 "DeleteFunction",
1428 "ListFunctions",
1429 "Invoke",
1430 "InvokeAsync",
1431 "InvokeWithResponseStream",
1432 "PublishVersion",
1433 "ListVersionsByFunction",
1434 "AddPermission",
1435 "RemovePermission",
1436 "GetPolicy",
1437 "CreateEventSourceMapping",
1438 "ListEventSourceMappings",
1439 "GetEventSourceMapping",
1440 "UpdateEventSourceMapping",
1441 "DeleteEventSourceMapping",
1442 "GetFunctionConfiguration",
1443 "UpdateFunctionConfiguration",
1444 "UpdateFunctionCode",
1445 "GetAccountSettings",
1446 "CreateAlias",
1447 "GetAlias",
1448 "ListAliases",
1449 "UpdateAlias",
1450 "DeleteAlias",
1451 "PublishLayerVersion",
1452 "GetLayerVersion",
1453 "GetLayerVersionByArn",
1454 "DeleteLayerVersion",
1455 "ListLayerVersions",
1456 "ListLayers",
1457 "GetLayerVersionPolicy",
1458 "AddLayerVersionPermission",
1459 "RemoveLayerVersionPermission",
1460 "CreateFunctionUrlConfig",
1461 "GetFunctionUrlConfig",
1462 "UpdateFunctionUrlConfig",
1463 "DeleteFunctionUrlConfig",
1464 "ListFunctionUrlConfigs",
1465 "PutFunctionConcurrency",
1466 "GetFunctionConcurrency",
1467 "DeleteFunctionConcurrency",
1468 "PutProvisionedConcurrencyConfig",
1469 "GetProvisionedConcurrencyConfig",
1470 "DeleteProvisionedConcurrencyConfig",
1471 "ListProvisionedConcurrencyConfigs",
1472 "CreateCodeSigningConfig",
1473 "GetCodeSigningConfig",
1474 "UpdateCodeSigningConfig",
1475 "DeleteCodeSigningConfig",
1476 "ListCodeSigningConfigs",
1477 "PutFunctionCodeSigningConfig",
1478 "GetFunctionCodeSigningConfig",
1479 "DeleteFunctionCodeSigningConfig",
1480 "ListFunctionsByCodeSigningConfig",
1481 "PutFunctionEventInvokeConfig",
1482 "GetFunctionEventInvokeConfig",
1483 "UpdateFunctionEventInvokeConfig",
1484 "DeleteFunctionEventInvokeConfig",
1485 "ListFunctionEventInvokeConfigs",
1486 "PutRuntimeManagementConfig",
1487 "GetRuntimeManagementConfig",
1488 "PutFunctionScalingConfig",
1489 "GetFunctionScalingConfig",
1490 "PutFunctionRecursionConfig",
1491 "GetFunctionRecursionConfig",
1492 "TagResource",
1493 "UntagResource",
1494 "ListTags",
1495 "CreateCapacityProvider",
1496 "GetCapacityProvider",
1497 "ListCapacityProviders",
1498 "UpdateCapacityProvider",
1499 "DeleteCapacityProvider",
1500 "ListFunctionVersionsByCapacityProvider",
1501 "GetDurableExecution",
1502 "GetDurableExecutionHistory",
1503 "GetDurableExecutionState",
1504 "ListDurableExecutionsByFunction",
1505 "CheckpointDurableExecution",
1506 "StopDurableExecution",
1507 "SendDurableExecutionCallbackSuccess",
1508 "SendDurableExecutionCallbackFailure",
1509 "SendDurableExecutionCallbackHeartbeat",
1510 ]
1511 }
1512
1513 fn iam_enforceable(&self) -> bool {
1514 true
1515 }
1516
1517 fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
1521 let (action_str, resource_name) = Self::resolve_action(request)?;
1526 let action = iam_action_name_for(action_str)?;
1534 let accounts = self.state.read();
1535 let empty = LambdaState::new(&request.account_id, &request.region);
1536 let state = accounts.get(&request.account_id).unwrap_or(&empty);
1537 let resource = match action_str {
1538 "GetFunction"
1543 | "DeleteFunction"
1544 | "Invoke"
1545 | "InvokeAsync"
1546 | "InvokeWithResponseStream"
1547 | "PublishVersion"
1548 | "ListVersionsByFunction"
1549 | "AddPermission"
1550 | "RemovePermission"
1551 | "GetPolicy"
1552 | "GetFunctionConfiguration"
1553 | "UpdateFunctionConfiguration"
1554 | "UpdateFunctionCode"
1555 | "CreateAlias"
1556 | "GetAlias"
1557 | "UpdateAlias"
1558 | "DeleteAlias"
1559 | "ListAliases"
1560 | "PutFunctionConcurrency"
1561 | "GetFunctionConcurrency"
1562 | "DeleteFunctionConcurrency"
1563 | "PutProvisionedConcurrencyConfig"
1564 | "GetProvisionedConcurrencyConfig"
1565 | "DeleteProvisionedConcurrencyConfig"
1566 | "ListProvisionedConcurrencyConfigs"
1567 | "PutFunctionEventInvokeConfig"
1568 | "GetFunctionEventInvokeConfig"
1569 | "UpdateFunctionEventInvokeConfig"
1570 | "DeleteFunctionEventInvokeConfig"
1571 | "ListFunctionEventInvokeConfigs"
1572 | "PutRuntimeManagementConfig"
1573 | "GetRuntimeManagementConfig"
1574 | "PutFunctionScalingConfig"
1575 | "GetFunctionScalingConfig"
1576 | "PutFunctionRecursionConfig"
1577 | "GetFunctionRecursionConfig"
1578 | "PutFunctionCodeSigningConfig"
1579 | "GetFunctionCodeSigningConfig"
1580 | "DeleteFunctionCodeSigningConfig"
1581 | "CreateFunctionUrlConfig"
1582 | "GetFunctionUrlConfig"
1583 | "UpdateFunctionUrlConfig"
1584 | "DeleteFunctionUrlConfig"
1585 | "ListFunctionUrlConfigs"
1586 | "ListDurableExecutionsByFunction" => {
1587 let raw = resource_name.unwrap_or_default();
1588 if raw.is_empty() {
1589 "*".to_string()
1590 } else {
1591 let name = normalize_function_name(&raw);
1597 format!(
1598 "arn:aws:lambda:{}:{}:function:{}",
1599 state.region, state.account_id, name
1600 )
1601 }
1602 }
1603 "CreateFunction" => {
1604 serde_json::from_slice::<Value>(&request.body)
1609 .ok()
1610 .and_then(|v| {
1611 v.get("FunctionName").and_then(|f| f.as_str()).map(|n| {
1612 format!(
1613 "arn:aws:lambda:{}:{}:function:{}",
1614 state.region, state.account_id, n
1615 )
1616 })
1617 })
1618 .unwrap_or_else(|| "*".to_string())
1619 }
1620 _ => "*".to_string(),
1621 };
1622 Some(fakecloud_core::auth::IamAction {
1623 service: "lambda",
1624 action,
1625 resource,
1626 })
1627 }
1628
1629 fn iam_condition_keys_for(
1630 &self,
1631 request: &AwsRequest,
1632 action: &fakecloud_core::auth::IamAction,
1633 ) -> std::collections::BTreeMap<String, Vec<String>> {
1634 let mut out = std::collections::BTreeMap::new();
1635 if action.action == "AddPermission" {
1636 if action.resource != "*" {
1637 out.insert(
1638 "lambda:functionarn".to_string(),
1639 vec![action.resource.clone()],
1640 );
1641 }
1642 if let Ok(body) = serde_json::from_slice::<Value>(&request.body) {
1643 if let Some(principal) = body.get("Principal").and_then(|p| p.as_str()) {
1644 out.insert("lambda:principal".to_string(), vec![principal.to_string()]);
1645 }
1646 }
1647 }
1648 out
1649 }
1650}
1651
1652#[path = "../service_event_sources.rs"]
1653mod service_event_sources;
1654#[path = "../service_permissions.rs"]
1655mod service_permissions;
1656
1657#[cfg(test)]
1658#[path = "../service_tests.rs"]
1659mod tests;