#[cfg(feature = "audit")]
use log::{info, warn};
use crate::{
app_type::ApplicationType,
error::{Error, Result},
metrics::{MetricsProvider, RoleSystemMetrics},
permission::Permission,
resource::Resource,
role::{Role, RoleElevation},
storage::{MemoryStorage, Storage},
subject::Subject,
};
use chrono::{DateTime, Utc};
use dashmap::DashMap;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
use std::time::{Duration, Instant};
#[derive(Debug, Clone, PartialEq)]
pub enum AccessResult {
Granted,
Denied(String),
}
impl AccessResult {
pub fn is_granted(&self) -> bool {
matches!(self, AccessResult::Granted)
}
pub fn is_denied(&self) -> bool {
!self.is_granted()
}
pub fn denial_reason(&self) -> Option<&str> {
match self {
AccessResult::Denied(reason) => Some(reason),
AccessResult::Granted => None,
}
}
}
impl From<bool> for AccessResult {
fn from(granted: bool) -> Self {
if granted {
AccessResult::Granted
} else {
AccessResult::Denied("Access denied".to_string())
}
}
}
#[derive(Debug, Clone)]
pub struct RoleSystemConfig {
pub max_hierarchy_depth: usize,
pub enable_caching: bool,
pub cache_ttl_seconds: u64,
pub enable_audit: bool,
}
impl Default for RoleSystemConfig {
fn default() -> Self {
Self {
max_hierarchy_depth: 10,
enable_caching: true,
cache_ttl_seconds: 300, enable_audit: true,
}
}
}
#[derive(Debug, Clone)]
pub struct UserPermissions {
pub computed_permissions: HashMap<String, AccessResult>,
pub last_updated: DateTime<Utc>,
}
impl UserPermissions {
pub fn new(permissions: HashMap<String, AccessResult>) -> Self {
Self {
computed_permissions: permissions,
last_updated: Utc::now(),
}
}
pub fn is_expired(&self, ttl_seconds: u64) -> bool {
let now = Utc::now();
let ttl = chrono::Duration::seconds(ttl_seconds as i64);
now.signed_duration_since(self.last_updated) > ttl
}
}
pub struct RoleSystem<S = MemoryStorage>
where
S: Storage,
{
storage: S,
config: RoleSystemConfig,
role_hierarchy: DashMap<String, HashSet<String>>,
subject_roles: DashMap<String, HashSet<String>>,
role_elevations: DashMap<String, Vec<RoleElevation>>,
permission_cache: DashMap<(String, String), UserPermissions>,
metrics: Arc<RoleSystemMetrics>,
}
impl RoleSystem<MemoryStorage> {
pub fn new() -> Self {
Self::with_config(RoleSystemConfig::default())
}
pub fn with_config(config: RoleSystemConfig) -> Self {
Self {
storage: MemoryStorage::new(),
config,
role_hierarchy: DashMap::new(),
subject_roles: DashMap::new(),
role_elevations: DashMap::new(),
permission_cache: DashMap::new(),
metrics: Arc::new(RoleSystemMetrics::new()),
}
}
}
impl<S> MetricsProvider for RoleSystem<S>
where
S: Storage,
{
fn metrics(&self) -> &RoleSystemMetrics {
&self.metrics
}
}
impl<S> RoleSystem<S>
where
S: Storage,
{
pub fn with_storage(storage: S, config: RoleSystemConfig) -> Self {
Self {
storage,
config,
role_hierarchy: DashMap::new(),
subject_roles: DashMap::new(),
role_elevations: DashMap::new(),
permission_cache: DashMap::new(),
metrics: Arc::new(RoleSystemMetrics::new()),
}
}
pub fn register_role(&mut self, role: Role) -> Result<()> {
let role_name = role.name().to_string();
if self.storage.role_exists(&role_name)? {
return Err(Error::RoleAlreadyExists(role_name));
}
self.storage.store_role(role)?;
#[cfg(feature = "audit")]
info!("Role '{role_name}' registered");
Ok(())
}
pub fn get_role(&self, name: &str) -> Result<Option<Role>> {
self.storage.get_role(name)
}
pub fn update_role(&mut self, role: Role) -> Result<()> {
let role_name = role.name().to_string();
if !self.storage.role_exists(&role_name)? {
return Err(Error::RoleNotFound(role_name));
}
self.storage.update_role(role)?;
self.clear_role_cache(&role_name);
#[cfg(feature = "audit")]
info!("Role '{role_name}' updated");
Ok(())
}
pub fn add_role_inheritance(&mut self, child: &str, parent: &str) -> Result<()> {
if !self.storage.role_exists(child)? {
return Err(Error::RoleNotFound(child.to_string()));
}
if !self.storage.role_exists(parent)? {
return Err(Error::RoleNotFound(parent.to_string()));
}
if self.would_create_cycle(child, parent)? {
return Err(Error::CircularDependency(child.to_string()));
}
if self.would_exceed_max_depth(child, parent)? {
return Err(Error::MaxDepthExceeded(self.config.max_hierarchy_depth));
}
self.role_hierarchy
.entry(child.to_string())
.or_default()
.insert(parent.to_string());
#[cfg(feature = "audit")]
info!("Role inheritance added: '{child}' inherits from '{parent}'");
Ok(())
}
pub fn remove_role_inheritance(&mut self, child: &str, parent: &str) -> Result<()> {
if let Some(mut parents) = self.role_hierarchy.get_mut(child) {
parents.remove(parent);
if parents.is_empty() {
drop(parents);
self.role_hierarchy.remove(child);
}
}
#[cfg(feature = "audit")]
info!("Role inheritance removed: '{child}' no longer inherits from '{parent}'");
Ok(())
}
pub fn assign_role(&mut self, subject: &Subject, role_name: &str) -> Result<()> {
if !self.storage.role_exists(role_name)? {
self.metrics.record_error("RoleNotFound");
return Err(Error::RoleNotFound(role_name.to_string()));
}
self.subject_roles
.entry(subject.id().to_string())
.or_default()
.insert(role_name.to_string());
self.clear_subject_cache(subject.id());
self.metrics.record_role_assignment(subject.id());
#[cfg(feature = "audit")]
info!(
"Role '{}' assigned to subject '{}'",
role_name,
subject.id()
);
Ok(())
}
pub fn remove_role(&mut self, subject: &Subject, role_name: &str) -> Result<()> {
if let Some(mut roles) = self.subject_roles.get_mut(subject.id()) {
roles.remove(role_name);
if roles.is_empty() {
drop(roles);
self.subject_roles.remove(subject.id());
}
}
self.clear_subject_cache(subject.id());
self.metrics.record_role_removal(subject.id());
#[cfg(feature = "audit")]
info!(
"Role '{}' removed from subject '{}'",
role_name,
subject.id()
);
Ok(())
}
pub fn elevate_role(
&mut self,
subject: &Subject,
role_name: &str,
duration: Option<Duration>,
) -> Result<()> {
if !self.storage.role_exists(role_name)? {
self.metrics.record_error("RoleNotFound");
return Err(Error::RoleNotFound(role_name.to_string()));
}
let elevation = RoleElevation::new(role_name.to_string(), duration);
self.role_elevations
.entry(subject.id().to_string())
.or_default()
.push(elevation);
self.clear_subject_cache(subject.id());
self.metrics.record_role_elevation(subject.id());
#[cfg(feature = "audit")]
info!(
"Role '{}' elevated for subject '{}' with duration {:?}",
role_name,
subject.id(),
duration
);
Ok(())
}
pub fn check_permission(
&self,
subject: &Subject,
action: &str,
resource: &Resource,
) -> Result<bool> {
self.check_permission_with_context(subject, action, resource, &HashMap::new())
}
pub fn check_permission_with_context(
&self,
subject: &Subject,
action: &str,
resource: &Resource,
context: &HashMap<String, String>,
) -> Result<bool> {
let _timer = self.start_timer("permission_check");
let context_hash = if context.is_empty() {
String::new()
} else {
let mut sorted_context: Vec<_> = context.iter().collect();
sorted_context.sort_by_key(|(k, _)| *k);
format!("{sorted_context:?}")
};
let permission_key = format!("{}:{}:{}", action, resource.id(), context_hash);
let cache_key = (subject.id().to_string(), permission_key);
if self.config.enable_caching {
if let Some(entry) = self.permission_cache.get(&cache_key) {
let permissions = entry.value();
let cache_still_valid = !permissions.is_expired(self.config.cache_ttl_seconds);
let elevations_still_valid = true;
if cache_still_valid && elevations_still_valid {
if let Some(result) = permissions.computed_permissions.get(action) {
self.metrics.record_cache_hit();
return Ok(result.is_granted());
}
}
}
self.metrics.record_cache_miss();
}
let result = self.check_permission_internal(subject, action, resource, context)?;
if self.config.enable_caching {
let mut user_permissions = if let Some(existing) = self.permission_cache.get(&cache_key)
{
existing.value().clone()
} else {
UserPermissions::new(HashMap::new())
};
user_permissions
.computed_permissions
.insert(action.to_string(), result.into());
user_permissions.last_updated = Utc::now();
self.permission_cache.insert(cache_key, user_permissions);
}
#[cfg(feature = "audit")]
{
let granted = result;
if granted {
info!(
"Permission GRANTED for subject '{}', action '{}', resource '{}'",
subject.id(),
action,
resource.id()
);
} else {
warn!(
"Permission DENIED for subject '{}', action '{}', resource '{}'",
subject.id(),
action,
resource.id()
);
}
}
Ok(result)
}
pub fn get_subject_roles(&self, subject: &Subject) -> Result<HashSet<String>> {
let mut all_roles = HashSet::new();
if let Some(direct_roles) = self.subject_roles.get(subject.id()) {
for role in direct_roles.iter() {
all_roles.insert(role.clone());
self.collect_inherited_roles(role, &mut all_roles, 0)?;
}
}
if let Some(elevations) = self.role_elevations.get(subject.id()) {
let now = Instant::now();
for elevation in elevations.iter() {
if !elevation.is_expired(now) {
all_roles.insert(elevation.role_name().to_string());
self.collect_inherited_roles(elevation.role_name(), &mut all_roles, 0)?;
}
}
}
Ok(all_roles)
}
pub fn create_standard_roles(&mut self) -> Result<()> {
#[cfg(feature = "audit")]
info!("Creating standard roles: admin, editor, viewer, guest");
let admin_role = Role::new("admin")
.with_description("Full system access")
.add_permission(Permission::super_admin());
let editor_role = Role::new("editor")
.with_description("Create and edit content")
.add_permission(Permission::new("create", "*"))
.add_permission(Permission::new("read", "*"))
.add_permission(Permission::new("update", "*"))
.add_permission(Permission::new("delete", "*"));
let viewer_role = Role::new("viewer")
.with_description("Read-only access")
.add_permission(Permission::new("read", "*"));
let guest_role = Role::new("guest")
.with_description("Limited read access")
.add_permission(Permission::new("read", "public"));
self.register_role(admin_role)?;
self.register_role(editor_role)?;
self.register_role(viewer_role)?;
self.register_role(guest_role)?;
Ok(())
}
pub fn create_application_roles(&mut self, app_type: ApplicationType) -> Result<()> {
match app_type {
ApplicationType::WebApp => self.create_web_app_roles()?,
ApplicationType::ApiService => self.create_api_service_roles()?,
ApplicationType::Cms => self.create_cms_roles()?,
ApplicationType::Ecommerce => self.create_ecommerce_roles()?,
ApplicationType::AdminDashboard => self.create_admin_dashboard_roles()?,
}
Ok(())
}
fn create_web_app_roles(&mut self) -> Result<()> {
#[cfg(feature = "audit")]
info!("Creating web application roles");
let user_role = Role::new("user")
.with_description("Standard authenticated user")
.add_permission(Permission::new("read", "content"))
.add_permission(Permission::new("read", "profile"))
.add_permission(Permission::new("update", "profile:self"));
let premium_role = Role::new("premium_user")
.with_description("Premium tier user with extra access")
.add_permission(Permission::new("read", "content"))
.add_permission(Permission::new("read", "profile"))
.add_permission(Permission::new("update", "profile:self"))
.add_permission(Permission::new("read", "premium_content"));
let moderator_role = Role::new("moderator")
.with_description("Content moderation capabilities")
.add_permission(Permission::new("read", "*"))
.add_permission(Permission::new("update", "content"))
.add_permission(Permission::new("delete", "content"));
self.register_role(user_role)?;
self.register_role(premium_role)?;
self.register_role(moderator_role)?;
self.create_standard_roles()?;
Ok(())
}
fn create_api_service_roles(&mut self) -> Result<()> {
#[cfg(feature = "audit")]
info!("Creating API service roles");
let service_role = Role::new("service")
.with_description("Service-to-service API access")
.add_permission(Permission::new("read", "api"))
.add_permission(Permission::new("create", "api"));
let consumer_role = Role::new("consumer")
.with_description("API consumer with read access")
.add_permission(Permission::new("read", "api"));
let system_role = Role::new("system")
.with_description("Internal system operations")
.add_permission(Permission::new("*", "system"));
self.register_role(service_role)?;
self.register_role(consumer_role)?;
self.register_role(system_role)?;
self.create_standard_roles()?;
Ok(())
}
fn create_cms_roles(&mut self) -> Result<()> {
#[cfg(feature = "audit")]
info!("Creating CMS roles");
let author_role = Role::new("author")
.with_description("Content creator")
.add_permission(Permission::new("create", "content"))
.add_permission(Permission::new("read", "content"))
.add_permission(Permission::new("update", "content:own"))
.add_permission(Permission::new("delete", "content:own"));
let publisher_role = Role::new("publisher")
.with_description("Content publisher")
.add_permission(Permission::new("read", "content"))
.add_permission(Permission::new("update", "content"))
.add_permission(Permission::new("publish", "content"));
self.register_role(author_role)?;
self.register_role(publisher_role)?;
self.create_standard_roles()?;
Ok(())
}
fn create_ecommerce_roles(&mut self) -> Result<()> {
#[cfg(feature = "audit")]
info!("Creating e-commerce roles");
let customer_role = Role::new("customer")
.with_description("Shopping customer")
.add_permission(Permission::new("read", "product"))
.add_permission(Permission::new("create", "order"))
.add_permission(Permission::new("read", "order:own"));
let vendor_role = Role::new("vendor")
.with_description("Product vendor")
.add_permission(Permission::new("create", "product"))
.add_permission(Permission::new("read", "product:own"))
.add_permission(Permission::new("update", "product:own"))
.add_permission(Permission::new("delete", "product:own"))
.add_permission(Permission::new("read", "order:for_products"));
self.register_role(customer_role)?;
self.register_role(vendor_role)?;
self.create_standard_roles()?;
Ok(())
}
fn create_admin_dashboard_roles(&mut self) -> Result<()> {
#[cfg(feature = "audit")]
info!("Creating admin dashboard roles");
let support_role = Role::new("support")
.with_description("Customer support staff")
.add_permission(Permission::new("read", "*"))
.add_permission(Permission::new("update", "user:status"));
let analytics_role = Role::new("analytics")
.with_description("Data analysis")
.add_permission(Permission::new("read", "*"))
.add_permission(Permission::new("export", "data"));
self.register_role(support_role)?;
self.register_role(analytics_role)?;
self.create_standard_roles()?;
Ok(())
}
fn check_permission_internal(
&self,
subject: &Subject,
action: &str,
resource: &Resource,
context: &HashMap<String, String>,
) -> Result<bool> {
let subject_roles = self.get_subject_roles(subject)?;
for role_name in subject_roles {
if let Some(role) = self.storage.get_role(&role_name)?
&& role.has_permission(action, resource.resource_type(), context)
{
return Ok(true);
}
}
Ok(false)
}
fn collect_inherited_roles(
&self,
role_name: &str,
collected: &mut HashSet<String>,
depth: usize,
) -> Result<()> {
if depth >= self.config.max_hierarchy_depth {
return Err(Error::MaxDepthExceeded(self.config.max_hierarchy_depth));
}
if let Some(parents) = self.role_hierarchy.get(role_name) {
for parent in parents.iter() {
if collected.insert(parent.clone()) {
self.collect_inherited_roles(parent, collected, depth + 1)?;
}
}
}
Ok(())
}
fn would_create_cycle(&self, child: &str, parent: &str) -> Result<bool> {
let mut visited = HashSet::new();
self.has_path(parent, child, &mut visited, 0)
}
fn would_exceed_max_depth(&self, child: &str, parent: &str) -> Result<bool> {
let child_downward_depth = self.calculate_downward_depth(child)?;
let parent_upward_depth = self.calculate_upward_depth(parent)?;
let total_depth = parent_upward_depth + 1 + child_downward_depth;
Ok(total_depth > self.config.max_hierarchy_depth)
}
fn calculate_downward_depth(&self, role_name: &str) -> Result<usize> {
let mut max_depth = 0;
let mut visited = HashSet::new();
self.calculate_downward_depth_recursive(role_name, &mut visited, 0, &mut max_depth)?;
Ok(max_depth)
}
fn calculate_downward_depth_recursive(
&self,
role_name: &str,
visited: &mut HashSet<String>,
current_depth: usize,
max_depth: &mut usize,
) -> Result<()> {
if current_depth > self.config.max_hierarchy_depth {
return Err(Error::MaxDepthExceeded(self.config.max_hierarchy_depth));
}
if !visited.insert(role_name.to_string()) {
return Ok(()); }
*max_depth = std::cmp::max(*max_depth, current_depth);
for entry in self.role_hierarchy.iter() {
let (child, parents) = (entry.key(), entry.value());
if parents.contains(role_name) {
self.calculate_downward_depth_recursive(
child,
visited,
current_depth + 1,
max_depth,
)?;
}
}
Ok(())
}
fn calculate_upward_depth(&self, role_name: &str) -> Result<usize> {
let mut max_depth = 0;
let mut visited = HashSet::new();
self.calculate_upward_depth_recursive(role_name, &mut visited, 0, &mut max_depth)?;
Ok(max_depth)
}
fn calculate_upward_depth_recursive(
&self,
role_name: &str,
visited: &mut HashSet<String>,
current_depth: usize,
max_depth: &mut usize,
) -> Result<()> {
if current_depth > self.config.max_hierarchy_depth {
return Err(Error::MaxDepthExceeded(self.config.max_hierarchy_depth));
}
if !visited.insert(role_name.to_string()) {
return Ok(()); }
*max_depth = std::cmp::max(*max_depth, current_depth);
if let Some(parents) = self.role_hierarchy.get(role_name) {
for parent in parents.iter() {
self.calculate_upward_depth_recursive(
parent,
visited,
current_depth + 1,
max_depth,
)?;
}
}
Ok(())
}
fn has_path(
&self,
from: &str,
to: &str,
visited: &mut HashSet<String>,
depth: usize,
) -> Result<bool> {
if depth >= self.config.max_hierarchy_depth {
return Err(Error::MaxDepthExceeded(self.config.max_hierarchy_depth));
}
if from == to {
return Ok(true);
}
if !visited.insert(from.to_string()) {
return Ok(false); }
if let Some(parents) = self.role_hierarchy.get(from) {
for parent in parents.iter() {
if self.has_path(parent, to, visited, depth + 1)? {
return Ok(true);
}
}
}
Ok(false)
}
fn clear_subject_cache(&self, subject_id: &str) {
if !self.config.enable_caching {
return;
}
let keys_to_remove: Vec<_> = self
.permission_cache
.iter()
.filter(|entry| entry.key().0 == subject_id)
.map(|entry| entry.key().clone())
.collect();
for key in keys_to_remove {
self.permission_cache.remove(&key);
}
}
fn clear_role_cache(&self, _role_name: &str) {
if !self.config.enable_caching {
return;
}
self.permission_cache.clear();
}
}
impl<S: Storage> RoleSystem<S> {
pub fn storage(&self) -> &S {
&self.storage
}
pub fn subject_roles(&self) -> &DashMap<String, HashSet<String>> {
&self.subject_roles
}
pub fn role_hierarchy(&self) -> &DashMap<String, HashSet<String>> {
&self.role_hierarchy
}
pub fn config(&self) -> &RoleSystemConfig {
&self.config
}
}
impl<S: Storage> RoleSystem<S> {
pub fn assign_roles<I>(&mut self, subject: &Subject, roles: I) -> Result<()>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
for role in roles {
self.assign_role(subject, role.as_ref())?;
}
Ok(())
}
pub fn remove_roles<I>(&mut self, subject: &Subject, roles: I) -> Result<()>
where
I: IntoIterator,
I::Item: AsRef<str>,
{
for role in roles {
self.remove_role(subject, role.as_ref())?;
}
Ok(())
}
pub fn check_permissions_batch(
&self,
subject: &Subject,
permissions: &[(&str, &Resource)],
) -> Result<Vec<(String, String, bool)>> {
permissions
.iter()
.map(|(action, resource)| {
let result = self.check_permission(subject, action, resource)?;
Ok((action.to_string(), resource.id().to_string(), result))
})
.collect()
}
pub fn bulk_assign_roles(
&mut self,
assignments: &[(Subject, Vec<String>)],
) -> Result<Vec<Result<()>>> {
let mut results = Vec::new();
for (subject, roles) in assignments {
let role_refs: Vec<&str> = roles.iter().map(|s| s.as_str()).collect();
let result = self.assign_roles(subject, role_refs);
results.push(result);
}
Ok(results)
}
pub fn get_permission_summary(&self, subject: &Subject) -> Result<PermissionSummary> {
let roles = self.get_subject_roles(subject)?;
let mut permissions = Vec::new();
for role_name in &roles {
if let Some(role) = self.storage.get_role(role_name)? {
for permission in role.permissions().permissions() {
permissions.push(permission.clone());
}
}
}
let effective_permissions_count = permissions.len();
Ok(PermissionSummary {
subject_id: subject.id().to_string(),
roles: roles.into_iter().collect(),
permissions,
effective_permissions_count,
})
}
}
#[derive(Debug, Clone)]
pub struct PermissionSummary {
pub subject_id: String,
pub roles: Vec<String>,
pub permissions: Vec<Permission>,
pub effective_permissions_count: usize,
}
impl<S> Default for RoleSystem<S>
where
S: Storage + Default,
{
fn default() -> Self {
Self::with_storage(S::default(), RoleSystemConfig::default())
}
}