1use serde::{Deserialize, Serialize};
39
40#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
42pub enum ResourceType {
43 Graph,
45 FileSystem,
47 Network,
49 Environment,
51 System,
53 Plugin,
55 Query,
57 Admin,
59 User,
61 Custom(String),
63}
64
65impl Default for ResourceType {
66 fn default() -> Self {
67 ResourceType::Custom(String::new())
68 }
69}
70
71#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
73pub enum Action {
74 Read,
76 Write,
78 Execute,
80 Delete,
82 Create,
84 Update,
86 Admin,
88 Custom(String),
90}
91
92#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
94pub struct Capability {
95 pub resource_type: ResourceType,
97 pub action: Action,
99 pub scope: Option<String>,
101 pub conditions: Option<std::collections::HashMap<String, serde_json::Value>>,
103}
104
105impl Capability {
106 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 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 pub fn matches(&self, resource_type: &ResourceType, action: &Action, scope: Option<&str>) -> bool {
133 if &self.resource_type != resource_type || &self.action != action {
135 return false;
136 }
137
138 if self.scope.is_none() {
140 return true;
141 }
142
143 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 self.scope_matches(cap_scope, req_scope)
153 }
154
155 fn scope_matches(&self, cap_scope: &str, req_scope: &str) -> bool {
157 if cap_scope == req_scope {
159 return true;
160 }
161
162 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 false
174 }
175
176 pub fn attenuate(mut self, new_scope: Option<String>) -> Self {
178 match (&self.scope, &new_scope) {
180 (Some(current), Some(new)) => {
181 if !self.scope_matches(current, new) {
182 self.scope = new_scope;
184 }
185 }
186 (Some(_), None) => {
187 }
190 (None, Some(new_scope)) => {
191 self.scope = Some(new_scope.to_string());
193 }
194 (None, None) => {
195 }
197 }
198 self
199 }
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct CapabilitySet {
205 pub capabilities: Vec<Capability>,
207 pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
209}
210
211impl CapabilitySet {
212 pub fn new() -> Self {
214 Self {
215 capabilities: Vec::new(),
216 metadata: None,
217 }
218 }
219
220 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 pub fn add_capability(&mut self, capability: Capability) {
230 if !self.capabilities.contains(&capability) {
232 self.capabilities.push(capability);
233 }
234 }
235
236 pub fn remove_capability(&mut self, capability: &Capability) {
238 self.capabilities.retain(|c| c != capability);
239 }
240
241 pub fn has_capability(&self, capability: &Capability) -> bool {
243 self.capabilities.contains(capability)
244 }
245
246 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 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 pub fn attenuate(&self, restrictions: Vec<Capability>) -> CapabilitySet {
260 let mut new_set = CapabilitySet::new();
261
262 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 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 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 pub fn is_empty(&self) -> bool {
298 self.capabilities.is_empty()
299 }
300
301 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
313pub struct CapabilityService {
315 config: CapabilityConfig,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
320pub struct CapabilityConfig {
321 pub enable_logging: bool,
323 pub enable_auditing: bool,
325 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 pub fn new() -> Self {
342 Self {
343 config: CapabilityConfig::default(),
344 }
345 }
346
347 pub fn with_config(config: CapabilityConfig) -> Self {
349 Self { config }
350 }
351
352 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 allowed
369 }
370
371 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 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 pub fn attenuate_capabilities(
399 &self,
400 cap_set: &CapabilitySet,
401 restrictions: Vec<Capability>,
402 ) -> CapabilitySet {
403 cap_set.attenuate(restrictions)
404 }
405
406 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#[derive(Debug, Clone)]
447pub enum PresetCapabilitySet {
448 ReadOnly,
450 ReadWrite,
452 Admin,
454 NetworkAccess,
456 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 assert!(cap.matches(&ResourceType::Graph, &Action::Read, Some("users:*")));
478
479 assert!(cap.matches(&ResourceType::Graph, &Action::Read, Some("users:123")));
481
482 assert!(!cap.matches(&ResourceType::Network, &Action::Read, Some("users:123")));
484
485 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 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 assert!(broad_cap.matches(&ResourceType::Graph, &Action::Read, Some("posts:123")));
516
517 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}