use axum::{ body::Body, extract::State, http::{Request, StatusCode}, middleware::Next, response::{IntoResponse, Response}, Json, }; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::RwLock; use tracing::{debug, warn}; use uuid::Uuid; use super::auth::{AuthenticatedUser, Permission, Role}; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RbacConfig { pub cache_ttl_seconds: u64, pub enable_permission_cache: bool, pub enable_group_inheritance: bool, pub default_deny: bool, pub audit_all_decisions: bool, } impl Default for RbacConfig { fn default() -> Self { Self { cache_ttl_seconds: 300, enable_permission_cache: true, enable_group_inheritance: true, default_deny: true, audit_all_decisions: false, } } } #[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct ResourcePermission { pub resource_type: String, pub resource_id: String, pub permission: String, } impl ResourcePermission { pub fn new(resource_type: &str, resource_id: &str, permission: &str) -> Self { Self { resource_type: resource_type.to_string(), resource_id: resource_id.to_string(), permission: permission.to_string(), } } pub fn read(resource_type: &str, resource_id: &str) -> Self { Self::new(resource_type, resource_id, "read") } pub fn write(resource_type: &str, resource_id: &str) -> Self { Self::new(resource_type, resource_id, "write") } pub fn delete(resource_type: &str, resource_id: &str) -> Self { Self::new(resource_type, resource_id, "delete") } pub fn admin(resource_type: &str, resource_id: &str) -> Self { Self::new(resource_type, resource_id, "admin") } } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub enum AccessDecision { Allow, Deny, NotApplicable, } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct AccessDecisionResult { pub decision: AccessDecision, pub reason: String, pub evaluated_at: DateTime, pub cache_hit: bool, pub matched_rule: Option, } impl AccessDecisionResult { pub fn allow(reason: &str) -> Self { Self { decision: AccessDecision::Allow, reason: reason.to_string(), evaluated_at: Utc::now(), cache_hit: false, matched_rule: None, } } pub fn deny(reason: &str) -> Self { Self { decision: AccessDecision::Deny, reason: reason.to_string(), evaluated_at: Utc::now(), cache_hit: false, matched_rule: None, } } pub fn with_cache_hit(mut self) -> Self { self.cache_hit = true; self } pub fn with_rule(mut self, rule: String) -> Self { self.matched_rule = Some(rule); self } pub fn is_allowed(&self) -> bool { self.decision == AccessDecision::Allow } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct RoutePermission { pub path_pattern: String, pub method: String, pub required_permission: String, pub required_roles: Vec, pub allow_anonymous: bool, pub description: Option, } impl RoutePermission { pub fn new(path_pattern: &str, method: &str, permission: &str) -> Self { Self { path_pattern: path_pattern.to_string(), method: method.to_string(), required_permission: permission.to_string(), required_roles: Vec::new(), allow_anonymous: false, description: None, } } pub fn with_roles(mut self, roles: Vec) -> Self { self.required_roles = roles; self } pub fn with_anonymous(mut self, allow: bool) -> Self { self.allow_anonymous = allow; self } pub fn with_description(mut self, desc: &str) -> Self { self.description = Some(desc.to_string()); self } pub fn matches_path(&self, path: &str) -> bool { if self.path_pattern.contains('*') { let pattern_parts: Vec<&str> = self.path_pattern.split('/').collect(); let path_parts: Vec<&str> = path.split('/').collect(); if pattern_parts.len() > path_parts.len() && !self.path_pattern.ends_with("*") { return false; } for (i, pattern_part) in pattern_parts.iter().enumerate() { if *pattern_part == "*" || *pattern_part == "**" { if *pattern_part == "**" { return true; } continue; } if pattern_part.starts_with(':') { continue; } if i >= path_parts.len() || *pattern_part != path_parts[i] { return false; } } pattern_parts.len() <= path_parts.len() || self.path_pattern.contains("**") } else if self.path_pattern.contains(':') { let pattern_parts: Vec<&str> = self.path_pattern.split('/').collect(); let path_parts: Vec<&str> = path.split('/').collect(); if pattern_parts.len() != path_parts.len() { return false; } for (pattern_part, path_part) in pattern_parts.iter().zip(path_parts.iter()) { if pattern_part.starts_with(':') { continue; } if *pattern_part != *path_part { return false; } } true } else { self.path_pattern == path } } } #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ResourceAcl { pub resource_type: String, pub resource_id: String, pub owner_id: Option, pub permissions: HashMap>, pub group_permissions: HashMap>, pub public_permissions: HashSet, pub created_at: DateTime, pub updated_at: DateTime, } impl ResourceAcl { pub fn new(resource_type: &str, resource_id: &str) -> Self { let now = Utc::now(); Self { resource_type: resource_type.to_string(), resource_id: resource_id.to_string(), owner_id: None, permissions: HashMap::new(), group_permissions: HashMap::new(), public_permissions: HashSet::new(), created_at: now, updated_at: now, } } pub fn with_owner(mut self, owner_id: Uuid) -> Self { self.owner_id = Some(owner_id); self } pub fn grant_user(&mut self, user_id: Uuid, permission: &str) { self.permissions .entry(user_id) .or_default() .insert(permission.to_string()); self.updated_at = Utc::now(); } pub fn revoke_user(&mut self, user_id: Uuid, permission: &str) { if let Some(perms) = self.permissions.get_mut(&user_id) { perms.remove(permission); if perms.is_empty() { self.permissions.remove(&user_id); } } self.updated_at = Utc::now(); } pub fn grant_group(&mut self, group_name: &str, permission: &str) { self.group_permissions .entry(group_name.to_string()) .or_default() .insert(permission.to_string()); self.updated_at = Utc::now(); } pub fn revoke_group(&mut self, group_name: &str, permission: &str) { if let Some(perms) = self.group_permissions.get_mut(group_name) { perms.remove(permission); if perms.is_empty() { self.group_permissions.remove(group_name); } } self.updated_at = Utc::now(); } pub fn set_public(&mut self, permission: &str) { self.public_permissions.insert(permission.to_string()); self.updated_at = Utc::now(); } pub fn remove_public(&mut self, permission: &str) { self.public_permissions.remove(permission); self.updated_at = Utc::now(); } pub fn check_access(&self, user_id: Option, groups: &[String], permission: &str) -> bool { if self.public_permissions.contains(permission) { return true; } if let Some(uid) = user_id { if self.owner_id == Some(uid) { return true; } if let Some(user_perms) = self.permissions.get(&uid) { if user_perms.contains(permission) || user_perms.contains("admin") { return true; } } } for group in groups { if let Some(group_perms) = self.group_permissions.get(group) { if group_perms.contains(permission) || group_perms.contains("admin") { return true; } } } false } } #[derive(Debug, Clone)] struct CacheEntry { value: T, expires_at: DateTime, } impl CacheEntry { fn new(value: T, ttl_seconds: u64) -> Self { Self { value, expires_at: Utc::now() + chrono::Duration::seconds(ttl_seconds as i64), } } fn is_expired(&self) -> bool { Utc::now() > self.expires_at } } pub struct RbacManager { config: RbacConfig, route_permissions: Arc>>, resource_acls: Arc>>, permission_cache: Arc>>>, user_groups: Arc>>>, } impl RbacManager { pub fn new(config: RbacConfig) -> Self { Self { config, route_permissions: Arc::new(RwLock::new(Vec::new())), resource_acls: Arc::new(RwLock::new(HashMap::new())), permission_cache: Arc::new(RwLock::new(HashMap::new())), user_groups: Arc::new(RwLock::new(HashMap::new())), } } pub fn with_defaults() -> Self { Self::new(RbacConfig::default()) } pub async fn register_route(&self, permission: RoutePermission) { let mut routes = self.route_permissions.write().await; routes.push(permission); } pub async fn register_routes(&self, permissions: Vec) { let mut routes = self.route_permissions.write().await; routes.extend(permissions); } pub async fn check_route_access( &self, path: &str, method: &str, user: &AuthenticatedUser, ) -> AccessDecisionResult { let cache_key = format!("route:{}:{}:{}", path, method, user.user_id); if self.config.enable_permission_cache { let cache = self.permission_cache.read().await; if let Some(entry) = cache.get(&cache_key) { if !entry.is_expired() { return entry.value.clone().with_cache_hit(); } } } let routes = self.route_permissions.read().await; let method_upper = method.to_uppercase(); for route in routes.iter() { if route.method.to_uppercase() != method_upper && route.method != "*" { continue; } if !route.matches_path(path) { continue; } // Check allow_anonymous FIRST before authentication check if route.allow_anonymous { let result = AccessDecisionResult::allow("Anonymous access allowed") .with_rule(route.path_pattern.clone()); self.cache_result(&cache_key, &result).await; return result; } // Only check authentication after confirming route is not anonymous if !user.is_authenticated() { let result = AccessDecisionResult::deny("Authentication required"); return result; } if !route.required_roles.is_empty() { let has_role = route.required_roles.iter().any(|r| { let role = r.parse::().unwrap_or(Role::Anonymous); user.has_role(&role) }); if !has_role { let result = AccessDecisionResult::deny("Insufficient role") .with_rule(route.path_pattern.clone()); return result; } } if !route.required_permission.is_empty() { let has_permission = self .check_permission_string(user, &route.required_permission) .await; if !has_permission { let result = AccessDecisionResult::deny("Missing required permission") .with_rule(route.path_pattern.clone()); return result; } } let result = AccessDecisionResult::allow("Access granted") .with_rule(route.path_pattern.clone()); self.cache_result(&cache_key, &result).await; return result; } if self.config.default_deny { AccessDecisionResult::deny("No matching route permission found") } else { AccessDecisionResult::allow("Default allow - no matching rule") } } pub async fn check_resource_access( &self, user: &AuthenticatedUser, resource_type: &str, resource_id: &str, permission: &str, ) -> AccessDecisionResult { let cache_key = format!( "resource:{}:{}:{}:{}", resource_type, resource_id, permission, user.user_id ); if self.config.enable_permission_cache { let cache = self.permission_cache.read().await; if let Some(entry) = cache.get(&cache_key) { if !entry.is_expired() { return entry.value.clone().with_cache_hit(); } } } if user.is_admin() { let result = AccessDecisionResult::allow("Admin access"); self.cache_result(&cache_key, &result).await; return result; } let acl_key = format!("{}:{}", resource_type, resource_id); let acls = self.resource_acls.read().await; if let Some(acl) = acls.get(&acl_key) { let user_groups = self.get_user_groups(user.user_id).await; let user_id = if user.is_authenticated() { Some(user.user_id) } else { None }; if acl.check_access(user_id, &user_groups, permission) { let result = AccessDecisionResult::allow("ACL permission granted"); self.cache_result(&cache_key, &result).await; return result; } let result = AccessDecisionResult::deny("ACL permission denied"); return result; } if self.config.default_deny { AccessDecisionResult::deny("No ACL found for resource") } else { AccessDecisionResult::allow("Default allow - no ACL defined") } } pub async fn set_resource_acl(&self, acl: ResourceAcl) { let key = format!("{}:{}", acl.resource_type, acl.resource_id); let mut acls = self.resource_acls.write().await; acls.insert(key, acl); self.invalidate_cache_prefix("resource:").await; } pub async fn get_resource_acl( &self, resource_type: &str, resource_id: &str, ) -> Option { let key = format!("{resource_type}:{resource_id}"); let acls = self.resource_acls.read().await; acls.get(&key).cloned() } pub async fn delete_resource_acl(&self, resource_type: &str, resource_id: &str) { let key = format!("{resource_type}:{resource_id}"); let mut acls = self.resource_acls.write().await; acls.remove(&key); self.invalidate_cache_prefix("resource:").await; } pub async fn set_user_groups(&self, user_id: Uuid, groups: Vec) { let mut user_groups = self.user_groups.write().await; user_groups.insert(user_id, groups); self.invalidate_cache_prefix("resource:").await; } pub async fn add_user_to_group(&self, user_id: Uuid, group: &str) { let mut user_groups = self.user_groups.write().await; user_groups .entry(user_id) .or_default() .push(group.to_string()); self.invalidate_cache_prefix("resource:").await; } pub async fn remove_user_from_group(&self, user_id: Uuid, group: &str) { let mut user_groups = self.user_groups.write().await; if let Some(groups) = user_groups.get_mut(&user_id) { groups.retain(|g| g != group); } self.invalidate_cache_prefix("resource:").await; } pub async fn get_user_groups(&self, user_id: Uuid) -> Vec { let user_groups = self.user_groups.read().await; user_groups.get(&user_id).cloned().unwrap_or_default() } pub async fn invalidate_user_cache(&self, user_id: Uuid) { let prefix = format!(":{user_id}"); let mut cache = self.permission_cache.write().await; cache.retain(|k, _| !k.ends_with(&prefix)); } pub async fn clear_cache(&self) { let mut cache = self.permission_cache.write().await; cache.clear(); } async fn cache_result(&self, key: &str, result: &AccessDecisionResult) { if !self.config.enable_permission_cache { return; } let mut cache = self.permission_cache.write().await; cache.insert( key.to_string(), CacheEntry::new(result.clone(), self.config.cache_ttl_seconds), ); } async fn invalidate_cache_prefix(&self, prefix: &str) { let mut cache = self.permission_cache.write().await; cache.retain(|k, _| !k.starts_with(prefix)); } pub async fn check_permission_string(&self, user: &AuthenticatedUser, permission_str: &str) -> bool { let permission = match permission_str.to_lowercase().as_str() { "read" => Permission::Read, "write" => Permission::Write, "delete" => Permission::Delete, "admin" => Permission::Admin, "manage_users" | "users.manage" => Permission::ManageUsers, "manage_bots" | "bots.manage" => Permission::ManageBots, "view_analytics" | "analytics.view" => Permission::ViewAnalytics, "manage_settings" | "settings.manage" => Permission::ManageSettings, "execute_tasks" | "tasks.execute" => Permission::ExecuteTasks, "view_logs" | "logs.view" => Permission::ViewLogs, "manage_secrets" | "secrets.manage" => Permission::ManageSecrets, "access_api" | "api.access" => Permission::AccessApi, "manage_files" | "files.manage" => Permission::ManageFiles, "send_messages" | "messages.send" => Permission::SendMessages, "view_conversations" | "conversations.view" => Permission::ViewConversations, "manage_webhooks" | "webhooks.manage" => Permission::ManageWebhooks, "manage_integrations" | "integrations.manage" => Permission::ManageIntegrations, _ => return false, }; user.has_permission(&permission) } pub fn config(&self) -> &RbacConfig { &self.config } } /// RBAC middleware function for use with middleware::from_fn /// This version takes the RbacManager as a parameter instead of State pub async fn rbac_middleware_fn( request: Request, next: Next, rbac: Arc, ) -> Response { let path = request.uri().path().to_string(); let method = request.method().to_string(); let user = request .extensions() .get::() .cloned() .unwrap_or_else(AuthenticatedUser::anonymous); debug!( "RBAC check: {} {} | user_id={} authenticated={} roles={:?}", method, path, user.user_id, user.is_authenticated(), user.roles ); let decision = rbac.check_route_access(&path, &method, &user).await; debug!( "RBAC decision for {} {}: {:?} - {}", method, path, decision.decision, decision.reason ); if !decision.is_allowed() { if !user.is_authenticated() { warn!( "RBAC: Unauthorized access attempt to {} {} (no auth)", method, path ); return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "unauthorized", "message": "Authentication required" })), ) .into_response(); } warn!( "RBAC: Forbidden access to {} {} for user {} with roles {:?}", method, path, user.user_id, user.roles ); return ( StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "forbidden", "message": decision.reason })), ) .into_response(); } next.run(request).await } pub async fn rbac_middleware( State(rbac): State>, request: Request, next: Next, ) -> Response { let path = request.uri().path().to_string(); let method = request.method().to_string(); let user = request .extensions() .get::() .cloned() .unwrap_or_else(AuthenticatedUser::anonymous); let decision = rbac.check_route_access(&path, &method, &user).await; if rbac.config.audit_all_decisions { debug!( "RBAC decision for {} {} by user {}: {:?} - {}", method, path, user.user_id, decision.decision, decision.reason ); } if !decision.is_allowed() { if !user.is_authenticated() { return ( StatusCode::UNAUTHORIZED, Json(serde_json::json!({ "error": "unauthorized", "message": "Authentication required" })), ) .into_response(); } return ( StatusCode::FORBIDDEN, Json(serde_json::json!({ "error": "forbidden", "message": decision.reason })), ) .into_response(); } next.run(request).await } #[derive(Clone)] pub struct RequirePermission { pub permission: String, } impl RequirePermission { pub fn new(permission: &str) -> Self { Self { permission: permission.to_string(), } } } #[derive(Clone)] pub struct RequireRole { pub role: Role, } impl RequireRole { pub fn new(role: Role) -> Self { Self { role } } } #[derive(Clone)] pub struct RequireResourceAccess { pub resource_type: String, pub permission: String, } impl RequireResourceAccess { pub fn new(resource_type: &str, permission: &str) -> Self { Self { resource_type: resource_type.to_string(), permission: permission.to_string(), } } } #[derive(Clone)] pub struct RbacMiddlewareState { pub rbac_manager: Arc, pub required_permission: Option, pub required_roles: Vec, pub resource_type: Option, } impl RbacMiddlewareState { pub fn new(rbac_manager: Arc) -> Self { Self { rbac_manager, required_permission: None, required_roles: Vec::new(), resource_type: None, } } pub fn with_permission(mut self, permission: &str) -> Self { self.required_permission = Some(permission.to_string()); self } pub fn with_roles(mut self, roles: Vec) -> Self { self.required_roles = roles; self } pub fn with_resource_type(mut self, resource_type: &str) -> Self { self.resource_type = Some(resource_type.to_string()); self } } pub async fn require_permission_middleware( State(state): State, request: Request, next: Next, ) -> Result { let user = request .extensions() .get::() .cloned() .unwrap_or_else(AuthenticatedUser::anonymous); if let Some(ref required_perm) = state.required_permission { let has_permission = state .rbac_manager .check_permission_string(&user, required_perm) .await; if !has_permission { warn!( "Permission denied for user {}: missing permission {}", user.user_id, required_perm ); return Err(RbacError::PermissionDenied(format!( "Missing required permission: {required_perm}" ))); } } if !state.required_roles.is_empty() { let has_required_role = state .required_roles .iter() .any(|role| user.has_role(role)); if !has_required_role { warn!( "Role check failed for user {}: required one of {:?}", user.user_id, state.required_roles ); return Err(RbacError::InsufficientRole(format!( "Required role: {:?}", state.required_roles ))); } } Ok(next.run(request).await) } pub async fn require_admin_middleware( request: Request, next: Next, ) -> Result { let user = request .extensions() .get::() .cloned() .unwrap_or_else(AuthenticatedUser::anonymous); if !user.is_admin() && !user.is_super_admin() { warn!("Admin access denied for user {}", user.user_id); return Err(RbacError::AdminRequired); } Ok(next.run(request).await) } pub async fn require_super_admin_middleware( request: Request, next: Next, ) -> Result { let user = request .extensions() .get::() .cloned() .unwrap_or_else(AuthenticatedUser::anonymous); if !user.is_super_admin() { warn!("Super admin access denied for user {}", user.user_id); return Err(RbacError::SuperAdminRequired); } Ok(next.run(request).await) } #[derive(Debug, Clone)] pub enum RbacError { PermissionDenied(String), InsufficientRole(String), AdminRequired, SuperAdminRequired, ResourceAccessDenied(String), } impl IntoResponse for RbacError { fn into_response(self) -> Response { let (status, message) = match self { Self::PermissionDenied(msg) => (StatusCode::FORBIDDEN, msg), Self::InsufficientRole(msg) => (StatusCode::FORBIDDEN, msg), Self::AdminRequired => ( StatusCode::FORBIDDEN, "Administrator access required".to_string(), ), Self::SuperAdminRequired => ( StatusCode::FORBIDDEN, "Super administrator access required".to_string(), ), Self::ResourceAccessDenied(msg) => (StatusCode::FORBIDDEN, msg), }; let body = serde_json::json!({ "error": "access_denied", "message": message, "code": "RBAC_DENIED" }); (status, Json(body)).into_response() } } pub fn create_permission_layer( rbac_manager: Arc, permission: &str, ) -> RbacMiddlewareState { RbacMiddlewareState::new(rbac_manager).with_permission(permission) } pub fn create_role_layer(rbac_manager: Arc, roles: Vec) -> RbacMiddlewareState { RbacMiddlewareState::new(rbac_manager).with_roles(roles) } pub fn create_admin_layer(rbac_manager: Arc) -> RbacMiddlewareState { RbacMiddlewareState::new(rbac_manager).with_roles(vec![Role::Admin, Role::SuperAdmin]) } pub fn build_default_route_permissions() -> Vec { vec![ // ===================================================================== // PUBLIC / ANONYMOUS ROUTES (no auth required) // ===================================================================== RoutePermission::new("/health", "GET", "").with_anonymous(true), RoutePermission::new("/healthz", "GET", "").with_anonymous(true), RoutePermission::new("/api/health", "GET", "").with_anonymous(true), RoutePermission::new("/api/version", "GET", "").with_anonymous(true), RoutePermission::new("/api/product", "GET", "").with_anonymous(true), RoutePermission::new("/api/bot/config", "GET", "").with_anonymous(true), RoutePermission::new("/api/i18n/**", "GET", "").with_anonymous(true), // WhatsApp webhook - anonymous for Meta verification and message delivery RoutePermission::new("/webhook/whatsapp/:bot_id", "GET", "").with_anonymous(true), RoutePermission::new("/webhook/whatsapp/:bot_id", "POST", "").with_anonymous(true), // Auth routes - login must be anonymous RoutePermission::new("/api/auth", "GET", "").with_anonymous(true), RoutePermission::new("/api/auth/login", "POST", "").with_anonymous(true), // Client error reporting - anonymous to catch all JS errors RoutePermission::new("/api/client-errors", "POST", "").with_anonymous(true), RoutePermission::new("/api/auth/bootstrap", "POST", "").with_anonymous(true), RoutePermission::new("/api/auth/refresh", "POST", "").with_anonymous(true), RoutePermission::new("/api/auth/logout", "POST", ""), RoutePermission::new("/api/auth/me", "GET", "").with_anonymous(true), RoutePermission::new("/api/auth/**", "GET", ""), RoutePermission::new("/api/auth/**", "POST", ""), // WebSocket - anonymous for chat support RoutePermission::new("/ws", "GET", "").with_anonymous(true), RoutePermission::new("/ws/**", "GET", "").with_anonymous(true), // Chat - ANONYMOUS for customer support RoutePermission::new("/api/chat/**", "GET", "").with_anonymous(true), RoutePermission::new("/api/chat/**", "POST", "").with_anonymous(true), // Sessions - anonymous can create sessions for chat RoutePermission::new("/api/sessions", "POST", "").with_anonymous(true), RoutePermission::new("/api/sessions", "GET", ""), RoutePermission::new("/api/sessions/**", "GET", ""), // ===================================================================== // AUTHENTICATED USER ROUTES (any logged-in user) // ===================================================================== // Drive / Files RoutePermission::new("/api/drive/**", "GET", ""), RoutePermission::new("/api/drive/**", "POST", ""), RoutePermission::new("/api/drive/**", "PUT", ""), RoutePermission::new("/api/drive/**", "DELETE", ""), RoutePermission::new("/api/files/**", "GET", ""), RoutePermission::new("/api/files/**", "POST", ""), RoutePermission::new("/api/files/**", "PUT", ""), RoutePermission::new("/api/files/**", "DELETE", ""), // Mail RoutePermission::new("/api/mail/**", "GET", ""), RoutePermission::new("/api/mail/**", "POST", ""), RoutePermission::new("/api/mail/**", "PUT", ""), RoutePermission::new("/api/mail/**", "DELETE", ""), // Calendar RoutePermission::new("/api/calendar/**", "GET", ""), RoutePermission::new("/api/calendar/**", "POST", ""), RoutePermission::new("/api/calendar/**", "PUT", ""), RoutePermission::new("/api/calendar/**", "DELETE", ""), // Tasks RoutePermission::new("/api/tasks/**", "GET", ""), RoutePermission::new("/api/tasks/**", "POST", ""), RoutePermission::new("/api/tasks/**", "PUT", ""), RoutePermission::new("/api/tasks/**", "PATCH", ""), RoutePermission::new("/api/tasks/**", "DELETE", ""), // Docs / Paper RoutePermission::new("/api/docs/**", "GET", ""), RoutePermission::new("/api/docs/**", "POST", ""), RoutePermission::new("/api/docs/**", "PUT", ""), RoutePermission::new("/api/docs/**", "DELETE", ""), RoutePermission::new("/api/paper/**", "GET", ""), RoutePermission::new("/api/paper/**", "POST", ""), RoutePermission::new("/api/paper/**", "PUT", ""), RoutePermission::new("/api/paper/**", "DELETE", ""), // Sheet RoutePermission::new("/api/sheet/**", "GET", ""), RoutePermission::new("/api/sheet/**", "POST", ""), RoutePermission::new("/api/sheet/**", "PUT", ""), RoutePermission::new("/api/sheet/**", "DELETE", ""), // Slides RoutePermission::new("/api/slides/**", "GET", ""), RoutePermission::new("/api/slides/**", "POST", ""), RoutePermission::new("/api/slides/**", "PUT", ""), RoutePermission::new("/api/slides/**", "DELETE", ""), // Meet RoutePermission::new("/api/meet/**", "GET", ""), RoutePermission::new("/api/meet/**", "POST", ""), RoutePermission::new("/api/meet/**", "PUT", ""), RoutePermission::new("/api/meet/**", "DELETE", ""), // Research RoutePermission::new("/api/research/**", "GET", ""), RoutePermission::new("/api/research/**", "POST", ""), RoutePermission::new("/api/research/**", "PUT", ""), RoutePermission::new("/api/research/**", "DELETE", ""), // Sources RoutePermission::new("/api/sources/**", "GET", ""), RoutePermission::new("/api/sources/**", "POST", ""), RoutePermission::new("/api/sources/**", "PUT", ""), RoutePermission::new("/api/sources/**", "DELETE", ""), // Canvas RoutePermission::new("/api/canvas/**", "GET", ""), RoutePermission::new("/api/canvas/**", "POST", ""), RoutePermission::new("/api/canvas/**", "PUT", ""), RoutePermission::new("/api/canvas/**", "DELETE", ""), // Video / Player RoutePermission::new("/api/video/**", "GET", ""), RoutePermission::new("/api/video/**", "POST", ""), RoutePermission::new("/api/player/**", "GET", ""), RoutePermission::new("/api/player/**", "POST", ""), // Workspaces RoutePermission::new("/api/workspaces/**", "GET", ""), RoutePermission::new("/api/workspaces/**", "POST", ""), RoutePermission::new("/api/workspaces/**", "PUT", ""), RoutePermission::new("/api/workspaces/**", "DELETE", ""), // Projects RoutePermission::new("/api/projects/**", "GET", ""), RoutePermission::new("/api/projects/**", "POST", ""), RoutePermission::new("/api/projects/**", "PUT", ""), RoutePermission::new("/api/projects/**", "DELETE", ""), // Goals RoutePermission::new("/api/goals/**", "GET", ""), RoutePermission::new("/api/goals/**", "POST", ""), RoutePermission::new("/api/goals/**", "PUT", ""), RoutePermission::new("/api/goals/**", "DELETE", ""), // Settings (user's own settings) RoutePermission::new("/api/settings/**", "GET", ""), RoutePermission::new("/api/settings/**", "POST", ""), RoutePermission::new("/api/settings/**", "PUT", ""), // Bots (read for all authenticated users) RoutePermission::new("/api/bots", "GET", ""), RoutePermission::new("/api/bots/:id", "GET", ""), RoutePermission::new("/api/bots/:id/**", "GET", ""), // Autotask RoutePermission::new("/api/autotask/**", "GET", ""), RoutePermission::new("/api/autotask/**", "POST", ""), RoutePermission::new("/api/autotask/**", "PUT", ""), RoutePermission::new("/api/autotask/**", "DELETE", ""), // Designer RoutePermission::new("/api/designer/**", "GET", ""), RoutePermission::new("/api/designer/**", "POST", ""), RoutePermission::new("/api/designer/**", "PUT", ""), RoutePermission::new("/api/designer/**", "DELETE", ""), // Dashboards RoutePermission::new("/api/dashboards/**", "GET", ""), RoutePermission::new("/api/dashboards/**", "POST", ""), RoutePermission::new("/api/dashboards/**", "PUT", ""), RoutePermission::new("/api/dashboards/**", "DELETE", ""), // DB/Table access RoutePermission::new("/api/db/**", "GET", ""), RoutePermission::new("/api/db/**", "POST", ""), RoutePermission::new("/api/db/**", "PUT", ""), RoutePermission::new("/api/db/**", "DELETE", ""), // CRM / Contacts RoutePermission::new("/api/crm/**", "GET", ""), RoutePermission::new("/api/crm/**", "POST", ""), RoutePermission::new("/api/crm/**", "PUT", ""), RoutePermission::new("/api/crm/**", "DELETE", ""), RoutePermission::new("/api/contacts/**", "GET", ""), RoutePermission::new("/api/contacts/**", "POST", ""), RoutePermission::new("/api/contacts/**", "PUT", ""), RoutePermission::new("/api/contacts/**", "DELETE", ""), // Marketing / Campaigns RoutePermission::new("/api/marketing/**", "GET", ""), RoutePermission::new("/api/marketing/**", "POST", ""), RoutePermission::new("/api/marketing/**", "PUT", ""), RoutePermission::new("/api/marketing/**", "DELETE", ""), // CRM Campaigns RoutePermission::new("/api/crm/campaigns/**", "GET", ""), RoutePermission::new("/api/crm/campaigns/**", "POST", ""), RoutePermission::new("/api/crm/campaigns/**", "PUT", ""), RoutePermission::new("/api/crm/campaigns/**", "DELETE", ""), RoutePermission::new("/api/crm/lists/**", "GET", ""), RoutePermission::new("/api/crm/lists/**", "POST", ""), RoutePermission::new("/api/crm/lists/**", "PUT", ""), RoutePermission::new("/api/crm/lists/**", "DELETE", ""), RoutePermission::new("/api/crm/templates/**", "GET", ""), RoutePermission::new("/api/crm/templates/**", "POST", ""), RoutePermission::new("/api/crm/templates/**", "PUT", ""), RoutePermission::new("/api/crm/templates/**", "DELETE", ""), // Billing / Products RoutePermission::new("/api/billing/**", "GET", ""), RoutePermission::new("/api/billing/**", "POST", ""), RoutePermission::new("/api/products/**", "GET", ""), RoutePermission::new("/api/products/**", "POST", ""), RoutePermission::new("/api/products/**", "PUT", ""), RoutePermission::new("/api/products/**", "DELETE", ""), // Tickets RoutePermission::new("/api/tickets/**", "GET", ""), RoutePermission::new("/api/tickets/**", "POST", ""), RoutePermission::new("/api/tickets/**", "PUT", ""), RoutePermission::new("/api/tickets/**", "DELETE", ""), // Learn RoutePermission::new("/api/learn/**", "GET", ""), RoutePermission::new("/api/learn/**", "POST", ""), // Social RoutePermission::new("/api/social/**", "GET", ""), RoutePermission::new("/api/social/**", "POST", ""), // LLM RoutePermission::new("/api/llm/**", "GET", ""), RoutePermission::new("/api/llm/**", "POST", ""), // Email RoutePermission::new("/api/email/**", "GET", ""), RoutePermission::new("/api/email/**", "POST", ""), RoutePermission::new("/api/email/**", "PUT", ""), RoutePermission::new("/api/email/**", "DELETE", ""), // Messaging channels RoutePermission::new("/api/telegram/**", "GET", ""), RoutePermission::new("/api/telegram/**", "POST", ""), RoutePermission::new("/api/whatsapp/**", "GET", ""), RoutePermission::new("/api/whatsapp/**", "POST", ""), RoutePermission::new("/api/msteams/**", "GET", ""), RoutePermission::new("/api/msteams/**", "POST", ""), RoutePermission::new("/api/instagram/**", "GET", ""), RoutePermission::new("/api/instagram/**", "POST", ""), // Pages RoutePermission::new("/api/pages/**", "GET", ""), RoutePermission::new("/api/pages/**", "POST", ""), RoutePermission::new("/api/pages/**", "PUT", ""), RoutePermission::new("/api/pages/**", "DELETE", ""), // Insights RoutePermission::new("/api/insights/**", "GET", ""), RoutePermission::new("/api/insights/**", "POST", ""), // App logs RoutePermission::new("/api/app-logs/**", "GET", ""), RoutePermission::new("/api/app-logs/**", "POST", ""), // User profile (own user) RoutePermission::new("/api/user/**", "GET", ""), RoutePermission::new("/api/user/**", "PUT", ""), // ===================================================================== // UI ROUTES (HTMX endpoints) - authenticated users // ===================================================================== RoutePermission::new("/api/ui/tasks/**", "GET", "").with_anonymous(true), RoutePermission::new("/api/ui/tasks/**", "POST", ""), RoutePermission::new("/api/ui/tasks/**", "PUT", ""), RoutePermission::new("/api/ui/tasks/**", "PATCH", ""), RoutePermission::new("/api/ui/tasks/**", "DELETE", ""), RoutePermission::new("/api/ui/calendar/**", "GET", ""), RoutePermission::new("/api/ui/calendar/**", "POST", ""), RoutePermission::new("/api/ui/drive/**", "GET", ""), RoutePermission::new("/api/ui/drive/**", "POST", ""), RoutePermission::new("/api/ui/mail/**", "GET", ""), RoutePermission::new("/api/ui/mail/**", "POST", ""), RoutePermission::new("/api/ui/docs/**", "GET", ""), RoutePermission::new("/api/ui/docs/**", "POST", ""), RoutePermission::new("/api/ui/paper/**", "GET", ""), RoutePermission::new("/api/ui/paper/**", "POST", ""), RoutePermission::new("/api/ui/sheet/**", "GET", ""), RoutePermission::new("/api/ui/sheet/**", "POST", ""), RoutePermission::new("/api/ui/slides/**", "GET", ""), RoutePermission::new("/api/ui/slides/**", "POST", ""), RoutePermission::new("/api/ui/meet/**", "GET", ""), RoutePermission::new("/api/ui/meet/**", "POST", ""), RoutePermission::new("/api/ui/research/**", "GET", ""), RoutePermission::new("/api/ui/research/**", "POST", ""), RoutePermission::new("/api/ui/sources/**", "GET", ""), RoutePermission::new("/api/ui/sources/**", "POST", ""), RoutePermission::new("/api/ui/canvas/**", "GET", ""), RoutePermission::new("/api/ui/video/**", "GET", ""), RoutePermission::new("/api/ui/player/**", "GET", ""), RoutePermission::new("/api/ui/workspaces/**", "GET", ""), RoutePermission::new("/api/ui/projects/**", "GET", ""), RoutePermission::new("/api/ui/goals/**", "GET", ""), RoutePermission::new("/api/ui/designer/**", "GET", ""), RoutePermission::new("/api/ui/dashboards/**", "GET", ""), RoutePermission::new("/api/ui/crm/**", "GET", ""), RoutePermission::new("/api/ui/billing/**", "GET", ""), RoutePermission::new("/api/ui/products/**", "GET", ""), RoutePermission::new("/api/ui/tickets/**", "GET", ""), RoutePermission::new("/api/ui/learn/**", "GET", ""), RoutePermission::new("/api/ui/social/**", "GET", ""), RoutePermission::new("/api/ui/settings/**", "GET", ""), RoutePermission::new("/api/ui/autotask/**", "GET", ""), RoutePermission::new("/api/ui/email/**", "GET", ""), RoutePermission::new("/api/ui/email/**", "POST", ""), // ===================================================================== // ADMIN ROUTES (requires Admin or SuperAdmin role) // ===================================================================== RoutePermission::new("/api/users", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/:id", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/:id", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/:id", "DELETE", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/**", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/**", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/users/**", "DELETE", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Groups management RoutePermission::new("/api/groups/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/groups/**", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/groups/**", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/groups/**", "DELETE", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Bot management (create/delete) RoutePermission::new("/api/bots", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/bots/:id", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/bots/:id", "DELETE", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/bots/:id/**", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/bots/:id/**", "DELETE", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Analytics (admin view) RoutePermission::new("/api/analytics/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into(), "Moderator".into()]), RoutePermission::new("/api/ui/analytics/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into(), "Moderator".into()]), // Monitoring RoutePermission::new("/api/monitoring/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/ui/monitoring/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Audit logs RoutePermission::new("/api/audit/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/ui/audit/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Security settings RoutePermission::new("/api/security/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/security/**", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/security/**", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/ui/security/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Admin panel RoutePermission::new("/api/admin/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/admin/**", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/admin/**", "PUT", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/admin/**", "DELETE", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), RoutePermission::new("/api/ui/admin/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into()]), // Attendant (customer service) RoutePermission::new("/api/attendant/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into(), "Moderator".into()]), RoutePermission::new("/api/attendant/**", "POST", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into(), "Moderator".into()]), RoutePermission::new("/api/ui/attendant/**", "GET", "") .with_roles(vec!["Admin".into(), "SuperAdmin".into(), "Moderator".into()]), // ===================================================================== // SUPER ADMIN ONLY ROUTES // ===================================================================== RoutePermission::new("/api/rbac/**", "GET", "") .with_roles(vec!["SuperAdmin".into()]), RoutePermission::new("/api/rbac/**", "POST", "") .with_roles(vec!["SuperAdmin".into()]), RoutePermission::new("/api/rbac/**", "PUT", "") .with_roles(vec!["SuperAdmin".into()]), RoutePermission::new("/api/rbac/**", "DELETE", "") .with_roles(vec!["SuperAdmin".into()]), ] } #[cfg(test)] mod tests { use super::*; #[test] fn test_route_permission_exact_match() { let route = RoutePermission::new("/api/users", "GET", "users.read"); assert!(route.matches_path("/api/users")); assert!(!route.matches_path("/api/users/123")); assert!(!route.matches_path("/api/user")); } #[test] fn test_route_permission_param_match() { let route = RoutePermission::new("/api/users/:id", "GET", "users.read"); assert!(route.matches_path("/api/users/123")); assert!(route.matches_path("/api/users/abc")); assert!(!route.matches_path("/api/users")); assert!(!route.matches_path("/api/users/123/profile")); } #[test] fn test_route_permission_wildcard_match() { let route = RoutePermission::new("/api/drive/**", "GET", "drive.read"); assert!(route.matches_path("/api/drive")); assert!(route.matches_path("/api/drive/files")); assert!(route.matches_path("/api/drive/files/123")); assert!(route.matches_path("/api/drive/a/b/c/d")); assert!(!route.matches_path("/api/mail")); } #[test] fn test_route_permission_single_wildcard() { let route = RoutePermission::new("/api/*/info", "GET", "info.read"); assert!(route.matches_path("/api/users/info")); assert!(route.matches_path("/api/bots/info")); } #[test] fn test_resource_acl_owner_access() { let owner_id = Uuid::new_v4(); let other_id = Uuid::new_v4(); let acl = ResourceAcl::new("file", "123").with_owner(owner_id); assert!(acl.check_access(Some(owner_id), &[], "read")); assert!(acl.check_access(Some(owner_id), &[], "write")); assert!(acl.check_access(Some(owner_id), &[], "delete")); assert!(!acl.check_access(Some(other_id), &[], "read")); } #[test] fn test_resource_acl_user_permissions() { let user_id = Uuid::new_v4(); let mut acl = ResourceAcl::new("file", "123"); acl.grant_user(user_id, "read"); assert!(acl.check_access(Some(user_id), &[], "read")); assert!(!acl.check_access(Some(user_id), &[], "write")); } #[test] fn test_resource_acl_group_permissions() { let user_id = Uuid::new_v4(); let mut acl = ResourceAcl::new("file", "123"); acl.grant_group("editors", "write"); assert!(acl.check_access(Some(user_id), &["editors".into()], "write")); assert!(!acl.check_access(Some(user_id), &["viewers".into()], "write")); } #[test] fn test_resource_acl_public_permissions() { let mut acl = ResourceAcl::new("file", "123"); acl.set_public("read"); assert!(acl.check_access(None, &[], "read")); assert!(!acl.check_access(None, &[], "write")); } #[test] fn test_resource_acl_admin_access() { let user_id = Uuid::new_v4(); let mut acl = ResourceAcl::new("file", "123"); acl.grant_user(user_id, "admin"); assert!(acl.check_access(Some(user_id), &[], "read")); assert!(acl.check_access(Some(user_id), &[], "write")); assert!(acl.check_access(Some(user_id), &[], "delete")); } #[test] fn test_access_decision_result() { let allow = AccessDecisionResult::allow("Test allow"); assert!(allow.is_allowed()); let deny = AccessDecisionResult::deny("Test deny"); assert!(!deny.is_allowed()); } #[test] fn test_resource_permission_builders() { let read = ResourcePermission::read("file", "123"); assert_eq!(read.permission, "read"); let write = ResourcePermission::write("file", "123"); assert_eq!(write.permission, "write"); let delete = ResourcePermission::delete("file", "123"); assert_eq!(delete.permission, "delete"); } #[tokio::test] async fn test_rbac_manager_creation() { let manager = RbacManager::with_defaults(); let routes = build_default_route_permissions(); manager.register_routes(routes).await; let user = AuthenticatedUser::anonymous(); let decision = manager .check_route_access("/api/health", "GET", &user) .await; assert!(decision.is_allowed()); } #[tokio::test] async fn test_user_groups() { let manager = RbacManager::with_defaults(); let user_id = Uuid::new_v4(); manager.set_user_groups(user_id, vec!["editors".into(), "viewers".into()]).await; let groups = manager.get_user_groups(user_id).await; assert_eq!(groups.len(), 2); assert!(groups.contains(&"editors".into())); } #[tokio::test] async fn test_resource_acl_management() { let manager = RbacManager::with_defaults(); let owner_id = Uuid::new_v4(); let acl = ResourceAcl::new("document", "doc-123").with_owner(owner_id); manager.set_resource_acl(acl).await; let retrieved = manager.get_resource_acl("document", "doc-123").await; assert!(retrieved.is_some()); assert_eq!(retrieved.as_ref().and_then(|a| a.owner_id), Some(owner_id)); } }