use super::provider::PermissionProvider;
use super::rate_limiter::RateLimiter;
use super::stats::{CacheStats, PermissionCheckStats};
use super::types::{PermissionAction, PermissionConfig, PermissionError, RolePolicy};
use dashmap::DashMap;
#[cfg(feature = "cache")]
use oxcache::Cache;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::Duration;
use tokio::sync::Mutex as TokioMutex;
const DEFAULT_RATE_LIMIT_MAX_REQUESTS: u32 = 100;
const DEFAULT_RATE_LIMIT_WINDOW_SECS: u64 = 60;
const DEFAULT_POLICY_CACHE_CAPACITY: usize = 4096;
#[cfg(feature = "cache")]
#[derive(Clone)]
pub struct PermissionContext {
role: String,
policy_cache: Arc<Cache<String, RolePolicy>>,
cache_capacity: usize,
rate_limiter: Option<Arc<RateLimiter>>,
check_stats: Arc<PermissionCheckStats>,
permission_provider: Option<Arc<dyn PermissionProvider>>,
in_flight: DashMap<String, Arc<InFlightEntry>>,
}
struct InFlightEntry {
result: TokioMutex<Option<RolePolicy>>,
done: Arc<AtomicBool>,
}
#[cfg(feature = "cache")]
impl std::fmt::Debug for PermissionContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionContext")
.field("role", &self.role)
.field("rate_limiter", &self.rate_limiter.is_some())
.field("has_permission_provider", &self.permission_provider.is_some())
.finish_non_exhaustive()
}
}
#[cfg(feature = "cache")]
impl PermissionContext {
pub async fn new_default() -> Result<Self, PermissionError> {
Self::with_cache_size("admin".to_string(), DEFAULT_POLICY_CACHE_CAPACITY).await
}
pub async fn new_default_with_rate_limit(role: String) -> Result<Self, PermissionError> {
Self::with_cache_size_and_rate_limit(
role,
DEFAULT_POLICY_CACHE_CAPACITY,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
DEFAULT_RATE_LIMIT_WINDOW_SECS,
)
.await
}
pub async fn with_cache_size(role: String, cache_capacity: usize) -> Result<Self, PermissionError> {
let policy_cache = Cache::builder()
.capacity(cache_capacity as u64)
.build()
.await
.map_err(|_| PermissionError::InvalidCacheCapacity)?;
Ok(Self {
role,
policy_cache: Arc::new(policy_cache),
cache_capacity,
rate_limiter: Some(Arc::new(RateLimiter::new(
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
Duration::from_secs(DEFAULT_RATE_LIMIT_WINDOW_SECS),
10000,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: None,
in_flight: DashMap::new(),
})
}
pub async fn with_cache_size_and_rate_limit(
role: String,
cache_capacity: usize,
max_requests: u32,
window_secs: u64,
) -> Result<Self, PermissionError> {
let policy_cache = Cache::builder()
.capacity(cache_capacity as u64)
.build()
.await
.map_err(|_| PermissionError::InvalidCacheCapacity)?;
Ok(Self {
role,
policy_cache: Arc::new(policy_cache),
cache_capacity,
rate_limiter: Some(Arc::new(RateLimiter::new(
max_requests,
Duration::from_secs(window_secs),
10000,
max_requests,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: None,
in_flight: DashMap::new(),
})
}
pub async fn with_config(
role: String,
config: &crate::foundation::config::DbConfig,
) -> Result<Self, PermissionError> {
let cache_capacity = config.cache_config.policy_cache_capacity as usize;
Self::with_cache_size(role, cache_capacity).await
}
pub async fn with_config_and_rate_limit(
role: String,
config: &crate::foundation::config::DbConfig,
max_requests: u32,
window_secs: u64,
) -> Result<Self, PermissionError> {
let cache_capacity = config.cache_config.policy_cache_capacity as usize;
Self::with_cache_size_and_rate_limit(role, cache_capacity, max_requests, window_secs).await
}
pub fn role(&self) -> &str {
&self.role
}
pub fn check_stats(&self) -> &Arc<PermissionCheckStats> {
&self.check_stats
}
pub fn new_with_defaults(role: String) -> Self {
let cache = tokio::runtime::Handle::current()
.block_on(async {
Cache::builder()
.capacity(DEFAULT_POLICY_CACHE_CAPACITY as u64)
.build()
.await
})
.expect("Failed to create cache");
Self {
role,
policy_cache: Arc::new(cache),
cache_capacity: DEFAULT_POLICY_CACHE_CAPACITY,
rate_limiter: Some(Arc::new(RateLimiter::new(
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
Duration::from_secs(DEFAULT_RATE_LIMIT_WINDOW_SECS),
10000,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: None,
in_flight: DashMap::new(),
}
}
pub fn new_with_config(role: String, config: &crate::foundation::config::DbConfig) -> Self {
let cache_capacity = config.cache_config.policy_cache_capacity as usize;
let cache = tokio::runtime::Handle::current()
.block_on(async { Cache::builder().capacity(cache_capacity as u64).build().await })
.expect("Failed to create cache");
Self {
role,
policy_cache: Arc::new(cache),
cache_capacity,
rate_limiter: Some(Arc::new(RateLimiter::new(
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
Duration::from_secs(DEFAULT_RATE_LIMIT_WINDOW_SECS),
10000,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: None,
in_flight: DashMap::new(),
}
}
pub fn new(role: String, policy_cache: Arc<Cache<String, RolePolicy>>) -> Self {
Self {
role,
policy_cache,
cache_capacity: DEFAULT_POLICY_CACHE_CAPACITY,
rate_limiter: Some(Arc::new(RateLimiter::new(
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
Duration::from_secs(DEFAULT_RATE_LIMIT_WINDOW_SECS),
10000,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: None,
in_flight: DashMap::new(),
}
}
pub fn new_with_provider(
role: String,
policy_cache: Arc<Cache<String, RolePolicy>>,
permission_provider: Arc<dyn PermissionProvider>,
) -> Self {
Self {
role,
policy_cache,
cache_capacity: DEFAULT_POLICY_CACHE_CAPACITY,
rate_limiter: Some(Arc::new(RateLimiter::new(
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
Duration::from_secs(DEFAULT_RATE_LIMIT_WINDOW_SECS),
10000,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: Some(permission_provider),
in_flight: DashMap::new(),
}
}
pub fn new_with_provider_and_config(
role: String,
policy_cache: Arc<Cache<String, RolePolicy>>,
permission_provider: Arc<dyn PermissionProvider>,
config: &crate::foundation::config::DbConfig,
) -> Self {
Self {
role,
policy_cache,
cache_capacity: config.cache_config.policy_cache_capacity as usize,
rate_limiter: Some(Arc::new(RateLimiter::new(
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
Duration::from_secs(DEFAULT_RATE_LIMIT_WINDOW_SECS),
10000,
DEFAULT_RATE_LIMIT_MAX_REQUESTS,
))),
check_stats: Arc::new(PermissionCheckStats::new()),
permission_provider: Some(permission_provider),
in_flight: DashMap::new(),
}
}
fn do_load_policy(&self) -> Option<RolePolicy> {
if let Some(provider) = &self.permission_provider {
if let Some(policy) = provider.get_role_policy(&self.role) {
return Some(policy);
}
}
None
}
async fn get_or_load_policy_coalesced(&self) -> Option<RolePolicy> {
if let Some(policy) = self.policy_cache.get(&self.role).await.ok().flatten() {
return Some(policy);
}
let entry = self.in_flight.entry(self.role.clone()).or_insert_with(|| {
Arc::new(InFlightEntry {
result: TokioMutex::new(None),
done: Arc::new(AtomicBool::new(false)),
})
});
loop {
let mut guard = entry.result.lock().await;
if entry.done.load(Ordering::SeqCst) {
self.check_stats.record_stampede();
let leader_result = (*guard).clone();
if let Some(policy) = leader_result {
drop(guard);
self.check_stats.record_cache_hit();
return Some(policy);
}
entry.done.store(false, Ordering::SeqCst);
drop(guard);
continue;
} else {
let result = self.do_load_policy();
*guard = result.clone();
entry.done.store(true, Ordering::SeqCst);
drop(guard);
if let Some(ref policy) = result {
self.policy_cache.set(&self.role, policy).await.ok();
}
return result;
}
}
}
pub fn set_permission_provider(&mut self, provider: Arc<dyn PermissionProvider>) {
self.permission_provider = Some(provider);
}
pub async fn try_reload_policy(&self) -> bool {
if let Some(provider) = &self.permission_provider {
if let Some(policy) = provider.get_role_policy(&self.role) {
self.policy_cache.set(&self.role, &policy).await.ok();
return true;
}
}
false
}
pub async fn check_table_access(&self, table: &str, operation: &PermissionAction) -> bool {
if let Some(limiter) = &self.rate_limiter {
if !limiter.check(&self.role).await {
self.check_stats.record_rate_limited();
return false;
}
}
if let Some(policy) = self.policy_cache.get(&self.role).await.ok().flatten() {
let allowed = policy.allows(table, operation);
if allowed {
self.check_stats.record_allowed();
} else {
self.check_stats.record_denied();
}
self.check_stats.record_cache_hit();
return allowed;
}
self.check_stats.record_cache_miss();
match self.get_or_load_policy_coalesced().await {
Some(policy) => {
let allowed = policy.allows(table, operation);
if allowed {
self.check_stats.record_allowed();
} else {
self.check_stats.record_denied();
}
allowed
}
None => {
self.check_stats.record_denied();
false
}
}
}
pub async fn verify_operation(&self, table: &str, operation: &PermissionAction, _conditions: Option<&str>) -> bool {
self.check_table_access(table, operation).await
}
pub async fn batch_check_permissions(&self, permissions: &[(String, PermissionAction)]) -> Vec<bool> {
let mut results = Vec::with_capacity(permissions.len());
for (table, operation) in permissions {
results.push(self.check_table_access(table, operation).await);
}
results
}
pub async fn load_policy(&self, config: &PermissionConfig) -> Result<(), String> {
if let Some(policy) = config.get_role_policy(&self.role) {
self.policy_cache.set(&self.role, policy).await.ok();
Ok(())
} else {
Err("Role not found in permission config".to_string())
}
}
pub async fn cache_stats(&self) -> CacheStats {
CacheStats {
cached_roles: self.policy_cache.len().await.unwrap_or(0) as usize,
capacity: self.cache_capacity,
}
}
pub async fn get_cache_metrics(&self) -> (f64, u64, u64, usize) {
let snapshot = self.check_stats.snapshot();
let hit_rate = snapshot.cache_hit_rate();
let miss_count = snapshot.cache_misses;
let stampede_count = snapshot.stampede_events;
let cache_size = self.policy_cache.len().await.unwrap_or(0) as usize;
(hit_rate, miss_count, stampede_count, cache_size)
}
pub async fn clear_cache(&self) {
self.policy_cache.clear().await.ok();
}
}
#[cfg(all(test, feature = "cache"))]
mod tests {
use super::*;
use crate::access::permission::provider::MemoryPermissionProvider;
use crate::access::permission::types::TablePermission;
#[cfg(feature = "permission-engine")]
use futures;
async fn create_test_cache() -> Arc<Cache<String, RolePolicy>> {
Arc::new(
Cache::builder()
.capacity(256)
.build()
.await
.expect("Failed to create test cache"),
)
}
#[tokio::test]
async fn test_permission_context_creation() {
let cache = create_test_cache().await;
let ctx = PermissionContext::new("admin".to_string(), cache);
assert_eq!(ctx.role(), "admin");
}
#[tokio::test]
async fn test_permission_context_load_policy_then_check_access() {
let config = PermissionConfig {
roles: [(
"test_role".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select],
}],
},
)]
.into_iter()
.collect(),
};
let ctx = PermissionContext::with_cache_size("test_role".to_string(), 256)
.await
.unwrap();
ctx.load_policy(&config).await.unwrap();
assert!(ctx.check_table_access("users", &PermissionAction::Select).await);
assert!(!ctx.check_table_access("users", &PermissionAction::Delete).await);
}
#[tokio::test]
async fn test_permission_context_check_table_access_with_config_role_missing_denies() {
let _config = PermissionConfig {
roles: [(
"defined_role".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select],
}],
},
)]
.into_iter()
.collect(),
};
let ctx = PermissionContext::with_cache_size("missing_role".to_string(), 256)
.await
.unwrap();
assert!(!ctx.check_table_access("users", &PermissionAction::Select).await);
}
#[tokio::test]
async fn test_permission_context_check_table_access_rate_limited_denies() {
let config = PermissionConfig {
roles: [(
"test_role".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select],
}],
},
)]
.into_iter()
.collect(),
};
let ctx = PermissionContext::with_cache_size_and_rate_limit("test_role".to_string(), 256, 1, 60)
.await
.unwrap();
ctx.load_policy(&config).await.unwrap();
assert!(ctx.check_table_access("users", &PermissionAction::Select).await);
assert!(!ctx.check_table_access("users", &PermissionAction::Select).await);
}
#[tokio::test]
async fn test_cache_miss_reload_success() {
let config = PermissionConfig {
roles: [(
"test_role".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "users".to_string(),
operations: vec![PermissionAction::Select, PermissionAction::Insert],
}],
},
)]
.into_iter()
.collect(),
};
let provider = Arc::new(MemoryPermissionProvider::new());
provider
.add_role("test_role", config.roles.get("test_role").unwrap().clone())
.await;
let cache = create_test_cache().await;
let ctx = PermissionContext::new_with_provider("test_role".to_string(), cache, provider);
assert!(ctx.check_table_access("users", &PermissionAction::Select).await);
assert!(ctx.check_table_access("users", &PermissionAction::Insert).await);
assert!(!ctx.check_table_access("users", &PermissionAction::Delete).await);
assert!(!ctx.check_table_access("orders", &PermissionAction::Select).await);
}
#[tokio::test]
async fn test_cache_miss_no_provider_safe_deny() {
let cache = create_test_cache().await;
let ctx = PermissionContext::new("test_role".to_string(), cache);
assert!(!ctx.check_table_access("users", &PermissionAction::Select).await);
let stats = ctx.check_stats().snapshot();
assert_eq!(stats.cache_misses, 1);
assert_eq!(stats.denied_checks, 1);
}
#[tokio::test]
async fn test_cache_miss_role_not_found_safe_deny() {
let provider = Arc::new(MemoryPermissionProvider::new());
let cache = create_test_cache().await;
let ctx = PermissionContext::new_with_provider("non_existent_role".to_string(), cache, provider);
assert!(!ctx.check_table_access("users", &PermissionAction::Select).await);
let stats = ctx.check_stats().snapshot();
assert_eq!(stats.cache_misses, 1);
assert_eq!(stats.denied_checks, 1);
}
#[tokio::test]
async fn test_try_reload_policy_success() {
let config = PermissionConfig {
roles: [(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "*".to_string(),
operations: vec![
PermissionAction::Select,
PermissionAction::Insert,
PermissionAction::Update,
PermissionAction::Delete,
],
}],
},
)]
.into_iter()
.collect(),
};
let provider = Arc::new(MemoryPermissionProvider::new());
provider
.add_role("admin", config.roles.get("admin").unwrap().clone())
.await;
let cache = create_test_cache().await;
let ctx = PermissionContext::new_with_provider("admin".to_string(), cache, provider);
let result = ctx.try_reload_policy().await;
assert!(result);
let cached = ctx.policy_cache.get(&"admin".to_string()).await.ok().flatten();
assert!(cached.is_some());
}
#[tokio::test]
async fn test_try_reload_policy_no_provider() {
let cache = create_test_cache().await;
let ctx = PermissionContext::new("admin".to_string(), cache);
let result = ctx.try_reload_policy().await;
assert!(!result);
}
#[tokio::test]
async fn test_cache_hit_then_miss_reload() {
let config = PermissionConfig {
roles: [(
"editor".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "articles".to_string(),
operations: vec![
PermissionAction::Select,
PermissionAction::Insert,
PermissionAction::Update,
],
}],
},
)]
.into_iter()
.collect(),
};
let provider = Arc::new(MemoryPermissionProvider::new());
provider
.add_role("editor", config.roles.get("editor").unwrap().clone())
.await;
let cache = create_test_cache().await;
let ctx = PermissionContext::new_with_provider("editor".to_string(), cache, provider);
assert!(ctx.check_table_access("articles", &PermissionAction::Select).await);
let stats_after_hit = ctx.check_stats().snapshot();
assert!(stats_after_hit.cache_hits > 0 || stats_after_hit.cache_misses > 0);
ctx.clear_cache().await;
assert!(ctx.check_table_access("articles", &PermissionAction::Insert).await);
assert!(!ctx.check_table_access("articles", &PermissionAction::Delete).await);
}
#[tokio::test]
async fn test_set_permission_provider() {
let config = PermissionConfig {
roles: [(
"viewer".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "reports".to_string(),
operations: vec![PermissionAction::Select],
}],
},
)]
.into_iter()
.collect(),
};
let provider = Arc::new(MemoryPermissionProvider::new());
provider
.add_role("viewer", config.roles.get("viewer").unwrap().clone())
.await;
let cache = create_test_cache().await;
let mut ctx = PermissionContext::new("viewer".to_string(), cache);
assert!(!ctx.check_table_access("reports", &PermissionAction::Select).await);
ctx.set_permission_provider(provider);
ctx.clear_cache().await;
assert!(ctx.check_table_access("reports", &PermissionAction::Select).await);
assert!(!ctx.check_table_access("reports", &PermissionAction::Insert).await);
}
#[tokio::test]
async fn test_permission_context_with_config() {
use crate::foundation::config::{CacheConfig, DbConfig};
let config = DbConfig {
url: "sqlite::memory:".to_string(),
cache_config: CacheConfig {
policy_cache_capacity: 8192,
..Default::default()
},
..Default::default()
};
let ctx = PermissionContext::with_config("admin".to_string(), &config)
.await
.unwrap();
let stats = ctx.cache_stats().await;
assert_eq!(stats.capacity, 8192);
}
#[test]
fn test_permission_context_new_with_config() {
use crate::foundation::config::{CacheConfig, DbConfig};
let rt = tokio::runtime::Runtime::new().unwrap();
let _guard = rt.enter();
let config = DbConfig {
url: "sqlite::memory:".to_string(),
cache_config: CacheConfig {
policy_cache_capacity: 16384,
..Default::default()
},
..Default::default()
};
let ctx = PermissionContext::new_with_config("admin".to_string(), &config);
let stats = rt.block_on(async { ctx.cache_stats().await });
assert_eq!(stats.capacity, 16384);
}
#[tokio::test]
async fn test_permission_context_default_capacity() {
let ctx = PermissionContext::new_default().await.unwrap();
let stats = ctx.cache_stats().await;
assert_eq!(stats.capacity, 4096);
}
#[tokio::test]
async fn test_permission_context_custom_capacity() {
let ctx = PermissionContext::with_cache_size("admin".to_string(), 2048)
.await
.unwrap();
let stats = ctx.cache_stats().await;
assert_eq!(stats.capacity, 2048);
}
#[tokio::test]
async fn test_permission_context_with_config_and_rate_limit() {
use crate::foundation::config::{CacheConfig, DbConfig};
let config = DbConfig {
url: "sqlite::memory:".to_string(),
cache_config: CacheConfig {
policy_cache_capacity: 4096,
..Default::default()
},
..Default::default()
};
let ctx = PermissionContext::with_config_and_rate_limit(
"admin".to_string(),
&config,
10, 60, )
.await
.unwrap();
let stats = ctx.cache_stats().await;
assert_eq!(stats.capacity, 4096);
}
#[tokio::test]
async fn test_cache_stampede_counter_increments() {
use crate::access::permission::types::TablePermission;
let config = PermissionConfig {
roles: [(
"reports_role".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "reports".to_string(),
operations: vec![PermissionAction::Select],
}],
},
)]
.into_iter()
.collect(),
};
let provider = Arc::new(MemoryPermissionProvider::new());
provider
.add_role("reports_role", config.roles.get("reports_role").unwrap().clone())
.await;
let cache = create_test_cache().await;
let ctx = PermissionContext::new_with_provider("reports_role".to_string(), cache, provider);
let initial = ctx.check_stats.snapshot();
assert_eq!(initial.stampede_events, 0);
ctx.clear_cache().await;
let _ = ctx.check_table_access("reports", &PermissionAction::Select).await;
let after_first = ctx.check_stats.snapshot();
assert_eq!(
after_first.stampede_events, 0,
"Single request should not count as stampede"
);
ctx.clear_cache().await;
let mut handles = Vec::new();
for _ in 0..10 {
let ctx_clone = ctx.clone();
handles.push(tokio::spawn(async move {
ctx_clone.check_table_access("reports", &PermissionAction::Select).await
}));
}
let results = futures::future::join_all(handles).await;
for r in results {
assert!(r.is_ok_and(|v| v));
}
let after_concurrent = ctx.check_stats.snapshot();
assert!(
after_concurrent.stampede_events > 0,
"Concurrent requests should trigger stampede protection, got {} events",
after_concurrent.stampede_events
);
}
#[tokio::test]
async fn test_get_cache_metrics() {
let config = PermissionConfig {
roles: [(
"admin".to_string(),
RolePolicy {
tables: vec![TablePermission {
name: "*".to_string(),
operations: vec![
PermissionAction::Select,
PermissionAction::Insert,
PermissionAction::Update,
PermissionAction::Delete,
],
}],
},
)]
.into_iter()
.collect(),
};
let provider = Arc::new(MemoryPermissionProvider::new());
provider
.add_role("admin", config.roles.get("admin").unwrap().clone())
.await;
let cache = create_test_cache().await;
let ctx = PermissionContext::new_with_provider("admin".to_string(), cache, provider);
ctx.clear_cache().await;
ctx.check_table_access("users", &PermissionAction::Select).await;
ctx.check_table_access("users", &PermissionAction::Select).await;
ctx.check_table_access("orders", &PermissionAction::Select).await;
let (hit_rate, miss_count, _stampede_count, _cache_size) = ctx.get_cache_metrics().await;
assert!(hit_rate > 0.0, "Should have cache hit rate > 0 after previous access");
assert!(
miss_count >= 1,
"Should have at least 1 cache miss for the first access"
);
}
}