1use std::collections::HashMap;
37
38#[derive(Debug, Clone, Default)]
40pub struct MatchContext {
41 pub tenant: Option<String>,
43 pub principal: Option<String>,
45 pub service: Option<String>,
47}
48
49impl MatchContext {
50 pub fn new() -> Self {
52 Self::default()
53 }
54
55 fn to_vars(&self) -> HashMap<&str, &str> {
57 let mut vars = HashMap::new();
58 if let Some(ref t) = self.tenant {
59 vars.insert("tenant", t.as_str());
60 }
61 if let Some(ref p) = self.principal {
62 vars.insert("principal", p.as_str());
63 }
64 if let Some(ref s) = self.service {
65 vars.insert("service", s.as_str());
66 }
67 vars
68 }
69}
70
71impl From<&crate::context::WamiContext> for MatchContext {
73 fn from(ctx: &crate::context::WamiContext) -> Self {
74 Self {
75 tenant: Some(ctx.tenant_path().as_string()),
76 principal: Some(ctx.caller_arn().resource.resource_id.clone()),
77 service: Some(ctx.caller_arn().service.to_string()),
78 }
79 }
80}
81
82fn substitute_variables(pattern: &str, vars: &HashMap<&str, &str>) -> String {
86 let mut result = String::with_capacity(pattern.len());
87 let mut chars = pattern.chars().peekable();
88
89 while let Some(c) = chars.next() {
90 if c == '$' && chars.peek() == Some(&'{') {
91 chars.next(); let mut var_name = String::new();
93 for vc in chars.by_ref() {
94 if vc == '}' {
95 break;
96 }
97 var_name.push(vc);
98 }
99 if let Some(&val) = vars.get(var_name.as_str()) {
100 result.push_str(val);
101 } else {
102 result.push_str("${");
104 result.push_str(&var_name);
105 result.push('}');
106 }
107 } else {
108 result.push(c);
109 }
110 }
111
112 result
113}
114
115pub fn matches_arn_pattern(pattern: &str, text: &str, ctx: &MatchContext) -> bool {
131 let vars = ctx.to_vars();
133 let resolved = substitute_variables(pattern, &vars);
134
135 glob_match(&resolved, text)
137}
138
139pub fn glob_match(pattern: &str, text: &str) -> bool {
148 if pattern == "*" || pattern == "**" {
150 return true;
151 }
152 if pattern == text {
153 return true;
154 }
155 if !pattern.contains('*') {
156 return pattern == text;
157 }
158
159 if glob_match_recursive(pattern.as_bytes(), text.as_bytes()) {
160 return true;
161 }
162
163 if let Some(prefix) = pattern.strip_suffix("/**") {
166 return text == prefix || glob_match_recursive(prefix.as_bytes(), text.as_bytes());
167 }
168
169 false
170}
171
172fn glob_match_recursive(pattern: &[u8], text: &[u8]) -> bool {
177 let mut pi = 0usize;
178 let mut ti = 0usize;
179
180 let mut dstar_pi: i64 = -1;
182 let mut dstar_ti: i64 = -1;
183
184 let mut star_pi: i64 = -1;
186 let mut star_ti: i64 = -1;
187
188 while ti < text.len() || pi < pattern.len() {
189 if pi < pattern.len() {
190 if pi + 1 < pattern.len() && pattern[pi] == b'*' && pattern[pi + 1] == b'*' {
192 dstar_pi = pi as i64;
193 dstar_ti = ti as i64;
194 pi += 2;
195 if pi < pattern.len() && pattern[pi] == b'/' {
197 pi += 1;
198 }
199 star_pi = -1;
201 star_ti = -1;
202 continue;
203 }
204
205 if pattern[pi] == b'*' {
207 star_pi = pi as i64;
208 star_ti = ti as i64;
209 pi += 1;
210 continue;
211 }
212
213 if ti < text.len() && pattern[pi] == text[ti] {
215 pi += 1;
216 ti += 1;
217 continue;
218 }
219 }
220
221 if star_pi >= 0 {
223 let st = star_ti as usize;
224 if st < text.len() && text[st] != b'/' {
225 star_ti += 1;
226 ti = star_ti as usize;
227 pi = star_pi as usize + 1;
228 continue;
229 }
230 }
232
233 if dstar_pi >= 0 {
235 dstar_ti += 1;
236 let st = dstar_ti as usize;
237 if st <= text.len() {
238 ti = st;
239 pi = dstar_pi as usize + 2;
240 if pi < pattern.len() && pattern[pi] == b'/' {
241 pi += 1;
242 }
243 star_pi = -1;
245 star_ti = -1;
246 continue;
247 }
248 }
249
250 return false;
251 }
252
253 true
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 #[test]
263 fn test_substitute_tenant() {
264 let vars: HashMap<&str, &str> = [("tenant", "12345678")].into();
265 assert_eq!(
266 substitute_variables("arn:wami:iam:${tenant}:wami:*:user/*", &vars),
267 "arn:wami:iam:12345678:wami:*:user/*"
268 );
269 }
270
271 #[test]
272 fn test_substitute_multiple_vars() {
273 let vars: HashMap<&str, &str> = [("tenant", "12345678"), ("service", "iam")].into();
274 assert_eq!(
275 substitute_variables("arn:wami:${service}:${tenant}:wami:*:user/*", &vars),
276 "arn:wami:iam:12345678:wami:*:user/*"
277 );
278 }
279
280 #[test]
281 fn test_substitute_unknown_var_left_as_is() {
282 let vars: HashMap<&str, &str> = HashMap::new();
283 assert_eq!(
284 substitute_variables("arn:wami:iam:${unknown}:wami:*:user/*", &vars),
285 "arn:wami:iam:${unknown}:wami:*:user/*"
286 );
287 }
288
289 #[test]
290 fn test_substitute_no_vars() {
291 let vars: HashMap<&str, &str> = HashMap::new();
292 assert_eq!(
293 substitute_variables("arn:wami:iam:*:user/*", &vars),
294 "arn:wami:iam:*:user/*"
295 );
296 }
297
298 #[test]
301 fn test_glob_exact_match() {
302 assert!(glob_match("hello", "hello"));
303 assert!(!glob_match("hello", "world"));
304 }
305
306 #[test]
307 fn test_glob_star_all() {
308 assert!(glob_match("*", "anything"));
309 assert!(glob_match("*", ""));
310 }
311
312 #[test]
313 fn test_glob_single_star_within_segment() {
314 assert!(glob_match("user/*", "user/alice"));
316 assert!(glob_match("user/*", "user/bob"));
317 assert!(glob_match("*/alice", "user/alice"));
318 }
319
320 #[test]
321 fn test_glob_single_star_does_not_cross_slash() {
322 assert!(!glob_match("space/*/db", "space/le-zinc/sub/db"));
324 assert!(glob_match("space/*/db", "space/le-zinc/db"));
325 }
326
327 #[test]
328 fn test_glob_star_prefix_suffix() {
329 assert!(glob_match("iam:*", "iam:GetUser"));
330 assert!(glob_match("iam:Get*", "iam:GetUser"));
331 assert!(!glob_match("iam:Delete*", "iam:GetUser"));
332 }
333
334 #[test]
337 fn test_glob_double_star_matches_everything() {
338 assert!(glob_match("**", "anything/with/slashes"));
339 assert!(glob_match("**", ""));
340 }
341
342 #[test]
343 fn test_glob_double_star_multi_segment() {
344 assert!(glob_match("space/le-zinc/**", "space/le-zinc/db/menu"));
345 assert!(glob_match(
346 "space/le-zinc/**",
347 "space/le-zinc/db/menu/items"
348 ));
349 assert!(glob_match("space/le-zinc/**", "space/le-zinc"));
350 assert!(!glob_match("space/le-zinc/**", "space/other/db"));
351 }
352
353 #[test]
354 fn test_glob_double_star_in_middle() {
355 assert!(glob_match(
356 "arn:wami:**/user/*",
357 "arn:wami:iam:12345678:wami:999:user/alice"
358 ));
359 assert!(glob_match("a/**/z", "a/b/c/d/z"));
360 assert!(glob_match("a/**/z", "a/z"));
361 }
362
363 #[test]
364 fn test_glob_double_star_at_start() {
365 assert!(glob_match(
366 "**/user/alice",
367 "arn:wami:iam:123:wami:999:user/alice"
368 ));
369 }
370
371 #[test]
374 fn test_arn_pattern_with_variables() {
375 let ctx = MatchContext {
376 tenant: Some("12345678".into()),
377 principal: Some("alice".into()),
378 service: Some("iam".into()),
379 };
380
381 assert!(matches_arn_pattern(
382 "arn:wami:iam:${tenant}:wami:*:user/*",
383 "arn:wami:iam:12345678:wami:999:user/alice",
384 &ctx,
385 ));
386 }
387
388 #[test]
389 fn test_arn_pattern_tenant_mismatch() {
390 let ctx = MatchContext {
391 tenant: Some("99999999".into()),
392 ..Default::default()
393 };
394
395 assert!(!matches_arn_pattern(
396 "arn:wami:iam:${tenant}:wami:*:user/*",
397 "arn:wami:iam:12345678:wami:999:user/alice",
398 &ctx,
399 ));
400 }
401
402 #[test]
403 fn test_arn_pattern_double_star_space_scoping() {
404 let ctx = MatchContext {
405 tenant: Some("12345678".into()),
406 ..Default::default()
407 };
408
409 let pattern = "arn:wami:hub:${tenant}:wami:*:space/le-zinc/**";
411
412 assert!(matches_arn_pattern(
413 pattern,
414 "arn:wami:hub:12345678:wami:999:space/le-zinc/db/menu",
415 &ctx,
416 ));
417
418 assert!(matches_arn_pattern(
419 pattern,
420 "arn:wami:hub:12345678:wami:999:space/le-zinc/persona/chef",
421 &ctx,
422 ));
423
424 assert!(!matches_arn_pattern(
425 pattern,
426 "arn:wami:hub:12345678:wami:999:space/other-space/db/menu",
427 &ctx,
428 ));
429 }
430
431 #[test]
432 fn test_arn_pattern_no_context() {
433 let ctx = MatchContext::default();
434
435 assert!(matches_arn_pattern(
437 "arn:wami:iam:*:wami:*:user/*",
438 "arn:wami:iam:12345678:wami:999:user/alice",
439 &ctx,
440 ));
441 }
442
443 #[test]
444 fn test_arn_pattern_wildcard_all() {
445 let ctx = MatchContext::default();
446 assert!(matches_arn_pattern("*", "anything", &ctx));
447 }
448
449 #[test]
450 fn test_arn_pattern_exact_match() {
451 let ctx = MatchContext::default();
452 assert!(matches_arn_pattern(
453 "arn:wami:iam:12345678:wami:999:user/alice",
454 "arn:wami:iam:12345678:wami:999:user/alice",
455 &ctx,
456 ));
457 }
458
459 #[test]
462 fn test_glob_empty_pattern_empty_text() {
463 assert!(glob_match("", ""));
464 }
465
466 #[test]
467 fn test_glob_empty_pattern_nonempty_text() {
468 assert!(!glob_match("", "something"));
469 }
470
471 #[test]
472 fn test_glob_consecutive_stars() {
473 assert!(glob_match("a/**/*", "a/b/c"));
475 }
476
477 #[test]
478 fn test_glob_star_at_colon_boundary() {
479 assert!(glob_match(
481 "arn:wami:iam:*:wami:*:user/alice",
482 "arn:wami:iam:12345678:wami:999:user/alice"
483 ));
484 }
485
486 #[test]
487 fn test_backward_compat_existing_patterns() {
488 assert!(glob_match(
490 "arn:wami:iam:*:user/*",
491 "arn:wami:iam:12345678:wami:999:user/alice"
492 ));
493 assert!(glob_match("*.example.com", "api.example.com"));
494 assert!(glob_match("test-*-prod", "test-api-prod"));
495 assert!(!glob_match(
496 "arn:*:role/*",
497 "arn:wami:iam:12345678:wami:999:user/alice"
498 ));
499 }
500}