use std::collections::HashMap;
#[derive(Debug, Clone, Default)]
pub struct MatchContext {
pub tenant: Option<String>,
pub principal: Option<String>,
pub service: Option<String>,
}
impl MatchContext {
pub fn new() -> Self {
Self::default()
}
fn to_vars(&self) -> HashMap<&str, &str> {
let mut vars = HashMap::new();
if let Some(ref t) = self.tenant {
vars.insert("tenant", t.as_str());
}
if let Some(ref p) = self.principal {
vars.insert("principal", p.as_str());
}
if let Some(ref s) = self.service {
vars.insert("service", s.as_str());
}
vars
}
}
impl From<&crate::context::WamiContext> for MatchContext {
fn from(ctx: &crate::context::WamiContext) -> Self {
Self {
tenant: Some(ctx.tenant_path().as_string()),
principal: Some(ctx.caller_arn().resource.resource_id.clone()),
service: Some(ctx.caller_arn().service.to_string()),
}
}
}
fn substitute_variables(pattern: &str, vars: &HashMap<&str, &str>) -> String {
let mut result = String::with_capacity(pattern.len());
let mut chars = pattern.chars().peekable();
while let Some(c) = chars.next() {
if c == '$' && chars.peek() == Some(&'{') {
chars.next(); let mut var_name = String::new();
for vc in chars.by_ref() {
if vc == '}' {
break;
}
var_name.push(vc);
}
if let Some(&val) = vars.get(var_name.as_str()) {
result.push_str(val);
} else {
result.push_str("${");
result.push_str(&var_name);
result.push('}');
}
} else {
result.push(c);
}
}
result
}
pub fn matches_arn_pattern(pattern: &str, text: &str, ctx: &MatchContext) -> bool {
let vars = ctx.to_vars();
let resolved = substitute_variables(pattern, &vars);
glob_match(&resolved, text)
}
pub fn glob_match(pattern: &str, text: &str) -> bool {
if pattern == "*" || pattern == "**" {
return true;
}
if pattern == text {
return true;
}
if !pattern.contains('*') {
return pattern == text;
}
if glob_match_recursive(pattern.as_bytes(), text.as_bytes()) {
return true;
}
if let Some(prefix) = pattern.strip_suffix("/**") {
return text == prefix || glob_match_recursive(prefix.as_bytes(), text.as_bytes());
}
false
}
fn glob_match_recursive(pattern: &[u8], text: &[u8]) -> bool {
let mut pi = 0usize;
let mut ti = 0usize;
let mut dstar_pi: i64 = -1;
let mut dstar_ti: i64 = -1;
let mut star_pi: i64 = -1;
let mut star_ti: i64 = -1;
while ti < text.len() || pi < pattern.len() {
if pi < pattern.len() {
if pi + 1 < pattern.len() && pattern[pi] == b'*' && pattern[pi + 1] == b'*' {
dstar_pi = pi as i64;
dstar_ti = ti as i64;
pi += 2;
if pi < pattern.len() && pattern[pi] == b'/' {
pi += 1;
}
star_pi = -1;
star_ti = -1;
continue;
}
if pattern[pi] == b'*' {
star_pi = pi as i64;
star_ti = ti as i64;
pi += 1;
continue;
}
if ti < text.len() && pattern[pi] == text[ti] {
pi += 1;
ti += 1;
continue;
}
}
if star_pi >= 0 {
let st = star_ti as usize;
if st < text.len() && text[st] != b'/' {
star_ti += 1;
ti = star_ti as usize;
pi = star_pi as usize + 1;
continue;
}
}
if dstar_pi >= 0 {
dstar_ti += 1;
let st = dstar_ti as usize;
if st <= text.len() {
ti = st;
pi = dstar_pi as usize + 2;
if pi < pattern.len() && pattern[pi] == b'/' {
pi += 1;
}
star_pi = -1;
star_ti = -1;
continue;
}
}
return false;
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_substitute_tenant() {
let vars: HashMap<&str, &str> = [("tenant", "12345678")].into();
assert_eq!(
substitute_variables("arn:wami:iam:${tenant}:wami:*:user/*", &vars),
"arn:wami:iam:12345678:wami:*:user/*"
);
}
#[test]
fn test_substitute_multiple_vars() {
let vars: HashMap<&str, &str> = [("tenant", "12345678"), ("service", "iam")].into();
assert_eq!(
substitute_variables("arn:wami:${service}:${tenant}:wami:*:user/*", &vars),
"arn:wami:iam:12345678:wami:*:user/*"
);
}
#[test]
fn test_substitute_unknown_var_left_as_is() {
let vars: HashMap<&str, &str> = HashMap::new();
assert_eq!(
substitute_variables("arn:wami:iam:${unknown}:wami:*:user/*", &vars),
"arn:wami:iam:${unknown}:wami:*:user/*"
);
}
#[test]
fn test_substitute_no_vars() {
let vars: HashMap<&str, &str> = HashMap::new();
assert_eq!(
substitute_variables("arn:wami:iam:*:user/*", &vars),
"arn:wami:iam:*:user/*"
);
}
#[test]
fn test_glob_exact_match() {
assert!(glob_match("hello", "hello"));
assert!(!glob_match("hello", "world"));
}
#[test]
fn test_glob_star_all() {
assert!(glob_match("*", "anything"));
assert!(glob_match("*", ""));
}
#[test]
fn test_glob_single_star_within_segment() {
assert!(glob_match("user/*", "user/alice"));
assert!(glob_match("user/*", "user/bob"));
assert!(glob_match("*/alice", "user/alice"));
}
#[test]
fn test_glob_single_star_does_not_cross_slash() {
assert!(!glob_match("space/*/db", "space/le-zinc/sub/db"));
assert!(glob_match("space/*/db", "space/le-zinc/db"));
}
#[test]
fn test_glob_star_prefix_suffix() {
assert!(glob_match("iam:*", "iam:GetUser"));
assert!(glob_match("iam:Get*", "iam:GetUser"));
assert!(!glob_match("iam:Delete*", "iam:GetUser"));
}
#[test]
fn test_glob_double_star_matches_everything() {
assert!(glob_match("**", "anything/with/slashes"));
assert!(glob_match("**", ""));
}
#[test]
fn test_glob_double_star_multi_segment() {
assert!(glob_match("space/le-zinc/**", "space/le-zinc/db/menu"));
assert!(glob_match(
"space/le-zinc/**",
"space/le-zinc/db/menu/items"
));
assert!(glob_match("space/le-zinc/**", "space/le-zinc"));
assert!(!glob_match("space/le-zinc/**", "space/other/db"));
}
#[test]
fn test_glob_double_star_in_middle() {
assert!(glob_match(
"arn:wami:**/user/*",
"arn:wami:iam:12345678:wami:999:user/alice"
));
assert!(glob_match("a/**/z", "a/b/c/d/z"));
assert!(glob_match("a/**/z", "a/z"));
}
#[test]
fn test_glob_double_star_at_start() {
assert!(glob_match(
"**/user/alice",
"arn:wami:iam:123:wami:999:user/alice"
));
}
#[test]
fn test_arn_pattern_with_variables() {
let ctx = MatchContext {
tenant: Some("12345678".into()),
principal: Some("alice".into()),
service: Some("iam".into()),
};
assert!(matches_arn_pattern(
"arn:wami:iam:${tenant}:wami:*:user/*",
"arn:wami:iam:12345678:wami:999:user/alice",
&ctx,
));
}
#[test]
fn test_arn_pattern_tenant_mismatch() {
let ctx = MatchContext {
tenant: Some("99999999".into()),
..Default::default()
};
assert!(!matches_arn_pattern(
"arn:wami:iam:${tenant}:wami:*:user/*",
"arn:wami:iam:12345678:wami:999:user/alice",
&ctx,
));
}
#[test]
fn test_arn_pattern_double_star_space_scoping() {
let ctx = MatchContext {
tenant: Some("12345678".into()),
..Default::default()
};
let pattern = "arn:wami:hub:${tenant}:wami:*:space/le-zinc/**";
assert!(matches_arn_pattern(
pattern,
"arn:wami:hub:12345678:wami:999:space/le-zinc/db/menu",
&ctx,
));
assert!(matches_arn_pattern(
pattern,
"arn:wami:hub:12345678:wami:999:space/le-zinc/persona/chef",
&ctx,
));
assert!(!matches_arn_pattern(
pattern,
"arn:wami:hub:12345678:wami:999:space/other-space/db/menu",
&ctx,
));
}
#[test]
fn test_arn_pattern_no_context() {
let ctx = MatchContext::default();
assert!(matches_arn_pattern(
"arn:wami:iam:*:wami:*:user/*",
"arn:wami:iam:12345678:wami:999:user/alice",
&ctx,
));
}
#[test]
fn test_arn_pattern_wildcard_all() {
let ctx = MatchContext::default();
assert!(matches_arn_pattern("*", "anything", &ctx));
}
#[test]
fn test_arn_pattern_exact_match() {
let ctx = MatchContext::default();
assert!(matches_arn_pattern(
"arn:wami:iam:12345678:wami:999:user/alice",
"arn:wami:iam:12345678:wami:999:user/alice",
&ctx,
));
}
#[test]
fn test_glob_empty_pattern_empty_text() {
assert!(glob_match("", ""));
}
#[test]
fn test_glob_empty_pattern_nonempty_text() {
assert!(!glob_match("", "something"));
}
#[test]
fn test_glob_consecutive_stars() {
assert!(glob_match("a/**/*", "a/b/c"));
}
#[test]
fn test_glob_star_at_colon_boundary() {
assert!(glob_match(
"arn:wami:iam:*:wami:*:user/alice",
"arn:wami:iam:12345678:wami:999:user/alice"
));
}
#[test]
fn test_backward_compat_existing_patterns() {
assert!(glob_match(
"arn:wami:iam:*:user/*",
"arn:wami:iam:12345678:wami:999:user/alice"
));
assert!(glob_match("*.example.com", "api.example.com"));
assert!(glob_match("test-*-prod", "test-api-prod"));
assert!(!glob_match(
"arn:*:role/*",
"arn:wami:iam:12345678:wami:999:user/alice"
));
}
}