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