auth_framework/
authorization.rs

1//! Role-Based Access Control (RBAC) and Authorization framework.
2//!
3//! This module provides a comprehensive authorization system with support for
4//! roles, permissions, hierarchical access control, and dynamic policy evaluation.
5
6use crate::errors::{AuthError, Result};
7use async_trait::async_trait;
8use serde::{Deserialize, Serialize};
9use std::collections::{HashMap, HashSet};
10use std::time::SystemTime;
11
12/// A permission represents a specific action that can be performed on a resource
13#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
14pub struct Permission {
15    /// The resource being accessed (e.g., "users", "documents", "api")
16    pub resource: String,
17    /// The action being performed (e.g., "read", "write", "delete", "admin")
18    pub action: String,
19    /// Optional conditions for the permission
20    pub conditions: Option<AccessCondition>,
21    /// Optional resource-specific attributes (as key-value pairs)
22    pub attributes: Vec<(String, String)>,
23}
24
25impl Permission {
26    /// Create a new permission
27    pub fn new(resource: impl Into<String>, action: impl Into<String>) -> Self {
28        Self {
29            resource: resource.into(),
30            action: action.into(),
31            conditions: None,
32            attributes: Vec::new(),
33        }
34    }
35
36    /// Add a condition to this permission
37    pub fn with_condition(mut self, condition: AccessCondition) -> Self {
38        self.conditions = Some(condition);
39        self
40    }
41
42    /// Add an attribute to this permission
43    pub fn with_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
44        self.attributes.push((key.into(), value.into()));
45        self
46    }
47
48    /// Check if this permission matches a requested permission
49    pub fn matches(&self, requested: &Permission, context: &AccessContext) -> bool {
50        // Resource and action must match
51        if self.resource != requested.resource || self.action != requested.action {
52            return false;
53        }
54
55        // Check conditions if present
56        if let Some(condition) = &self.conditions {
57            return condition.evaluate(context);
58        }
59
60        true
61    }
62}
63
64/// Access conditions for dynamic permission evaluation
65#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
66pub enum AccessCondition {
67    /// Time-based access (only allow during certain hours)
68    TimeRange {
69        start_hour: u8,
70        end_hour: u8,
71        timezone: String,
72    },
73    /// Location-based access
74    IpWhitelist(Vec<String>),
75    /// User attribute condition
76    UserAttribute {
77        attribute: String,
78        value: String,
79        operator: ComparisonOperator,
80    },
81    /// Resource attribute condition
82    ResourceAttribute {
83        attribute: String,
84        value: String,
85        operator: ComparisonOperator,
86    },
87    /// Combine multiple conditions
88    And(Vec<AccessCondition>),
89    Or(Vec<AccessCondition>),
90    Not(Box<AccessCondition>),
91}
92
93#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
94pub enum ComparisonOperator {
95    Equals,
96    NotEquals,
97    GreaterThan,
98    LessThan,
99    Contains,
100    StartsWith,
101    EndsWith,
102}
103
104impl AccessCondition {
105    /// Evaluate the condition against the given context
106    pub fn evaluate(&self, context: &AccessContext) -> bool {
107        match self {
108            AccessCondition::TimeRange {
109                start_hour,
110                end_hour,
111                timezone: _,
112            } => {
113                // Simplified time check (in production, handle timezones properly)
114                let now = SystemTime::now()
115                    .duration_since(SystemTime::UNIX_EPOCH)
116                    .unwrap()
117                    .as_secs();
118                let hour = ((now / 3600) % 24) as u8;
119                hour >= *start_hour && hour <= *end_hour
120            }
121            AccessCondition::IpWhitelist(ips) => context
122                .ip_address
123                .as_ref()
124                .map(|ip| ips.contains(ip))
125                .unwrap_or(false),
126            AccessCondition::UserAttribute {
127                attribute,
128                value,
129                operator,
130            } => context
131                .user_attributes
132                .get(attribute)
133                .map(|attr_value| compare_values(attr_value, value, operator))
134                .unwrap_or(false),
135            AccessCondition::ResourceAttribute {
136                attribute,
137                value,
138                operator,
139            } => context
140                .resource_attributes
141                .get(attribute)
142                .map(|attr_value| compare_values(attr_value, value, operator))
143                .unwrap_or(false),
144            AccessCondition::And(conditions) => conditions.iter().all(|c| c.evaluate(context)),
145            AccessCondition::Or(conditions) => conditions.iter().any(|c| c.evaluate(context)),
146            AccessCondition::Not(condition) => !condition.evaluate(context),
147        }
148    }
149}
150
151fn compare_values(left: &str, right: &str, operator: &ComparisonOperator) -> bool {
152    match operator {
153        ComparisonOperator::Equals => left == right,
154        ComparisonOperator::NotEquals => left != right,
155        ComparisonOperator::GreaterThan => left > right,
156        ComparisonOperator::LessThan => left < right,
157        ComparisonOperator::Contains => left.contains(right),
158        ComparisonOperator::StartsWith => left.starts_with(right),
159        ComparisonOperator::EndsWith => left.ends_with(right),
160    }
161}
162
163/// A role groups permissions and can be assigned to users
164#[derive(Debug, Clone, Serialize, Deserialize)]
165pub struct Role {
166    /// Unique role identifier
167    pub id: String,
168    /// Human-readable role name
169    pub name: String,
170    /// Role description
171    pub description: String,
172    /// Permissions granted by this role
173    pub permissions: HashSet<Permission>,
174    /// Parent roles (for hierarchical RBAC)
175    pub parent_roles: HashSet<String>,
176    /// Role metadata
177    pub metadata: HashMap<String, String>,
178    /// When the role was created
179    pub created_at: SystemTime,
180    /// When the role was last modified
181    pub updated_at: SystemTime,
182}
183
184impl Role {
185    /// Create a new role
186    pub fn new(id: impl Into<String>, name: impl Into<String>) -> Self {
187        let now = SystemTime::now();
188        Self {
189            id: id.into(),
190            name: name.into(),
191            description: String::new(),
192            permissions: HashSet::new(),
193            parent_roles: HashSet::new(),
194            metadata: HashMap::new(),
195            created_at: now,
196            updated_at: now,
197        }
198    }
199
200    /// Add a permission to this role
201    pub fn add_permission(&mut self, permission: Permission) {
202        self.permissions.insert(permission);
203        self.updated_at = SystemTime::now();
204    }
205
206    /// Remove a permission from this role
207    pub fn remove_permission(&mut self, permission: &Permission) {
208        self.permissions.remove(permission);
209        self.updated_at = SystemTime::now();
210    }
211
212    /// Add a parent role
213    pub fn add_parent_role(&mut self, role_id: impl Into<String>) {
214        self.parent_roles.insert(role_id.into());
215        self.updated_at = SystemTime::now();
216    }
217
218    /// Check if this role has a specific permission
219    pub fn has_permission(&self, permission: &Permission, context: &AccessContext) -> bool {
220        self.permissions
221            .iter()
222            .any(|p| p.matches(permission, context))
223    }
224}
225
226/// Context information for access control decisions
227#[derive(Debug, Clone)]
228pub struct AccessContext {
229    /// User ID making the request
230    pub user_id: String,
231    /// User attributes (department, level, etc.)
232    pub user_attributes: HashMap<String, String>,
233    /// Resource being accessed
234    pub resource_id: Option<String>,
235    /// Resource attributes
236    pub resource_attributes: HashMap<String, String>,
237    /// Request IP address
238    pub ip_address: Option<String>,
239    /// Request timestamp
240    pub timestamp: SystemTime,
241    /// Additional context data
242    pub metadata: HashMap<String, String>,
243}
244
245impl AccessContext {
246    /// Create a new access context
247    pub fn new(user_id: impl Into<String>) -> Self {
248        Self {
249            user_id: user_id.into(),
250            user_attributes: HashMap::new(),
251            resource_id: None,
252            resource_attributes: HashMap::new(),
253            ip_address: None,
254            timestamp: SystemTime::now(),
255            metadata: HashMap::new(),
256        }
257    }
258
259    /// Add user attribute
260    pub fn with_user_attribute(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
261        self.user_attributes.insert(key.into(), value.into());
262        self
263    }
264
265    /// Set resource information
266    pub fn with_resource(mut self, resource_id: impl Into<String>) -> Self {
267        self.resource_id = Some(resource_id.into());
268        self
269    }
270
271    /// Add resource attribute
272    pub fn with_resource_attribute(
273        mut self,
274        key: impl Into<String>,
275        value: impl Into<String>,
276    ) -> Self {
277        self.resource_attributes.insert(key.into(), value.into());
278        self
279    }
280
281    /// Set IP address
282    pub fn with_ip_address(mut self, ip: impl Into<String>) -> Self {
283        self.ip_address = Some(ip.into());
284        self
285    }
286}
287
288/// User role assignment
289#[derive(Debug, Clone, Serialize, Deserialize)]
290pub struct UserRole {
291    /// User ID
292    pub user_id: String,
293    /// Role ID
294    pub role_id: String,
295    /// When the role was assigned
296    pub assigned_at: SystemTime,
297    /// Optional expiration time
298    pub expires_at: Option<SystemTime>,
299    /// Who assigned the role
300    pub assigned_by: String,
301}
302
303/// Policy evaluation result
304#[derive(Debug, Clone)]
305pub struct AuthorizationResult {
306    /// Whether access is granted
307    pub granted: bool,
308    /// Reason for the decision
309    pub reason: String,
310    /// Applicable permissions
311    pub permissions: Vec<Permission>,
312    /// Policy evaluation time
313    pub evaluation_time: std::time::Duration,
314}
315
316/// Authorization storage trait
317#[async_trait]
318pub trait AuthorizationStorage: Send + Sync {
319    /// Store a role
320    async fn store_role(&self, role: &Role) -> Result<()>;
321
322    /// Get a role by ID
323    async fn get_role(&self, role_id: &str) -> Result<Option<Role>>;
324
325    /// Update a role
326    async fn update_role(&self, role: &Role) -> Result<()>;
327
328    /// Delete a role
329    async fn delete_role(&self, role_id: &str) -> Result<()>;
330
331    /// List all roles
332    async fn list_roles(&self) -> Result<Vec<Role>>;
333
334    /// Assign a role to a user
335    async fn assign_role(&self, user_role: &UserRole) -> Result<()>;
336
337    /// Remove a role from a user
338    async fn remove_role(&self, user_id: &str, role_id: &str) -> Result<()>;
339
340    /// Get user's roles
341    async fn get_user_roles(&self, user_id: &str) -> Result<Vec<UserRole>>;
342
343    /// Get users with a specific role
344    async fn get_role_users(&self, role_id: &str) -> Result<Vec<UserRole>>;
345}
346
347/// Authorization engine for evaluating permissions
348pub struct AuthorizationEngine<S: AuthorizationStorage> {
349    storage: S,
350    role_cache: std::sync::RwLock<HashMap<String, Role>>,
351}
352
353impl<S: AuthorizationStorage> AuthorizationEngine<S> {
354    /// Create a new authorization engine
355    pub fn new(storage: S) -> Self {
356        Self {
357            storage,
358            role_cache: std::sync::RwLock::new(HashMap::new()),
359        }
360    }
361
362    /// Check if a user has permission to perform an action
363    pub async fn check_permission(
364        &self,
365        user_id: &str,
366        permission: &Permission,
367        context: &AccessContext,
368    ) -> Result<AuthorizationResult> {
369        let start_time = std::time::Instant::now();
370
371        // Get user's roles
372        let user_roles = self.storage.get_user_roles(user_id).await?;
373
374        let mut applicable_permissions = Vec::new();
375        let mut granted = false;
376        let mut reason = "No matching permissions found".to_string();
377
378        for user_role in user_roles {
379            // Check if role assignment is still valid
380            if let Some(expires_at) = user_role.expires_at
381                && SystemTime::now() > expires_at
382            {
383                continue;
384            }
385
386            // Get role permissions (including inherited)
387            let role_permissions = self.get_role_permissions(&user_role.role_id).await?;
388
389            for role_permission in role_permissions {
390                if role_permission.matches(permission, context) {
391                    applicable_permissions.push(role_permission);
392                    granted = true;
393                    reason = format!("Permission granted via role: {}", user_role.role_id);
394                    break;
395                }
396            }
397
398            if granted {
399                break;
400            }
401        }
402
403        let evaluation_time = start_time.elapsed();
404
405        Ok(AuthorizationResult {
406            granted,
407            reason,
408            permissions: applicable_permissions,
409            evaluation_time,
410        })
411    }
412
413    /// Get all permissions for a role (including inherited permissions)
414    async fn get_role_permissions(&self, role_id: &str) -> Result<Vec<Permission>> {
415        let mut all_permissions = Vec::new();
416        let mut visited_roles = HashSet::new();
417
418        self.collect_role_permissions(role_id, &mut all_permissions, &mut visited_roles)
419            .await?;
420
421        Ok(all_permissions)
422    }
423
424    /// Recursively collect permissions from role hierarchy
425    fn collect_role_permissions<'a>(
426        &'a self,
427        role_id: &'a str,
428        permissions: &'a mut Vec<Permission>,
429        visited: &'a mut HashSet<String>,
430    ) -> std::pin::Pin<Box<dyn std::future::Future<Output = Result<()>> + Send + 'a>> {
431        Box::pin(async move {
432            // Prevent infinite loops
433            if visited.contains(role_id) {
434                return Ok(());
435            }
436            visited.insert(role_id.to_string());
437
438            // Get role from cache or storage
439            let role = match self.get_cached_role(role_id).await? {
440                Some(role) => role,
441                None => return Ok(()),
442            };
443
444            // Add role's direct permissions
445            permissions.extend(role.permissions.iter().cloned());
446
447            // Recursively collect from parent roles
448            for parent_role_id in &role.parent_roles {
449                self.collect_role_permissions(parent_role_id, permissions, visited)
450                    .await?;
451            }
452
453            Ok(())
454        })
455    }
456
457    /// Get role from cache or storage
458    async fn get_cached_role(&self, role_id: &str) -> Result<Option<Role>> {
459        // Check cache first
460        {
461            let cache = self
462                .role_cache
463                .read()
464                .map_err(|_| AuthError::internal("Failed to acquire role cache lock"))?;
465            if let Some(role) = cache.get(role_id) {
466                return Ok(Some(role.clone()));
467            }
468        }
469
470        // Load from storage
471        if let Some(role) = self.storage.get_role(role_id).await? {
472            // Update cache
473            {
474                let mut cache = self
475                    .role_cache
476                    .write()
477                    .map_err(|_| AuthError::internal("Failed to acquire role cache lock"))?;
478                cache.insert(role_id.to_string(), role.clone());
479            }
480            Ok(Some(role))
481        } else {
482            Ok(None)
483        }
484    }
485
486    /// Invalidate role cache
487    pub fn invalidate_role_cache(&self, role_id: &str) -> Result<()> {
488        let mut cache = self
489            .role_cache
490            .write()
491            .map_err(|_| AuthError::internal("Failed to acquire role cache lock"))?;
492        cache.remove(role_id);
493        Ok(())
494    }
495
496    /// Create a new role
497    pub async fn create_role(&self, role: Role) -> Result<()> {
498        self.storage.store_role(&role).await?;
499        self.invalidate_role_cache(&role.id)?;
500        Ok(())
501    }
502
503    /// Assign a role to a user
504    pub async fn assign_role(&self, user_id: &str, role_id: &str, assigned_by: &str) -> Result<()> {
505        // Verify role exists
506        if self.storage.get_role(role_id).await?.is_none() {
507            return Err(AuthError::validation(format!(
508                "Role '{}' does not exist",
509                role_id
510            )));
511        }
512
513        let user_role = UserRole {
514            user_id: user_id.to_string(),
515            role_id: role_id.to_string(),
516            assigned_at: SystemTime::now(),
517            expires_at: None,
518            assigned_by: assigned_by.to_string(),
519        };
520
521        self.storage.assign_role(&user_role).await
522    }
523
524    /// Check if user has any of the specified roles
525    pub async fn has_any_role(&self, user_id: &str, role_ids: &[String]) -> Result<bool> {
526        let user_roles = self.storage.get_user_roles(user_id).await?;
527        Ok(user_roles.iter().any(|ur| role_ids.contains(&ur.role_id)))
528    }
529}
530
531/// Predefined permissions for common operations
532pub struct CommonPermissions;
533
534impl CommonPermissions {
535    /// User management permissions
536    pub fn user_read() -> Permission {
537        Permission::new("users", "read")
538    }
539
540    pub fn user_write() -> Permission {
541        Permission::new("users", "write")
542    }
543
544    pub fn user_delete() -> Permission {
545        Permission::new("users", "delete")
546    }
547
548    pub fn user_admin() -> Permission {
549        Permission::new("users", "admin")
550    }
551
552    /// Document management permissions
553    pub fn document_read() -> Permission {
554        Permission::new("documents", "read")
555    }
556
557    pub fn document_write() -> Permission {
558        Permission::new("documents", "write")
559    }
560
561    pub fn document_delete() -> Permission {
562        Permission::new("documents", "delete")
563    }
564
565    /// API access permissions
566    pub fn api_read() -> Permission {
567        Permission::new("api", "read")
568    }
569
570    pub fn api_write() -> Permission {
571        Permission::new("api", "write")
572    }
573
574    /// System administration permissions
575    pub fn system_admin() -> Permission {
576        Permission::new("system", "admin")
577    }
578}
579
580#[cfg(test)]
581mod tests {
582    use super::*;
583
584    #[test]
585    fn test_permission_matching() {
586        let context = AccessContext::new("user123");
587
588        let permission = Permission::new("users", "read");
589        let requested = Permission::new("users", "read");
590
591        assert!(permission.matches(&requested, &context));
592
593        let different_action = Permission::new("users", "write");
594        assert!(!permission.matches(&different_action, &context));
595    }
596
597    #[test]
598    fn test_access_condition_evaluation() {
599        let mut context = AccessContext::new("user123");
600        context
601            .user_attributes
602            .insert("department".to_string(), "engineering".to_string());
603
604        let condition = AccessCondition::UserAttribute {
605            attribute: "department".to_string(),
606            value: "engineering".to_string(),
607            operator: ComparisonOperator::Equals,
608        };
609
610        assert!(condition.evaluate(&context));
611
612        let wrong_condition = AccessCondition::UserAttribute {
613            attribute: "department".to_string(),
614            value: "sales".to_string(),
615            operator: ComparisonOperator::Equals,
616        };
617
618        assert!(!wrong_condition.evaluate(&context));
619    }
620
621    #[test]
622    fn test_role_hierarchy() {
623        let mut admin_role = Role::new("admin", "Administrator");
624        admin_role.add_permission(CommonPermissions::system_admin());
625
626        let mut manager_role = Role::new("manager", "Manager");
627        manager_role.add_permission(CommonPermissions::user_write());
628        manager_role.add_parent_role("admin");
629
630        let context = AccessContext::new("user123");
631
632        // Manager should have user_write permission
633        assert!(manager_role.has_permission(&CommonPermissions::user_write(), &context));
634
635        // But not system_admin (would need to check parent role)
636        assert!(!manager_role.has_permission(&CommonPermissions::system_admin(), &context));
637    }
638}
639
640