core_policy/
resource_matcher.rs

1//! # Resource Matcher
2//!
3//! Extensible resource matching system that allows custom matchers to be registered
4//! for handling domain-specific resource types without modifying core types.
5//!
6//! ## Usage Example
7//!
8//! ```rust
9//! use core_policy::resource_matcher::{ResourceMatcher, ResourceMatcherRegistry};
10//! use core_policy::Resource;
11//!
12//! // 1. Implement custom matcher
13//! struct S3BucketMatcher;
14//!
15//! impl ResourceMatcher for S3BucketMatcher {
16//!     fn matches(&self, pattern: &Resource, target: &Resource) -> bool {
17//!         // Custom logic for S3
18//!         match (pattern, target) {
19//!             (Resource::Custom { resource_type: rt1, path: p },
20//!              Resource::Custom { resource_type: rt2, path: t })
21//!              if rt1 == "s3" && rt2 == "s3" => {
22//!                  // Simple example: exact match or wildcard
23//!                  p == t || p == "*"
24//!             }
25//!             _ => false,
26//!         }
27//!     }
28//! }
29//!
30//! // 2. Register matcher
31//! let mut registry = ResourceMatcherRegistry::new();
32//! registry.register("s3", Box::new(S3BucketMatcher));
33//!
34//! // 3. Use matcher
35//! let pattern = Resource::Custom {
36//!     resource_type: "s3".into(),
37//!     path: "*".into(),
38//! };
39//! let target = Resource::Custom {
40//!     resource_type: "s3".into(),
41//!     path: "bucket-1".into(),
42//! };
43//!
44//! assert!(registry.matches(&pattern, &target));
45//! ```
46
47use crate::Resource;
48use alloc::boxed::Box;
49use alloc::collections::BTreeMap;
50use alloc::string::String;
51use alloc::vec::Vec;
52
53/// Trait for implementing custom resource matching logic
54pub trait ResourceMatcher: Send + Sync {
55    /// Returns true if the pattern matches the target
56    fn matches(&self, pattern: &Resource, target: &Resource) -> bool;
57
58    /// Returns the priority of this matcher (higher means checked first)
59    fn priority(&self) -> u32 {
60        0
61    }
62
63    /// Returns the name of the matcher strategy
64    fn name(&self) -> &str {
65        "ResourceMatcher"
66    }
67}
68
69/// Registry for custom resource matchers
70pub struct ResourceMatcherRegistry {
71    matchers: BTreeMap<String, Box<dyn ResourceMatcher>>,
72}
73
74impl ResourceMatcherRegistry {
75    /// Creates a new empty registry
76    pub fn new() -> Self {
77        Self {
78            matchers: BTreeMap::new(),
79        }
80    }
81
82    /// Registers a new matcher for a resource type
83    ///
84    /// `Some(old_matcher)` if there was a previous matcher, `None` if new
85    ///
86    pub fn register(
87        &mut self,
88        resource_type: impl Into<String>,
89        matcher: Box<dyn ResourceMatcher>,
90    ) -> Option<Box<dyn ResourceMatcher>> {
91        self.matchers.insert(resource_type.into(), matcher)
92    }
93
94    /// Unregisters a matcher
95    ///
96    /// # Returns
97    ///
98    /// `Some(matcher)` if it existed, `None` if it was not registered
99    pub fn unregister(&mut self, resource_type: &str) -> Option<Box<dyn ResourceMatcher>> {
100        self.matchers.remove(resource_type)
101    }
102
103    /// Checks if there is a matcher registered for a type
104    pub fn has_matcher(&self, resource_type: &str) -> bool {
105        self.matchers.contains_key(resource_type)
106    }
107
108    /// Executes matching using the appropriate matcher
109    ///
110    /// If there is no custom matcher, it uses the default `Resource::matches()` method.
111    ///
112    pub fn matches(&self, pattern: &Resource, target: &Resource) -> bool {
113        // Try custom matcher first
114        if let Resource::Custom { resource_type, .. } = pattern {
115            if let Some(matcher) = self.matchers.get(resource_type) {
116                return matcher.matches(pattern, target);
117            }
118        }
119
120        // Fallback to default matching
121        pattern.matches(target)
122    }
123
124    /// Lists all registered resource types
125    pub fn list_matchers(&self) -> Vec<String> {
126        self.matchers.keys().cloned().collect()
127    }
128
129    /// Counts the number of registered matchers
130    pub fn count(&self) -> usize {
131        self.matchers.len()
132    }
133
134    /// Clears all matchers
135    pub fn clear(&mut self) {
136        self.matchers.clear();
137    }
138}
139
140impl Default for ResourceMatcherRegistry {
141    fn default() -> Self {
142        Self::new()
143    }
144}
145
146#[cfg(test)]
147mod tests {
148    use super::*;
149    use alloc::string::ToString;
150
151    // Test matcher: always returns true
152    struct AlwaysMatcher;
153    impl ResourceMatcher for AlwaysMatcher {
154        fn matches(&self, _pattern: &Resource, _target: &Resource) -> bool {
155            true
156        }
157    }
158
159    // Test matcher: always returns false
160    struct NeverMatcher;
161    impl ResourceMatcher for NeverMatcher {
162        fn matches(&self, _pattern: &Resource, _target: &Resource) -> bool {
163            false
164        }
165    }
166
167    // Test matcher: exact path matching
168    struct ExactMatcher;
169    impl ResourceMatcher for ExactMatcher {
170        fn matches(&self, pattern: &Resource, target: &Resource) -> bool {
171            match (pattern, target) {
172                (
173                    Resource::Custom {
174                        resource_type: t1,
175                        path: p1,
176                    },
177                    Resource::Custom {
178                        resource_type: t2,
179                        path: p2,
180                    },
181                ) => t1 == t2 && p1 == p2,
182                _ => false,
183            }
184        }
185    }
186
187    // Test matcher with custom priority
188    struct PriorityMatcher;
189    impl ResourceMatcher for PriorityMatcher {
190        fn matches(&self, _: &Resource, _: &Resource) -> bool {
191            true
192        }
193
194        fn priority(&self) -> u32 {
195            100
196        }
197    }
198
199    #[test]
200    fn test_registry_new() {
201        let registry = ResourceMatcherRegistry::new();
202        assert_eq!(registry.count(), 0);
203    }
204
205    #[test]
206    fn test_register_matcher() {
207        let mut registry = ResourceMatcherRegistry::new();
208
209        registry.register("test", Box::new(AlwaysMatcher));
210        assert_eq!(registry.count(), 1);
211        assert!(registry.has_matcher("test"));
212    }
213
214    #[test]
215    fn test_register_duplicate_replaces() {
216        let mut registry = ResourceMatcherRegistry::new();
217
218        let old = registry.register("test", Box::new(AlwaysMatcher));
219        assert!(old.is_none());
220
221        let old = registry.register("test", Box::new(NeverMatcher));
222        assert!(old.is_some());
223        assert_eq!(registry.count(), 1);
224    }
225
226    #[test]
227    fn test_unregister_matcher() {
228        let mut registry = ResourceMatcherRegistry::new();
229
230        registry.register("test", Box::new(AlwaysMatcher));
231        assert_eq!(registry.count(), 1);
232
233        let removed = registry.unregister("test");
234        assert!(removed.is_some());
235        assert_eq!(registry.count(), 0);
236    }
237
238    #[test]
239    fn test_unregister_nonexistent() {
240        let mut registry = ResourceMatcherRegistry::new();
241
242        let removed = registry.unregister("nonexistent");
243        assert!(removed.is_none());
244    }
245
246    #[test]
247    fn test_has_matcher() {
248        let mut registry = ResourceMatcherRegistry::new();
249
250        assert!(!registry.has_matcher("test"));
251
252        registry.register("test", Box::new(AlwaysMatcher));
253        assert!(registry.has_matcher("test"));
254
255        registry.unregister("test");
256        assert!(!registry.has_matcher("test"));
257    }
258
259    #[test]
260    fn test_matches_with_custom_matcher() {
261        let mut registry = ResourceMatcherRegistry::new();
262        registry.register("test", Box::new(AlwaysMatcher));
263
264        let pattern = Resource::Custom {
265            resource_type: "test".into(),
266            path: "anything".into(),
267        };
268        let target = Resource::Custom {
269            resource_type: "test".into(),
270            path: "different".into(),
271        };
272
273        assert!(registry.matches(&pattern, &target));
274    }
275
276    #[test]
277    fn test_matches_without_custom_matcher_uses_default() {
278        let registry = ResourceMatcherRegistry::new();
279
280        let pattern = Resource::File("/home/*".into());
281        let target = Resource::File("/home/user".into());
282
283        // Without custom matcher, it should use the default Resource::matches() method
284        assert!(registry.matches(&pattern, &target));
285    }
286
287    #[test]
288    fn test_exact_matcher() {
289        let mut registry = ResourceMatcherRegistry::new();
290        registry.register("exact", Box::new(ExactMatcher));
291
292        let pattern = Resource::Custom {
293            resource_type: "exact".into(),
294            path: "/path/to/file".into(),
295        };
296        let target_match = Resource::Custom {
297            resource_type: "exact".into(),
298            path: "/path/to/file".into(),
299        };
300        let target_no_match = Resource::Custom {
301            resource_type: "exact".into(),
302            path: "/different/path".into(),
303        };
304
305        assert!(registry.matches(&pattern, &target_match));
306        assert!(!registry.matches(&pattern, &target_no_match));
307    }
308
309    #[test]
310    fn test_list_matchers() {
311        let mut registry = ResourceMatcherRegistry::new();
312
313        registry.register("s3", Box::new(AlwaysMatcher));
314        registry.register("docker", Box::new(NeverMatcher));
315
316        let list = registry.list_matchers();
317        assert_eq!(list.len(), 2);
318        assert!(list.contains(&"s3".to_string()));
319        assert!(list.contains(&"docker".to_string()));
320    }
321
322    #[test]
323    fn test_count() {
324        let mut registry = ResourceMatcherRegistry::new();
325        assert_eq!(registry.count(), 0);
326
327        registry.register("a", Box::new(AlwaysMatcher));
328        assert_eq!(registry.count(), 1);
329
330        registry.register("b", Box::new(NeverMatcher));
331        assert_eq!(registry.count(), 2);
332
333        registry.unregister("a");
334        assert_eq!(registry.count(), 1);
335    }
336
337    #[test]
338    fn test_clear() {
339        let mut registry = ResourceMatcherRegistry::new();
340
341        registry.register("a", Box::new(AlwaysMatcher));
342        registry.register("b", Box::new(NeverMatcher));
343        assert_eq!(registry.count(), 2);
344
345        registry.clear();
346        assert_eq!(registry.count(), 0);
347    }
348
349    #[test]
350    fn test_matcher_priority() {
351        let matcher = PriorityMatcher;
352        assert_eq!(matcher.priority(), 100);
353    }
354
355    #[test]
356    fn test_fallback_to_default_file_matching() {
357        let registry = ResourceMatcherRegistry::new();
358
359        // Test File matching (without custom matcher)
360        let pattern = Resource::File("/data/*.txt".into());
361        let target = Resource::File("/data/file.txt".into());
362        assert!(registry.matches(&pattern, &target));
363    }
364
365    #[test]
366    fn test_fallback_to_default_usb_matching() {
367        let registry = ResourceMatcherRegistry::new();
368
369        // Test USB matching (without custom matcher)
370        let pattern = Resource::Usb("usb-*".into());
371        let target = Resource::Usb("usb-keyboard".into());
372        assert!(registry.matches(&pattern, &target));
373    }
374
375    #[test]
376    fn test_fallback_to_default_all_matching() {
377        let registry = ResourceMatcherRegistry::new();
378
379        // Test All wildcard (without custom matcher)
380        let pattern = Resource::All;
381        let target = Resource::File("/anything".into());
382        assert!(registry.matches(&pattern, &target));
383    }
384}