use crate::SecurityResult;
use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::RwLock;
#[derive(Debug, Clone)]
pub struct RbacConfig {
pub enable_cache: bool,
pub cache_ttl: u64,
pub enable_audit: bool,
pub enable_hierarchy: bool,
pub role_hierarchy: HashMap<String, Vec<String>>,
}
impl Default for RbacConfig {
fn default() -> Self {
let mut role_hierarchy = HashMap::new();
role_hierarchy.insert(
"ADMIN".to_string(),
vec![
"MODERATOR".to_string(),
"USER".to_string(),
"GUEST".to_string(),
],
);
role_hierarchy
.insert("MODERATOR".to_string(), vec!["USER".to_string(), "GUEST".to_string()]);
role_hierarchy.insert("USER".to_string(), vec!["GUEST".to_string()]);
Self {
enable_cache: true,
cache_ttl: 300, enable_audit: true,
enable_hierarchy: true,
role_hierarchy,
}
}
}
impl RbacConfig {
pub fn new() -> Self {
Self::default()
}
pub fn enable_cache(mut self, enable: bool) -> Self {
self.enable_cache = enable;
self
}
pub fn cache_ttl(mut self, ttl: Duration) -> Self {
self.cache_ttl = ttl.as_secs();
self
}
pub fn enable_audit(mut self, enable: bool) -> Self {
self.enable_audit = enable;
self
}
pub fn enable_hierarchy(mut self, enable: bool) -> Self {
self.enable_hierarchy = enable;
self
}
pub fn role_hierarchy(mut self, hierarchy: HashMap<String, Vec<String>>) -> Self {
self.role_hierarchy = hierarchy;
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PermissionEntry {
pub id: String,
pub name: String,
pub description: String,
pub resource: String,
pub action: String,
pub roles: Vec<String>,
}
impl PermissionEntry {
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
description: impl Into<String>,
resource: impl Into<String>,
action: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
description: description.into(),
resource: resource.into(),
action: action.into(),
roles: Vec::new(),
}
}
pub fn add_role(mut self, role: impl Into<String>) -> Self {
self.roles.push(role.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct RolePermission {
pub role: String,
pub permissions: HashSet<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserRole {
pub user_id: String,
pub roles: HashSet<String>,
pub direct_permissions: HashSet<String>,
pub expires_at: Option<DateTime<Utc>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditLog {
pub timestamp: DateTime<Utc>,
pub user_id: String,
pub permission: String,
pub resource: Option<String>,
pub granted: bool,
pub reason: Option<String>,
pub ip_address: Option<String>,
pub user_agent: Option<String>,
}
#[async_trait::async_trait]
pub trait AuditLogger: Send + Sync {
async fn log(&self, entry: AuditLog) -> SecurityResult<()>;
}
#[derive(Debug, Clone)]
pub struct ConsoleAuditLogger;
impl ConsoleAuditLogger {
pub fn new() -> Self {
Self
}
}
impl Default for ConsoleAuditLogger {
fn default() -> Self {
Self::new()
}
}
#[async_trait::async_trait]
impl AuditLogger for ConsoleAuditLogger {
async fn log(&self, entry: AuditLog) -> SecurityResult<()> {
let status = if entry.granted { "GRANTED" } else { "DENIED" };
tracing::info!(
"[AUDIT] {} | User: {} | Permission: {} | Resource: {:?} | Reason: {:?}",
status,
entry.user_id,
entry.permission,
entry.resource,
entry.reason
);
Ok(())
}
}
#[derive(Debug, Clone)]
struct CacheEntry {
permissions: HashSet<String>,
expires_at: DateTime<Utc>,
}
#[derive(Clone)]
pub struct RbacManager {
config: RbacConfig,
user_roles: Arc<RwLock<HashMap<String, UserRole>>>,
role_permissions: Arc<RwLock<HashMap<String, HashSet<String>>>>,
permissions: Arc<RwLock<HashMap<String, PermissionEntry>>>,
cache: Arc<RwLock<HashMap<String, CacheEntry>>>,
audit_logger: Option<Arc<dyn AuditLogger>>,
}
impl RbacManager {
pub fn new(config: RbacConfig) -> Self {
Self {
config,
user_roles: Arc::new(RwLock::new(HashMap::new())),
role_permissions: Arc::new(RwLock::new(HashMap::new())),
permissions: Arc::new(RwLock::new(HashMap::new())),
cache: Arc::new(RwLock::new(HashMap::new())),
audit_logger: None,
}
}
}
impl fmt::Debug for RbacManager {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RbacManager")
.field("config", &self.config)
.field("user_roles", &"<hidden>")
.field("role_permissions", &"<hidden>")
.field("permissions", &"<hidden>")
.field("cache", &"<hidden>")
.field("audit_logger", &self.audit_logger.as_ref().map(|_| "<logger>"))
.finish()
}
}
impl RbacManager {
pub fn with_audit_logger(mut self, logger: Arc<dyn AuditLogger>) -> Self {
self.audit_logger = Some(logger);
self
}
pub async fn add_user_role(&self, user_role: UserRole) -> SecurityResult<()> {
let user_id = user_role.user_id.clone();
let mut user_roles = self.user_roles.write().await;
user_roles.insert(user_id.clone(), user_role);
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.remove(&user_id);
}
Ok(())
}
pub async fn add_role_permission(
&self,
role: String,
permissions: Vec<String>,
) -> SecurityResult<()> {
let mut role_permissions = self.role_permissions.write().await;
role_permissions.insert(role, permissions.into_iter().collect());
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.clear();
}
Ok(())
}
pub async fn add_permission(&self, permission: PermissionEntry) -> SecurityResult<()> {
let mut permissions = self.permissions.write().await;
permissions.insert(permission.id.clone(), permission);
Ok(())
}
pub async fn load_permissions_from_db(&self) -> SecurityResult<()> {
tracing::info!("Loading permissions from database...");
self.add_permission(
PermissionEntry::new("user.read", "user:read", "Read user information", "user", "read")
.add_role("USER")
.add_role("ADMIN"),
)
.await?;
self.add_permission(
PermissionEntry::new(
"user.write",
"user:write",
"Write user information",
"user",
"write",
)
.add_role("ADMIN"),
)
.await?;
self.add_role_permission("USER".to_string(), vec!["user.read".to_string()])
.await?;
self.add_role_permission(
"ADMIN".to_string(),
vec!["user.read".to_string(), "user.write".to_string()],
)
.await?;
Ok(())
}
pub async fn check_permission(&self, user_id: &str, permission: &str) -> SecurityResult<bool> {
self.check_permission_with_context(user_id, permission, None, None, None)
.await
}
pub async fn check_permission_with_context(
&self,
user_id: &str,
permission: &str,
resource: Option<String>,
ip_address: Option<String>,
user_agent: Option<String>,
) -> SecurityResult<bool> {
if self.config.enable_cache
&& let Some(cached) = self.get_cached_permissions(user_id).await
{
let granted = cached.contains(permission);
self.audit_log(user_id, permission, resource, granted, None, ip_address, user_agent)
.await;
return Ok(granted);
}
let permissions = self.get_user_permissions(user_id).await?;
let granted = permissions.contains(permission);
if self.config.enable_cache {
self.cache_permissions(user_id, permissions).await;
}
self.audit_log(user_id, permission, resource, granted, None, ip_address, user_agent)
.await;
Ok(granted)
}
pub async fn check_role(&self, user_id: &str, role: &str) -> SecurityResult<bool> {
let user_roles = self.user_roles.read().await;
if let Some(user_role) = user_roles.get(user_id) {
if let Some(expires_at) = user_role.expires_at
&& Utc::now() > expires_at
{
return Ok(false);
}
if user_role.roles.contains(role) {
return Ok(true);
}
if self.config.enable_hierarchy {
for user_role_name in &user_role.roles {
if self.role_inherits_role(user_role_name, role) {
return Ok(true);
}
}
}
}
Ok(false)
}
async fn get_user_permissions(&self, user_id: &str) -> SecurityResult<HashSet<String>> {
let user_roles = self.user_roles.read().await;
let role_permissions = self.role_permissions.read().await;
let mut permissions = HashSet::new();
if let Some(user_role) = user_roles.get(user_id) {
if let Some(expires_at) = user_role.expires_at
&& Utc::now() > expires_at
{
return Ok(permissions);
}
permissions.extend(user_role.direct_permissions.clone());
for role_name in &user_role.roles {
if let Some(role_perms) = role_permissions.get(role_name) {
permissions.extend(role_perms.clone());
}
if self.config.enable_hierarchy {
let inherited_roles = self.get_all_inherited_roles(role_name);
for inherited_role in inherited_roles {
if let Some(role_perms) = role_permissions.get(&inherited_role) {
permissions.extend(role_perms.clone());
}
}
}
}
}
Ok(permissions)
}
fn get_all_inherited_roles(&self, role: &str) -> HashSet<String> {
let mut inherited = HashSet::new();
let mut to_check = vec![role.to_string()];
while let Some(check) = to_check.pop() {
if let Some(children) = self.config.role_hierarchy.get(&check) {
for child in children {
if inherited.insert(child.clone()) {
to_check.push(child.clone());
}
}
}
}
inherited
}
fn role_inherits_role(&self, role: &str, target: &str) -> bool {
if role == target {
return true;
}
if let Some(children) = self.config.role_hierarchy.get(role) {
children.iter().any(|child| {
child == target || self.config.role_hierarchy.contains_key(child)
})
} else {
false
}
}
async fn get_cached_permissions(&self, user_id: &str) -> Option<HashSet<String>> {
let cache = self.cache.read().await;
if let Some(entry) = cache.get(user_id)
&& entry.expires_at > Utc::now()
{
return Some(entry.permissions.clone());
}
None
}
async fn cache_permissions(&self, user_id: &str, permissions: HashSet<String>) {
let expires_at = Utc::now() + chrono::Duration::seconds(self.config.cache_ttl as i64);
let entry = CacheEntry {
permissions,
expires_at,
};
let mut cache = self.cache.write().await;
cache.insert(user_id.to_string(), entry);
}
pub async fn clear_cache(&self) {
let mut cache = self.cache.write().await;
cache.clear();
}
pub async fn clear_user_cache(&self, user_id: &str) {
let mut cache = self.cache.write().await;
cache.remove(user_id);
}
async fn audit_log(
&self,
user_id: &str,
permission: &str,
resource: Option<String>,
granted: bool,
reason: Option<String>,
ip_address: Option<String>,
user_agent: Option<String>,
) {
if self.config.enable_audit
&& let Some(logger) = &self.audit_logger
{
let entry = AuditLog {
timestamp: Utc::now(),
user_id: user_id.to_string(),
permission: permission.to_string(),
resource,
granted,
reason,
ip_address,
user_agent,
};
let _ = logger.log(entry).await;
}
}
pub async fn get_user_roles(&self, user_id: &str) -> SecurityResult<HashSet<String>> {
let user_roles = self.user_roles.read().await;
if let Some(user_role) = user_roles.get(user_id) {
if let Some(expires_at) = user_role.expires_at
&& Utc::now() > expires_at
{
return Ok(HashSet::new());
}
return Ok(user_role.roles.clone());
}
Ok(HashSet::new())
}
pub async fn assign_role(&self, user_id: &str, role: &str) -> SecurityResult<()> {
let mut user_roles = self.user_roles.write().await;
let user_role = user_roles
.entry(user_id.to_string())
.or_insert_with(|| UserRole {
user_id: user_id.to_string(),
roles: HashSet::new(),
direct_permissions: HashSet::new(),
expires_at: None,
});
user_role.roles.insert(role.to_string());
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.remove(user_id);
}
Ok(())
}
pub async fn revoke_role(&self, user_id: &str, role: &str) -> SecurityResult<()> {
let mut user_roles = self.user_roles.write().await;
if let Some(user_role) = user_roles.get_mut(user_id) {
user_role.roles.remove(role);
if self.config.enable_cache {
let mut cache = self.cache.write().await;
cache.remove(user_id);
}
}
Ok(())
}
}
impl Default for RbacManager {
fn default() -> Self {
Self::new(RbacConfig::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::sync::atomic::{AtomicUsize, Ordering};
fn make_user_role(user_id: &str, roles: &[&str]) -> UserRole {
UserRole {
user_id: user_id.to_string(),
roles: roles.iter().map(|r| r.to_string()).collect(),
direct_permissions: HashSet::new(),
expires_at: None,
}
}
fn make_user_role_with_permissions(
user_id: &str,
roles: &[&str],
permissions: &[&str],
) -> UserRole {
UserRole {
user_id: user_id.to_string(),
roles: roles.iter().map(|r| r.to_string()).collect(),
direct_permissions: permissions.iter().map(|p| p.to_string()).collect(),
expires_at: None,
}
}
fn make_expired_user_role(user_id: &str, roles: &[&str]) -> UserRole {
UserRole {
user_id: user_id.to_string(),
roles: roles.iter().map(|r| r.to_string()).collect(),
direct_permissions: HashSet::new(),
expires_at: Some(Utc::now() - chrono::Duration::seconds(1)),
}
}
#[derive(Debug)]
struct CapturingAuditLogger {
entries: Arc<RwLock<Vec<AuditLog>>>,
}
impl CapturingAuditLogger {
fn new() -> Self {
Self {
entries: Arc::new(RwLock::new(Vec::new())),
}
}
async fn logged_entries(&self) -> Vec<AuditLog> {
self.entries.read().await.clone()
}
}
#[async_trait::async_trait]
impl AuditLogger for CapturingAuditLogger {
async fn log(&self, entry: AuditLog) -> SecurityResult<()> {
self.entries.write().await.push(entry);
Ok(())
}
}
struct CountingAuditLogger {
count: AtomicUsize,
}
impl CountingAuditLogger {
fn new() -> Self {
Self {
count: AtomicUsize::new(0),
}
}
fn invocation_count(&self) -> usize {
self.count.load(Ordering::SeqCst)
}
}
#[async_trait::async_trait]
impl AuditLogger for CountingAuditLogger {
async fn log(&self, _entry: AuditLog) -> SecurityResult<()> {
self.count.fetch_add(1, Ordering::SeqCst);
Ok(())
}
}
#[test]
fn test_config_default_values() {
let config = RbacConfig::default();
assert!(config.enable_cache);
assert!(config.enable_audit);
assert!(config.enable_hierarchy);
assert_eq!(config.cache_ttl, 300);
}
#[test]
fn test_config_default_hierarchy() {
let config = RbacConfig::default();
let admin_children = config.role_hierarchy.get("ADMIN").unwrap();
assert!(admin_children.contains(&"MODERATOR".to_string()));
assert!(admin_children.contains(&"USER".to_string()));
assert!(admin_children.contains(&"GUEST".to_string()));
let mod_children = config.role_hierarchy.get("MODERATOR").unwrap();
assert!(mod_children.contains(&"USER".to_string()));
assert!(mod_children.contains(&"GUEST".to_string()));
let user_children = config.role_hierarchy.get("USER").unwrap();
assert!(user_children.contains(&"GUEST".to_string()));
}
#[test]
fn test_config_builder_chain() {
let config = RbacConfig::new()
.enable_cache(false)
.enable_audit(false)
.enable_hierarchy(false)
.cache_ttl(Duration::from_secs(600));
assert!(!config.enable_cache);
assert!(!config.enable_audit);
assert!(!config.enable_hierarchy);
assert_eq!(config.cache_ttl, 600);
}
#[test]
fn test_config_custom_role_hierarchy() {
let mut custom = HashMap::new();
custom.insert("SUPER".to_string(), vec!["OPERATOR".to_string()]);
let config = RbacConfig::new().role_hierarchy(custom);
let children = config.role_hierarchy.get("SUPER").unwrap();
assert!(children.contains(&"OPERATOR".to_string()));
assert!(config.role_hierarchy.get("ADMIN").is_none());
}
#[test]
fn test_permission_entry_new() {
let entry = PermissionEntry::new("p1", "user:read", "Read users", "user", "read");
assert_eq!(entry.id, "p1");
assert_eq!(entry.name, "user:read");
assert_eq!(entry.description, "Read users");
assert_eq!(entry.resource, "user");
assert_eq!(entry.action, "read");
assert!(entry.roles.is_empty());
}
#[test]
fn test_permission_entry_add_roles() {
let entry = PermissionEntry::new("p1", "doc:write", "Write docs", "doc", "write")
.add_role("ADMIN")
.add_role("EDITOR");
assert_eq!(entry.roles, vec!["ADMIN", "EDITOR"]);
}
#[test]
fn test_permission_entry_serialization() {
let entry = PermissionEntry::new("p1", "user:delete", "Delete users", "user", "delete")
.add_role("ADMIN");
let json = serde_json::to_string(&entry).unwrap();
let deserialized: PermissionEntry = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.id, entry.id);
assert_eq!(deserialized.name, entry.name);
assert_eq!(deserialized.roles, entry.roles);
}
#[tokio::test]
async fn test_assign_role_to_new_user() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.assign_role("alice", "USER").await.unwrap();
let roles = mgr.get_user_roles("alice").await.unwrap();
assert!(roles.contains("USER"));
}
#[tokio::test]
async fn test_assign_multiple_roles() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.assign_role("bob", "USER").await.unwrap();
mgr.assign_role("bob", "MODERATOR").await.unwrap();
let roles = mgr.get_user_roles("bob").await.unwrap();
assert!(roles.contains("USER"));
assert!(roles.contains("MODERATOR"));
assert_eq!(roles.len(), 2);
}
#[tokio::test]
async fn test_assign_duplicate_role_is_idempotent() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.assign_role("carol", "USER").await.unwrap();
mgr.assign_role("carol", "USER").await.unwrap();
let roles = mgr.get_user_roles("carol").await.unwrap();
assert_eq!(roles.len(), 1);
}
#[tokio::test]
async fn test_revoke_role() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.assign_role("dave", "USER").await.unwrap();
mgr.assign_role("dave", "ADMIN").await.unwrap();
mgr.revoke_role("dave", "ADMIN").await.unwrap();
let roles = mgr.get_user_roles("dave").await.unwrap();
assert!(!roles.contains("ADMIN"));
assert!(roles.contains("USER"));
}
#[tokio::test]
async fn test_revoke_nonexistent_role_is_noop() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.assign_role("eve", "USER").await.unwrap();
mgr.revoke_role("eve", "SUPERADMIN").await.unwrap();
let roles = mgr.get_user_roles("eve").await.unwrap();
assert!(roles.contains("USER"));
}
#[tokio::test]
async fn test_revoke_from_nonexistent_user_is_noop() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.revoke_role("ghost", "USER").await.unwrap();
let roles = mgr.get_user_roles("ghost").await.unwrap();
assert!(roles.is_empty());
}
#[tokio::test]
async fn test_check_permission_granted() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role("u1", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["doc.read".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u1", "doc.read").await.unwrap());
}
#[tokio::test]
async fn test_check_permission_denied() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role("u2", &["GUEST"]))
.await
.unwrap();
mgr.add_role_permission("GUEST".to_string(), vec!["doc.read".to_string()])
.await
.unwrap();
assert!(!mgr.check_permission("u2", "doc.write").await.unwrap());
}
#[tokio::test]
async fn test_check_permission_unknown_user_denied() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
assert!(!mgr.check_permission("nobody", "any.thing").await.unwrap());
}
#[tokio::test]
async fn test_direct_permissions() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role_with_permissions("u3", &["GUEST"], &["special.perm"]))
.await
.unwrap();
mgr.add_role_permission("GUEST".to_string(), vec!["guest.read".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u3", "special.perm").await.unwrap());
assert!(mgr.check_permission("u3", "guest.read").await.unwrap());
}
#[tokio::test]
async fn test_check_role_direct() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.assign_role("u10", "EDITOR").await.unwrap();
assert!(mgr.check_role("u10", "EDITOR").await.unwrap());
assert!(!mgr.check_role("u10", "ADMIN").await.unwrap());
}
#[tokio::test]
async fn test_check_role_via_hierarchy() {
let mgr = RbacManager::new(
RbacConfig::new()
.enable_hierarchy(true)
.enable_cache(false)
.enable_audit(false),
);
mgr.assign_role("admin_user", "ADMIN").await.unwrap();
assert!(mgr.check_role("admin_user", "USER").await.unwrap());
assert!(mgr.check_role("admin_user", "GUEST").await.unwrap());
}
#[tokio::test]
async fn test_check_role_hierarchy_disabled() {
let mgr = RbacManager::new(
RbacConfig::new()
.enable_hierarchy(false)
.enable_cache(false)
.enable_audit(false),
);
mgr.assign_role("mod_user", "MODERATOR").await.unwrap();
assert!(mgr.check_role("mod_user", "MODERATOR").await.unwrap());
assert!(!mgr.check_role("mod_user", "USER").await.unwrap());
}
#[tokio::test]
async fn test_admin_inherits_user_permissions() {
let mgr = RbacManager::new(
RbacConfig::new()
.enable_hierarchy(true)
.enable_cache(false)
.enable_audit(false),
);
mgr.add_user_role(make_user_role("admin1", &["ADMIN"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["profile.read".to_string()])
.await
.unwrap();
mgr.add_role_permission("ADMIN".to_string(), vec!["admin.panel".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("admin1", "admin.panel").await.unwrap());
assert!(
mgr.check_permission("admin1", "profile.read")
.await
.unwrap()
);
}
#[tokio::test]
async fn test_moderator_inherits_user_and_guest_permissions() {
let mgr = RbacManager::new(
RbacConfig::new()
.enable_hierarchy(true)
.enable_cache(false)
.enable_audit(false),
);
mgr.add_user_role(make_user_role("mod1", &["MODERATOR"]))
.await
.unwrap();
mgr.add_role_permission("GUEST".to_string(), vec!["public.read".to_string()])
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["profile.read".to_string()])
.await
.unwrap();
mgr.add_role_permission("MODERATOR".to_string(), vec!["mod.ban".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("mod1", "mod.ban").await.unwrap());
assert!(mgr.check_permission("mod1", "profile.read").await.unwrap());
assert!(mgr.check_permission("mod1", "public.read").await.unwrap());
}
#[tokio::test]
async fn test_leaf_role_does_not_inherit_parent() {
let mgr = RbacManager::new(
RbacConfig::new()
.enable_hierarchy(true)
.enable_cache(false)
.enable_audit(false),
);
mgr.add_user_role(make_user_role("guest1", &["GUEST"]))
.await
.unwrap();
mgr.add_role_permission("ADMIN".to_string(), vec!["admin.super".to_string()])
.await
.unwrap();
assert!(!mgr.check_permission("guest1", "admin.super").await.unwrap());
}
#[tokio::test]
async fn test_multi_level_inheritance_chain() {
let mgr = RbacManager::new(
RbacConfig::new()
.enable_hierarchy(true)
.enable_cache(false)
.enable_audit(false),
);
mgr.add_user_role(make_user_role("super_admin", &["ADMIN"]))
.await
.unwrap();
mgr.add_role_permission("GUEST".to_string(), vec!["guest.view".to_string()])
.await
.unwrap();
assert!(
mgr.check_permission("super_admin", "guest.view")
.await
.unwrap()
);
}
#[tokio::test]
async fn test_expired_user_role_returns_no_roles() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_expired_user_role("expired_user", &["ADMIN"]))
.await
.unwrap();
let roles = mgr.get_user_roles("expired_user").await.unwrap();
assert!(roles.is_empty());
}
#[tokio::test]
async fn test_expired_user_role_denies_permission() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_expired_user_role("expired_user", &["ADMIN"]))
.await
.unwrap();
mgr.add_role_permission("ADMIN".to_string(), vec!["admin.read".to_string()])
.await
.unwrap();
assert!(
!mgr.check_permission("expired_user", "admin.read")
.await
.unwrap()
);
}
#[tokio::test]
async fn test_expired_user_role_check_role_fails() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_expired_user_role("expired_user", &["ADMIN"]))
.await
.unwrap();
assert!(!mgr.check_role("expired_user", "ADMIN").await.unwrap());
}
#[tokio::test]
async fn test_cache_hit_returns_same_result() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("cached_user", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["cache.perm".to_string()])
.await
.unwrap();
assert!(
mgr.check_permission("cached_user", "cache.perm")
.await
.unwrap()
);
assert!(
mgr.check_permission("cached_user", "cache.perm")
.await
.unwrap()
);
}
#[tokio::test]
async fn test_cache_invalidated_on_role_assignment() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["p1".to_string()])
.await
.unwrap();
mgr.add_role_permission("ADMIN".to_string(), vec!["p2".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u", "p1").await.unwrap());
assert!(!mgr.check_permission("u", "p2").await.unwrap());
mgr.assign_role("u", "ADMIN").await.unwrap();
assert!(mgr.check_permission("u", "p2").await.unwrap());
}
#[tokio::test]
async fn test_cache_invalidated_on_role_revocation() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("u", &["USER", "ADMIN"]))
.await
.unwrap();
mgr.add_role_permission("ADMIN".to_string(), vec!["admin.x".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u", "admin.x").await.unwrap());
mgr.revoke_role("u", "ADMIN").await.unwrap();
assert!(!mgr.check_permission("u", "admin.x").await.unwrap());
}
#[tokio::test]
async fn test_cache_invalidated_on_add_user_role() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["p1".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u", "p1").await.unwrap());
mgr.add_user_role(make_user_role("u", &["GUEST"]))
.await
.unwrap();
assert!(!mgr.check_permission("u", "p1").await.unwrap());
}
#[tokio::test]
async fn test_add_role_permission_clears_all_cache() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("a", &["USER"]))
.await
.unwrap();
mgr.add_user_role(make_user_role("b", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["old.perm".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("a", "old.perm").await.unwrap());
assert!(mgr.check_permission("b", "old.perm").await.unwrap());
mgr.add_role_permission("USER".to_string(), vec!["new.perm".to_string()])
.await
.unwrap();
assert!(!mgr.check_permission("a", "old.perm").await.unwrap());
assert!(mgr.check_permission("a", "new.perm").await.unwrap());
}
#[tokio::test]
async fn test_clear_cache_manual() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["p".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u", "p").await.unwrap());
mgr.clear_cache().await;
assert!(mgr.check_permission("u", "p").await.unwrap());
}
#[tokio::test]
async fn test_clear_user_cache_targeted() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(true).enable_audit(false));
mgr.add_user_role(make_user_role("u1", &["USER"]))
.await
.unwrap();
mgr.add_user_role(make_user_role("u2", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["shared.perm".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u1", "shared.perm").await.unwrap());
assert!(mgr.check_permission("u2", "shared.perm").await.unwrap());
mgr.clear_user_cache("u1").await;
assert!(mgr.check_permission("u1", "shared.perm").await.unwrap());
}
#[tokio::test]
async fn test_no_cache_mode() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["p".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("u", "p").await.unwrap());
assert!(mgr.check_permission("u", "p").await.unwrap());
}
#[tokio::test]
async fn test_audit_log_records_granted() {
let logger = Arc::new(CapturingAuditLogger::new());
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(true))
.with_audit_logger(logger.clone());
mgr.add_user_role(make_user_role("audit_user", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["file.read".to_string()])
.await
.unwrap();
mgr.check_permission("audit_user", "file.read")
.await
.unwrap();
let entries = logger.logged_entries().await;
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].user_id, "audit_user");
assert_eq!(entries[0].permission, "file.read");
assert!(entries[0].granted);
}
#[tokio::test]
async fn test_audit_log_records_denied() {
let logger = Arc::new(CapturingAuditLogger::new());
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(true))
.with_audit_logger(logger.clone());
mgr.add_user_role(make_user_role("audit_user", &["USER"]))
.await
.unwrap();
mgr.check_permission("audit_user", "secret.write")
.await
.unwrap();
let entries = logger.logged_entries().await;
assert_eq!(entries.len(), 1);
assert!(!entries[0].granted);
}
#[tokio::test]
async fn test_audit_log_with_context() {
let logger = Arc::new(CapturingAuditLogger::new());
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(true))
.with_audit_logger(logger.clone());
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["res.read".to_string()])
.await
.unwrap();
mgr.check_permission_with_context(
"u",
"res.read",
Some("doc/123".to_string()),
Some("10.0.0.1".to_string()),
Some("TestAgent/1.0".to_string()),
)
.await
.unwrap();
let entries = logger.logged_entries().await;
assert_eq!(entries.len(), 1);
assert_eq!(entries[0].resource.as_deref(), Some("doc/123"));
assert_eq!(entries[0].ip_address.as_deref(), Some("10.0.0.1"));
assert_eq!(entries[0].user_agent.as_deref(), Some("TestAgent/1.0"));
}
#[tokio::test]
async fn test_audit_disabled_no_logs() {
let logger = Arc::new(CountingAuditLogger::new());
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false))
.with_audit_logger(logger.clone());
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.check_permission("u", "any.perm").await.unwrap();
assert_eq!(logger.invocation_count(), 0);
}
#[tokio::test]
async fn test_audit_log_timestamp_is_recent() {
let logger = Arc::new(CapturingAuditLogger::new());
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(true))
.with_audit_logger(logger.clone());
mgr.add_user_role(make_user_role("u", &["USER"]))
.await
.unwrap();
mgr.add_role_permission("USER".to_string(), vec!["x".to_string()])
.await
.unwrap();
let before = Utc::now();
mgr.check_permission("u", "x").await.unwrap();
let after = Utc::now();
let entries = logger.logged_entries().await;
assert!(entries[0].timestamp >= before);
assert!(entries[0].timestamp <= after);
}
#[tokio::test]
async fn test_console_audit_logger_does_not_error() {
let logger = ConsoleAuditLogger::new();
let entry = AuditLog {
timestamp: Utc::now(),
user_id: "test".to_string(),
permission: "test.perm".to_string(),
resource: None,
granted: true,
reason: None,
ip_address: None,
user_agent: None,
};
assert!(logger.log(entry).await.is_ok());
}
#[tokio::test]
async fn test_add_and_store_permission_entry() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
let perm = PermissionEntry::new("p.x", "x:do", "Do X", "x", "do").add_role("OPERATOR");
mgr.add_permission(perm).await.unwrap();
}
#[tokio::test]
async fn test_load_permissions_from_db_populates_defaults() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.load_permissions_from_db().await.unwrap();
mgr.add_user_role(make_user_role("default_user", &["USER"]))
.await
.unwrap();
assert!(
mgr.check_permission("default_user", "user.read")
.await
.unwrap()
);
assert!(
!mgr.check_permission("default_user", "user.write")
.await
.unwrap()
);
mgr.add_user_role(make_user_role("default_admin", &["ADMIN"]))
.await
.unwrap();
assert!(
mgr.check_permission("default_admin", "user.read")
.await
.unwrap()
);
assert!(
mgr.check_permission("default_admin", "user.write")
.await
.unwrap()
);
}
#[test]
fn test_manager_default_construction() {
let mgr = RbacManager::default();
let debug = format!("{:?}", mgr);
assert!(debug.contains("RbacManager"));
}
#[test]
fn test_manager_debug_format() {
let mgr = RbacManager::new(RbacConfig::new());
let debug = format!("{:?}", mgr);
assert!(debug.contains("config"));
assert!(debug.contains("<hidden>"));
}
#[test]
fn test_manager_with_audit_logger_debug() {
let mgr = RbacManager::new(RbacConfig::new())
.with_audit_logger(Arc::new(ConsoleAuditLogger::new()));
let debug = format!("{:?}", mgr);
assert!(debug.contains("<logger>"));
}
#[test]
fn test_user_role_serialization() {
let role = make_user_role("alice", &["ADMIN", "USER"]);
let json = serde_json::to_string(&role).unwrap();
let deserialized: UserRole = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.user_id, "alice");
assert!(deserialized.roles.contains("ADMIN"));
}
#[test]
fn test_role_permission_serialization() {
let rp = RolePermission {
role: "EDITOR".to_string(),
permissions: {
let mut s = HashSet::new();
s.insert("doc.edit".to_string());
s.insert("doc.read".to_string());
s
},
};
let json = serde_json::to_string(&rp).unwrap();
let deserialized: RolePermission = serde_json::from_str(&json).unwrap();
assert_eq!(deserialized.role, "EDITOR");
assert!(deserialized.permissions.contains("doc.edit"));
}
#[tokio::test]
async fn test_user_with_no_roles_gets_no_permissions() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role("empty_user", &[]))
.await
.unwrap();
assert!(
!mgr.check_permission("empty_user", "anything")
.await
.unwrap()
);
}
#[tokio::test]
async fn test_role_with_no_permissions() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role("u", &["EMPTY_ROLE"]))
.await
.unwrap();
mgr.add_role_permission("EMPTY_ROLE".to_string(), vec![])
.await
.unwrap();
assert!(!mgr.check_permission("u", "any.perm").await.unwrap());
}
#[tokio::test]
#[ignore] async fn test_multiple_users_isolated() {
let mgr = RbacManager::new(RbacConfig::new().enable_cache(false).enable_audit(false));
mgr.add_user_role(make_user_role("alice", &["ADMIN"]))
.await
.unwrap();
mgr.add_user_role(make_user_role("bob", &["GUEST"]))
.await
.unwrap();
mgr.add_role_permission("ADMIN".to_string(), vec!["admin.panel".to_string()])
.await
.unwrap();
mgr.add_role_permission("GUEST".to_string(), vec!["guest.view".to_string()])
.await
.unwrap();
assert!(mgr.check_permission("alice", "admin.panel").await.unwrap());
assert!(!mgr.check_permission("alice", "guest.view").await.unwrap());
assert!(!mgr.check_permission("bob", "admin.panel").await.unwrap());
assert!(mgr.check_permission("bob", "guest.view").await.unwrap());
}
}