role_system/
context_integration.rs

1//! Contextual permission checking with external authentication contexts.
2
3use crate::{
4    auth_context::AuthenticationContext,
5    core::RoleSystem,
6    error::Result,
7    resource::Resource,
8    subject::Subject,
9    storage::Storage,
10};
11use std::collections::HashMap;
12
13// Extension methods for RoleSystem when working with AuthenticationContext
14pub trait ContextualPermissions<T: AuthenticationContext> {
15    /// Check permission using an authentication context
16    fn check_contextual_permission(
17        &self,
18        context: &T,
19        action: &str,
20        resource: &Resource,
21        additional_context: Option<HashMap<String, String>>,
22    ) -> Result<bool>;
23    
24    /// Check permission against a list of required scopes
25    fn check_scope_permission(
26        &self,
27        context: &T,
28        required_scopes: &[String],
29    ) -> Result<bool>;
30}
31
32impl<S, T> ContextualPermissions<T> for RoleSystem<S>
33where
34    S: Storage,
35    T: AuthenticationContext,
36{
37    /// Check permission using an authentication context.
38    /// 
39    /// This method combines role-based permissions with any scopes/permissions
40    /// granted directly by the authentication context (e.g., JWT token scopes).
41    /// 
42    /// # Arguments
43    /// 
44    /// * `context` - The authentication context to use
45    /// * `action` - The action to check permission for
46    /// * `resource` - The resource to check permission for
47    /// * `additional_context` - Additional context values for conditional permissions
48    /// 
49    /// # Returns
50    /// 
51    /// `true` if permission is granted, `false` otherwise
52    fn check_contextual_permission(
53        &self,
54        context: &T,
55        action: &str,
56        resource: &Resource,
57        _additional_context: Option<HashMap<String, String>>, // Prefixed with _ to mark as intentionally unused
58    ) -> Result<bool> {
59        // 1. Check if context is valid
60        if !context.is_valid() {
61            #[cfg(feature = "audit")]
62            log::warn!(
63                "Permission check denied: invalid authentication context for action '{}' on resource '{}'",
64                action,
65                resource.id()
66            );
67            
68            return Ok(false);
69        }
70        
71        // 2. Check if the context grants the permission directly through scopes
72        let permission_string = format!("{}:{}", action, resource.resource_type());
73        let instance_permission_string = format!("{}:{}:{}", action, resource.resource_type(), resource.id());
74        
75        let granted_scopes = context.get_granted_scopes();
76        let has_scope = granted_scopes.iter().any(|scope| {
77            scope == &permission_string || 
78            scope == &instance_permission_string || 
79            scope == "*:*" ||
80            scope == &format!("*:{}", resource.resource_type()) ||
81            scope == &format!("{}:*", action)
82        });
83        
84        if has_scope {
85            #[cfg(feature = "audit")]
86            log::info!(
87                "Permission granted via authentication context scope for action '{}' on resource '{}'",
88                action,
89                resource.id()
90            );
91            
92            return Ok(true);
93        }
94        
95        // 3. Check role-based permissions for the user from the context
96        let subject = Subject::user(context.get_user_id().as_ref() as &str);
97        
98        // Note: We're not using the additional_context or context data
99        // from the authentication context for now
100        
101        // Delegate to the standard permission check
102        self.check_permission(&subject, action, resource)
103    }
104    
105    /// Check permission against a list of required scopes.
106    /// 
107    /// This is useful for API endpoints that require specific scopes.
108    /// 
109    /// # Arguments
110    /// 
111    /// * `context` - The authentication context to use
112    /// * `required_scopes` - List of required scopes (any match grants permission)
113    /// 
114    /// # Returns
115    /// 
116    /// `true` if any required scope is granted, `false` otherwise
117    fn check_scope_permission(
118        &self,
119        context: &T,
120        required_scopes: &[String],
121    ) -> Result<bool> {
122        // Check if context is valid
123        if !context.is_valid() {
124            return Ok(false);
125        }
126        
127        let granted_scopes = context.get_granted_scopes();
128        
129        // Check if any required scope is present in granted scopes
130        for required in required_scopes {
131            if granted_scopes.contains(required) {
132                return Ok(true);
133            }
134            
135            // Check for wildcard scopes
136            if required.contains(':') {
137                // Split into parts (e.g., "read:documents")
138                let parts: Vec<&str> = required.split(':').collect();
139                
140                // Check if wildcards cover this scope
141                if granted_scopes.contains(&"*:*".to_string()) {
142                    return Ok(true);
143                }
144                
145                if parts.len() >= 2 {
146                    let action = parts[0];
147                    let resource = parts[1];
148                    
149                    // Check action wildcards (e.g., "*:documents")
150                    if granted_scopes.contains(&format!("*:{}", resource)) {
151                        return Ok(true);
152                    }
153                    
154                    // Check resource wildcards (e.g., "read:*")
155                    if granted_scopes.contains(&format!("{}:*", action)) {
156                        return Ok(true);
157                    }
158                }
159            }
160        }
161        
162        Ok(false)
163    }
164}
165
166#[cfg(test)]
167mod tests {
168    use super::*;
169    use crate::auth_context::JwtContext;
170    use crate::core::RoleSystem;
171    use crate::permission::Permission;
172    use crate::role::Role;
173    use std::collections::HashMap;
174    
175    #[test]
176    fn test_jwt_context_permissions() {
177        let mut role_system = RoleSystem::new();
178        
179        // Create and register roles
180        let editor = Role::new("editor")
181            .add_permission(Permission::new("edit", "documents"))
182            .add_permission(Permission::new("read", "documents"));
183        
184        role_system.register_role(editor).unwrap();
185        
186        // Create a simple HashMap for the payload instead of using serde_json
187        let mut payload = HashMap::new();
188        payload.insert("exp".to_string(), "1625097600".to_string());
189        payload.insert("iat".to_string(), "1625011200".to_string());
190        
191        // Create JWT context with direct scope grants
192        let context = JwtContext::new(
193            "user123".to_string(),
194            vec!["read:documents".to_string()],
195            true,
196            payload
197        );
198        
199        // Test document resource
200        let document = Resource::new("doc1", "documents");
201        
202        // Direct scope permission should be granted
203        assert!(role_system.check_contextual_permission(&context, "read", &document, None).unwrap());
204        
205        // Permission not in token scopes should be denied (since user has no roles)
206        assert!(!role_system.check_contextual_permission(&context, "edit", &document, None).unwrap());
207        
208        // Now assign the editor role to the user
209        let subject = Subject::user(context.get_user_id().as_ref() as &str);
210        role_system.assign_role(&subject, "editor").unwrap();
211        
212        // Now edit permission should be granted via role
213        assert!(role_system.check_contextual_permission(&context, "edit", &document, None).unwrap());
214    }
215    
216    #[test]
217    fn test_scope_permission() {
218        let role_system = RoleSystem::new();
219        
220        // Create a simple HashMap for the payload
221        let payload = HashMap::new();
222        
223        // Create JWT context with scopes
224        let context = JwtContext::new(
225            "user123".to_string(),
226            vec!["read:documents".to_string(), "write:posts".to_string()],
227            true,
228            payload
229        );
230        
231        // Check individual scope permissions
232        assert!(role_system.check_scope_permission(&context, &["read:documents".to_string()]).unwrap());
233        assert!(role_system.check_scope_permission(&context, &["write:posts".to_string()]).unwrap());
234        assert!(!role_system.check_scope_permission(&context, &["admin:users".to_string()]).unwrap());
235        
236        // Check multiple required scopes (any match should succeed)
237        assert!(role_system.check_scope_permission(
238            &context, 
239            &["read:documents".to_string(), "admin:users".to_string()]
240        ).unwrap());
241        
242        // Check with wildcard
243        let admin_context = JwtContext::new(
244            "admin".to_string(),
245            vec!["*:*".to_string()],
246            true,
247            HashMap::new()
248        );
249        
250        assert!(role_system.check_scope_permission(&admin_context, &["read:any".to_string()]).unwrap());
251        assert!(role_system.check_scope_permission(&admin_context, &["admin:users".to_string()]).unwrap());
252    }
253}