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 signing_profile_version_arn: None,
142 signing_job_arn: None,
143 runtime_version_config: None,
144 master_arn: None,
145 state_reason: None,
146 state_reason_code: None,
147 last_update_status_reason: None,
148 last_update_status_reason_code: None,
149 }
150 }
151
152 fn state_with(func: LambdaFunction) -> SharedLambdaState {
153 let mut mas: fakecloud_core::multi_account::MultiAccountState<LambdaState> =
154 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
155 mas.get_or_create("123456789012")
156 .functions
157 .insert(func.function_name.clone(), func);
158 Arc::new(RwLock::new(mas))
159 }
160
161 #[test]
162 fn parse_function_name_accepts_valid_arn() {
163 assert_eq!(
164 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
165 Some("my-fn")
166 );
167 }
168
169 #[test]
170 fn parse_function_name_accepts_qualified_arn() {
171 assert_eq!(
174 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"),
175 Some("my-fn")
176 );
177 assert_eq!(
178 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:7"),
179 Some("my-fn")
180 );
181 }
182
183 #[test]
184 fn parse_function_name_rejects_malformed() {
185 assert_eq!(parse_function_name(""), None);
186 assert_eq!(parse_function_name("not-an-arn"), None);
187 assert_eq!(parse_function_name("arn:aws:lambda:"), None);
188 assert_eq!(parse_function_name("arn:aws:lambda:us-east-1"), None);
189 assert_eq!(
190 parse_function_name("arn:aws:lambda:us-east-1:123456789012"),
191 None
192 );
193 assert_eq!(
195 parse_function_name("arn:aws:lambda:us-east-1:123456789012:event-source-mapping:uuid"),
196 None
197 );
198 assert_eq!(
200 parse_function_name("arn:aws:lambda::123456789012:function:f"),
201 None
202 );
203 assert_eq!(
204 parse_function_name("arn:aws:lambda:us-east-1::function:f"),
205 None
206 );
207 assert_eq!(
209 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:"),
210 None
211 );
212 assert_eq!(parse_function_name("arn:aws:s3:::my-bucket"), None);
214 }
215
216 #[test]
217 fn returns_stored_policy_for_lambda_arn() {
218 let doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
219 let state = state_with(func_with_policy("my-fn", Some(doc)));
220 let provider = LambdaResourcePolicyProvider::new(state);
221 assert_eq!(
222 provider.resource_policy(
223 "lambda",
224 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
225 ),
226 Some(doc.to_string())
227 );
228 }
229
230 #[test]
231 fn qualified_arn_resolves_to_unqualified_function_policy() {
232 let doc = r#"{"Statement":[]}"#;
236 let state = state_with(func_with_policy("my-fn", Some(doc)));
237 let provider = LambdaResourcePolicyProvider::new(state);
238 assert_eq!(
239 provider.resource_policy(
240 "lambda",
241 "arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"
242 ),
243 Some(doc.to_string())
244 );
245 }
246
247 #[test]
248 fn returns_none_when_function_has_no_policy() {
249 let state = state_with(func_with_policy("my-fn", None));
250 let provider = LambdaResourcePolicyProvider::new(state);
251 assert_eq!(
252 provider.resource_policy(
253 "lambda",
254 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
255 ),
256 None
257 );
258 }
259
260 #[test]
261 fn returns_none_when_function_missing() {
262 let state = state_with(func_with_policy("other", Some("{}")));
263 let provider = LambdaResourcePolicyProvider::new(state);
264 assert_eq!(
265 provider.resource_policy(
266 "lambda",
267 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
268 ),
269 None
270 );
271 }
272
273 #[test]
274 fn returns_none_for_non_lambda_service_prefix() {
275 let state = state_with(func_with_policy("my-fn", Some("{}")));
276 let provider = LambdaResourcePolicyProvider::new(state);
277 assert_eq!(
278 provider.resource_policy("s3", "arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
279 None
280 );
281 assert_eq!(
282 provider.resource_policy(
283 "sns",
284 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
285 ),
286 None
287 );
288 }
289
290 #[test]
291 fn service_prefix_match_is_case_insensitive() {
292 let state = state_with(func_with_policy("my-fn", Some("{}")));
293 let provider = LambdaResourcePolicyProvider::new(state);
294 assert!(provider
295 .resource_policy(
296 "LAMBDA",
297 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
298 )
299 .is_some());
300 }
301
302 #[test]
303 fn shared_constructor_wraps_in_arc() {
304 let state = state_with(func_with_policy("my-fn", Some("doc")));
305 let arc = LambdaResourcePolicyProvider::shared(state);
306 assert_eq!(
307 arc.resource_policy(
308 "lambda",
309 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
310 )
311 .as_deref(),
312 Some("doc")
313 );
314 }
315}