use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Permission {
pub resource: String,
pub action: String,
}
impl Permission {
pub fn new(resource: impl Into<String>, action: impl Into<String>) -> Self {
Self {
resource: resource.into(),
action: action.into(),
}
}
pub fn matches(&self, other: &Permission) -> bool {
let resource_match = self.resource == "*" || self.resource == other.resource;
let action_match = self.action == "*" || self.action == other.action;
resource_match && action_match
}
pub fn from_string(s: &str) -> Result<Self, RbacError> {
let parts: Vec<&str> = s.split(':').collect();
if parts.len() != 2 {
return Err(RbacError::InvalidPermissionFormat(s.to_string()));
}
Ok(Permission::new(parts[0], parts[1]))
}
pub fn to_string(&self) -> String {
format!("{}:{}", self.resource, self.action)
}
}
impl fmt::Display for Permission {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}:{}", self.resource, self.action)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Role {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
pub permissions: HashSet<Permission>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub inherits_from: Vec<String>,
}
impl Role {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
description: None,
permissions: HashSet::new(),
inherits_from: Vec::new(),
}
}
pub fn add_permission(&mut self, permission: Permission) {
self.permissions.insert(permission);
}
pub fn add_permissions(&mut self, permissions: Vec<Permission>) {
self.permissions.extend(permissions);
}
pub fn add_parent(&mut self, parent_role: impl Into<String>) {
self.inherits_from.push(parent_role.into());
}
pub fn with_description(mut self, description: impl Into<String>) -> Self {
self.description = Some(description.into());
self
}
pub fn has_permission(&self, permission: &Permission) -> bool {
self.permissions.iter().any(|p| p.matches(permission))
}
}
#[derive(Debug, Clone)]
pub struct RbacPolicy {
roles: HashMap<String, Role>,
permission_cache: HashMap<String, HashSet<Permission>>,
}
impl Default for RbacPolicy {
fn default() -> Self {
Self::new()
}
}
impl RbacPolicy {
pub fn new() -> Self {
let mut policy = Self {
roles: HashMap::new(),
permission_cache: HashMap::new(),
};
policy.add_default_roles();
policy
}
fn add_default_roles(&mut self) {
let mut admin = Role::new("admin");
admin.description = Some("System administrator with full access".to_string());
admin.add_permission(Permission::new("*", "*"));
self.add_role(admin);
let mut developer = Role::new("developer");
developer.description = Some("Developer with asset management and API access".to_string());
developer.add_permissions(vec![
Permission::new("asset", "read"),
Permission::new("asset", "write"),
Permission::new("asset", "delete"),
Permission::new("api-key", "create"),
Permission::new("api-key", "read"),
]);
self.add_role(developer);
let mut viewer = Role::new("viewer");
viewer.description = Some("Read-only access to assets".to_string());
viewer.add_permissions(vec![
Permission::new("asset", "read"),
Permission::new("dependency", "read"),
]);
self.add_role(viewer);
let mut user = Role::new("user");
user.description = Some("Regular user with basic permissions".to_string());
user.add_permissions(vec![
Permission::new("asset", "read"),
Permission::new("asset", "write"),
]);
self.add_role(user);
}
pub fn add_role(&mut self, role: Role) {
let role_name = role.name.clone();
self.roles.insert(role_name.clone(), role);
self.invalidate_cache(&role_name);
}
pub fn get_role(&self, name: &str) -> Option<&Role> {
self.roles.get(name)
}
pub fn list_roles(&self) -> Vec<&Role> {
self.roles.values().collect()
}
pub fn remove_role(&mut self, name: &str) -> Option<Role> {
self.invalidate_cache(name);
self.roles.remove(name)
}
fn invalidate_cache(&mut self, role_name: &str) {
self.permission_cache.remove(role_name);
let dependent_roles: Vec<String> = self
.roles
.iter()
.filter(|(_, role)| role.inherits_from.contains(&role_name.to_string()))
.map(|(name, _)| name.clone())
.collect();
for role in dependent_roles {
self.permission_cache.remove(&role);
}
}
pub fn get_role_permissions(&mut self, role_name: &str) -> Option<HashSet<Permission>> {
if let Some(cached) = self.permission_cache.get(role_name) {
return Some(cached.clone());
}
let (direct_permissions, parent_roles) = {
let role = self.roles.get(role_name)?;
(role.permissions.clone(), role.inherits_from.clone())
};
let mut permissions = direct_permissions;
for parent_role_name in &parent_roles {
if let Some(parent_permissions) = self.get_role_permissions(parent_role_name) {
permissions.extend(parent_permissions);
}
}
self.permission_cache
.insert(role_name.to_string(), permissions.clone());
Some(permissions)
}
pub fn has_permission(&mut self, roles: &[String], permission: &Permission) -> bool {
for role_name in roles {
if let Some(role_permissions) = self.get_role_permissions(role_name) {
if role_permissions.iter().any(|p| p.matches(permission)) {
return true;
}
}
}
false
}
pub fn has_any_permission(
&mut self,
roles: &[String],
permissions: &[Permission],
) -> bool {
permissions
.iter()
.any(|p| self.has_permission(roles, p))
}
pub fn has_all_permissions(
&mut self,
roles: &[String],
permissions: &[Permission],
) -> bool {
permissions
.iter()
.all(|p| self.has_permission(roles, p))
}
}
#[derive(Debug, thiserror::Error)]
pub enum RbacError {
#[error("Invalid permission format: {0}. Expected format: resource:action")]
InvalidPermissionFormat(String),
#[error("Role not found: {0}")]
RoleNotFound(String),
#[error("Permission denied: {0}")]
PermissionDenied(String),
#[error("Circular role inheritance detected")]
CircularInheritance,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_creation() {
let perm = Permission::new("asset", "read");
assert_eq!(perm.resource, "asset");
assert_eq!(perm.action, "read");
assert_eq!(perm.to_string(), "asset:read");
}
#[test]
fn test_permission_from_string() {
let perm = Permission::from_string("asset:write").unwrap();
assert_eq!(perm.resource, "asset");
assert_eq!(perm.action, "write");
assert!(Permission::from_string("invalid").is_err());
}
#[test]
fn test_permission_wildcard_matching() {
let perm1 = Permission::new("*", "*");
let perm2 = Permission::new("asset", "read");
assert!(perm1.matches(&perm2));
assert!(!perm2.matches(&perm1));
}
#[test]
fn test_role_creation() {
let mut role = Role::new("developer");
role.add_permission(Permission::new("asset", "read"));
role.add_permission(Permission::new("asset", "write"));
assert_eq!(role.name, "developer");
assert_eq!(role.permissions.len(), 2);
}
#[test]
fn test_rbac_policy() {
let mut policy = RbacPolicy::new();
assert!(policy.get_role("admin").is_some());
assert!(policy.get_role("developer").is_some());
assert!(policy.get_role("viewer").is_some());
}
#[test]
fn test_permission_checking() {
let mut policy = RbacPolicy::new();
let admin_roles = vec!["admin".to_string()];
let viewer_roles = vec!["viewer".to_string()];
let read_perm = Permission::new("asset", "read");
let delete_perm = Permission::new("asset", "delete");
assert!(policy.has_permission(&admin_roles, &read_perm));
assert!(policy.has_permission(&admin_roles, &delete_perm));
assert!(policy.has_permission(&viewer_roles, &read_perm));
assert!(!policy.has_permission(&viewer_roles, &delete_perm));
}
#[test]
fn test_role_inheritance() {
let mut policy = RbacPolicy::new();
let mut moderator = Role::new("moderator");
moderator.add_parent("viewer");
moderator.add_permission(Permission::new("asset", "delete"));
policy.add_role(moderator);
let moderator_roles = vec!["moderator".to_string()];
assert!(policy.has_permission(
&moderator_roles,
&Permission::new("asset", "read")
));
assert!(policy.has_permission(
&moderator_roles,
&Permission::new("asset", "delete")
));
}
#[test]
fn test_has_any_permission() {
let mut policy = RbacPolicy::new();
let developer_roles = vec!["developer".to_string()];
let permissions = vec![
Permission::new("asset", "delete"),
Permission::new("user", "admin"),
];
assert!(policy.has_any_permission(&developer_roles, &permissions));
}
#[test]
fn test_has_all_permissions() {
let mut policy = RbacPolicy::new();
let admin_roles = vec!["admin".to_string()];
let permissions = vec![
Permission::new("asset", "read"),
Permission::new("asset", "write"),
Permission::new("asset", "delete"),
];
assert!(policy.has_all_permissions(&admin_roles, &permissions));
}
#[test]
fn test_cache_invalidation() {
let mut policy = RbacPolicy::new();
let _ = policy.get_role_permissions("developer");
assert!(policy.permission_cache.contains_key("developer"));
if let Some(role) = policy.roles.get_mut("developer") {
role.add_permission(Permission::new("new", "permission"));
}
policy.invalidate_cache("developer");
assert!(!policy.permission_cache.contains_key("developer"));
}
}