Skip to main content

elif_auth/middleware/
guards.rs

1//! Authentication guards for HTTP requests
2//!
3//! Provides authentication middleware that can work with any auth provider
4
5use std::collections::{HashMap, HashSet};
6
7use crate::{AuthError, AuthResult, UserContext};
8
9/// Resource-action pair for RBAC permissions
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct ResourceAction {
12    /// Resource identifier (e.g., "users", "articles")
13    pub resource: String,
14
15    /// Action identifier (e.g., "create", "read", "update", "delete")
16    pub action: String,
17}
18
19impl ResourceAction {
20    /// Create a new resource-action pair
21    pub fn new(resource: impl Into<String>, action: impl Into<String>) -> Self {
22        Self {
23            resource: resource.into(),
24            action: action.into(),
25        }
26    }
27}
28
29/// Authentication guard configuration
30#[derive(Debug, Clone)]
31pub struct AuthGuardConfig {
32    /// Paths that skip authentication
33    pub skip_paths: HashSet<String>,
34
35    /// Whether authentication is optional
36    pub optional: bool,
37
38    /// Required roles (any of these roles grants access)
39    pub required_roles: Vec<String>,
40
41    /// Required permissions (any of these permissions grants access)
42    pub required_permissions: Vec<String>,
43
44    /// Whether to require all roles (true) or any role (false)
45    pub require_all_roles: bool,
46
47    /// Whether to require all permissions (true) or any permission (false)
48    pub require_all_permissions: bool,
49
50    /// Required resource-action permissions for RBAC
51    pub required_resource_actions: Vec<ResourceAction>,
52
53    /// Authorization context for conditional permissions
54    pub auth_context: HashMap<String, serde_json::Value>,
55}
56
57impl Default for AuthGuardConfig {
58    fn default() -> Self {
59        Self {
60            skip_paths: ["/health", "/metrics"]
61                .iter()
62                .map(|s| s.to_string())
63                .collect(),
64            optional: false,
65            required_roles: Vec::new(),
66            required_permissions: Vec::new(),
67            require_all_roles: false,
68            require_all_permissions: false,
69            required_resource_actions: Vec::new(),
70            auth_context: HashMap::new(),
71        }
72    }
73}
74
75/// Base authentication guard trait
76pub trait AuthGuard {
77    /// Check if the path should skip authentication
78    fn should_skip_path(&self, path: &str) -> bool;
79
80    /// Check if authentication is optional
81    fn is_optional(&self) -> bool;
82
83    /// Validate user context against guard requirements
84    fn validate_user(&self, user: &UserContext) -> AuthResult<()>;
85
86    /// Validate user context with RBAC resource-action permissions
87    fn validate_user_with_rbac(&self, user: &UserContext) -> AuthResult<()> {
88        // First run standard validation
89        self.validate_user(user)?;
90
91        // Then validate resource-action permissions
92        if !self.config().required_resource_actions.is_empty() {
93            self.validate_resource_actions(user)?;
94        }
95
96        Ok(())
97    }
98
99    /// Validate resource-action permissions
100    fn validate_resource_actions(&self, user: &UserContext) -> AuthResult<()> {
101        for resource_action in &self.config().required_resource_actions {
102            let permission_key = format!("{}.{}", resource_action.resource, resource_action.action);
103
104            if !user.permissions.contains(&permission_key) {
105                return Err(AuthError::insufficient_permissions(format!(
106                    "User lacks permission for {}.{}",
107                    resource_action.resource, resource_action.action
108                )));
109            }
110        }
111
112        Ok(())
113    }
114
115    /// Get guard configuration
116    fn config(&self) -> &AuthGuardConfig;
117}
118
119/// Required authentication guard - authentication must succeed
120#[derive(Debug, Clone)]
121pub struct RequireAuth {
122    config: AuthGuardConfig,
123}
124
125impl RequireAuth {
126    /// Create a new required authentication guard
127    pub fn new() -> Self {
128        Self {
129            config: AuthGuardConfig::default(),
130        }
131    }
132
133    /// Create with custom configuration
134    pub fn with_config(config: AuthGuardConfig) -> Self {
135        Self { config }
136    }
137
138    /// Add a path to skip authentication
139    pub fn skip_path<S: Into<String>>(mut self, path: S) -> Self {
140        self.config.skip_paths.insert(path.into());
141        self
142    }
143
144    /// Add multiple paths to skip authentication
145    pub fn skip_paths<I, S>(mut self, paths: I) -> Self
146    where
147        I: IntoIterator<Item = S>,
148        S: Into<String>,
149    {
150        for path in paths {
151            self.config.skip_paths.insert(path.into());
152        }
153        self
154    }
155
156    /// Require specific role
157    pub fn require_role<S: Into<String>>(mut self, role: S) -> Self {
158        self.config.required_roles.push(role.into());
159        self
160    }
161
162    /// Require specific roles
163    pub fn require_roles<I, S>(mut self, roles: I) -> Self
164    where
165        I: IntoIterator<Item = S>,
166        S: Into<String>,
167    {
168        self.config
169            .required_roles
170            .extend(roles.into_iter().map(|r| r.into()));
171        self
172    }
173
174    /// Require specific permission
175    pub fn require_permission<S: Into<String>>(mut self, permission: S) -> Self {
176        self.config.required_permissions.push(permission.into());
177        self
178    }
179
180    /// Require specific permissions
181    pub fn require_permissions<I, S>(mut self, permissions: I) -> Self
182    where
183        I: IntoIterator<Item = S>,
184        S: Into<String>,
185    {
186        self.config
187            .required_permissions
188            .extend(permissions.into_iter().map(|p| p.into()));
189        self
190    }
191
192    /// Require ALL specified roles instead of ANY
193    pub fn require_all_roles(mut self) -> Self {
194        self.config.require_all_roles = true;
195        self
196    }
197
198    /// Require ALL specified permissions instead of ANY
199    pub fn require_all_permissions(mut self) -> Self {
200        self.config.require_all_permissions = true;
201        self
202    }
203
204    /// Require specific resource-action permission
205    pub fn require_resource_action(
206        mut self,
207        resource: impl Into<String>,
208        action: impl Into<String>,
209    ) -> Self {
210        self.config
211            .required_resource_actions
212            .push(ResourceAction::new(resource, action));
213        self
214    }
215
216    /// Require multiple resource-action permissions
217    pub fn require_resource_actions<I>(mut self, resource_actions: I) -> Self
218    where
219        I: IntoIterator<Item = (String, String)>,
220    {
221        for (resource, action) in resource_actions {
222            self.config
223                .required_resource_actions
224                .push(ResourceAction::new(resource, action));
225        }
226        self
227    }
228
229    /// Add context for conditional permissions
230    pub fn with_context<K, V>(mut self, key: K, value: V) -> Self
231    where
232        K: Into<String>,
233        V: Into<serde_json::Value>,
234    {
235        self.config.auth_context.insert(key.into(), value.into());
236        self
237    }
238
239    /// Add multiple context values
240    pub fn with_contexts<I, K, V>(mut self, contexts: I) -> Self
241    where
242        I: IntoIterator<Item = (K, V)>,
243        K: Into<String>,
244        V: Into<serde_json::Value>,
245    {
246        for (key, value) in contexts {
247            self.config.auth_context.insert(key.into(), value.into());
248        }
249        self
250    }
251}
252
253impl Default for RequireAuth {
254    fn default() -> Self {
255        Self::new()
256    }
257}
258
259impl AuthGuard for RequireAuth {
260    fn should_skip_path(&self, path: &str) -> bool {
261        self.config.skip_paths.contains(path)
262    }
263
264    fn is_optional(&self) -> bool {
265        false // Required authentication is never optional
266    }
267
268    fn validate_user(&self, user: &UserContext) -> AuthResult<()> {
269        // Check role requirements
270        if !self.config.required_roles.is_empty() {
271            let has_required_roles = if self.config.require_all_roles {
272                // Check if user has ALL required roles
273                self.config
274                    .required_roles
275                    .iter()
276                    .all(|role| user.roles.contains(role))
277            } else {
278                // Check if user has ANY required role
279                self.config
280                    .required_roles
281                    .iter()
282                    .any(|role| user.roles.contains(role))
283            };
284
285            if !has_required_roles {
286                return Err(AuthError::insufficient_permissions(format!(
287                    "User lacks required roles: {:?}",
288                    self.config.required_roles
289                )));
290            }
291        }
292
293        // Check permission requirements
294        if !self.config.required_permissions.is_empty() {
295            let has_required_permissions = if self.config.require_all_permissions {
296                // Check if user has ALL required permissions
297                self.config
298                    .required_permissions
299                    .iter()
300                    .all(|perm| user.permissions.contains(perm))
301            } else {
302                // Check if user has ANY required permission
303                self.config
304                    .required_permissions
305                    .iter()
306                    .any(|perm| user.permissions.contains(perm))
307            };
308
309            if !has_required_permissions {
310                return Err(AuthError::insufficient_permissions(format!(
311                    "User lacks required permissions: {:?}",
312                    self.config.required_permissions
313                )));
314            }
315        }
316
317        Ok(())
318    }
319
320    fn config(&self) -> &AuthGuardConfig {
321        &self.config
322    }
323}
324
325/// Optional authentication guard - authentication failure is allowed
326#[derive(Debug, Clone)]
327pub struct OptionalAuth {
328    config: AuthGuardConfig,
329}
330
331impl OptionalAuth {
332    /// Create a new optional authentication guard
333    pub fn new() -> Self {
334        let mut config = AuthGuardConfig::default();
335        config.optional = true;
336
337        Self { config }
338    }
339
340    /// Create with custom configuration
341    pub fn with_config(mut config: AuthGuardConfig) -> Self {
342        config.optional = true;
343        Self { config }
344    }
345
346    /// Add a path to skip authentication
347    pub fn skip_path<S: Into<String>>(mut self, path: S) -> Self {
348        self.config.skip_paths.insert(path.into());
349        self
350    }
351
352    /// Add multiple paths to skip authentication
353    pub fn skip_paths<I, S>(mut self, paths: I) -> Self
354    where
355        I: IntoIterator<Item = S>,
356        S: Into<String>,
357    {
358        for path in paths {
359            self.config.skip_paths.insert(path.into());
360        }
361        self
362    }
363}
364
365impl Default for OptionalAuth {
366    fn default() -> Self {
367        Self::new()
368    }
369}
370
371impl AuthGuard for OptionalAuth {
372    fn should_skip_path(&self, path: &str) -> bool {
373        self.config.skip_paths.contains(path)
374    }
375
376    fn is_optional(&self) -> bool {
377        true // Optional authentication allows failures
378    }
379
380    fn validate_user(&self, _user: &UserContext) -> AuthResult<()> {
381        // Optional auth doesn't validate - any authenticated user is allowed
382        Ok(())
383    }
384
385    fn config(&self) -> &AuthGuardConfig {
386        &self.config
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use super::*;
393    use chrono::Utc;
394    use std::collections::HashMap;
395
396    fn create_test_user() -> UserContext {
397        UserContext {
398            user_id: "123".to_string(),
399            username: "test@example.com".to_string(),
400            roles: vec!["user".to_string(), "moderator".to_string()],
401            permissions: vec!["read".to_string(), "write".to_string()],
402            auth_provider: "test".to_string(),
403            authenticated_at: Utc::now(),
404            expires_at: None,
405            additional_data: HashMap::new(),
406        }
407    }
408
409    #[test]
410    fn test_require_auth_creation() {
411        let guard = RequireAuth::new();
412        assert!(!guard.is_optional());
413        assert!(guard.should_skip_path("/health"));
414        assert!(guard.should_skip_path("/metrics"));
415        assert!(!guard.should_skip_path("/protected"));
416    }
417
418    #[test]
419    fn test_require_auth_custom_skip_paths() {
420        let guard = RequireAuth::new()
421            .skip_path("/public")
422            .skip_paths(["/docs", "/swagger"]);
423
424        assert!(guard.should_skip_path("/public"));
425        assert!(guard.should_skip_path("/docs"));
426        assert!(guard.should_skip_path("/swagger"));
427        assert!(!guard.should_skip_path("/private"));
428    }
429
430    #[test]
431    fn test_require_auth_role_validation_any() {
432        let user = create_test_user();
433
434        // Should pass - user has 'user' role
435        let guard = RequireAuth::new().require_role("user");
436        assert!(guard.validate_user(&user).is_ok());
437
438        // Should pass - user has 'moderator' role
439        let guard = RequireAuth::new().require_role("moderator");
440        assert!(guard.validate_user(&user).is_ok());
441
442        // Should fail - user doesn't have 'admin' role
443        let guard = RequireAuth::new().require_role("admin");
444        assert!(guard.validate_user(&user).is_err());
445
446        // Should pass - user has one of the required roles
447        let guard = RequireAuth::new().require_roles(["admin", "moderator"]);
448        assert!(guard.validate_user(&user).is_ok());
449    }
450
451    #[test]
452    fn test_require_auth_role_validation_all() {
453        let user = create_test_user();
454
455        // Should pass - user has both required roles
456        let guard = RequireAuth::new()
457            .require_roles(["user", "moderator"])
458            .require_all_roles();
459        assert!(guard.validate_user(&user).is_ok());
460
461        // Should fail - user doesn't have 'admin' role
462        let guard = RequireAuth::new()
463            .require_roles(["user", "admin"])
464            .require_all_roles();
465        assert!(guard.validate_user(&user).is_err());
466    }
467
468    #[test]
469    fn test_require_auth_permission_validation_any() {
470        let user = create_test_user();
471
472        // Should pass - user has 'read' permission
473        let guard = RequireAuth::new().require_permission("read");
474        assert!(guard.validate_user(&user).is_ok());
475
476        // Should fail - user doesn't have 'delete' permission
477        let guard = RequireAuth::new().require_permission("delete");
478        assert!(guard.validate_user(&user).is_err());
479
480        // Should pass - user has one of the required permissions
481        let guard = RequireAuth::new().require_permissions(["delete", "write"]);
482        assert!(guard.validate_user(&user).is_ok());
483    }
484
485    #[test]
486    fn test_require_auth_permission_validation_all() {
487        let user = create_test_user();
488
489        // Should pass - user has both required permissions
490        let guard = RequireAuth::new()
491            .require_permissions(["read", "write"])
492            .require_all_permissions();
493        assert!(guard.validate_user(&user).is_ok());
494
495        // Should fail - user doesn't have 'delete' permission
496        let guard = RequireAuth::new()
497            .require_permissions(["read", "delete"])
498            .require_all_permissions();
499        assert!(guard.validate_user(&user).is_err());
500    }
501
502    #[test]
503    fn test_optional_auth_creation() {
504        let guard = OptionalAuth::new();
505        assert!(guard.is_optional());
506        assert!(guard.should_skip_path("/health"));
507        assert!(guard.should_skip_path("/metrics"));
508    }
509
510    #[test]
511    fn test_optional_auth_validation() {
512        let user = create_test_user();
513        let guard = OptionalAuth::new();
514
515        // Optional auth always passes validation
516        assert!(guard.validate_user(&user).is_ok());
517    }
518
519    #[test]
520    fn test_optional_auth_custom_skip_paths() {
521        let guard = OptionalAuth::new()
522            .skip_path("/api")
523            .skip_paths(["/v1", "/v2"]);
524
525        assert!(guard.should_skip_path("/api"));
526        assert!(guard.should_skip_path("/v1"));
527        assert!(guard.should_skip_path("/v2"));
528        assert!(!guard.should_skip_path("/protected"));
529    }
530
531    #[test]
532    fn test_resource_action_creation() {
533        let resource_action = ResourceAction::new("users", "create");
534        assert_eq!(resource_action.resource, "users");
535        assert_eq!(resource_action.action, "create");
536    }
537
538    #[test]
539    fn test_require_auth_rbac_resource_actions() {
540        let mut user = create_test_user();
541        user.permissions = vec![
542            "users.create".to_string(),
543            "articles.read".to_string(),
544            "articles.edit".to_string(),
545        ];
546
547        // Should pass - user has users.create permission
548        let guard = RequireAuth::new().require_resource_action("users", "create");
549        assert!(guard.validate_user_with_rbac(&user).is_ok());
550
551        // Should fail - user doesn't have users.delete permission
552        let guard = RequireAuth::new().require_resource_action("users", "delete");
553        assert!(guard.validate_user_with_rbac(&user).is_err());
554
555        // Should pass - user has both required permissions
556        let guard = RequireAuth::new().require_resource_actions([
557            ("users".to_string(), "create".to_string()),
558            ("articles".to_string(), "read".to_string()),
559        ]);
560        assert!(guard.validate_user_with_rbac(&user).is_ok());
561
562        // Should fail - user doesn't have users.delete permission
563        let guard = RequireAuth::new().require_resource_actions([
564            ("users".to_string(), "create".to_string()),
565            ("users".to_string(), "delete".to_string()),
566        ]);
567        assert!(guard.validate_user_with_rbac(&user).is_err());
568    }
569
570    #[test]
571    fn test_require_auth_with_context() {
572        let guard = RequireAuth::new()
573            .with_context("owner", "user123")
574            .with_contexts([
575                ("department".to_string(), serde_json::json!("engineering")),
576                ("role_level".to_string(), serde_json::json!(5)),
577            ]);
578
579        assert_eq!(guard.config().auth_context.get("owner").unwrap(), "user123");
580        assert_eq!(
581            guard.config().auth_context.get("department").unwrap(),
582            "engineering"
583        );
584        assert_eq!(
585            guard.config().auth_context.get("role_level").unwrap(),
586            &serde_json::json!(5)
587        );
588    }
589
590    #[test]
591    fn test_validate_resource_actions() {
592        let mut user = create_test_user();
593        user.permissions = vec!["users.create".to_string(), "articles.read".to_string()];
594
595        let guard = RequireAuth::new()
596            .require_resource_action("users", "create")
597            .require_resource_action("articles", "read");
598
599        // Should pass validation
600        assert!(guard.validate_resource_actions(&user).is_ok());
601
602        // Add a permission the user doesn't have
603        let guard = RequireAuth::new()
604            .require_resource_action("users", "create")
605            .require_resource_action("users", "delete");
606
607        // Should fail validation
608        assert!(guard.validate_resource_actions(&user).is_err());
609    }
610}