role_system/
resource.rs

1//! Resource definitions for access control.
2
3use std::collections::HashMap;
4
5/// A resource represents something that can be accessed or acted upon.
6#[derive(Debug, Clone, PartialEq, Eq)]
7#[cfg_attr(feature = "persistence", derive(serde::Serialize, serde::Deserialize))]
8pub struct Resource {
9    /// Unique identifier for the resource.
10    id: String,
11    /// Type of resource (e.g., "document", "user", "project").
12    resource_type: String,
13    /// Optional name for the resource.
14    name: Option<String>,
15    /// Additional attributes for the resource.
16    attributes: HashMap<String, String>,
17    /// Hierarchical path for the resource (e.g., "/projects/web-app/documents/readme.md").
18    path: Option<String>,
19}
20
21impl Resource {
22    /// Create a new resource.
23    pub fn new(id: impl Into<String>, resource_type: impl Into<String>) -> Self {
24        let id = id.into();
25        let resource_type = resource_type.into();
26        
27        // Validate resource ID for path traversal attempts
28        if id.contains("..") || id.contains('\0') {
29            panic!("Resource ID cannot contain path traversal sequences or null characters");
30        }
31        
32        // Validate resource type for path traversal attempts
33        if resource_type.contains("..") || resource_type.contains('\0') {
34            panic!("Resource type cannot contain path traversal sequences or null characters");
35        }
36        
37        Self {
38            id,
39            resource_type,
40            name: None,
41            attributes: HashMap::new(),
42            path: None,
43        }
44    }
45
46    /// Get the resource's unique identifier.
47    pub fn id(&self) -> &str {
48        &self.id
49    }
50
51    /// Get the resource type.
52    pub fn resource_type(&self) -> &str {
53        &self.resource_type
54    }
55
56    /// Set the resource name.
57    pub fn with_name(mut self, name: impl Into<String>) -> Self {
58        self.name = Some(name.into());
59        self
60    }
61
62    /// Get the resource name.
63    pub fn name(&self) -> Option<&str> {
64        self.name.as_deref()
65    }
66
67    /// Set the resource name.
68    pub fn set_name(&mut self, name: impl Into<String>) {
69        self.name = Some(name.into());
70    }
71
72    /// Set the resource path.
73    pub fn with_path(mut self, path: impl Into<String>) -> Self {
74        self.path = Some(path.into());
75        self
76    }
77
78    /// Get the resource path.
79    pub fn path(&self) -> Option<&str> {
80        self.path.as_deref()
81    }
82
83    /// Set the resource path.
84    pub fn set_path(&mut self, path: impl Into<String>) {
85        self.path = Some(path.into());
86    }
87
88    /// Add an attribute to the resource.
89    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
90        self.attributes.insert(key.into(), value.into());
91        self
92    }
93
94    /// Set an attribute on the resource.
95    pub fn set_attribute(&mut self, key: impl Into<String>, value: impl Into<String>) {
96        self.attributes.insert(key.into(), value.into());
97    }
98
99    /// Get an attribute value.
100    pub fn attribute(&self, key: &str) -> Option<&str> {
101        self.attributes.get(key).map(|s| s.as_str())
102    }
103
104    /// Get all attributes.
105    pub fn attributes(&self) -> &HashMap<String, String> {
106        &self.attributes
107    }
108
109    /// Remove an attribute.
110    pub fn remove_attribute(&mut self, key: &str) -> Option<String> {
111        self.attributes.remove(key)
112    }
113
114    /// Check if the resource has a specific attribute.
115    pub fn has_attribute(&self, key: &str) -> bool {
116        self.attributes.contains_key(key)
117    }
118
119    /// Get the effective name for display purposes.
120    pub fn effective_name(&self) -> &str {
121        self.name.as_deref().unwrap_or(&self.id)
122    }
123
124    /// Check if this resource matches a pattern.
125    /// Patterns can include wildcards (*) and hierarchical matching.
126    pub fn matches_pattern(&self, pattern: &str) -> bool {
127        if pattern == "*" {
128            return true;
129        }
130
131        // Exact match
132        if pattern == self.id || pattern == self.resource_type {
133            return true;
134        }
135
136        // Type wildcard (e.g., "documents/*")
137        if let Some(type_prefix) = pattern.strip_suffix("/*") {
138            if self.resource_type == type_prefix {
139                return true;
140            }
141        }
142
143        // Path matching
144        if let Some(resource_path) = &self.path {
145            if self.matches_path_pattern(resource_path, pattern) {
146                return true;
147            }
148        }
149
150        false
151    }
152
153    /// Check if the resource is within a specific parent path.
154    pub fn is_under_path(&self, parent_path: &str) -> bool {
155        if let Some(resource_path) = &self.path {
156            resource_path.starts_with(parent_path)
157        } else {
158            false
159        }
160    }
161
162    /// Get the parent path of this resource.
163    pub fn parent_path(&self) -> Option<String> {
164        self.path.as_ref().and_then(|p| {
165            p.rfind('/').map(|i| p[..i].to_string())
166        })
167    }
168
169    fn matches_path_pattern(&self, path: &str, pattern: &str) -> bool {
170        // Simple glob-style matching
171        if pattern.contains('*') {
172            let parts: Vec<&str> = pattern.split('*').collect();
173            if parts.len() == 2 {
174                let prefix = parts[0];
175                let suffix = parts[1];
176                return path.starts_with(prefix) && path.ends_with(suffix);
177            }
178        }
179        
180        path == pattern
181    }
182}
183
184impl std::fmt::Display for Resource {
185    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
186        match (&self.name, &self.path) {
187            (Some(name), Some(path)) => write!(f, "{} ({}:{} at {})", name, self.resource_type, self.id, path),
188            (Some(name), None) => write!(f, "{} ({}:{})", name, self.resource_type, self.id),
189            (None, Some(path)) => write!(f, "{}:{} at {}", self.resource_type, self.id, path),
190            (None, None) => write!(f, "{}:{}", self.resource_type, self.id),
191        }
192    }
193}
194
195/// Builder for creating resources with a fluent API.
196#[derive(Debug, Default)]
197pub struct ResourceBuilder {
198    id: Option<String>,
199    resource_type: Option<String>,
200    name: Option<String>,
201    path: Option<String>,
202    attributes: HashMap<String, String>,
203}
204
205impl ResourceBuilder {
206    /// Create a new resource builder.
207    pub fn new() -> Self {
208        Self::default()
209    }
210
211    /// Set the resource ID.
212    pub fn id(mut self, id: impl Into<String>) -> Self {
213        self.id = Some(id.into());
214        self
215    }
216
217    /// Set the resource type.
218    pub fn resource_type(mut self, resource_type: impl Into<String>) -> Self {
219        self.resource_type = Some(resource_type.into());
220        self
221    }
222
223    /// Set the resource name.
224    pub fn name(mut self, name: impl Into<String>) -> Self {
225        self.name = Some(name.into());
226        self
227    }
228
229    /// Set the resource path.
230    pub fn path(mut self, path: impl Into<String>) -> Self {
231        self.path = Some(path.into());
232        self
233    }
234
235    /// Add an attribute.
236    pub fn attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
237        self.attributes.insert(key.into(), value.into());
238        self
239    }
240
241    /// Build the resource.
242    pub fn build(self) -> Result<Resource, String> {
243        let id = self.id.ok_or("Resource ID is required")?;
244        let resource_type = self.resource_type.ok_or("Resource type is required")?;
245
246        let mut resource = Resource::new(id, resource_type);
247        
248        if let Some(name) = self.name {
249            resource = resource.with_name(name);
250        }
251        
252        if let Some(path) = self.path {
253            resource = resource.with_path(path);
254        }
255
256        for (key, value) in self.attributes {
257            resource = resource.with_attribute(key, value);
258        }
259
260        Ok(resource)
261    }
262}
263
264/// Common resource types for convenience.
265pub mod types {
266    use super::Resource;
267
268    /// Create a document resource.
269    pub fn document(id: impl Into<String>) -> Resource {
270        Resource::new(id, "document")
271    }
272
273    /// Create a user resource.
274    pub fn user(id: impl Into<String>) -> Resource {
275        Resource::new(id, "user")
276    }
277
278    /// Create a project resource.
279    pub fn project(id: impl Into<String>) -> Resource {
280        Resource::new(id, "project")
281    }
282
283    /// Create a file resource.
284    pub fn file(id: impl Into<String>) -> Resource {
285        Resource::new(id, "file")
286    }
287
288    /// Create a database resource.
289    pub fn database(id: impl Into<String>) -> Resource {
290        Resource::new(id, "database")
291    }
292
293    /// Create an API endpoint resource.
294    pub fn api_endpoint(id: impl Into<String>) -> Resource {
295        Resource::new(id, "api_endpoint")
296    }
297}
298
299#[cfg(test)]
300mod tests {
301    use super::*;
302    use super::types::*;
303
304    #[test]
305    fn test_resource_creation() {
306        let resource = Resource::new("doc123", "document")
307            .with_name("My Document")
308            .with_path("/projects/web-app/docs/readme.md")
309            .with_attribute("owner", "john@example.com")
310            .with_attribute("created", "2024-01-01");
311
312        assert_eq!(resource.id(), "doc123");
313        assert_eq!(resource.resource_type(), "document");
314        assert_eq!(resource.name(), Some("My Document"));
315        assert_eq!(resource.path(), Some("/projects/web-app/docs/readme.md"));
316        assert_eq!(resource.attribute("owner"), Some("john@example.com"));
317        assert_eq!(resource.effective_name(), "My Document");
318    }
319
320    #[test]
321    fn test_resource_pattern_matching() {
322        let resource = Resource::new("doc1", "document")
323            .with_path("/projects/web-app/docs/readme.md");
324
325        assert!(resource.matches_pattern("*"));
326        assert!(resource.matches_pattern("doc1"));
327        assert!(resource.matches_pattern("document"));
328        assert!(resource.matches_pattern("document/*"));
329        assert!(!resource.matches_pattern("user"));
330        assert!(!resource.matches_pattern("users/*"));
331    }
332
333    #[test]
334    fn test_resource_path_operations() {
335        let resource = Resource::new("doc1", "document")
336            .with_path("/projects/web-app/docs/readme.md");
337
338        assert!(resource.is_under_path("/projects"));
339        assert!(resource.is_under_path("/projects/web-app"));
340        assert!(!resource.is_under_path("/other"));
341
342        assert_eq!(resource.parent_path(), Some("/projects/web-app/docs".to_string()));
343    }
344
345    #[test]
346    fn test_resource_builder() {
347        let resource = ResourceBuilder::new()
348            .id("test-id")
349            .resource_type("test-type")
350            .name("Test Resource")
351            .path("/test/path")
352            .attribute("key", "value")
353            .build()
354            .unwrap();
355
356        assert_eq!(resource.id(), "test-id");
357        assert_eq!(resource.resource_type(), "test-type");
358        assert_eq!(resource.name(), Some("Test Resource"));
359        assert_eq!(resource.path(), Some("/test/path"));
360        assert_eq!(resource.attribute("key"), Some("value"));
361    }
362
363    #[test]
364    fn test_common_resource_types() {
365        let doc = document("doc1");
366        let user_res = user("user1");
367        let proj = project("proj1");
368
369        assert_eq!(doc.resource_type(), "document");
370        assert_eq!(user_res.resource_type(), "user");
371        assert_eq!(proj.resource_type(), "project");
372    }
373
374    #[test]
375    fn test_resource_effective_name() {
376        let named_resource = Resource::new("r1", "type").with_name("Named Resource");
377        let unnamed_resource = Resource::new("r2", "type");
378
379        assert_eq!(named_resource.effective_name(), "Named Resource");
380        assert_eq!(unnamed_resource.effective_name(), "r2");
381    }
382}