1use crate::rule::{
9 AclAction, AclRuleFilter, BitmaskAuth, EndpointPattern, RequestContext, RequestMeta,
10 RuleMatcher,
11};
12use std::collections::HashMap;
13use std::sync::Arc;
14
15pub struct AclRule<A> {
17 pub(crate) matcher: Arc<dyn RuleMatcher<A>>,
18}
19
20impl<A> Clone for AclRule<A> {
21 fn clone(&self) -> Self {
22 Self {
23 matcher: self.matcher.clone(),
24 }
25 }
26}
27
28impl<A> std::fmt::Debug for AclRule<A> {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 f.debug_struct("AclRule")
31 .field("matcher", &self.matcher)
32 .finish()
33 }
34}
35
36impl<A> AclRule<A> {
37 pub fn from_matcher(matcher: Arc<dyn RuleMatcher<A>>) -> Self {
39 Self { matcher }
40 }
41
42 pub fn from_matcher_with_methods(
44 matcher: Arc<dyn RuleMatcher<A>>,
45 methods: Vec<http::Method>,
46 action: AclAction,
47 ) -> Self
48 where
49 A: Send + Sync + 'static,
50 {
51 Self {
52 matcher: Arc::new(MethodFilterMatcher {
53 inner: matcher,
54 methods,
55 action,
56 }),
57 }
58 }
59
60 pub fn action(&self) -> &AclAction {
62 self.matcher.action()
63 }
64
65 pub fn description(&self) -> Option<&str> {
67 self.matcher.description()
68 }
69}
70
71struct MethodFilterMatcher<A> {
73 inner: Arc<dyn RuleMatcher<A>>,
74 methods: Vec<http::Method>,
75 action: AclAction,
76}
77
78impl<A> std::fmt::Debug for MethodFilterMatcher<A> {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("MethodFilterMatcher")
81 .field("methods", &self.methods)
82 .field("inner", &self.inner)
83 .finish()
84 }
85}
86
87impl<A> RuleMatcher<A> for MethodFilterMatcher<A>
88where
89 A: Send + Sync,
90{
91 fn matches(&self, auth: &A, meta: &RequestMeta) -> bool {
92 (self.methods.is_empty() || self.methods.contains(&meta.method))
93 && self.inner.matches(auth, meta)
94 }
95
96 fn action(&self) -> &AclAction {
97 &self.action
98 }
99
100 fn description(&self) -> Option<&str> {
101 self.inner.description()
102 }
103}
104
105pub struct AclTable<A = BitmaskAuth> {
131 pub(crate) exact_rules: HashMap<String, Vec<AclRule<A>>>,
133 pub(crate) pattern_rules: Vec<(EndpointPattern, Vec<AclRule<A>>)>,
135 pub(crate) default_action: AclAction,
137}
138
139impl<A> Clone for AclTable<A> {
140 fn clone(&self) -> Self {
141 Self {
142 exact_rules: self.exact_rules.clone(),
143 pattern_rules: self.pattern_rules.clone(),
144 default_action: self.default_action.clone(),
145 }
146 }
147}
148
149impl<A> std::fmt::Debug for AclTable<A> {
150 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
151 f.debug_struct("AclTable")
152 .field("exact_rules_count", &self.exact_rules.len())
153 .field("pattern_rules_count", &self.pattern_rules.len())
154 .field("default_action", &self.default_action)
155 .finish()
156 }
157}
158
159impl<A> Default for AclTable<A> {
160 fn default() -> Self {
161 Self {
162 exact_rules: HashMap::new(),
163 pattern_rules: Vec::new(),
164 default_action: AclAction::Deny,
165 }
166 }
167}
168
169impl<A> AclTable<A> {
171 pub fn exact_rules(&self) -> &HashMap<String, Vec<AclRule<A>>> {
173 &self.exact_rules
174 }
175
176 pub fn pattern_rules(&self) -> &[(EndpointPattern, Vec<AclRule<A>>)] {
178 &self.pattern_rules
179 }
180
181 pub fn default_action(&self) -> AclAction {
183 self.default_action.clone()
184 }
185
186 pub fn evaluate_request(&self, auth: &A, meta: &RequestMeta) -> AclAction {
191 self.evaluate_request_with_match(auth, meta).0
192 }
193
194 pub fn evaluate_request_with_match(
196 &self,
197 auth: &A,
198 meta: &RequestMeta,
199 ) -> (AclAction, Option<(String, usize)>) {
200 if let Some(rules) = self.exact_rules.get(&meta.path) {
202 for (idx, rule) in rules.iter().enumerate() {
203 if rule.matcher.matches(auth, meta) {
204 tracing::debug!(
205 endpoint = %meta.path,
206 filter_index = idx,
207 filter_description = ?rule.description(),
208 ip = %meta.ip,
209 action = ?rule.action(),
210 "ACL exact match"
211 );
212 return (rule.action().clone(), Some((meta.path.clone(), idx)));
213 }
214 }
215 }
216
217 for (pattern, rules) in &self.pattern_rules {
219 if pattern.matches(&meta.path) {
220 let mut meta_with_params = meta.clone();
221 meta_with_params.path_params = pattern.extract_named_params(&meta.path);
222
223 for (idx, rule) in rules.iter().enumerate() {
224 if rule.matcher.matches(auth, &meta_with_params) {
225 tracing::debug!(
226 endpoint = ?pattern,
227 filter_index = idx,
228 filter_description = ?rule.description(),
229 ip = %meta.ip,
230 action = ?rule.action(),
231 "ACL pattern match"
232 );
233 return (
234 rule.action().clone(),
235 Some((format!("{:?}", pattern), idx)),
236 );
237 }
238 }
239 }
240 }
241
242 tracing::debug!(
243 path = %meta.path,
244 ip = %meta.ip,
245 action = ?self.default_action,
246 "No ACL rule matched, using default action"
247 );
248 (self.default_action.clone(), None)
249 }
250
251 pub fn is_request_allowed(&self, auth: &A, meta: &RequestMeta) -> bool {
253 self.evaluate_request(auth, meta) == AclAction::Allow
254 }
255}
256
257impl AclTable<BitmaskAuth> {
259 pub fn new() -> Self {
261 Self::default()
262 }
263
264 pub fn builder() -> AclTableBuilder<BitmaskAuth> {
266 AclTableBuilder::new()
267 }
268
269 pub fn evaluate(&self, path: &str, ctx: &RequestContext) -> AclAction {
294 self.evaluate_with_match(path, ctx).0
295 }
296
297 pub fn evaluate_with_match(
302 &self,
303 path: &str,
304 ctx: &RequestContext,
305 ) -> (AclAction, Option<(String, usize)>) {
306 let meta = RequestMeta {
307 method: http::Method::GET,
308 path: path.to_string(),
309 path_params: HashMap::new(),
310 ip: ctx.ip,
311 };
312 let auth = BitmaskAuth {
313 roles: ctx.roles,
314 id: ctx.id.to_string(),
315 };
316 self.evaluate_request_with_match(&auth, &meta)
317 }
318
319 pub fn is_allowed(&self, path: &str, ctx: &RequestContext) -> bool {
321 self.evaluate(path, ctx) == AclAction::Allow
322 }
323}
324
325pub struct AclTableBuilder<A = BitmaskAuth> {
327 exact_rules: HashMap<String, Vec<AclRule<A>>>,
328 pattern_rules: Vec<(EndpointPattern, Vec<AclRule<A>>)>,
329 default_action: AclAction,
330}
331
332impl<A> Default for AclTableBuilder<A> {
333 fn default() -> Self {
334 Self {
335 exact_rules: HashMap::new(),
336 pattern_rules: Vec::new(),
337 default_action: AclAction::Deny,
338 }
339 }
340}
341
342impl<A> std::fmt::Debug for AclTableBuilder<A> {
343 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
344 f.debug_struct("AclTableBuilder")
345 .field("default_action", &self.default_action)
346 .finish()
347 }
348}
349
350impl<A: 'static> AclTableBuilder<A> {
352 pub fn new() -> Self {
354 Self::default()
355 }
356
357 pub fn default_action(mut self, action: AclAction) -> Self {
361 self.default_action = action;
362 self
363 }
364
365 pub fn add_exact_matcher(
367 mut self,
368 endpoint: impl Into<String>,
369 matcher: impl RuleMatcher<A> + 'static,
370 ) -> Self {
371 let rule = AclRule {
372 matcher: Arc::new(matcher),
373 };
374 self.exact_rules.entry(endpoint.into()).or_default().push(rule);
375 self
376 }
377
378 pub fn add_pattern_matcher(
380 mut self,
381 pattern: EndpointPattern,
382 matcher: impl RuleMatcher<A> + 'static,
383 ) -> Self {
384 let rule = AclRule {
385 matcher: Arc::new(matcher),
386 };
387 for (existing_pattern, rules) in &mut self.pattern_rules {
388 let is_match = match (existing_pattern, &pattern) {
389 (EndpointPattern::Any, EndpointPattern::Any) => true,
390 (EndpointPattern::Prefix(a), EndpointPattern::Prefix(b)) => a == b,
391 (EndpointPattern::Glob(a), EndpointPattern::Glob(b)) => a == b,
392 (EndpointPattern::Exact(a), EndpointPattern::Exact(b)) => a == b,
393 _ => false,
394 };
395 if is_match {
396 rules.push(rule);
397 return self;
398 }
399 }
400 self.pattern_rules.push((pattern, vec![rule]));
401 self
402 }
403
404 pub fn add_any_matcher(self, matcher: impl RuleMatcher<A> + 'static) -> Self {
406 self.add_pattern_matcher(EndpointPattern::Any, matcher)
407 }
408
409 pub fn build(self) -> AclTable<A> {
411 AclTable {
412 exact_rules: self.exact_rules,
413 pattern_rules: self.pattern_rules,
414 default_action: self.default_action,
415 }
416 }
417
418 pub fn build_shared(self) -> Arc<AclTable<A>> {
420 Arc::new(self.build())
421 }
422}
423
424impl AclTableBuilder<BitmaskAuth> {
426 pub fn add_exact(self, endpoint: impl Into<String>, filter: AclRuleFilter) -> Self {
428 self.add_exact_matcher(endpoint, filter)
429 }
430
431 pub fn add_exact_filters(
433 mut self,
434 endpoint: impl Into<String>,
435 filters: impl IntoIterator<Item = AclRuleFilter>,
436 ) -> Self {
437 let endpoint = endpoint.into();
438 let rules: Vec<AclRule<BitmaskAuth>> = filters
439 .into_iter()
440 .map(|f| AclRule {
441 matcher: Arc::new(f),
442 })
443 .collect();
444 self.exact_rules.entry(endpoint).or_default().extend(rules);
445 self
446 }
447
448 pub fn add_prefix(self, prefix: impl Into<String>, filter: AclRuleFilter) -> Self {
450 let pattern = EndpointPattern::Prefix(prefix.into());
451 self.add_pattern_matcher(pattern, filter)
452 }
453
454 pub fn add_glob(self, glob: impl Into<String>, filter: AclRuleFilter) -> Self {
456 let pattern = EndpointPattern::Glob(glob.into());
457 self.add_pattern_matcher(pattern, filter)
458 }
459
460 pub fn add_any(self, filter: AclRuleFilter) -> Self {
462 self.add_pattern_matcher(EndpointPattern::Any, filter)
463 }
464
465 pub fn add_pattern(self, pattern: EndpointPattern, filter: AclRuleFilter) -> Self {
467 self.add_pattern_matcher(pattern, filter)
468 }
469}
470
471#[derive(Debug, Clone)]
473pub struct RuleEntry {
474 pub pattern: EndpointPattern,
476 pub filter: AclRuleFilter,
478}
479
480impl RuleEntry {
481 pub fn new(pattern: EndpointPattern, filter: AclRuleFilter) -> Self {
483 Self { pattern, filter }
484 }
485
486 pub fn exact(endpoint: impl Into<String>, filter: AclRuleFilter) -> Self {
488 Self::new(EndpointPattern::Exact(endpoint.into()), filter)
489 }
490
491 pub fn prefix(prefix: impl Into<String>, filter: AclRuleFilter) -> Self {
493 Self::new(EndpointPattern::Prefix(prefix.into()), filter)
494 }
495
496 pub fn glob(glob: impl Into<String>, filter: AclRuleFilter) -> Self {
498 Self::new(EndpointPattern::Glob(glob.into()), filter)
499 }
500
501 pub fn any(filter: AclRuleFilter) -> Self {
503 Self::new(EndpointPattern::Any, filter)
504 }
505}
506
507pub trait AclRuleProvider: Send + Sync {
534 type Error: std::error::Error + Send + Sync + 'static;
536
537 fn load_rules(&self) -> Result<Vec<RuleEntry>, Self::Error>;
539}
540
541#[derive(Debug, Clone)]
543pub struct StaticRuleProvider {
544 rules: Vec<RuleEntry>,
545}
546
547impl StaticRuleProvider {
548 pub fn new(rules: Vec<RuleEntry>) -> Self {
550 Self { rules }
551 }
552}
553
554impl AclRuleProvider for StaticRuleProvider {
555 type Error = std::convert::Infallible;
556
557 fn load_rules(&self) -> Result<Vec<RuleEntry>, Self::Error> {
558 Ok(self.rules.clone())
559 }
560}
561
562#[cfg(test)]
563mod tests {
564 use super::*;
565 use std::net::IpAddr;
566
567 const ROLE_ADMIN: u32 = 0b001;
568 const ROLE_USER: u32 = 0b010;
569 const ROLE_GUEST: u32 = 0b100;
570
571 #[test]
572 fn test_table_evaluation() {
573 let table = AclTable::builder()
574 .default_action(AclAction::Deny)
575 .add_any(
577 AclRuleFilter::new()
578 .role_mask(ROLE_ADMIN)
579 .action(AclAction::Allow),
580 )
581 .add_prefix(
583 "/api/",
584 AclRuleFilter::new()
585 .role_mask(ROLE_USER)
586 .action(AclAction::Allow),
587 )
588 .build();
589
590 let ip: IpAddr = "127.0.0.1".parse().unwrap();
591
592 let admin_ctx = RequestContext::new(ROLE_ADMIN, ip, "admin1");
594 assert!(table.is_allowed("/admin/dashboard", &admin_ctx));
595 assert!(table.is_allowed("/api/users", &admin_ctx));
596
597 let user_ctx = RequestContext::new(ROLE_USER, ip, "user1");
599 assert!(table.is_allowed("/api/users", &user_ctx));
600 assert!(!table.is_allowed("/admin/dashboard", &user_ctx));
601
602 let guest_ctx = RequestContext::new(ROLE_GUEST, ip, "guest1");
604 assert!(!table.is_allowed("/api/users", &guest_ctx));
605 }
606
607 #[test]
608 fn test_exact_before_pattern() {
609 let table = AclTable::builder()
611 .default_action(AclAction::Deny)
612 .add_exact(
614 "/public",
615 AclRuleFilter::new()
616 .role_mask(u32::MAX)
617 .action(AclAction::Allow),
618 )
619 .add_any(
621 AclRuleFilter::new()
622 .role_mask(u32::MAX)
623 .action(AclAction::Deny),
624 )
625 .build();
626
627 let ip: IpAddr = "127.0.0.1".parse().unwrap();
628 let ctx = RequestContext::new(0b1, ip, "anyone");
629
630 assert!(table.is_allowed("/public", &ctx));
631 assert!(!table.is_allowed("/private", &ctx));
632 }
633
634 #[test]
635 fn test_role_bitmask() {
636 let table = AclTable::builder()
637 .default_action(AclAction::Deny)
638 .add_exact(
639 "/shared",
640 AclRuleFilter::new()
641 .role_mask(ROLE_ADMIN | ROLE_USER) .action(AclAction::Allow),
643 )
644 .build();
645
646 let ip: IpAddr = "127.0.0.1".parse().unwrap();
647
648 assert!(table.is_allowed("/shared", &RequestContext::new(ROLE_ADMIN, ip, "a")));
650 assert!(table.is_allowed("/shared", &RequestContext::new(ROLE_USER, ip, "u")));
652 assert!(!table.is_allowed("/shared", &RequestContext::new(ROLE_GUEST, ip, "g")));
654 assert!(table.is_allowed(
656 "/shared",
657 &RequestContext::new(ROLE_ADMIN | ROLE_USER, ip, "au")
658 ));
659 }
660
661 #[test]
662 fn test_generic_table_custom_auth() {
663 #[derive(Debug, Clone)]
664 struct CustomAuth {
665 role: String,
666 }
667
668 #[derive(Debug)]
669 struct RequireRole {
670 role: String,
671 action: AclAction,
672 }
673
674 impl RuleMatcher<CustomAuth> for RequireRole {
675 fn matches(&self, auth: &CustomAuth, _meta: &RequestMeta) -> bool {
676 auth.role == self.role
677 }
678 fn action(&self) -> &AclAction {
679 &self.action
680 }
681 }
682
683 let table: AclTable<CustomAuth> = AclTableBuilder::new()
684 .default_action(AclAction::Deny)
685 .add_exact_matcher(
686 "/admin",
687 RequireRole {
688 role: "admin".to_string(),
689 action: AclAction::Allow,
690 },
691 )
692 .build();
693
694 let ip: IpAddr = "127.0.0.1".parse().unwrap();
695 let meta = RequestMeta {
696 method: http::Method::GET,
697 path: "/admin".to_string(),
698 path_params: HashMap::new(),
699 ip,
700 };
701
702 let admin = CustomAuth {
703 role: "admin".to_string(),
704 };
705 assert!(table.is_request_allowed(&admin, &meta));
706
707 let user = CustomAuth {
708 role: "user".to_string(),
709 };
710 assert!(!table.is_request_allowed(&user, &meta));
711 }
712}