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