use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
pub enum Ring {
Admin = 0,
Standard = 1,
Restricted = 2,
Sandboxed = 3,
}
pub struct RingEnforcer {
assignments: HashMap<String, Ring>,
permissions: HashMap<Ring, Vec<String>>,
}
impl RingEnforcer {
pub fn new() -> Self {
Self {
assignments: HashMap::new(),
permissions: HashMap::new(),
}
}
pub fn assign(&mut self, agent_id: &str, ring: Ring) {
self.assignments.insert(agent_id.to_string(), ring);
}
pub fn get_ring(&self, agent_id: &str) -> Option<Ring> {
self.assignments.get(agent_id).copied()
}
pub fn check_access(&self, agent_id: &str, action: &str) -> bool {
match self.get_ring(agent_id) {
Some(Ring::Admin) => true,
Some(Ring::Sandboxed) => false,
Some(ring) => self
.permissions
.get(&ring)
.map_or(false, |allowed| allowed.iter().any(|a| a == action)),
None => false,
}
}
pub fn set_ring_permissions(&mut self, ring: Ring, allowed_actions: Vec<String>) {
self.permissions.insert(ring, allowed_actions);
}
}
impl Default for RingEnforcer {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_admin_ring_allows_everything() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("root-agent", Ring::Admin);
assert!(enforcer.check_access("root-agent", "any.action"));
assert!(enforcer.check_access("root-agent", "shell:rm"));
assert!(enforcer.check_access("root-agent", "deploy.prod"));
}
#[test]
fn test_sandboxed_ring_denies_everything() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("sandbox-agent", Ring::Sandboxed);
assert!(!enforcer.check_access("sandbox-agent", "data.read"));
assert!(!enforcer.check_access("sandbox-agent", "any.action"));
}
#[test]
fn test_standard_ring_with_permissions() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("agent-1", Ring::Standard);
enforcer.set_ring_permissions(
Ring::Standard,
vec!["data.read".to_string(), "data.write".to_string()],
);
assert!(enforcer.check_access("agent-1", "data.read"));
assert!(enforcer.check_access("agent-1", "data.write"));
assert!(!enforcer.check_access("agent-1", "shell:rm"));
}
#[test]
fn test_restricted_ring_with_permissions() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("agent-2", Ring::Restricted);
enforcer.set_ring_permissions(Ring::Restricted, vec!["data.read".to_string()]);
assert!(enforcer.check_access("agent-2", "data.read"));
assert!(!enforcer.check_access("agent-2", "data.write"));
}
#[test]
fn test_unknown_agent_denied() {
let enforcer = RingEnforcer::new();
assert!(!enforcer.check_access("unknown-agent", "data.read"));
}
#[test]
fn test_get_ring_returns_none_for_unknown() {
let enforcer = RingEnforcer::new();
assert_eq!(enforcer.get_ring("unknown"), None);
}
#[test]
fn test_get_ring_returns_assigned() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("admin-agent", Ring::Admin);
enforcer.assign("sandbox-agent", Ring::Sandboxed);
assert_eq!(enforcer.get_ring("admin-agent"), Some(Ring::Admin));
assert_eq!(enforcer.get_ring("sandbox-agent"), Some(Ring::Sandboxed));
}
#[test]
fn test_reassign_ring_overrides() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("agent", Ring::Admin);
assert!(enforcer.check_access("agent", "anything"));
enforcer.assign("agent", Ring::Sandboxed);
assert!(!enforcer.check_access("agent", "anything"));
}
#[test]
fn test_standard_ring_no_permissions_denies() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("agent", Ring::Standard);
assert!(!enforcer.check_access("agent", "data.read"));
}
#[test]
fn test_ring_ordering() {
assert!(Ring::Admin < Ring::Standard);
assert!(Ring::Standard < Ring::Restricted);
assert!(Ring::Restricted < Ring::Sandboxed);
}
#[test]
fn test_ring_serde_roundtrip() {
let ring = Ring::Restricted;
let json = serde_json::to_string(&ring).unwrap();
let deserialized: Ring = serde_json::from_str(&json).unwrap();
assert_eq!(ring, deserialized);
}
#[test]
fn test_multiple_agents_different_rings() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("admin", Ring::Admin);
enforcer.assign("standard", Ring::Standard);
enforcer.assign("restricted", Ring::Restricted);
enforcer.assign("sandboxed", Ring::Sandboxed);
enforcer.set_ring_permissions(Ring::Standard, vec!["data.read".to_string()]);
enforcer.set_ring_permissions(Ring::Restricted, vec!["data.read".to_string()]);
assert!(enforcer.check_access("admin", "shell:rm"));
assert!(enforcer.check_access("standard", "data.read"));
assert!(!enforcer.check_access("standard", "shell:rm"));
assert!(enforcer.check_access("restricted", "data.read"));
assert!(!enforcer.check_access("restricted", "shell:rm"));
assert!(!enforcer.check_access("sandboxed", "data.read"));
}
#[test]
fn test_set_ring_permissions_replaces_previous() {
let mut enforcer = RingEnforcer::new();
enforcer.assign("agent", Ring::Standard);
enforcer.set_ring_permissions(Ring::Standard, vec!["data.read".to_string()]);
assert!(enforcer.check_access("agent", "data.read"));
enforcer.set_ring_permissions(Ring::Standard, vec!["data.write".to_string()]);
assert!(!enforcer.check_access("agent", "data.read"));
assert!(enforcer.check_access("agent", "data.write"));
}
#[test]
fn test_default_impl() {
let enforcer = RingEnforcer::default();
assert_eq!(enforcer.get_ring("any"), None);
}
}