1use std::collections::HashMap;
2
3pub type SecurityScheme = String;
5
6pub type Entitlements = HashMap<SecurityScheme, Vec<String>>;
8
9pub type RequirementSet = HashMap<SecurityScheme, Vec<String>>;
11
12pub type Requirements = Vec<RequirementSet>;
14
15#[derive(Debug, Clone, PartialEq, Eq)]
17pub enum Pattern {
18 Structured {
20 resource: String,
21 name: String,
22 verb: String,
23 },
24 Opaque(String),
26}
27
28impl Pattern {
29 pub fn parse(s: &str) -> Self {
31 let parts: Vec<&str> = s.split(':').collect();
32 match parts.len() {
33 3 => Self::Structured {
34 resource: parts[0].to_string(),
35 name: parts[1].to_string(),
36 verb: parts[2].to_string(),
37 },
38 2 => Self::Structured {
39 resource: parts[0].to_string(),
40 name: "*".to_string(),
41 verb: parts[1].to_string(),
42 },
43 _ => Self::Opaque(s.to_string()),
44 }
45 }
46
47 pub fn satisfies(&self, required: &Pattern) -> bool {
49 match (self, required) {
50 (Self::Opaque(e), Self::Opaque(r)) => e == r,
51 (
52 Self::Structured {
53 resource: er,
54 name: en,
55 verb: ev,
56 },
57 Self::Structured {
58 resource: rr,
59 name: rn,
60 verb: rv,
61 },
62 ) => {
63 if er != rr {
65 return false;
66 }
67
68 if ev != rv && ev != "all" {
70 return false;
71 }
72
73 if en != rn && en != "*" && !en.is_empty() && rn != "*" && !rn.is_empty() {
75 return false;
76 }
77
78 true
79 }
80 _ => false,
82 }
83 }
84}
85
86pub struct EntitlementsChecker {
88 anonymous_entitlements: Vec<Pattern>,
89 base_entitlements: Vec<Pattern>,
90 default_scheme: String,
91}
92
93impl EntitlementsChecker {
94 pub fn new(anonymous_entitlements: Vec<String>, default_scheme: String) -> Self {
95 let parsed_anon = anonymous_entitlements.iter().map(|s| Pattern::parse(s)).collect();
96 Self {
97 anonymous_entitlements: parsed_anon,
98 base_entitlements: Vec::new(),
99 default_scheme,
100 }
101 }
102
103 pub fn with_base_entitlements(mut self, patterns: Vec<String>) -> Self {
112 self.base_entitlements = patterns.iter().map(|s| Pattern::parse(s)).collect();
113 self
114 }
115
116 pub fn verify(&self, user_entitlements: &Entitlements, requirements: &Requirements) -> bool {
118 if requirements.is_empty() {
119 return true;
120 }
121
122 let parsed: HashMap<String, Vec<Pattern>> = user_entitlements
123 .iter()
124 .map(|(scheme, list)| (scheme.clone(), list.iter().map(|s| Pattern::parse(s)).collect()))
125 .collect();
126
127 let is_anonymous = parsed.is_empty() || parsed.values().all(|v| v.is_empty());
128
129 for req_set in requirements {
130 if self.verify_set(&parsed, req_set, is_anonymous) {
131 return true;
132 }
133 }
134
135 false
136 }
137
138 fn verify_set(
143 &self,
144 user_patterns: &HashMap<String, Vec<Pattern>>,
145 req_set: &RequirementSet,
146 is_anonymous: bool,
147 ) -> bool {
148 for (scheme, required_patterns) in req_set {
149 let user_list_present = user_patterns.contains_key(scheme);
150 let has_fallback = scheme == &self.default_scheme
151 && (!self.base_entitlements.is_empty()
152 || (is_anonymous && !self.anonymous_entitlements.is_empty()));
153 if !user_list_present && !has_fallback {
154 return false;
155 }
156
157 let empty: Vec<Pattern> = Vec::new();
158 let user_list = user_patterns.get(scheme).unwrap_or(&empty);
159
160 for req_str in required_patterns {
161 let req_p = Pattern::parse(req_str);
162 let satisfied_by_user = user_list.iter().any(|p| p.satisfies(&req_p));
163 let satisfied_by_base = scheme == &self.default_scheme
164 && self.base_entitlements.iter().any(|p| p.satisfies(&req_p));
165 let satisfied_by_anon = scheme == &self.default_scheme
166 && is_anonymous
167 && self.anonymous_entitlements.iter().any(|p| p.satisfies(&req_p));
168 if !satisfied_by_user && !satisfied_by_base && !satisfied_by_anon {
169 return false;
170 }
171 }
172 }
173 true
174 }
175
176 pub fn verify_resource(
178 &self,
179 user_entitlements: &Entitlements,
180 resource: &str,
181 name: &str,
182 verb: &str,
183 additional_requirements: &Requirements,
184 ) -> bool {
185 let identity_req = format!("{}:{}:{}", resource, name, verb);
186
187 if additional_requirements.is_empty() {
188 let mut set = RequirementSet::new();
189 set.insert(self.default_scheme.clone(), vec![identity_req]);
190 return self.verify(user_entitlements, &vec![set]);
191 }
192
193 let mut combined_requirements = Vec::new();
194 for set in additional_requirements {
195 let mut new_set = set.clone();
196 new_set
197 .entry(self.default_scheme.clone())
198 .or_default()
199 .push(identity_req.clone());
200 combined_requirements.push(new_set);
201 }
202
203 self.verify(user_entitlements, &combined_requirements)
204 }
205}
206
207#[cfg(test)]
208mod tests {
209 use super::*;
210
211 #[test]
212 fn test_pattern_parse() {
213 assert_eq!(
214 Pattern::parse("pages:/foo:read"),
215 Pattern::Structured {
216 resource: "pages".to_string(),
217 name: "/foo".to_string(),
218 verb: "read".to_string()
219 }
220 );
221 assert_eq!(
222 Pattern::parse("pages:read"),
223 Pattern::Structured {
224 resource: "pages".to_string(),
225 name: "*".to_string(),
226 verb: "read".to_string()
227 }
228 );
229 assert_eq!(Pattern::parse("admin"), Pattern::Opaque("admin".to_string()));
230 }
231
232 #[test]
233 fn test_verify() {
234 let checker = EntitlementsChecker::new(vec!["anonymous:read".to_string()], "bearer".to_string());
235
236 let mut entitlements = Entitlements::new();
237 entitlements.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
238
239 let mut req_set = RequirementSet::new();
241 req_set.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
242 let requirements = vec![req_set];
243 assert!(checker.verify(&entitlements, &requirements));
244
245 let mut req_set = RequirementSet::new();
247 req_set.insert("bearer".to_string(), vec!["pages:*:read".to_string()]);
248 let requirements = vec![req_set];
249 assert!(checker.verify(&entitlements, &requirements));
250
251 let mut req_set = RequirementSet::new();
253 req_set.insert("bearer".to_string(), vec!["anonymous:*:read".to_string()]);
254 let requirements = vec![req_set];
255 assert!(!checker.verify(&entitlements, &requirements));
256
257 let empty_entitlements = Entitlements::new();
259 let mut req_set = RequirementSet::new();
260 req_set.insert("bearer".to_string(), vec!["anonymous:*:read".to_string()]);
261 let requirements = vec![req_set];
262 assert!(checker.verify(&empty_entitlements, &requirements));
263
264 let mut req_set = RequirementSet::new();
266 req_set.insert("bearer".to_string(), vec!["pages:foo:write".to_string()]);
267 let requirements = vec![req_set];
268 assert!(!checker.verify(&entitlements, &requirements));
269
270 let mut req_set1 = RequirementSet::new();
272 req_set1.insert("bearer".to_string(), vec!["pages:foo:write".to_string()]);
273 let mut req_set2 = RequirementSet::new();
274 req_set2.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
275 let requirements = vec![req_set1, req_set2];
276 assert!(checker.verify(&entitlements, &requirements));
277 }
278
279 #[test]
280 fn test_verify_resource() {
281 let checker = EntitlementsChecker::new(vec![], "bearer".to_string());
282 let mut entitlements = Entitlements::new();
283 entitlements.insert("bearer".to_string(), vec!["pages:foo:read".to_string(), "admin".to_string()]);
284
285 assert!(checker.verify_resource(&entitlements, "pages", "foo", "read", &vec![]));
287 assert!(!checker.verify_resource(&entitlements, "pages", "bar", "read", &vec![]));
288
289 let mut req_set = RequirementSet::new();
291 req_set.insert("bearer".to_string(), vec!["admin".to_string()]);
292 let additional = vec![req_set];
293 assert!(checker.verify_resource(&entitlements, "pages", "foo", "read", &additional));
294
295 let mut req_set = RequirementSet::new();
296 req_set.insert("bearer".to_string(), vec!["superadmin".to_string()]);
297 let additional = vec![req_set];
298 assert!(!checker.verify_resource(&entitlements, "pages", "foo", "read", &additional));
299 }
300
301 #[test]
302 fn test_anonymous_vs_base() {
303 let checker = EntitlementsChecker::new(
304 vec!["anon:read".to_string()],
305 "bearer".to_string(),
306 )
307 .with_base_entitlements(vec!["base:read".to_string()]);
308
309 let mut authed = Entitlements::new();
310 authed.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
311 let anonymous = Entitlements::new();
312
313 let need_anon = vec![{
314 let mut s = RequirementSet::new();
315 s.insert("bearer".to_string(), vec!["anon:read".to_string()]);
316 s
317 }];
318 let need_base = vec![{
319 let mut s = RequirementSet::new();
320 s.insert("bearer".to_string(), vec!["base:read".to_string()]);
321 s
322 }];
323
324 assert!(checker.verify(&authed, &need_base));
326 assert!(!checker.verify(&authed, &need_anon));
327
328 assert!(checker.verify(&anonymous, &need_base));
330 assert!(checker.verify(&anonymous, &need_anon));
331
332 let mut empty_list = Entitlements::new();
334 empty_list.insert("bearer".to_string(), vec![]);
335 assert!(checker.verify(&empty_list, &need_anon));
336
337 let mut other_scheme = Entitlements::new();
339 other_scheme.insert("oauth2".to_string(), vec!["scope1".to_string()]);
340 assert!(!checker.verify(&other_scheme, &need_anon));
341 }
342
343 #[test]
344 fn test_anonymous_vs_base_via_verify_resource() {
345 let base_checker = EntitlementsChecker::new(vec![], "bearer".to_string())
347 .with_base_entitlements(vec!["pages:/foo:read".to_string()]);
348 let mut authed = Entitlements::new();
349 authed.insert("bearer".to_string(), vec!["other:read".to_string()]);
350 assert!(base_checker.verify_resource(&authed, "pages", "/foo", "read", &vec![]));
351
352 let anonymous = Entitlements::new();
354 assert!(base_checker.verify_resource(&anonymous, "pages", "/foo", "read", &vec![]));
355
356 let anon_checker = EntitlementsChecker::new(
358 vec!["pages:/foo:read".to_string()],
359 "bearer".to_string(),
360 );
361 assert!(!anon_checker.verify_resource(&authed, "pages", "/foo", "read", &vec![]));
362
363 assert!(anon_checker.verify_resource(&anonymous, "pages", "/foo", "read", &vec![]));
365 }
366
367 #[test]
368 fn test_with_base_entitlements_replaces() {
369 let checker = EntitlementsChecker::new(vec![], "bearer".to_string())
370 .with_base_entitlements(vec!["first:read".to_string()])
371 .with_base_entitlements(vec!["second:read".to_string()]);
372
373 let mut authed = Entitlements::new();
374 authed.insert("bearer".to_string(), vec!["pages:foo:read".to_string()]);
375
376 let need_first = vec![{
377 let mut s = RequirementSet::new();
378 s.insert("bearer".to_string(), vec!["first:read".to_string()]);
379 s
380 }];
381 let need_second = vec![{
382 let mut s = RequirementSet::new();
383 s.insert("bearer".to_string(), vec!["second:read".to_string()]);
384 s
385 }];
386
387 assert!(!checker.verify(&authed, &need_first));
388 assert!(checker.verify(&authed, &need_second));
389 }
390}