fakecloud_lambda/
resource_policy.rs1use 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 parking_lot::RwLock;
100 use std::collections::BTreeMap;
101
102 fn func_with_policy(name: &str, policy: Option<&str>) -> LambdaFunction {
103 LambdaFunction {
104 function_name: name.to_string(),
105 function_arn: format!("arn:aws:lambda:us-east-1:123456789012:function:{name}"),
106 runtime: "python3.12".to_string(),
107 role: "arn:aws:iam::123456789012:role/r".to_string(),
108 handler: "index.handler".to_string(),
109 description: String::new(),
110 timeout: 3,
111 memory_size: 128,
112 code_sha256: String::new(),
113 code_size: 0,
114 version: "$LATEST".to_string(),
115 last_modified: Utc::now(),
116 tags: BTreeMap::new(),
117 environment: BTreeMap::new(),
118 architectures: Vec::new(),
119 package_type: "Zip".to_string(),
120 code_zip: None,
121 image_uri: None,
122 policy: policy.map(str::to_string),
123 layers: Vec::new(),
124 }
125 }
126
127 fn state_with(func: LambdaFunction) -> SharedLambdaState {
128 let mut mas: fakecloud_core::multi_account::MultiAccountState<LambdaState> =
129 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
130 mas.get_or_create("123456789012")
131 .functions
132 .insert(func.function_name.clone(), func);
133 Arc::new(RwLock::new(mas))
134 }
135
136 #[test]
137 fn parse_function_name_accepts_valid_arn() {
138 assert_eq!(
139 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
140 Some("my-fn")
141 );
142 }
143
144 #[test]
145 fn parse_function_name_accepts_qualified_arn() {
146 assert_eq!(
149 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"),
150 Some("my-fn")
151 );
152 assert_eq!(
153 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:7"),
154 Some("my-fn")
155 );
156 }
157
158 #[test]
159 fn parse_function_name_rejects_malformed() {
160 assert_eq!(parse_function_name(""), None);
161 assert_eq!(parse_function_name("not-an-arn"), None);
162 assert_eq!(parse_function_name("arn:aws:lambda:"), None);
163 assert_eq!(parse_function_name("arn:aws:lambda:us-east-1"), None);
164 assert_eq!(
165 parse_function_name("arn:aws:lambda:us-east-1:123456789012"),
166 None
167 );
168 assert_eq!(
170 parse_function_name("arn:aws:lambda:us-east-1:123456789012:event-source-mapping:uuid"),
171 None
172 );
173 assert_eq!(
175 parse_function_name("arn:aws:lambda::123456789012:function:f"),
176 None
177 );
178 assert_eq!(
179 parse_function_name("arn:aws:lambda:us-east-1::function:f"),
180 None
181 );
182 assert_eq!(
184 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:"),
185 None
186 );
187 assert_eq!(parse_function_name("arn:aws:s3:::my-bucket"), None);
189 }
190
191 #[test]
192 fn returns_stored_policy_for_lambda_arn() {
193 let doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
194 let state = state_with(func_with_policy("my-fn", Some(doc)));
195 let provider = LambdaResourcePolicyProvider::new(state);
196 assert_eq!(
197 provider.resource_policy(
198 "lambda",
199 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
200 ),
201 Some(doc.to_string())
202 );
203 }
204
205 #[test]
206 fn qualified_arn_resolves_to_unqualified_function_policy() {
207 let doc = r#"{"Statement":[]}"#;
211 let state = state_with(func_with_policy("my-fn", Some(doc)));
212 let provider = LambdaResourcePolicyProvider::new(state);
213 assert_eq!(
214 provider.resource_policy(
215 "lambda",
216 "arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"
217 ),
218 Some(doc.to_string())
219 );
220 }
221
222 #[test]
223 fn returns_none_when_function_has_no_policy() {
224 let state = state_with(func_with_policy("my-fn", None));
225 let provider = LambdaResourcePolicyProvider::new(state);
226 assert_eq!(
227 provider.resource_policy(
228 "lambda",
229 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
230 ),
231 None
232 );
233 }
234
235 #[test]
236 fn returns_none_when_function_missing() {
237 let state = state_with(func_with_policy("other", Some("{}")));
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"
243 ),
244 None
245 );
246 }
247
248 #[test]
249 fn returns_none_for_non_lambda_service_prefix() {
250 let state = state_with(func_with_policy("my-fn", Some("{}")));
251 let provider = LambdaResourcePolicyProvider::new(state);
252 assert_eq!(
253 provider.resource_policy("s3", "arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
254 None
255 );
256 assert_eq!(
257 provider.resource_policy(
258 "sns",
259 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
260 ),
261 None
262 );
263 }
264
265 #[test]
266 fn service_prefix_match_is_case_insensitive() {
267 let state = state_with(func_with_policy("my-fn", Some("{}")));
268 let provider = LambdaResourcePolicyProvider::new(state);
269 assert!(provider
270 .resource_policy(
271 "LAMBDA",
272 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
273 )
274 .is_some());
275 }
276
277 #[test]
278 fn shared_constructor_wraps_in_arc() {
279 let state = state_with(func_with_policy("my-fn", Some("doc")));
280 let arc = LambdaResourcePolicyProvider::shared(state);
281 assert_eq!(
282 arc.resource_policy(
283 "lambda",
284 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
285 )
286 .as_deref(),
287 Some("doc")
288 );
289 }
290}