use std::sync::Arc;
use fakecloud_core::auth::ResourcePolicyProvider;
use crate::state::SharedLambdaState;
pub struct LambdaResourcePolicyProvider {
state: SharedLambdaState,
}
impl LambdaResourcePolicyProvider {
pub fn new(state: SharedLambdaState) -> Self {
Self { state }
}
pub fn shared(state: SharedLambdaState) -> Arc<dyn ResourcePolicyProvider> {
Arc::new(Self::new(state))
}
}
impl ResourcePolicyProvider for LambdaResourcePolicyProvider {
fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String> {
if !service.eq_ignore_ascii_case("lambda") {
return None;
}
let function_name = parse_function_name(resource_arn)?;
let account_id = resource_arn.split(':').nth(4).unwrap_or("").to_string();
let accounts = self.state.read();
let state = accounts.get(&account_id)?;
state
.functions
.get(function_name)
.and_then(|f| f.policy.clone())
}
}
fn parse_function_name(arn: &str) -> Option<&str> {
let rest = arn.strip_prefix("arn:aws:lambda:")?;
let parts: Vec<&str> = rest.split(':').collect();
if parts.len() < 4 {
return None;
}
let region = parts[0];
let account = parts[1];
let resource_type = parts[2];
let name = parts[3];
if region.is_empty() || account.is_empty() {
return None;
}
if resource_type != "function" {
return None;
}
if name.is_empty() {
return None;
}
Some(name)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::state::{LambdaFunction, LambdaState};
use chrono::Utc;
use fakecloud_aws::arn::Arn;
use parking_lot::RwLock;
use std::collections::BTreeMap;
fn func_with_policy(name: &str, policy: Option<&str>) -> LambdaFunction {
LambdaFunction {
function_name: name.to_string(),
function_arn: Arn::new(
"lambda",
"us-east-1",
"123456789012",
&format!("function:{name}"),
)
.to_string(),
runtime: "python3.12".to_string(),
role: "arn:aws:iam::123456789012:role/r".to_string(),
handler: "index.handler".to_string(),
description: String::new(),
timeout: 3,
memory_size: 128,
code_sha256: String::new(),
code_size: 0,
version: "$LATEST".to_string(),
last_modified: Utc::now(),
tags: BTreeMap::new(),
environment: BTreeMap::new(),
architectures: Vec::new(),
package_type: "Zip".to_string(),
code_zip: None,
image_uri: None,
policy: policy.map(str::to_string),
layers: Vec::new(),
revision_id: "test-rev".to_string(),
tracing_mode: None,
kms_key_arn: None,
ephemeral_storage_size: None,
vpc_config: None,
snap_start: None,
dead_letter_config_arn: None,
file_system_configs: Vec::new(),
logging_config: None,
image_config: None,
signing_profile_version_arn: None,
signing_job_arn: None,
runtime_version_config: None,
master_arn: None,
state_reason: None,
state_reason_code: None,
last_update_status_reason: None,
last_update_status_reason_code: None,
}
}
fn state_with(func: LambdaFunction) -> SharedLambdaState {
let mut mas: fakecloud_core::multi_account::MultiAccountState<LambdaState> =
fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
mas.get_or_create("123456789012")
.functions
.insert(func.function_name.clone(), func);
Arc::new(RwLock::new(mas))
}
#[test]
fn parse_function_name_accepts_valid_arn() {
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
Some("my-fn")
);
}
#[test]
fn parse_function_name_accepts_qualified_arn() {
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"),
Some("my-fn")
);
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:7"),
Some("my-fn")
);
}
#[test]
fn parse_function_name_rejects_malformed() {
assert_eq!(parse_function_name(""), None);
assert_eq!(parse_function_name("not-an-arn"), None);
assert_eq!(parse_function_name("arn:aws:lambda:"), None);
assert_eq!(parse_function_name("arn:aws:lambda:us-east-1"), None);
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1:123456789012"),
None
);
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1:123456789012:event-source-mapping:uuid"),
None
);
assert_eq!(
parse_function_name("arn:aws:lambda::123456789012:function:f"),
None
);
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1::function:f"),
None
);
assert_eq!(
parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:"),
None
);
assert_eq!(parse_function_name("arn:aws:s3:::my-bucket"), None);
}
#[test]
fn returns_stored_policy_for_lambda_arn() {
let doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
let state = state_with(func_with_policy("my-fn", Some(doc)));
let provider = LambdaResourcePolicyProvider::new(state);
assert_eq!(
provider.resource_policy(
"lambda",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
),
Some(doc.to_string())
);
}
#[test]
fn qualified_arn_resolves_to_unqualified_function_policy() {
let doc = r#"{"Statement":[]}"#;
let state = state_with(func_with_policy("my-fn", Some(doc)));
let provider = LambdaResourcePolicyProvider::new(state);
assert_eq!(
provider.resource_policy(
"lambda",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"
),
Some(doc.to_string())
);
}
#[test]
fn returns_none_when_function_has_no_policy() {
let state = state_with(func_with_policy("my-fn", None));
let provider = LambdaResourcePolicyProvider::new(state);
assert_eq!(
provider.resource_policy(
"lambda",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
),
None
);
}
#[test]
fn returns_none_when_function_missing() {
let state = state_with(func_with_policy("other", Some("{}")));
let provider = LambdaResourcePolicyProvider::new(state);
assert_eq!(
provider.resource_policy(
"lambda",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
),
None
);
}
#[test]
fn returns_none_for_non_lambda_service_prefix() {
let state = state_with(func_with_policy("my-fn", Some("{}")));
let provider = LambdaResourcePolicyProvider::new(state);
assert_eq!(
provider.resource_policy("s3", "arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
None
);
assert_eq!(
provider.resource_policy(
"sns",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
),
None
);
}
#[test]
fn service_prefix_match_is_case_insensitive() {
let state = state_with(func_with_policy("my-fn", Some("{}")));
let provider = LambdaResourcePolicyProvider::new(state);
assert!(provider
.resource_policy(
"LAMBDA",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
)
.is_some());
}
#[test]
fn shared_constructor_wraps_in_arc() {
let state = state_with(func_with_policy("my-fn", Some("doc")));
let arc = LambdaResourcePolicyProvider::shared(state);
assert_eq!(
arc.resource_policy(
"lambda",
"arn:aws:lambda:us-east-1:123456789012:function:my-fn"
)
.as_deref(),
Some("doc")
);
}
}