structured_proxy/auth/
policy.rs1use globset::GlobMatcher;
7
8use crate::config::RoutePolicyConfig;
9
10pub struct RoutePolicy {
12 matcher: GlobMatcher,
13 methods: Vec<String>,
14 pub require_auth: bool,
16 pub required_roles: Vec<String>,
18}
19
20impl RoutePolicy {
21 fn matches_method(&self, method: &str) -> bool {
22 self.methods.iter().any(|m| m == "*" || m == method)
23 }
24}
25
26#[derive(Default)]
28pub struct Policies {
29 rules: Vec<RoutePolicy>,
30}
31
32impl Policies {
33 pub fn compile(configs: &[RoutePolicyConfig]) -> Result<Self, String> {
38 let rules = configs
39 .iter()
40 .map(|c| {
41 let matcher = globset::GlobBuilder::new(&c.path)
42 .literal_separator(true)
43 .build()
44 .map(|g| g.compile_matcher())
45 .map_err(|e| format!("invalid policy path {:?}: {e}", c.path))?;
46 Ok(RoutePolicy {
47 matcher,
48 methods: c.methods.iter().map(|m| m.to_uppercase()).collect(),
49 require_auth: c.require_auth,
50 required_roles: c.required_roles.clone(),
51 })
52 })
53 .collect::<Result<Vec<_>, String>>()?;
54 Ok(Self { rules })
55 }
56
57 pub fn match_rule(&self, path: &str, method: &str) -> Option<&RoutePolicy> {
59 self.rules
60 .iter()
61 .find(|r| r.matcher.is_match(path) && r.matches_method(method))
62 }
63}
64
65#[cfg(test)]
66mod tests {
67 use super::*;
68
69 fn policy(
70 path: &str,
71 methods: &[&str],
72 require_auth: bool,
73 roles: &[&str],
74 ) -> RoutePolicyConfig {
75 RoutePolicyConfig {
76 path: path.to_string(),
77 methods: methods.iter().map(|s| s.to_string()).collect(),
78 require_auth,
79 required_roles: roles.iter().map(|s| s.to_string()).collect(),
80 }
81 }
82
83 #[test]
84 fn matches_path_and_method() {
85 let p = Policies::compile(&[policy("/v1/admin/**", &["GET", "POST"], true, &["admin"])])
86 .unwrap();
87 assert!(p.match_rule("/v1/admin/users", "GET").is_some());
88 assert!(p.match_rule("/v1/admin/users", "POST").is_some());
89 assert!(p.match_rule("/v1/admin/users", "DELETE").is_none());
91 assert!(p.match_rule("/v1/public", "GET").is_none());
93 }
94
95 #[test]
96 fn wildcard_method_matches_any() {
97 let p = Policies::compile(&[policy("/v1/**", &["*"], true, &[])]).unwrap();
98 assert!(p.match_rule("/v1/x", "PATCH").is_some());
99 }
100
101 #[test]
102 fn first_matching_rule_wins() {
103 let p = Policies::compile(&[
104 policy("/v1/health", &["*"], false, &[]),
105 policy("/v1/**", &["*"], true, &["admin"]),
106 ])
107 .unwrap();
108 assert!(!p.match_rule("/v1/health", "GET").unwrap().require_auth);
110 assert!(p.match_rule("/v1/secret", "GET").unwrap().require_auth);
112 }
113}