use std::sync::Arc;
use std::time::{Duration, Instant};
use dashmap::DashMap;
use super::provider::PermissionProvider;
use super::types::RolePolicy;
#[cfg(feature = "tracing")]
macro_rules! warn_log {
($($arg:tt)*) => {
tracing::warn!("PermissionCache: {}", format!($($arg)*));
};
}
#[cfg(not(feature = "tracing"))]
macro_rules! warn_log {
($($arg:tt)*) => {
eprintln!("[WARN] PermissionCache: {}", format!($($arg)*));
};
}
const DEFAULT_TTL: Duration = Duration::from_secs(300);
const DEFAULT_REFRESH_INTERVAL: Duration = Duration::from_secs(60);
#[derive(Clone, Debug)]
struct CacheEntry {
value: RolePolicy,
inserted_at: Instant,
}
#[derive(Clone, Debug)]
pub struct PermissionCacheConfig {
pub ttl: Duration,
pub refresh_interval: Duration,
pub stale_while_revalidate: bool,
}
impl Default for PermissionCacheConfig {
fn default() -> Self {
Self {
ttl: DEFAULT_TTL,
refresh_interval: DEFAULT_REFRESH_INTERVAL,
stale_while_revalidate: true,
}
}
}
pub struct PermissionCache {
inner: Arc<DashMap<String, CacheEntry>>,
config: PermissionCacheConfig,
provider: Option<Arc<dyn PermissionProvider>>,
last_refresh: Arc<DashMap<String, Instant>>,
}
impl std::fmt::Debug for PermissionCache {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("PermissionCache")
.field("entry_count", &self.inner.len())
.field("config", &self.config)
.field("has_provider", &self.provider.is_some())
.finish_non_exhaustive()
}
}
impl Default for PermissionCache {
fn default() -> Self {
Self::new()
}
}
impl PermissionCache {
pub fn new() -> Self {
Self {
inner: Arc::new(DashMap::new()),
config: PermissionCacheConfig::default(),
provider: None,
last_refresh: Arc::new(DashMap::new()),
}
}
pub fn with_ttl(mut self, ttl: Duration) -> Self {
self.config.ttl = ttl;
self
}
pub fn with_refresh_interval(mut self, interval: Duration) -> Self {
self.config.refresh_interval = interval;
self
}
pub fn with_stale_while_revalidate(mut self, enabled: bool) -> Self {
self.config.stale_while_revalidate = enabled;
self
}
pub fn with_provider(mut self, provider: Arc<dyn PermissionProvider>) -> Self {
self.provider = Some(provider);
self
}
pub fn config(&self) -> &PermissionCacheConfig {
&self.config
}
pub fn insert(&self, key: &str, value: RolePolicy) {
let entry = CacheEntry {
value,
inserted_at: Instant::now(),
};
self.inner.insert(key.to_string(), entry);
}
pub fn invalidate(&self, key: &str) {
self.inner.remove(key);
self.last_refresh.remove(key);
}
pub fn clear(&self) {
self.inner.clear();
self.last_refresh.clear();
}
pub fn len(&self) -> usize {
self.inner.len()
}
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
pub fn get(&self, key: &str) -> Option<RolePolicy> {
if let Some(entry) = self.inner.get(key) {
let elapsed = entry.inserted_at.elapsed();
if elapsed < self.config.ttl {
return Some(entry.value.clone());
}
self.maybe_spawn_refresh(key);
if self.config.stale_while_revalidate {
Some(entry.value.clone())
} else {
None
}
} else {
None
}
}
pub fn is_expired(&self, key: &str) -> bool {
if let Some(entry) = self.inner.get(key) {
entry.inserted_at.elapsed() >= self.config.ttl
} else {
true
}
}
pub async fn refresh(&self, key: &str) {
if let Some(last) = self.last_refresh.get(key) {
if last.elapsed() < self.config.refresh_interval {
return;
}
}
self.last_refresh.insert(key.to_string(), Instant::now());
let provider = match &self.provider {
Some(p) => p.clone(),
None => {
warn_log!("refresh called without provider; key={}", key);
return;
}
};
let key_owned = key.to_string();
match provider.get_role_policy(&key_owned) {
Some(new_policy) => {
self.insert(&key_owned, new_policy);
}
None => {
warn_log!("refresh returned None for role '{}'; keeping stale value", key_owned);
}
}
}
fn maybe_spawn_refresh(&self, key: &str) {
if self.provider.is_none() {
return;
}
if let Some(last) = self.last_refresh.get(key) {
if last.elapsed() < self.config.refresh_interval {
return;
}
}
let key_owned = key.to_string();
let provider = self.provider.clone().unwrap();
let ttl = self.config.ttl;
let inner = self.inner.clone();
let last_refresh = self.last_refresh.clone();
tokio::spawn(async move {
last_refresh.insert(key_owned.clone(), Instant::now());
match provider.get_role_policy(&key_owned) {
Some(new_policy) => {
let entry = CacheEntry {
value: new_policy,
inserted_at: Instant::now(),
};
inner.insert(key_owned, entry);
}
None => {
warn_log!(
"Background refresh returned None for role '{}'; keeping stale value (ttl={:?})",
key_owned,
ttl
);
}
}
});
}
}
impl Clone for PermissionCache {
fn clone(&self) -> Self {
Self {
inner: self.inner.clone(),
config: self.config.clone(),
provider: self.provider.clone(),
last_refresh: self.last_refresh.clone(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::access::permission::types::{PermissionAction, TablePermission};
fn sample_policy(table: &str) -> RolePolicy {
RolePolicy {
tables: vec![TablePermission {
name: table.to_string(),
operations: vec![PermissionAction::Select],
}],
}
}
#[tokio::test]
async fn test_insert_and_get_fresh() {
let cache = PermissionCache::new().with_ttl(Duration::from_secs(60));
cache.insert("admin", sample_policy("users"));
let got = cache.get("admin");
assert!(got.is_some());
assert_eq!(got.unwrap().tables.len(), 1);
}
#[tokio::test]
async fn test_get_missing_returns_none() {
let cache = PermissionCache::new();
assert!(cache.get("ghost").is_none());
}
#[tokio::test]
async fn test_expired_without_swr_returns_none() {
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_stale_while_revalidate(false);
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(cache.get("admin").is_none());
}
#[tokio::test]
async fn test_expired_with_swr_returns_stale() {
let cache = PermissionCache::new()
.with_ttl(Duration::from_millis(1))
.with_stale_while_revalidate(true);
cache.insert("admin", sample_policy("users"));
tokio::time::sleep(Duration::from_millis(20)).await;
let got = cache.get("admin");
assert!(got.is_some(), "SWR should return stale value");
}
#[tokio::test]
async fn test_is_expired() {
let cache = PermissionCache::new().with_ttl(Duration::from_millis(10));
cache.insert("admin", sample_policy("users"));
assert!(!cache.is_expired("admin"));
tokio::time::sleep(Duration::from_millis(20)).await;
assert!(cache.is_expired("admin"));
assert!(cache.is_expired("ghost"));
}
#[tokio::test]
async fn test_invalidate_and_clear() {
let cache = PermissionCache::new();
cache.insert("a", sample_policy("t1"));
cache.insert("b", sample_policy("t2"));
assert_eq!(cache.len(), 2);
cache.invalidate("a");
assert_eq!(cache.len(), 1);
assert!(cache.get("a").is_none());
cache.clear();
assert!(cache.is_empty());
}
}