Skip to main content

mockforge_ui/
rbac.rs

1//! RBAC (Role-Based Access Control) middleware and permission enforcement
2//!
3//! This module provides middleware for enforcing role-based access control
4//! on admin endpoints, ensuring users can only perform actions they're authorized for.
5
6use axum::{
7    extract::Request,
8    http::{HeaderMap, StatusCode},
9    middleware::Next,
10    response::Response,
11};
12use mockforge_collab::models::UserRole;
13use mockforge_collab::permissions::{Permission, RolePermissions};
14use serde::{Deserialize, Serialize};
15
16fn is_truthy_env(name: &str) -> bool {
17    matches!(
18        std::env::var(name).ok().as_deref().map(str::to_ascii_lowercase).as_deref(),
19        Some("1") | Some("true") | Some("yes") | Some("on")
20    )
21}
22
23fn is_development_environment() -> bool {
24    if cfg!(test) {
25        return true;
26    }
27
28    matches!(
29        std::env::var("ENVIRONMENT")
30            .unwrap_or_else(|_| "production".to_string())
31            .to_ascii_lowercase()
32            .as_str(),
33        "development" | "dev" | "local"
34    )
35}
36
37fn is_dev_auth_enabled() -> bool {
38    cfg!(test) || is_truthy_env("MOCKFORGE_ENABLE_DEV_AUTH")
39}
40
41fn allow_unauthenticated_dev_access() -> bool {
42    is_development_environment()
43        && is_dev_auth_enabled()
44        && (is_truthy_env("MOCKFORGE_ALLOW_UNAUTHENTICATED")
45            || is_truthy_env("MOCKFORGE_ALLOW_UNAUTHENTICATED_DEV"))
46}
47
48/// User context extracted from request
49#[derive(Debug, Clone, Serialize, Deserialize)]
50pub struct UserContext {
51    /// User ID
52    pub user_id: String,
53    /// Username
54    pub username: String,
55    /// User role
56    pub role: UserRole,
57    /// User email (optional)
58    pub email: Option<String>,
59}
60
61/// Admin action to permission mapping
62pub struct AdminActionPermissions;
63
64impl AdminActionPermissions {
65    /// Map admin action to required permissions
66    /// Returns a list of permissions (user must have at least one if multiple)
67    pub fn get_required_permissions(action: &str) -> Vec<Permission> {
68        match action {
69            // Configuration changes require ManageSettings
70            "update_latency"
71            | "update_faults"
72            | "update_proxy"
73            | "update_traffic_shaping"
74            | "update_validation" => {
75                vec![Permission::ManageSettings]
76            }
77
78            // Server management requires ManageSettings (admin only)
79            "restart_servers" | "shutdown_servers" => {
80                vec![Permission::ManageSettings]
81            }
82
83            // Log management requires ManageSettings
84            "clear_logs" | "export_logs" => {
85                vec![Permission::ManageSettings]
86            }
87
88            // Fixture management requires MockUpdate/MockDelete
89            "create_fixture" => {
90                vec![Permission::MockCreate]
91            }
92            "update_fixture" | "rename_fixture" | "move_fixture" => {
93                vec![Permission::MockUpdate]
94            }
95            "delete_fixture" | "delete_fixtures_bulk" => {
96                vec![Permission::MockDelete]
97            }
98
99            // Route management requires MockUpdate
100            "enable_route" | "disable_route" | "create_route" | "update_route" | "delete_route" => {
101                vec![Permission::MockUpdate]
102            }
103
104            // Service management requires ManageSettings
105            "enable_service" | "disable_service" | "update_service_config" => {
106                vec![Permission::ManageSettings]
107            }
108
109            // User management requires ChangeRoles
110            "create_user" | "update_user" | "delete_user" | "change_role" => {
111                vec![Permission::ChangeRoles]
112            }
113
114            // Permission management requires ChangeRoles
115            "grant_permission" | "revoke_permission" => {
116                vec![Permission::ChangeRoles]
117            }
118
119            // API key management requires ManageSettings
120            "create_api_key" | "delete_api_key" | "rotate_api_key" => {
121                vec![Permission::ManageSettings]
122            }
123
124            // Security operations require ManageSettings
125            "update_security_policy" => {
126                vec![Permission::ManageSettings]
127            }
128
129            // Read operations require appropriate read permissions
130            "get_dashboard" | "get_logs" | "get_metrics" | "get_routes" | "get_fixtures"
131            | "get_config" => {
132                vec![Permission::WorkspaceRead, Permission::MockRead]
133            }
134
135            // Audit log access requires ManageSettings (sensitive)
136            "get_audit_logs" | "get_audit_stats" => {
137                vec![Permission::ManageSettings]
138            }
139
140            // Scenario-specific permissions
141            // Modify chaos rules - typically QA only
142            "modify_scenario_chaos_rules" | "update_scenario_chaos" => {
143                vec![Permission::ScenarioModifyChaosRules]
144            }
145            // Modify reality defaults - typically Platform team only
146            "modify_scenario_reality_defaults" | "update_scenario_reality" => {
147                vec![Permission::ScenarioModifyRealityDefaults]
148            }
149            // Promote scenarios between environments
150            "promote_scenario" | "create_scenario_promotion" => {
151                vec![Permission::ScenarioPromote]
152            }
153            // Approve scenario promotions
154            "approve_scenario_promotion" | "reject_scenario_promotion" => {
155                vec![Permission::ScenarioApprove]
156            }
157            // Modify drift budgets for scenarios
158            "modify_scenario_drift_budget" | "update_scenario_drift_budget" => {
159                vec![Permission::ScenarioModifyDriftBudgets]
160            }
161
162            // Default: require ManageSettings for unknown actions
163            _ => {
164                vec![Permission::ManageSettings]
165            }
166        }
167    }
168}
169
170/// Extract user context from request headers
171/// Currently supports:
172/// - Authorization: Bearer <token> (JWT with user info)
173/// - X-User-Id, X-Username, X-User-Role headers (for development/testing)
174pub fn extract_user_context(headers: &HeaderMap) -> Option<UserContext> {
175    // Try to extract from Authorization header (JWT)
176    if let Some(auth_header) = headers.get("authorization") {
177        if let Ok(auth_str) = auth_header.to_str() {
178            if let Some(token) = auth_str.strip_prefix("Bearer ") {
179                if let Some(user) = parse_jwt_token(token) {
180                    return Some(user);
181                }
182            }
183        }
184    }
185
186    // Fallback: Extract from custom headers (development/testing only)
187    if is_development_environment() && is_dev_auth_enabled() {
188        let user_id = headers.get("x-user-id")?.to_str().ok()?.to_string();
189        let username = headers.get("x-username")?.to_str().ok()?.to_string();
190        let role_str = headers.get("x-user-role")?.to_str().ok()?;
191        let role = parse_role(role_str)?;
192
193        return Some(UserContext {
194            user_id,
195            username,
196            role,
197            email: headers.get("x-user-email").and_then(|h| h.to_str().ok()).map(|s| s.to_string()),
198        });
199    }
200
201    None
202}
203
204/// Parse JWT token and extract user context
205/// Uses production JWT library (jsonwebtoken)
206fn parse_jwt_token(token: &str) -> Option<UserContext> {
207    use crate::auth::{claims_to_user_context, validate_token};
208
209    // Try to validate as production JWT token
210    if let Ok(claims) = validate_token(token) {
211        return Some(claims_to_user_context(&claims));
212    }
213
214    // Fallback: handle mock tokens from frontend (development/testing only)
215    if is_development_environment() && is_dev_auth_enabled() && token.starts_with("mock.") {
216        let parts: Vec<&str> = token.split('.').collect();
217        if parts.len() >= 3 {
218            // Decode payload (base64url)
219            let payload_part = parts[2];
220            // Replace URL-safe characters for standard base64
221            let base64_str = payload_part.replace('-', "+").replace('_', "/");
222            // Add padding if needed
223            let padding = (4 - (base64_str.len() % 4)) % 4;
224            let padded = format!("{}{}", base64_str, "=".repeat(padding));
225
226            // Decode base64
227            use base64::{engine::general_purpose, Engine as _};
228            if let Ok(decoded) = general_purpose::STANDARD.decode(&padded) {
229                if let Ok(payload_str) = String::from_utf8(decoded) {
230                    return parse_jwt_payload(&payload_str);
231                }
232            }
233        }
234    }
235
236    None
237}
238
239/// Parse JWT payload JSON
240fn parse_jwt_payload(payload_str: &str) -> Option<UserContext> {
241    if let Ok(payload) = serde_json::from_str::<serde_json::Value>(payload_str) {
242        let user_id = payload.get("sub")?.as_str()?.to_string();
243        let username = payload.get("username")?.as_str()?.to_string();
244        let role_str = payload.get("role")?.as_str()?;
245        let role = parse_role(role_str)?;
246
247        return Some(UserContext {
248            user_id,
249            username,
250            role,
251            email: payload.get("email").and_then(|v| v.as_str()).map(|s| s.to_string()),
252        });
253    }
254    None
255}
256
257/// Parse role string to UserRole enum
258fn parse_role(role_str: &str) -> Option<UserRole> {
259    match role_str.to_lowercase().as_str() {
260        "admin" => Some(UserRole::Admin),
261        "editor" => Some(UserRole::Editor),
262        "viewer" => Some(UserRole::Viewer),
263        _ => None,
264    }
265}
266
267/// Default user context for unauthenticated requests (development mode)
268/// In production, this should return None to enforce authentication
269pub fn get_default_user_context() -> Option<UserContext> {
270    // For development: allow unauthenticated access with admin role
271    // In production, this should be disabled
272    if allow_unauthenticated_dev_access() {
273        Some(UserContext {
274            user_id: "system".to_string(),
275            username: "system".to_string(),
276            role: UserRole::Admin,
277            email: None,
278        })
279    } else {
280        None
281    }
282}
283
284/// RBAC middleware to enforce permissions on admin endpoints
285pub async fn rbac_middleware(mut request: Request, next: Next) -> Result<Response, StatusCode> {
286    // Extract action name from request path and HTTP method
287    let path = request.uri().path();
288    let method = request.method().as_str();
289    let headers = request.headers();
290
291    // Skip RBAC for public routes
292    let is_public_route = path == "/"
293        || path.starts_with("/assets/")
294        || path == "/__mockforge/auth/login"
295        || path == "/__mockforge/auth/refresh"
296        || path == "/__mockforge/auth/logout"
297        || path == "/__mockforge/health"
298        || path.starts_with("/mockforge-")
299        || path == "/manifest.json"
300        || path == "/sw.js"
301        || path == "/api-docs";
302
303    if is_public_route {
304        return Ok(next.run(request).await);
305    }
306
307    // Map route to action name
308    let action_name = match (method, path) {
309        (_, p) if p.contains("/config/latency") => "update_latency",
310        (_, p) if p.contains("/config/faults") => "update_faults",
311        (_, p) if p.contains("/config/proxy") => "update_proxy",
312        (_, p) if p.contains("/config/traffic-shaping") => "update_traffic_shaping",
313        ("DELETE", p) if p.contains("/logs") => "clear_logs",
314        ("POST", p) if p.contains("/restart") => "restart_servers",
315        ("DELETE", p) if p.contains("/fixtures") => "delete_fixture",
316        ("POST", p) if p.contains("/fixtures") && p.contains("/rename") => "rename_fixture",
317        ("POST", p) if p.contains("/fixtures") && p.contains("/move") => "move_fixture",
318        ("GET", p) if p.contains("/audit/logs") => "get_audit_logs",
319        ("GET", p) if p.contains("/audit/stats") => "get_audit_stats",
320        ("GET", _) => "read", // Read operations
321        _ => "unknown",
322    };
323
324    // Extract user context from request
325    let user_context = extract_user_context(headers).or_else(get_default_user_context);
326
327    // If no user context and authentication is required, deny access
328    let user_context = match user_context {
329        Some(ctx) => ctx,
330        None => {
331            // For development: allow unauthenticated access if explicitly enabled
332            // In production, this should be disabled
333            if allow_unauthenticated_dev_access() {
334                // Use default admin context for development
335                get_default_user_context().unwrap_or_else(|| UserContext {
336                    user_id: "system".to_string(),
337                    username: "system".to_string(),
338                    role: UserRole::Admin,
339                    email: None,
340                })
341            } else {
342                return Err(StatusCode::UNAUTHORIZED);
343            }
344        }
345    };
346
347    // Get required permissions for this action
348    let required_permissions = AdminActionPermissions::get_required_permissions(action_name);
349
350    // Check if user has at least one of the required permissions
351    let has_permission = required_permissions
352        .iter()
353        .any(|&perm| RolePermissions::has_permission(user_context.role, perm));
354
355    if !has_permission {
356        // Log authorization failure
357        tracing::warn!(
358            user_id = %user_context.user_id,
359            username = %user_context.username,
360            role = ?user_context.role,
361            action = %action_name,
362            "Authorization denied: User does not have required permissions"
363        );
364
365        return Err(StatusCode::FORBIDDEN);
366    }
367
368    // User has permission, continue with request
369    // Store user context in request extensions for use in handlers
370    request.extensions_mut().insert(user_context);
371
372    Ok(next.run(request).await)
373}
374
375/// Helper to extract user context from request extensions
376pub fn get_user_context_from_request(request: &Request) -> Option<UserContext> {
377    request.extensions().get::<UserContext>().cloned()
378}
379
380/// Helper to get user context from axum State (if stored)
381pub fn get_user_context_from_state<T>(_state: &T) -> Option<UserContext>
382where
383    T: std::any::Any,
384{
385    // This is a placeholder - in practice, user context would be stored in request extensions
386    None
387}
388
389#[cfg(test)]
390mod tests {
391    use super::*;
392    use axum::http::{HeaderValue, Method};
393
394    #[test]
395    fn test_parse_role_valid() {
396        assert_eq!(parse_role("admin"), Some(UserRole::Admin));
397        assert_eq!(parse_role("Admin"), Some(UserRole::Admin));
398        assert_eq!(parse_role("ADMIN"), Some(UserRole::Admin));
399        assert_eq!(parse_role("editor"), Some(UserRole::Editor));
400        assert_eq!(parse_role("viewer"), Some(UserRole::Viewer));
401    }
402
403    #[test]
404    fn test_parse_role_invalid() {
405        assert_eq!(parse_role("invalid"), None);
406        assert_eq!(parse_role(""), None);
407        assert_eq!(parse_role("super_admin"), None);
408    }
409
410    #[test]
411    fn test_user_context_serialization() {
412        let context = UserContext {
413            user_id: "user123".to_string(),
414            username: "testuser".to_string(),
415            role: UserRole::Editor,
416            email: Some("test@example.com".to_string()),
417        };
418
419        let serialized = serde_json::to_string(&context).unwrap();
420        let deserialized: UserContext = serde_json::from_str(&serialized).unwrap();
421
422        assert_eq!(deserialized.user_id, context.user_id);
423        assert_eq!(deserialized.username, context.username);
424        assert_eq!(deserialized.role, context.role);
425        assert_eq!(deserialized.email, context.email);
426    }
427
428    #[test]
429    fn test_extract_user_context_from_headers() {
430        let mut headers = HeaderMap::new();
431        headers.insert("x-user-id", HeaderValue::from_static("user123"));
432        headers.insert("x-username", HeaderValue::from_static("testuser"));
433        headers.insert("x-user-role", HeaderValue::from_static("admin"));
434        headers.insert("x-user-email", HeaderValue::from_static("test@example.com"));
435
436        let context = extract_user_context(&headers).unwrap();
437        assert_eq!(context.user_id, "user123");
438        assert_eq!(context.username, "testuser");
439        assert_eq!(context.role, UserRole::Admin);
440        assert_eq!(context.email, Some("test@example.com".to_string()));
441    }
442
443    #[test]
444    fn test_extract_user_context_missing_headers() {
445        let headers = HeaderMap::new();
446        let context = extract_user_context(&headers);
447        assert!(context.is_none());
448    }
449
450    #[test]
451    fn test_extract_user_context_partial_headers() {
452        let mut headers = HeaderMap::new();
453        headers.insert("x-user-id", HeaderValue::from_static("user123"));
454        // Missing username and role
455
456        let context = extract_user_context(&headers);
457        assert!(context.is_none());
458    }
459
460    #[test]
461    fn test_extract_user_context_without_email() {
462        let mut headers = HeaderMap::new();
463        headers.insert("x-user-id", HeaderValue::from_static("user123"));
464        headers.insert("x-username", HeaderValue::from_static("testuser"));
465        headers.insert("x-user-role", HeaderValue::from_static("viewer"));
466
467        let context = extract_user_context(&headers).unwrap();
468        assert_eq!(context.user_id, "user123");
469        assert_eq!(context.username, "testuser");
470        assert_eq!(context.role, UserRole::Viewer);
471        assert_eq!(context.email, None);
472    }
473
474    #[test]
475    fn test_parse_jwt_payload() {
476        let payload_json = r#"{
477            "sub": "user456",
478            "username": "jwtuser",
479            "role": "editor",
480            "email": "jwt@example.com"
481        }"#;
482
483        let context = parse_jwt_payload(payload_json).unwrap();
484        assert_eq!(context.user_id, "user456");
485        assert_eq!(context.username, "jwtuser");
486        assert_eq!(context.role, UserRole::Editor);
487        assert_eq!(context.email, Some("jwt@example.com".to_string()));
488    }
489
490    #[test]
491    fn test_parse_jwt_payload_without_email() {
492        let payload_json = r#"{
493            "sub": "user456",
494            "username": "jwtuser",
495            "role": "viewer"
496        }"#;
497
498        let context = parse_jwt_payload(payload_json).unwrap();
499        assert_eq!(context.email, None);
500    }
501
502    #[test]
503    fn test_parse_jwt_payload_invalid_json() {
504        let payload_json = "invalid json";
505        let context = parse_jwt_payload(payload_json);
506        assert!(context.is_none());
507    }
508
509    #[test]
510    fn test_parse_jwt_payload_missing_fields() {
511        let payload_json = r#"{"sub": "user456"}"#;
512        let context = parse_jwt_payload(payload_json);
513        assert!(context.is_none());
514    }
515
516    #[test]
517    fn test_parse_jwt_payload_invalid_role() {
518        let payload_json = r#"{
519            "sub": "user456",
520            "username": "jwtuser",
521            "role": "invalid_role"
522        }"#;
523
524        let context = parse_jwt_payload(payload_json);
525        assert!(context.is_none());
526    }
527
528    #[test]
529    fn test_admin_action_permissions_config_changes() {
530        let perms = AdminActionPermissions::get_required_permissions("update_latency");
531        assert_eq!(perms, vec![Permission::ManageSettings]);
532
533        let perms = AdminActionPermissions::get_required_permissions("update_faults");
534        assert_eq!(perms, vec![Permission::ManageSettings]);
535
536        let perms = AdminActionPermissions::get_required_permissions("update_proxy");
537        assert_eq!(perms, vec![Permission::ManageSettings]);
538    }
539
540    #[test]
541    fn test_admin_action_permissions_fixture_management() {
542        let perms = AdminActionPermissions::get_required_permissions("create_fixture");
543        assert_eq!(perms, vec![Permission::MockCreate]);
544
545        let perms = AdminActionPermissions::get_required_permissions("update_fixture");
546        assert_eq!(perms, vec![Permission::MockUpdate]);
547
548        let perms = AdminActionPermissions::get_required_permissions("delete_fixture");
549        assert_eq!(perms, vec![Permission::MockDelete]);
550    }
551
552    #[test]
553    fn test_admin_action_permissions_user_management() {
554        let perms = AdminActionPermissions::get_required_permissions("create_user");
555        assert_eq!(perms, vec![Permission::ChangeRoles]);
556
557        let perms = AdminActionPermissions::get_required_permissions("change_role");
558        assert_eq!(perms, vec![Permission::ChangeRoles]);
559    }
560
561    #[test]
562    fn test_admin_action_permissions_read_operations() {
563        let perms = AdminActionPermissions::get_required_permissions("get_dashboard");
564        assert_eq!(perms, vec![Permission::WorkspaceRead, Permission::MockRead]);
565
566        let perms = AdminActionPermissions::get_required_permissions("get_logs");
567        assert_eq!(perms, vec![Permission::WorkspaceRead, Permission::MockRead]);
568    }
569
570    #[test]
571    fn test_admin_action_permissions_scenario_operations() {
572        let perms = AdminActionPermissions::get_required_permissions("modify_scenario_chaos_rules");
573        assert_eq!(perms, vec![Permission::ScenarioModifyChaosRules]);
574
575        let perms =
576            AdminActionPermissions::get_required_permissions("modify_scenario_reality_defaults");
577        assert_eq!(perms, vec![Permission::ScenarioModifyRealityDefaults]);
578
579        let perms = AdminActionPermissions::get_required_permissions("promote_scenario");
580        assert_eq!(perms, vec![Permission::ScenarioPromote]);
581
582        let perms = AdminActionPermissions::get_required_permissions("approve_scenario_promotion");
583        assert_eq!(perms, vec![Permission::ScenarioApprove]);
584    }
585
586    #[test]
587    fn test_admin_action_permissions_unknown_action() {
588        let perms = AdminActionPermissions::get_required_permissions("unknown_action");
589        assert_eq!(perms, vec![Permission::ManageSettings]);
590    }
591
592    #[test]
593    fn test_get_default_user_context_without_env_var() {
594        std::env::remove_var("MOCKFORGE_ALLOW_UNAUTHENTICATED");
595        let context = get_default_user_context();
596        assert!(context.is_none());
597    }
598
599    #[test]
600    fn test_get_default_user_context_with_env_var() {
601        std::env::set_var("MOCKFORGE_ALLOW_UNAUTHENTICATED", "1");
602        let context = get_default_user_context();
603        assert!(context.is_some());
604
605        let context = context.unwrap();
606        assert_eq!(context.user_id, "system");
607        assert_eq!(context.username, "system");
608        assert_eq!(context.role, UserRole::Admin);
609
610        std::env::remove_var("MOCKFORGE_ALLOW_UNAUTHENTICATED");
611    }
612
613    #[test]
614    fn test_all_permission_actions_covered() {
615        // Test that all defined actions map to valid permissions
616        let actions = vec![
617            "update_latency",
618            "update_faults",
619            "restart_servers",
620            "create_fixture",
621            "update_fixture",
622            "delete_fixture",
623            "enable_route",
624            "create_user",
625            "grant_permission",
626            "create_api_key",
627            "get_dashboard",
628            "get_audit_logs",
629            "modify_scenario_chaos_rules",
630            "promote_scenario",
631            "approve_scenario_promotion",
632        ];
633
634        for action in actions {
635            let perms = AdminActionPermissions::get_required_permissions(action);
636            assert!(!perms.is_empty(), "Action {} should have permissions", action);
637        }
638    }
639
640    #[test]
641    fn test_role_permissions_admin_has_all() {
642        // Admin should have all permissions
643        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ManageSettings));
644        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockCreate));
645        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockUpdate));
646        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockDelete));
647        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::WorkspaceRead));
648        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ChangeRoles));
649    }
650
651    #[test]
652    fn test_role_permissions_editor_limited() {
653        // Editor should have some permissions but not all
654        assert!(!RolePermissions::has_permission(UserRole::Editor, Permission::ManageSettings));
655        assert!(RolePermissions::has_permission(UserRole::Editor, Permission::MockUpdate));
656        assert!(!RolePermissions::has_permission(UserRole::Editor, Permission::ChangeRoles));
657    }
658
659    #[test]
660    fn test_role_permissions_viewer_readonly() {
661        // Viewer should only have read permissions
662        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::ManageSettings));
663        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockCreate));
664        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockUpdate));
665        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockDelete));
666        assert!(RolePermissions::has_permission(UserRole::Viewer, Permission::WorkspaceRead));
667        assert!(RolePermissions::has_permission(UserRole::Viewer, Permission::MockRead));
668    }
669
670    #[test]
671    fn test_scenario_permissions() {
672        // Test scenario-specific permissions
673        assert!(RolePermissions::has_permission(
674            UserRole::Admin,
675            Permission::ScenarioModifyChaosRules
676        ));
677        assert!(RolePermissions::has_permission(
678            UserRole::Admin,
679            Permission::ScenarioModifyRealityDefaults
680        ));
681        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ScenarioPromote));
682        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ScenarioApprove));
683    }
684
685    #[tokio::test]
686    async fn test_rbac_middleware_public_routes() {
687        use axum::routing::get;
688        use axum::{body::Body, middleware::from_fn, Router};
689        use tower::ServiceExt;
690
691        async fn handler() -> &'static str {
692            "OK"
693        }
694
695        let app = Router::new().route("/", get(handler)).layer(from_fn(rbac_middleware));
696
697        let request = axum::http::Request::builder()
698            .uri("/")
699            .method(Method::GET)
700            .body(Body::empty())
701            .unwrap();
702
703        let response = app.oneshot(request).await.unwrap();
704        assert_eq!(response.status(), StatusCode::OK);
705    }
706
707    #[tokio::test]
708    async fn test_rbac_middleware_health_route() {
709        use axum::routing::get;
710        use axum::{body::Body, middleware::from_fn, Router};
711        use tower::ServiceExt;
712
713        async fn handler() -> &'static str {
714            "OK"
715        }
716
717        let app = Router::new()
718            .route("/__mockforge/health", get(handler))
719            .layer(from_fn(rbac_middleware));
720
721        let request = axum::http::Request::builder()
722            .uri("/__mockforge/health")
723            .method(Method::GET)
724            .body(Body::empty())
725            .unwrap();
726
727        let response = app.oneshot(request).await.unwrap();
728        assert_eq!(response.status(), StatusCode::OK);
729    }
730
731    #[tokio::test]
732    async fn test_rbac_middleware_assets_route() {
733        use axum::routing::get;
734        use axum::{body::Body, middleware::from_fn, Router};
735        use tower::ServiceExt;
736
737        async fn handler() -> &'static str {
738            "OK"
739        }
740
741        let app = Router::new()
742            .route("/assets/style.css", get(handler))
743            .layer(from_fn(rbac_middleware));
744
745        let request = axum::http::Request::builder()
746            .uri("/assets/style.css")
747            .method(Method::GET)
748            .body(Body::empty())
749            .unwrap();
750
751        let response = app.oneshot(request).await.unwrap();
752        assert_eq!(response.status(), StatusCode::OK);
753    }
754
755    #[tokio::test]
756    async fn test_rbac_middleware_with_valid_headers() {
757        use axum::routing::get;
758        use axum::{body::Body, middleware::from_fn, Router};
759        use tower::ServiceExt;
760
761        async fn handler() -> &'static str {
762            "OK"
763        }
764
765        let app = Router::new().route("/api/test", get(handler)).layer(from_fn(rbac_middleware));
766
767        let request = axum::http::Request::builder()
768            .uri("/api/test")
769            .method(Method::GET)
770            .header("x-user-id", "user123")
771            .header("x-username", "testuser")
772            .header("x-user-role", "admin")
773            .body(Body::empty())
774            .unwrap();
775
776        let response = app.oneshot(request).await.unwrap();
777        assert_eq!(response.status(), StatusCode::OK);
778    }
779
780    #[test]
781    fn test_action_name_mapping() {
782        // Test route to action mapping logic
783        let test_cases = vec![
784            ("/config/latency", "update_latency"),
785            ("/config/faults", "update_faults"),
786            ("/config/proxy", "update_proxy"),
787            ("/logs", "clear_logs"),              // DELETE method
788            ("/fixtures/test", "delete_fixture"), // DELETE method
789            ("/audit/logs", "get_audit_logs"),    // GET method
790        ];
791
792        // These would be tested in the actual middleware
793        // Here we verify the logic exists
794        for (_path, expected_action) in test_cases {
795            assert!(!expected_action.is_empty());
796        }
797    }
798
799    #[test]
800    fn test_user_context_clone() {
801        let context = UserContext {
802            user_id: "user123".to_string(),
803            username: "testuser".to_string(),
804            role: UserRole::Editor,
805            email: Some("test@example.com".to_string()),
806        };
807
808        let cloned = context.clone();
809        assert_eq!(cloned.user_id, context.user_id);
810        assert_eq!(cloned.username, context.username);
811        assert_eq!(cloned.role, context.role);
812        assert_eq!(cloned.email, context.email);
813    }
814
815    #[test]
816    fn test_user_context_debug() {
817        let context = UserContext {
818            user_id: "user123".to_string(),
819            username: "testuser".to_string(),
820            role: UserRole::Viewer,
821            email: None,
822        };
823
824        let debug_str = format!("{:?}", context);
825        assert!(debug_str.contains("user123"));
826        assert!(debug_str.contains("testuser"));
827    }
828}