use crate::errors::{AuthError, Result};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use std::time::SystemTime;
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct Permission {
pub resource: String,
pub action: String,
pub conditions: Option<AccessCondition>,
pub attributes: Vec<(String, String)>,
}
impl Permission {
pub fn new(resource: impl Into<String>, action: impl Into<String>) -> Self {
Self {
resource: resource.into(),
action: action.into(),
conditions: None,
attributes: Vec::new(),
}
}
pub fn with_condition(mut self, condition: AccessCondition) -> Self {
self.conditions = Some(condition);
self
}
pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.attributes.push((key.into(), value.into()));
self
}
pub fn matches(&self, requested: &Permission, context: &AccessContext) -> bool {
if self.resource != requested.resource || self.action != requested.action {
return false;
}
if let Some(condition) = &self.conditions {
return condition.evaluate(context);
}
true
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AccessCondition {
TimeRange {
start_hour: u8,
end_hour: u8,
timezone: String,
},
IpWhitelist(Vec<String>),
UserAttribute {
attribute: String,
value: String,
operator: ComparisonOperator,
},
ResourceAttribute {
attribute: String,
value: String,
operator: ComparisonOperator,
},
And(Vec<AccessCondition>),
Or(Vec<AccessCondition>),
Not(Box<AccessCondition>),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ComparisonOperator {
Equals,
NotEquals,
GreaterThan,
LessThan,
Contains,
StartsWith,
EndsWith,
}
impl AccessCondition {
pub fn evaluate(&self, context: &AccessContext) -> bool {
match self {
AccessCondition::TimeRange {
start_hour,
end_hour,
timezone: _,
} => {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap()
.as_secs();
let hour = ((now / 3600) % 24) as u8;
hour >= *start_hour && hour <= *end_hour
}
AccessCondition::IpWhitelist(ips) => context
.ip_address
.as_ref()
.map(|ip| ips.contains(ip))
.unwrap_or(false),
AccessCondition::UserAttribute {
attribute,
value,
operator,
} => context
.user_attributes
.get(attribute)
.map(|attr_value| compare_values(attr_value, value, operator))
.unwrap_or(false),
AccessCondition::ResourceAttribute {
attribute,
value,
operator,
} => context
.resource_attributes
.get(attribute)
.map(|attr_value| compare_values(attr_value, value, operator))
.unwrap_or(false),
AccessCondition::And(conditions) => conditions.iter().all(|c| c.evaluate(context)),
AccessCondition::Or(conditions) => conditions.iter().any(|c| c.evaluate(context)),
AccessCondition::Not(condition) => !condition.evaluate(context),
}
}
}
fn compare_values(left: &str, right: &str, operator: &ComparisonOperator) -> bool {
match operator {
ComparisonOperator::Equals => left == right,
ComparisonOperator::NotEquals => left != right,
ComparisonOperator::GreaterThan => left > right,
ComparisonOperator::LessThan => left < right,
ComparisonOperator::Contains => left.contains(right),
ComparisonOperator::StartsWith => left.starts_with(right),
ComparisonOperator::EndsWith => left.ends_with(right),
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Role {
pub id: String,
pub name: String,
pub description: String,
pub permissions: HashSet<Permission>,
pub parent_roles: HashSet<String>,
pub metadata: HashMap<String, String>,
pub created_at: SystemTime,
pub updated_at: SystemTime,
}
impl Role {
pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
let now = SystemTime::now();
Self {
id: id.into(),
name: name.into(),
description: String::new(),
permissions: HashSet::new(),
parent_roles: HashSet::new(),
metadata: HashMap::new(),
created_at: now,
updated_at: now,
}
}
pub fn add_permission(&mut self, permission: Permission) {
self.permissions.insert(permission);
self.updated_at = SystemTime::now();
}
pub fn remove_permission(&mut self, permission: &Permission) {
self.permissions.remove(permission);
self.updated_at = SystemTime::now();
}
pub fn add_parent_role(&mut self, role_id: impl Into<String>) {
self.parent_roles.insert(role_id.into());
self.updated_at = SystemTime::now();
}
pub fn has_permission(&self, permission: &Permission, context: &AccessContext) -> bool {
self.permissions
.iter()
.any(|p| p.matches(permission, context))
}
}
#[derive(Debug, Clone)]
pub struct AccessContext {
pub user_id: String,
pub user_attributes: HashMap<String, String>,
pub resource_id: Option<String>,
pub resource_attributes: HashMap<String, String>,
pub ip_address: Option<String>,
pub timestamp: SystemTime,
pub metadata: HashMap<String, String>,
}
impl AccessContext {
pub fn new(user_id: impl Into<String>) -> Self {
Self {
user_id: user_id.into(),
user_attributes: HashMap::new(),
resource_id: None,
resource_attributes: HashMap::new(),
ip_address: None,
timestamp: SystemTime::now(),
metadata: HashMap::new(),
}
}
pub fn with_user_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.user_attributes.insert(key.into(), value.into());
self
}
pub fn with_resource(mut self, resource_id: impl Into<String>) -> Self {
self.resource_id = Some(resource_id.into());
self
}
pub fn with_resource_attribute(
mut self,
key: impl Into<String>,
value: impl Into<String>,
) -> Self {
self.resource_attributes.insert(key.into(), value.into());
self
}
pub fn with_ip_address(mut self, ip: impl Into<String>) -> Self {
self.ip_address = Some(ip.into());
self
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct UserRole {
pub user_id: String,
pub role_id: String,
pub assigned_at: SystemTime,
pub expires_at: Option<SystemTime>,
pub assigned_by: String,
}
#[derive(Debug, Clone)]
pub struct AuthorizationResult {
pub granted: bool,
pub reason: String,
pub permissions: Vec<Permission>,
pub evaluation_time: std::time::Duration,
}
#[async_trait]
pub trait AuthorizationStorage: Send + Sync {
async fn store_role(&self, role: &Role) -> Result<()>;
async fn get_role(&self, role_id: &str) -> Result<Option<Role>>;
async fn update_role(&self, role: &Role) -> Result<()>;
async fn delete_role(&self, role_id: &str) -> Result<()>;
async fn list_roles(&self) -> Result<Vec<Role>>;
async fn assign_role(&self, user_role: &UserRole) -> Result<()>;
async fn remove_role(&self, user_id: &str, role_id: &str) -> Result<()>;
async fn get_user_roles(&self, user_id: &str) -> Result<Vec<UserRole>>;
async fn get_role_users(&self, role_id: &str) -> Result<Vec<UserRole>>;
}
pub struct AuthorizationEngine<S: AuthorizationStorage> {
storage: S,
role_cache: std::sync::RwLock<HashMap<String, Role>>,
}
impl<S: AuthorizationStorage> AuthorizationEngine<S> {
pub fn new(storage: S) -> Self {
Self {
storage,
role_cache: std::sync::RwLock::new(HashMap::new()),
}
}
pub async fn check_permission(
&self,
user_id: &str,
permission: &Permission,
context: &AccessContext,
) -> Result<AuthorizationResult> {
let start_time = std::time::Instant::now();
let user_roles = self.storage.get_user_roles(user_id).await?;
let mut applicable_permissions = Vec::new();
let mut granted = false;
let mut reason = "No matching permissions found".to_string();
for user_role in user_roles {
if let Some(expires_at) = user_role.expires_at
&& SystemTime::now() > expires_at
{
continue;
}
let role_permissions = self.get_role_permissions(&user_role.role_id).await?;
for role_permission in role_permissions {
if role_permission.matches(permission, context) {
applicable_permissions.push(role_permission);
granted = true;
reason = format!("Permission granted via role: {}", user_role.role_id);
break;
}
}
if granted {
break;
}
}
let evaluation_time = start_time.elapsed();
Ok(AuthorizationResult {
granted,
reason,
permissions: applicable_permissions,
evaluation_time,
})
}
async fn get_role_permissions(&self, role_id: &str) -> Result<Vec<Permission>> {
let mut all_permissions = Vec::new();
let mut visited_roles = HashSet::new();
self.collect_role_permissions(role_id, &mut all_permissions, &mut visited_roles)
.await?;
Ok(all_permissions)
}
fn collect_role_permissions<'a>(
&'a self,
role_id: &'a str,
permissions: &'a mut Vec<Permission>,
visited: &'a mut HashSet<String>,
) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
Box::pin(async move {
if visited.contains(role_id) {
return Ok(());
}
visited.insert(role_id.to_string());
let role = match self.get_cached_role(role_id).await? {
Some(role) => role,
None => return Ok(()),
};
permissions.extend(role.permissions.iter().cloned());
for parent_role_id in &role.parent_roles {
self.collect_role_permissions(parent_role_id, permissions, visited)
.await?;
}
Ok(())
})
}
async fn get_cached_role(&self, role_id: &str) -> Result<Option<Role>> {
{
let cache = self
.role_cache
.read()
.map_err(|_| AuthError::internal("Failed to acquire role cache lock"))?;
if let Some(role) = cache.get(role_id) {
return Ok(Some(role.clone()));
}
}
if let Some(role) = self.storage.get_role(role_id).await? {
{
let mut cache = self
.role_cache
.write()
.map_err(|_| AuthError::internal("Failed to acquire role cache lock"))?;
cache.insert(role_id.to_string(), role.clone());
}
Ok(Some(role))
} else {
Ok(None)
}
}
pub fn invalidate_role_cache(&self, role_id: &str) -> Result<()> {
let mut cache = self
.role_cache
.write()
.map_err(|_| AuthError::internal("Failed to acquire role cache lock"))?;
cache.remove(role_id);
Ok(())
}
pub async fn create_role(&self, role: Role) -> Result<()> {
self.storage.store_role(&role).await?;
self.invalidate_role_cache(&role.id)?;
Ok(())
}
pub async fn assign_role(&self, user_id: &str, role_id: &str, assigned_by: &str) -> Result<()> {
if self.storage.get_role(role_id).await?.is_none() {
return Err(AuthError::validation(format!(
"Role '{}' does not exist",
role_id
)));
}
let user_role = UserRole {
user_id: user_id.to_string(),
role_id: role_id.to_string(),
assigned_at: SystemTime::now(),
expires_at: None,
assigned_by: assigned_by.to_string(),
};
self.storage.assign_role(&user_role).await
}
pub async fn has_any_role(&self, user_id: &str, role_ids: &[String]) -> Result<bool> {
let user_roles = self.storage.get_user_roles(user_id).await?;
Ok(user_roles.iter().any(|ur| role_ids.contains(&ur.role_id)))
}
}
pub struct CommonPermissions;
impl CommonPermissions {
pub fn user_read() -> Permission {
Permission::new("users", "read")
}
pub fn user_write() -> Permission {
Permission::new("users", "write")
}
pub fn user_delete() -> Permission {
Permission::new("users", "delete")
}
pub fn user_admin() -> Permission {
Permission::new("users", "admin")
}
pub fn document_read() -> Permission {
Permission::new("documents", "read")
}
pub fn document_write() -> Permission {
Permission::new("documents", "write")
}
pub fn document_delete() -> Permission {
Permission::new("documents", "delete")
}
pub fn api_read() -> Permission {
Permission::new("api", "read")
}
pub fn api_write() -> Permission {
Permission::new("api", "write")
}
pub fn system_admin() -> Permission {
Permission::new("system", "admin")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_permission_matching() {
let context = AccessContext::new("user123");
let permission = Permission::new("users", "read");
let requested = Permission::new("users", "read");
assert!(permission.matches(&requested, &context));
let different_action = Permission::new("users", "write");
assert!(!permission.matches(&different_action, &context));
}
#[test]
fn test_access_condition_evaluation() {
let mut context = AccessContext::new("user123");
context
.user_attributes
.insert("department".to_string(), "engineering".to_string());
let condition = AccessCondition::UserAttribute {
attribute: "department".to_string(),
value: "engineering".to_string(),
operator: ComparisonOperator::Equals,
};
assert!(condition.evaluate(&context));
let wrong_condition = AccessCondition::UserAttribute {
attribute: "department".to_string(),
value: "sales".to_string(),
operator: ComparisonOperator::Equals,
};
assert!(!wrong_condition.evaluate(&context));
}
#[test]
fn test_role_hierarchy() {
let mut admin_role = Role::new("admin", "Administrator");
admin_role.add_permission(CommonPermissions::system_admin());
let mut manager_role = Role::new("manager", "Manager");
manager_role.add_permission(CommonPermissions::user_write());
manager_role.add_parent_role("admin");
let context = AccessContext::new("user123");
assert!(manager_role.has_permission(&CommonPermissions::user_write(), &context));
assert!(!manager_role.has_permission(&CommonPermissions::system_admin(), &context));
}
}