Skip to main content

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