kotoba_security/
capabilities.rs

1//! # Kotoba Capabilities
2//!
3//! Capability-based security system inspired by Deno's permission model.
4//! Provides fine-grained, explicit permissions for secure resource access.
5//!
6//! ## Overview
7//!
8//! Capabilities represent specific permissions to perform actions on resources.
9//! Unlike role-based access control (RBAC), capabilities are granted explicitly
10//! and can be attenuated (restricted) for safer operations.
11//!
12//! ## Key Concepts
13//!
14//! - **Capability**: A specific permission to perform an action on a resource
15//! - **CapabilitySet**: A collection of capabilities granted to a principal
16//! - **Attenuation**: Creating a more restricted capability set from an existing one
17//! - **Principal**: An entity (user, service, process) that holds capabilities
18//!
19//! ## Examples
20//!
21//! ```rust
22//! use kotoba_security::capabilities::*;
23//!
24//! // Create specific capabilities
25//! let read_users = Capability::new(ResourceType::Graph, Action::Read, Some("users:*".to_string()));
26//! let write_posts = Capability::new(ResourceType::Graph, Action::Write, Some("posts:owned".to_string()));
27//!
28//! // Create a capability set
29//! let mut cap_set = CapabilitySet::new();
30//! cap_set.add_capability(read_users);
31//! cap_set.add_capability(write_posts);
32//!
33//! // Check permissions
34//! let service = CapabilityService::new();
35//! assert!(service.check_capability(&cap_set, &ResourceType::Graph, &Action::Read, Some("users:123")));
36//! ```
37
38use serde::{Deserialize, Serialize};
39
40/// Resource types that can be protected by capabilities
41#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum ResourceType {
43    /// Graph database operations
44    Graph,
45    /// File system access
46    FileSystem,
47    /// Network access
48    Network,
49    /// Environment variables
50    Environment,
51    /// System operations
52    System,
53    /// Plugin/Extension operations
54    Plugin,
55    /// Query execution
56    Query,
57    /// Administrative operations
58    Admin,
59    /// User management
60    User,
61    /// Custom resource type
62    Custom(String),
63}
64
65impl Default for ResourceType {
66    fn default() -> Self {
67        ResourceType::Custom(String::new())
68    }
69}
70
71/// Actions that can be performed on resources
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum Action {
74    /// Read access
75    Read,
76    /// Write access
77    Write,
78    /// Execute/run access
79    Execute,
80    /// Delete access
81    Delete,
82    /// Create access
83    Create,
84    /// Update/modify access
85    Update,
86    /// Administrative access
87    Admin,
88    /// Custom action
89    Custom(String),
90}
91
92/// Represents a specific capability/permission
93#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Capability {
95    /// The type of resource this capability applies to
96    pub resource_type: ResourceType,
97    /// The action allowed on the resource
98    pub action: Action,
99    /// Optional scope/pattern limiting the capability (e.g., "users:*", "files:/tmp/**")
100    pub scope: Option<String>,
101    /// Optional conditions or constraints
102    pub conditions: Option<std::collections::HashMap<String, serde_json::Value>>,
103}
104
105impl Capability {
106    /// Create a new capability
107    pub fn new(resource_type: ResourceType, action: Action, scope: Option<String>) -> Self {
108        Self {
109            resource_type,
110            action,
111            scope,
112            conditions: None,
113        }
114    }
115
116    /// Create a capability with conditions
117    pub fn with_conditions(
118        resource_type: ResourceType,
119        action: Action,
120        scope: Option<String>,
121        conditions: std::collections::HashMap<String, serde_json::Value>,
122    ) -> Self {
123        Self {
124            resource_type,
125            action,
126            scope,
127            conditions: Some(conditions),
128        }
129    }
130
131    /// Check if this capability matches a request
132    pub fn matches(&self, resource_type: &ResourceType, action: &Action, scope: Option<&str>) -> bool {
133        // Check resource type and action
134        if &self.resource_type != resource_type || &self.action != action {
135            return false;
136        }
137
138        // If capability has no scope restriction, allow all
139        if self.scope.is_none() {
140            return true;
141        }
142
143        // If request has no scope but capability does, deny
144        if scope.is_none() {
145            return false;
146        }
147
148        let cap_scope = self.scope.as_ref().unwrap();
149        let req_scope = scope.unwrap();
150
151        // Simple pattern matching (can be extended with glob patterns)
152        self.scope_matches(cap_scope, req_scope)
153    }
154
155    /// Check if capability scope matches request scope
156    fn scope_matches(&self, cap_scope: &str, req_scope: &str) -> bool {
157        // Exact match
158        if cap_scope == req_scope {
159            return true;
160        }
161
162        // Wildcard matching
163        if cap_scope.ends_with(":*") {
164            let prefix = &cap_scope[..cap_scope.len() - 2];
165            return req_scope.starts_with(prefix) && req_scope[prefix.len()..].starts_with(':');
166        }
167
168        if cap_scope == "*" {
169            return true;
170        }
171
172        // TODO: Implement more sophisticated pattern matching (globs, etc.)
173        false
174    }
175
176    /// Create an attenuated (more restrictive) version of this capability
177    pub fn attenuate(mut self, new_scope: Option<String>) -> Self {
178        // Can only make scope more restrictive, not less
179        match (&self.scope, &new_scope) {
180            (Some(current), Some(new)) => {
181                if !self.scope_matches(current, new) {
182                    // New scope is more restrictive, keep it
183                    self.scope = new_scope;
184                }
185            }
186            (Some(_), None) => {
187                // Removing scope restriction - not allowed for attenuation
188                // Keep original scope
189            }
190            (None, Some(new_scope)) => {
191                // Adding scope restriction - allowed
192                self.scope = Some(new_scope.to_string());
193            }
194            (None, None) => {
195                // No change
196            }
197        }
198        self
199    }
200}
201
202/// A set of capabilities granted to a principal
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct CapabilitySet {
205    /// The capabilities in this set
206    pub capabilities: Vec<Capability>,
207    /// Optional metadata about this capability set
208    pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
209}
210
211impl CapabilitySet {
212    /// Create a new empty capability set
213    pub fn new() -> Self {
214        Self {
215            capabilities: Vec::new(),
216            metadata: None,
217        }
218    }
219
220    /// Create a capability set with metadata
221    pub fn with_metadata(metadata: std::collections::HashMap<String, serde_json::Value>) -> Self {
222        Self {
223            capabilities: Vec::new(),
224            metadata: Some(metadata),
225        }
226    }
227
228    /// Add a capability to this set
229    pub fn add_capability(&mut self, capability: Capability) {
230        // Avoid duplicates
231        if !self.capabilities.contains(&capability) {
232            self.capabilities.push(capability);
233        }
234    }
235
236    /// Remove a capability from this set
237    pub fn remove_capability(&mut self, capability: &Capability) {
238        self.capabilities.retain(|c| c != capability);
239    }
240
241    /// Check if this set contains a specific capability
242    pub fn has_capability(&self, capability: &Capability) -> bool {
243        self.capabilities.contains(capability)
244    }
245
246    /// Check if this set allows a specific action on a resource
247    pub fn allows(&self, resource_type: &ResourceType, action: &Action, scope: Option<&str>) -> bool {
248        self.capabilities.iter().any(|cap| cap.matches(resource_type, action, scope))
249    }
250
251    /// Get all capabilities for a specific resource type
252    pub fn capabilities_for_resource(&self, resource_type: &ResourceType) -> Vec<&Capability> {
253        self.capabilities.iter()
254            .filter(|cap| &cap.resource_type == resource_type)
255            .collect()
256    }
257
258    /// Create an attenuated capability set (more restrictive)
259    pub fn attenuate(&self, restrictions: Vec<Capability>) -> CapabilitySet {
260        let mut new_set = CapabilitySet::new();
261
262        // Only keep capabilities that are allowed by the restrictions
263        for restriction in restrictions {
264            for cap in &self.capabilities {
265                if cap.resource_type == restriction.resource_type &&
266                   cap.action == restriction.action {
267                    let attenuated = cap.clone().attenuate(restriction.scope.clone());
268                    new_set.add_capability(attenuated);
269                }
270            }
271        }
272
273        new_set
274    }
275
276    /// Combine this capability set with another (union)
277    pub fn union(&self, other: &CapabilitySet) -> CapabilitySet {
278        let mut combined = self.clone();
279        for cap in &other.capabilities {
280            combined.add_capability(cap.clone());
281        }
282        combined
283    }
284
285    /// Create intersection of this set with another
286    pub fn intersection(&self, other: &CapabilitySet) -> CapabilitySet {
287        let mut result = CapabilitySet::new();
288        for cap in &self.capabilities {
289            if other.capabilities.contains(cap) {
290                result.add_capability(cap.clone());
291            }
292        }
293        result
294    }
295
296    /// Check if this set is empty
297    pub fn is_empty(&self) -> bool {
298        self.capabilities.is_empty()
299    }
300
301    /// Get the number of capabilities
302    pub fn len(&self) -> usize {
303        self.capabilities.len()
304    }
305}
306
307impl Default for CapabilitySet {
308    fn default() -> Self {
309        Self::new()
310    }
311}
312
313/// Service for managing and validating capabilities
314pub struct CapabilityService {
315    /// Optional configuration
316    config: CapabilityConfig,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct CapabilityConfig {
321    /// Whether to enable capability logging
322    pub enable_logging: bool,
323    /// Whether to enable capability auditing
324    pub enable_auditing: bool,
325    /// Default attenuation policies
326    pub default_attenuation: Option<Vec<Capability>>,
327}
328
329impl Default for CapabilityConfig {
330    fn default() -> Self {
331        Self {
332            enable_logging: false,
333            enable_auditing: false,
334            default_attenuation: None,
335        }
336    }
337}
338
339impl CapabilityService {
340    /// Create a new capability service with default config
341    pub fn new() -> Self {
342        Self {
343            config: CapabilityConfig::default(),
344        }
345    }
346
347    /// Create a capability service with custom config
348    pub fn with_config(config: CapabilityConfig) -> Self {
349        Self { config }
350    }
351
352    /// Check if a capability set allows a specific action
353    pub fn check_capability(
354        &self,
355        cap_set: &CapabilitySet,
356        resource_type: &ResourceType,
357        action: &Action,
358        scope: Option<&str>,
359    ) -> bool {
360        let allowed = cap_set.allows(resource_type, action, scope);
361
362        if self.config.enable_logging {
363            println!("Capability check: {:?}::{:?} on {:?} -> {}", resource_type, action, scope, allowed);
364        }
365
366        // TODO: Add auditing logic here
367
368        allowed
369    }
370
371    /// Grant capabilities to a principal (returns updated capability set)
372    pub fn grant_capabilities(
373        &self,
374        existing_caps: &CapabilitySet,
375        new_caps: Vec<Capability>,
376    ) -> CapabilitySet {
377        let mut updated = existing_caps.clone();
378        for cap in new_caps {
379            updated.add_capability(cap);
380        }
381        updated
382    }
383
384    /// Revoke capabilities from a principal
385    pub fn revoke_capabilities(
386        &self,
387        existing_caps: &CapabilitySet,
388        caps_to_revoke: Vec<Capability>,
389    ) -> CapabilitySet {
390        let mut updated = existing_caps.clone();
391        for cap in caps_to_revoke {
392            updated.remove_capability(&cap);
393        }
394        updated
395    }
396
397    /// Create an attenuated capability set for safer operations
398    pub fn attenuate_capabilities(
399        &self,
400        cap_set: &CapabilitySet,
401        restrictions: Vec<Capability>,
402    ) -> CapabilitySet {
403        cap_set.attenuate(restrictions)
404    }
405
406    /// Create predefined capability sets for common use cases
407    pub fn create_preset_capability_set(preset: PresetCapabilitySet) -> CapabilitySet {
408        let mut cap_set = CapabilitySet::new();
409
410        match preset {
411            PresetCapabilitySet::ReadOnly => {
412                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
413                cap_set.add_capability(Capability::new(ResourceType::Query, Action::Execute, None));
414            }
415            PresetCapabilitySet::ReadWrite => {
416                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
417                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Write, None));
418                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Create, None));
419                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Update, None));
420                cap_set.add_capability(Capability::new(ResourceType::Query, Action::Execute, None));
421            }
422            PresetCapabilitySet::Admin => {
423                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
424                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Write, None));
425                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Create, None));
426                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Update, None));
427                cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Delete, None));
428                cap_set.add_capability(Capability::new(ResourceType::Query, Action::Execute, None));
429                cap_set.add_capability(Capability::new(ResourceType::User, Action::Admin, None));
430                cap_set.add_capability(Capability::new(ResourceType::Admin, Action::Admin, None));
431            }
432            PresetCapabilitySet::NetworkAccess => {
433                cap_set.add_capability(Capability::new(ResourceType::Network, Action::Read, None));
434                cap_set.add_capability(Capability::new(ResourceType::Network, Action::Write, None));
435            }
436            PresetCapabilitySet::FileSystemRead => {
437                cap_set.add_capability(Capability::new(ResourceType::FileSystem, Action::Read, None));
438            }
439        }
440
441        cap_set
442    }
443}
444
445/// Predefined capability sets for common use cases
446#[derive(Debug, Clone)]
447pub enum PresetCapabilitySet {
448    /// Read-only access to graphs and queries
449    ReadOnly,
450    /// Read-write access to graphs and queries
451    ReadWrite,
452    /// Full administrative access
453    Admin,
454    /// Network access permissions
455    NetworkAccess,
456    /// File system read permissions
457    FileSystemRead,
458}
459
460#[cfg(test)]
461mod tests {
462    use super::*;
463
464    #[test]
465    fn test_capability_creation() {
466        let cap = Capability::new(ResourceType::Graph, Action::Read, Some("users:*".to_string()));
467        assert_eq!(cap.resource_type, ResourceType::Graph);
468        assert_eq!(cap.action, Action::Read);
469        assert_eq!(cap.scope, Some("users:*".to_string()));
470    }
471
472    #[test]
473    fn test_capability_matching() {
474        let cap = Capability::new(ResourceType::Graph, Action::Read, Some("users:*".to_string()));
475
476        // Exact match
477        assert!(cap.matches(&ResourceType::Graph, &Action::Read, Some("users:*")));
478
479        // Pattern match
480        assert!(cap.matches(&ResourceType::Graph, &Action::Read, Some("users:123")));
481
482        // No match - wrong resource
483        assert!(!cap.matches(&ResourceType::Network, &Action::Read, Some("users:123")));
484
485        // No match - wrong action
486        assert!(!cap.matches(&ResourceType::Graph, &Action::Write, Some("users:123")));
487    }
488
489    #[test]
490    fn test_capability_set_operations() {
491        let mut cap_set = CapabilitySet::new();
492
493        let read_cap = Capability::new(ResourceType::Graph, Action::Read, None);
494        let write_cap = Capability::new(ResourceType::Graph, Action::Write, None);
495
496        cap_set.add_capability(read_cap.clone());
497        cap_set.add_capability(write_cap.clone());
498
499        assert!(cap_set.has_capability(&read_cap));
500        assert!(cap_set.has_capability(&write_cap));
501        assert_eq!(cap_set.len(), 2);
502
503        // Test allowance checking
504        assert!(cap_set.allows(&ResourceType::Graph, &Action::Read, None));
505        assert!(cap_set.allows(&ResourceType::Graph, &Action::Write, None));
506        assert!(!cap_set.allows(&ResourceType::Graph, &Action::Delete, None));
507    }
508
509    #[test]
510    fn test_capability_attenuation() {
511        let broad_cap = Capability::new(ResourceType::Graph, Action::Read, None);
512        let attenuated = broad_cap.clone().attenuate(Some("users:*".to_string()));
513
514        // Original capability allows all
515        assert!(broad_cap.matches(&ResourceType::Graph, &Action::Read, Some("posts:123")));
516
517        // Attenuated capability only allows specific scope
518        assert!(attenuated.matches(&ResourceType::Graph, &Action::Read, Some("users:123")));
519        assert!(!attenuated.matches(&ResourceType::Graph, &Action::Read, Some("posts:123")));
520    }
521
522    #[test]
523    fn test_capability_service() {
524        let service = CapabilityService::new();
525        let mut cap_set = CapabilitySet::new();
526        cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
527
528        assert!(service.check_capability(&cap_set, &ResourceType::Graph, &Action::Read, None));
529        assert!(!service.check_capability(&cap_set, &ResourceType::Graph, &Action::Write, None));
530    }
531
532    #[test]
533    fn test_preset_capability_sets() {
534        let readonly = CapabilityService::create_preset_capability_set(PresetCapabilitySet::ReadOnly);
535        assert!(readonly.allows(&ResourceType::Graph, &Action::Read, None));
536        assert!(readonly.allows(&ResourceType::Query, &Action::Execute, None));
537        assert!(!readonly.allows(&ResourceType::Graph, &Action::Write, None));
538
539        let admin = CapabilityService::create_preset_capability_set(PresetCapabilitySet::Admin);
540        assert!(admin.allows(&ResourceType::Graph, &Action::Delete, None));
541        assert!(admin.allows(&ResourceType::Admin, &Action::Admin, None));
542    }
543}