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::HashMap;
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: HashMap::new(),
117 environment: HashMap::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 }
124 }
125
126 fn state_with(func: LambdaFunction) -> SharedLambdaState {
127 let mut mas: fakecloud_core::multi_account::MultiAccountState<LambdaState> =
128 fakecloud_core::multi_account::MultiAccountState::new("123456789012", "us-east-1", "");
129 mas.get_or_create("123456789012")
130 .functions
131 .insert(func.function_name.clone(), func);
132 Arc::new(RwLock::new(mas))
133 }
134
135 #[test]
136 fn parse_function_name_accepts_valid_arn() {
137 assert_eq!(
138 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
139 Some("my-fn")
140 );
141 }
142
143 #[test]
144 fn parse_function_name_accepts_qualified_arn() {
145 assert_eq!(
148 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"),
149 Some("my-fn")
150 );
151 assert_eq!(
152 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:my-fn:7"),
153 Some("my-fn")
154 );
155 }
156
157 #[test]
158 fn parse_function_name_rejects_malformed() {
159 assert_eq!(parse_function_name(""), None);
160 assert_eq!(parse_function_name("not-an-arn"), None);
161 assert_eq!(parse_function_name("arn:aws:lambda:"), None);
162 assert_eq!(parse_function_name("arn:aws:lambda:us-east-1"), None);
163 assert_eq!(
164 parse_function_name("arn:aws:lambda:us-east-1:123456789012"),
165 None
166 );
167 assert_eq!(
169 parse_function_name("arn:aws:lambda:us-east-1:123456789012:event-source-mapping:uuid"),
170 None
171 );
172 assert_eq!(
174 parse_function_name("arn:aws:lambda::123456789012:function:f"),
175 None
176 );
177 assert_eq!(
178 parse_function_name("arn:aws:lambda:us-east-1::function:f"),
179 None
180 );
181 assert_eq!(
183 parse_function_name("arn:aws:lambda:us-east-1:123456789012:function:"),
184 None
185 );
186 assert_eq!(parse_function_name("arn:aws:s3:::my-bucket"), None);
188 }
189
190 #[test]
191 fn returns_stored_policy_for_lambda_arn() {
192 let doc = r#"{"Version":"2012-10-17","Statement":[]}"#;
193 let state = state_with(func_with_policy("my-fn", Some(doc)));
194 let provider = LambdaResourcePolicyProvider::new(state);
195 assert_eq!(
196 provider.resource_policy(
197 "lambda",
198 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
199 ),
200 Some(doc.to_string())
201 );
202 }
203
204 #[test]
205 fn qualified_arn_resolves_to_unqualified_function_policy() {
206 let doc = r#"{"Statement":[]}"#;
210 let state = state_with(func_with_policy("my-fn", Some(doc)));
211 let provider = LambdaResourcePolicyProvider::new(state);
212 assert_eq!(
213 provider.resource_policy(
214 "lambda",
215 "arn:aws:lambda:us-east-1:123456789012:function:my-fn:PROD"
216 ),
217 Some(doc.to_string())
218 );
219 }
220
221 #[test]
222 fn returns_none_when_function_has_no_policy() {
223 let state = state_with(func_with_policy("my-fn", None));
224 let provider = LambdaResourcePolicyProvider::new(state);
225 assert_eq!(
226 provider.resource_policy(
227 "lambda",
228 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
229 ),
230 None
231 );
232 }
233
234 #[test]
235 fn returns_none_when_function_missing() {
236 let state = state_with(func_with_policy("other", Some("{}")));
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"
242 ),
243 None
244 );
245 }
246
247 #[test]
248 fn returns_none_for_non_lambda_service_prefix() {
249 let state = state_with(func_with_policy("my-fn", Some("{}")));
250 let provider = LambdaResourcePolicyProvider::new(state);
251 assert_eq!(
252 provider.resource_policy("s3", "arn:aws:lambda:us-east-1:123456789012:function:my-fn"),
253 None
254 );
255 assert_eq!(
256 provider.resource_policy(
257 "sns",
258 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
259 ),
260 None
261 );
262 }
263
264 #[test]
265 fn service_prefix_match_is_case_insensitive() {
266 let state = state_with(func_with_policy("my-fn", Some("{}")));
267 let provider = LambdaResourcePolicyProvider::new(state);
268 assert!(provider
269 .resource_policy(
270 "LAMBDA",
271 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
272 )
273 .is_some());
274 }
275
276 #[test]
277 fn shared_constructor_wraps_in_arc() {
278 let state = state_with(func_with_policy("my-fn", Some("doc")));
279 let arc = LambdaResourcePolicyProvider::shared(state);
280 assert_eq!(
281 arc.resource_policy(
282 "lambda",
283 "arn:aws:lambda:us-east-1:123456789012:function:my-fn"
284 )
285 .as_deref(),
286 Some("doc")
287 );
288 }
289}