1use std::sync::Arc;
17
18use fakecloud_core::auth::ResourcePolicyProvider;
19
20use crate::state::SharedLambdaState;
21
22pub struct LambdaResourcePolicyProvider {
27 state: SharedLambdaState,
28}
29
30impl LambdaResourcePolicyProvider {
31 pub fn new(state: SharedLambdaState) -> Self {
32 Self { state }
33 }
34
35 pub fn shared(state: SharedLambdaState) -> Arc<dyn ResourcePolicyProvider> {
39 Arc::new(Self::new(state))
40 }
41}
42
43impl ResourcePolicyProvider for LambdaResourcePolicyProvider {
44 fn resource_policy(&self, service: &str, resource_arn: &str) -> Option<String> {
45 if !service.eq_ignore_ascii_case("lambda") {
46 return None;
47 }
48 let function_name = parse_function_name(resource_arn)?;
49 let account_id = resource_arn.split(':').nth(4).unwrap_or("").to_string();
51 let accounts = self.state.read();
52 let state = accounts.get(&account_id)?;
53 state
54 .functions
55 .get(function_name)
56 .and_then(|f| f.policy.clone())
57 }
58}
59
60fn parse_function_name(arn: &str) -> Option<&str> {
70 let rest = arn.strip_prefix("arn:aws:lambda:")?;
71 let parts: Vec<&str> = rest.split(':').collect();
74 if parts.len() < 4 {
76 return None;
77 }
78 let region = parts[0];
79 let account = parts[1];
80 let resource_type = parts[2];
81 let name = parts[3];
82 if region.is_empty() || account.is_empty() {
83 return None;
84 }
85 if resource_type != "function" {
86 return None;
87 }
88 if name.is_empty() {
89 return None;
90 }
91 Some(name)
92}
93
94#[cfg(test)]
95mod tests {
96 use super::*;
97 use crate::state::{LambdaFunction, LambdaState};
98 use chrono::Utc;
99 use fakecloud_aws::arn::Arn;
100 use parking_lot::RwLock;
101 use std::collections::BTreeMap;
102
103 fn func_with_policy(name: &str, policy: Option<&str>) -> LambdaFunction {
104 LambdaFunction {
105 function_name: name.to_string(),
106 function_arn: Arn::new(
107 "lambda",
108 "us-east-1",
109 "123456789012",
110 &format!("function:{name}"),
111 )
112 .to_string(),
113 runtime: "python3.12".to_string(),
114 role: "arn:aws:iam::123456789012:role/r".to_string(),
115 handler: "index.handler".to_string(),
116 description: String::new(),
117 timeout: 3,
118 memory_size: 128,
119 code_sha256: String::new(),
120 code_size: 0,
121 version: "$LATEST".to_string(),
122 last_modified: Utc::now(),
123 tags: BTreeMap::new(),
124 environment: BTreeMap::new(),
125 architectures: Vec::new(),
126 package_type: "Zip".to_string(),
127 code_zip: None,
128 image_uri: None,
129 policy: policy.map(str::to_string),
130 layers: Vec::new(),
131 revision_id: "test-rev".to_string(),
132 tracing_mode: None,
133 kms_key_arn: None,
134 ephemeral_storage_size: None,
135 vpc_config: None,
136 snap_start: None,
137 dead_letter_config_arn: None,
138 file_system_configs: Vec::new(),
139 logging_config: None,
140 image_config: None,
141 durable_config: None,
142 signing_profile_version_arn: None,
143 signing_job_arn: None,
144 runtime_version_config: None,
145 master_arn: None,
146 state_reason: None,
147 state_reason_code: None,
148 last_update_status_reason: None,
149 last_update_status_reason_code: None,
150 }
151 }
152
153 fn state_with(func: LambdaFunction) -> SharedLambdaState {
154 let mut mas: fakecloud_core::multi_account::MultiAccountState<LambdaState> =
155 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
156 mas.get_or_create("123456789012")
157 .functions
158 .insert(func.function_name.clone(), func);
159 Arc::new(RwLock::new(mas))
160 }
161
162 #[test]
163 fn parse_function_name_accepts_valid_arn() {
164 assert_eq!(
165 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
166 Some("my-fn")
167 );
168 }
169
170 #[test]
171 fn parse_function_name_accepts_qualified_arn() {
172 assert_eq!(
175 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"),
176 Some("my-fn")
177 );
178 assert_eq!(
179 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:7"),
180 Some("my-fn")
181 );
182 }
183
184 #[test]
185 fn parse_function_name_rejects_malformed() {
186 assert_eq!(parse_function_name(""), None);
187 assert_eq!(parse_function_name("not-an-arn"), None);
188 assert_eq!(parse_function_name("arn:aws:lambda:"), None);
189 assert_eq!(parse_function_name("arn:aws:lambda:us-east-1"), None);
190 assert_eq!(
191 parse_function_name("arn:aws:lambda:us-east-1:123456789012"),
192 None
193 );
194 assert_eq!(
196 parse_function_name("arn:aws:lambda:us-east-1:123456789012:event-source-mapping:uuid"),
197 None
198 );
199 assert_eq!(
201 parse_function_name("arn:aws:lambda::123456789012:function:f"),
202 None
203 );
204 assert_eq!(
205 parse_function_name("arn:aws:lambda:us-east-1::function:f"),
206 None
207 );
208 assert_eq!(
210 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:"),
211 None
212 );
213 assert_eq!(parse_function_name("arn:aws:s3:::my-bucket"), None);
215 }
216
217 #[test]
218 fn returns_stored_policy_for_lambda_arn() {
219 let doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
220 let state = state_with(func_with_policy("my-fn", Some(doc)));
221 let provider = LambdaResourcePolicyProvider::new(state);
222 assert_eq!(
223 provider.resource_policy(
224 "lambda",
225 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
226 ),
227 Some(doc.to_string())
228 );
229 }
230
231 #[test]
232 fn qualified_arn_resolves_to_unqualified_function_policy() {
233 let doc = r#"{"Statement":[]}"#;
237 let state = state_with(func_with_policy("my-fn", Some(doc)));
238 let provider = LambdaResourcePolicyProvider::new(state);
239 assert_eq!(
240 provider.resource_policy(
241 "lambda",
242 "arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"
243 ),
244 Some(doc.to_string())
245 );
246 }
247
248 #[test]
249 fn returns_none_when_function_has_no_policy() {
250 let state = state_with(func_with_policy("my-fn", None));
251 let provider = LambdaResourcePolicyProvider::new(state);
252 assert_eq!(
253 provider.resource_policy(
254 "lambda",
255 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
256 ),
257 None
258 );
259 }
260
261 #[test]
262 fn returns_none_when_function_missing() {
263 let state = state_with(func_with_policy("other", Some("{}")));
264 let provider = LambdaResourcePolicyProvider::new(state);
265 assert_eq!(
266 provider.resource_policy(
267 "lambda",
268 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
269 ),
270 None
271 );
272 }
273
274 #[test]
275 fn returns_none_for_non_lambda_service_prefix() {
276 let state = state_with(func_with_policy("my-fn", Some("{}")));
277 let provider = LambdaResourcePolicyProvider::new(state);
278 assert_eq!(
279 provider.resource_policy("s3", "arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
280 None
281 );
282 assert_eq!(
283 provider.resource_policy(
284 "sns",
285 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
286 ),
287 None
288 );
289 }
290
291 #[test]
292 fn service_prefix_match_is_case_insensitive() {
293 let state = state_with(func_with_policy("my-fn", Some("{}")));
294 let provider = LambdaResourcePolicyProvider::new(state);
295 assert!(provider
296 .resource_policy(
297 "LAMBDA",
298 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
299 )
300 .is_some());
301 }
302
303 #[test]
304 fn shared_constructor_wraps_in_arc() {
305 let state = state_with(func_with_policy("my-fn", Some("doc")));
306 let arc = LambdaResourcePolicyProvider::shared(state);
307 assert_eq!(
308 arc.resource_policy(
309 "lambda",
310 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
311 )
312 .as_deref(),
313 Some("doc")
314 );
315 }
316}