use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ResourceType {
Graph,
FileSystem,
Network,
Environment,
System,
Plugin,
Query,
Admin,
User,
Custom(String),
}
impl Default for ResourceType {
fn default() -> Self {
ResourceType::Custom(String::new())
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum Action {
Read,
Write,
Execute,
Delete,
Create,
Update,
Admin,
Custom(String),
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Capability {
pub resource_type: ResourceType,
pub action: Action,
pub scope: Option<String>,
pub conditions: Option<std::collections::HashMap<String, serde_json::Value>>,
}
impl Capability {
pub fn new(resource_type: ResourceType, action: Action, scope: Option<String>) -> Self {
Self {
resource_type,
action,
scope,
conditions: None,
}
}
pub fn with_conditions(
resource_type: ResourceType,
action: Action,
scope: Option<String>,
conditions: std::collections::HashMap<String, serde_json::Value>,
) -> Self {
Self {
resource_type,
action,
scope,
conditions: Some(conditions),
}
}
pub fn matches(&self, resource_type: &ResourceType, action: &Action, scope: Option<&str>) -> bool {
if &self.resource_type != resource_type || &self.action != action {
return false;
}
if self.scope.is_none() {
return true;
}
if scope.is_none() {
return false;
}
let cap_scope = self.scope.as_ref().unwrap();
let req_scope = scope.unwrap();
self.scope_matches(cap_scope, req_scope)
}
fn scope_matches(&self, cap_scope: &str, req_scope: &str) -> bool {
if cap_scope == req_scope {
return true;
}
if cap_scope.ends_with(":*") {
let prefix = &cap_scope[..cap_scope.len() - 2];
return req_scope.starts_with(prefix) && req_scope[prefix.len()..].starts_with(':');
}
if cap_scope == "*" {
return true;
}
false
}
pub fn attenuate(mut self, new_scope: Option<String>) -> Self {
match (&self.scope, &new_scope) {
(Some(current), Some(new)) => {
if !self.scope_matches(current, new) {
self.scope = new_scope;
}
}
(Some(_), None) => {
}
(None, Some(new_scope)) => {
self.scope = Some(new_scope.to_string());
}
(None, None) => {
}
}
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilitySet {
pub capabilities: Vec<Capability>,
pub metadata: Option<std::collections::HashMap<String, serde_json::Value>>,
}
impl CapabilitySet {
pub fn new() -> Self {
Self {
capabilities: Vec::new(),
metadata: None,
}
}
pub fn with_metadata(metadata: std::collections::HashMap<String, serde_json::Value>) -> Self {
Self {
capabilities: Vec::new(),
metadata: Some(metadata),
}
}
pub fn add_capability(&mut self, capability: Capability) {
if !self.capabilities.contains(&capability) {
self.capabilities.push(capability);
}
}
pub fn remove_capability(&mut self, capability: &Capability) {
self.capabilities.retain(|c| c != capability);
}
pub fn has_capability(&self, capability: &Capability) -> bool {
self.capabilities.contains(capability)
}
pub fn allows(&self, resource_type: &ResourceType, action: &Action, scope: Option<&str>) -> bool {
self.capabilities.iter().any(|cap| cap.matches(resource_type, action, scope))
}
pub fn capabilities_for_resource(&self, resource_type: &ResourceType) -> Vec<&Capability> {
self.capabilities.iter()
.filter(|cap| &cap.resource_type == resource_type)
.collect()
}
pub fn attenuate(&self, restrictions: Vec<Capability>) -> CapabilitySet {
let mut new_set = CapabilitySet::new();
for restriction in restrictions {
for cap in &self.capabilities {
if cap.resource_type == restriction.resource_type &&
cap.action == restriction.action {
let attenuated = cap.clone().attenuate(restriction.scope.clone());
new_set.add_capability(attenuated);
}
}
}
new_set
}
pub fn union(&self, other: &CapabilitySet) -> CapabilitySet {
let mut combined = self.clone();
for cap in &other.capabilities {
combined.add_capability(cap.clone());
}
combined
}
pub fn intersection(&self, other: &CapabilitySet) -> CapabilitySet {
let mut result = CapabilitySet::new();
for cap in &self.capabilities {
if other.capabilities.contains(cap) {
result.add_capability(cap.clone());
}
}
result
}
pub fn is_empty(&self) -> bool {
self.capabilities.is_empty()
}
pub fn len(&self) -> usize {
self.capabilities.len()
}
}
impl Default for CapabilitySet {
fn default() -> Self {
Self::new()
}
}
pub struct CapabilityService {
config: CapabilityConfig,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CapabilityConfig {
pub enable_logging: bool,
pub enable_auditing: bool,
pub default_attenuation: Option<Vec<Capability>>,
}
impl Default for CapabilityConfig {
fn default() -> Self {
Self {
enable_logging: false,
enable_auditing: false,
default_attenuation: None,
}
}
}
impl CapabilityService {
pub fn new() -> Self {
Self {
config: CapabilityConfig::default(),
}
}
pub fn with_config(config: CapabilityConfig) -> Self {
Self { config }
}
pub fn check_capability(
&self,
cap_set: &CapabilitySet,
resource_type: &ResourceType,
action: &Action,
scope: Option<&str>,
) -> bool {
let allowed = cap_set.allows(resource_type, action, scope);
if self.config.enable_logging {
println!("Capability check: {:?}::{:?} on {:?} -> {}", resource_type, action, scope, allowed);
}
allowed
}
pub fn grant_capabilities(
&self,
existing_caps: &CapabilitySet,
new_caps: Vec<Capability>,
) -> CapabilitySet {
let mut updated = existing_caps.clone();
for cap in new_caps {
updated.add_capability(cap);
}
updated
}
pub fn revoke_capabilities(
&self,
existing_caps: &CapabilitySet,
caps_to_revoke: Vec<Capability>,
) -> CapabilitySet {
let mut updated = existing_caps.clone();
for cap in caps_to_revoke {
updated.remove_capability(&cap);
}
updated
}
pub fn attenuate_capabilities(
&self,
cap_set: &CapabilitySet,
restrictions: Vec<Capability>,
) -> CapabilitySet {
cap_set.attenuate(restrictions)
}
pub fn create_preset_capability_set(preset: PresetCapabilitySet) -> CapabilitySet {
let mut cap_set = CapabilitySet::new();
match preset {
PresetCapabilitySet::ReadOnly => {
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
cap_set.add_capability(Capability::new(ResourceType::Query, Action::Execute, None));
}
PresetCapabilitySet::ReadWrite => {
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Write, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Create, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Update, None));
cap_set.add_capability(Capability::new(ResourceType::Query, Action::Execute, None));
}
PresetCapabilitySet::Admin => {
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Write, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Create, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Update, None));
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Delete, None));
cap_set.add_capability(Capability::new(ResourceType::Query, Action::Execute, None));
cap_set.add_capability(Capability::new(ResourceType::User, Action::Admin, None));
cap_set.add_capability(Capability::new(ResourceType::Admin, Action::Admin, None));
}
PresetCapabilitySet::NetworkAccess => {
cap_set.add_capability(Capability::new(ResourceType::Network, Action::Read, None));
cap_set.add_capability(Capability::new(ResourceType::Network, Action::Write, None));
}
PresetCapabilitySet::FileSystemRead => {
cap_set.add_capability(Capability::new(ResourceType::FileSystem, Action::Read, None));
}
}
cap_set
}
}
#[derive(Debug, Clone)]
pub enum PresetCapabilitySet {
ReadOnly,
ReadWrite,
Admin,
NetworkAccess,
FileSystemRead,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_capability_creation() {
let cap = Capability::new(ResourceType::Graph, Action::Read, Some("users:*".to_string()));
assert_eq!(cap.resource_type, ResourceType::Graph);
assert_eq!(cap.action, Action::Read);
assert_eq!(cap.scope, Some("users:*".to_string()));
}
#[test]
fn test_capability_matching() {
let cap = Capability::new(ResourceType::Graph, Action::Read, Some("users:*".to_string()));
assert!(cap.matches(&ResourceType::Graph, &Action::Read, Some("users:*")));
assert!(cap.matches(&ResourceType::Graph, &Action::Read, Some("users:123")));
assert!(!cap.matches(&ResourceType::Network, &Action::Read, Some("users:123")));
assert!(!cap.matches(&ResourceType::Graph, &Action::Write, Some("users:123")));
}
#[test]
fn test_capability_set_operations() {
let mut cap_set = CapabilitySet::new();
let read_cap = Capability::new(ResourceType::Graph, Action::Read, None);
let write_cap = Capability::new(ResourceType::Graph, Action::Write, None);
cap_set.add_capability(read_cap.clone());
cap_set.add_capability(write_cap.clone());
assert!(cap_set.has_capability(&read_cap));
assert!(cap_set.has_capability(&write_cap));
assert_eq!(cap_set.len(), 2);
assert!(cap_set.allows(&ResourceType::Graph, &Action::Read, None));
assert!(cap_set.allows(&ResourceType::Graph, &Action::Write, None));
assert!(!cap_set.allows(&ResourceType::Graph, &Action::Delete, None));
}
#[test]
fn test_capability_attenuation() {
let broad_cap = Capability::new(ResourceType::Graph, Action::Read, None);
let attenuated = broad_cap.clone().attenuate(Some("users:*".to_string()));
assert!(broad_cap.matches(&ResourceType::Graph, &Action::Read, Some("posts:123")));
assert!(attenuated.matches(&ResourceType::Graph, &Action::Read, Some("users:123")));
assert!(!attenuated.matches(&ResourceType::Graph, &Action::Read, Some("posts:123")));
}
#[test]
fn test_capability_service() {
let service = CapabilityService::new();
let mut cap_set = CapabilitySet::new();
cap_set.add_capability(Capability::new(ResourceType::Graph, Action::Read, None));
assert!(service.check_capability(&cap_set, &ResourceType::Graph, &Action::Read, None));
assert!(!service.check_capability(&cap_set, &ResourceType::Graph, &Action::Write, None));
}
#[test]
fn test_preset_capability_sets() {
let readonly = CapabilityService::create_preset_capability_set(PresetCapabilitySet::ReadOnly);
assert!(readonly.allows(&ResourceType::Graph, &Action::Read, None));
assert!(readonly.allows(&ResourceType::Query, &Action::Execute, None));
assert!(!readonly.allows(&ResourceType::Graph, &Action::Write, None));
let admin = CapabilityService::create_preset_capability_set(PresetCapabilitySet::Admin);
assert!(admin.allows(&ResourceType::Graph, &Action::Delete, None));
assert!(admin.allows(&ResourceType::Admin, &Action::Admin, None));
}
}