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    // All GET requests to /__mockforge/* are read-only admin data — the admin server
293    // is already on a separate port that users explicitly enable with --admin, so
294    // requiring auth for read-only access is unnecessary and blocks tools like the TUI.
295    let is_public_route = path == "/"
296        || path.starts_with("/assets/")
297        || path == "/__mockforge/auth/login"
298        || path == "/__mockforge/auth/refresh"
299        || path == "/__mockforge/auth/logout"
300        || path == "/__mockforge/health"
301        || path.starts_with("/mockforge-")
302        || path == "/manifest.json"
303        || path == "/sw.js"
304        || path == "/api-docs"
305        || (method == "GET" && path.starts_with("/__mockforge/"));
306
307    if is_public_route {
308        return Ok(next.run(request).await);
309    }
310
311    // Map route to action name
312    let action_name = match (method, path) {
313        (_, p) if p.contains("/config/latency") => "update_latency",
314        (_, p) if p.contains("/config/faults") => "update_faults",
315        (_, p) if p.contains("/config/proxy") => "update_proxy",
316        (_, p) if p.contains("/config/traffic-shaping") => "update_traffic_shaping",
317        ("DELETE", p) if p.contains("/logs") => "clear_logs",
318        ("POST", p) if p.contains("/restart") => "restart_servers",
319        ("DELETE", p) if p.contains("/fixtures") => "delete_fixture",
320        ("POST", p) if p.contains("/fixtures") && p.contains("/rename") => "rename_fixture",
321        ("POST", p) if p.contains("/fixtures") && p.contains("/move") => "move_fixture",
322        ("GET", p) if p.contains("/audit/logs") => "get_audit_logs",
323        ("GET", p) if p.contains("/audit/stats") => "get_audit_stats",
324        ("GET", _) => "read", // Read operations
325        _ => "unknown",
326    };
327
328    // Extract user context from request
329    let user_context = extract_user_context(headers).or_else(get_default_user_context);
330
331    // If no user context and authentication is required, deny access
332    let user_context = match user_context {
333        Some(ctx) => ctx,
334        None => {
335            // For development: allow unauthenticated access if explicitly enabled
336            // In production, this should be disabled
337            if allow_unauthenticated_dev_access() {
338                // Use default admin context for development
339                get_default_user_context().unwrap_or_else(|| UserContext {
340                    user_id: "system".to_string(),
341                    username: "system".to_string(),
342                    role: UserRole::Admin,
343                    email: None,
344                })
345            } else {
346                return Err(StatusCode::UNAUTHORIZED);
347            }
348        }
349    };
350
351    // Get required permissions for this action
352    let required_permissions = AdminActionPermissions::get_required_permissions(action_name);
353
354    // Check if user has at least one of the required permissions
355    let has_permission = required_permissions
356        .iter()
357        .any(|&perm| RolePermissions::has_permission(user_context.role, perm));
358
359    if !has_permission {
360        // Log authorization failure
361        tracing::warn!(
362            user_id = %user_context.user_id,
363            username = %user_context.username,
364            role = ?user_context.role,
365            action = %action_name,
366            "Authorization denied: User does not have required permissions"
367        );
368
369        return Err(StatusCode::FORBIDDEN);
370    }
371
372    // User has permission, continue with request
373    // Store user context in request extensions for use in handlers
374    request.extensions_mut().insert(user_context);
375
376    Ok(next.run(request).await)
377}
378
379/// Helper to extract user context from request extensions
380pub fn get_user_context_from_request(request: &Request) -> Option<UserContext> {
381    request.extensions().get::<UserContext>().cloned()
382}
383
384/// Helper to get user context from axum State (if stored)
385pub fn get_user_context_from_state<T>(_state: &T) -> Option<UserContext>
386where
387    T: std::any::Any,
388{
389    // This is a placeholder - in practice, user context would be stored in request extensions
390    None
391}
392
393#[cfg(test)]
394mod tests {
395    use super::*;
396    use axum::http::{HeaderValue, Method};
397
398    #[test]
399    fn test_parse_role_valid() {
400        assert_eq!(parse_role("admin"), Some(UserRole::Admin));
401        assert_eq!(parse_role("Admin"), Some(UserRole::Admin));
402        assert_eq!(parse_role("ADMIN"), Some(UserRole::Admin));
403        assert_eq!(parse_role("editor"), Some(UserRole::Editor));
404        assert_eq!(parse_role("viewer"), Some(UserRole::Viewer));
405    }
406
407    #[test]
408    fn test_parse_role_invalid() {
409        assert_eq!(parse_role("invalid"), None);
410        assert_eq!(parse_role(""), None);
411        assert_eq!(parse_role("super_admin"), None);
412    }
413
414    #[test]
415    fn test_user_context_serialization() {
416        let context = UserContext {
417            user_id: "user123".to_string(),
418            username: "testuser".to_string(),
419            role: UserRole::Editor,
420            email: Some("test@example.com".to_string()),
421        };
422
423        let serialized = serde_json::to_string(&context).unwrap();
424        let deserialized: UserContext = serde_json::from_str(&serialized).unwrap();
425
426        assert_eq!(deserialized.user_id, context.user_id);
427        assert_eq!(deserialized.username, context.username);
428        assert_eq!(deserialized.role, context.role);
429        assert_eq!(deserialized.email, context.email);
430    }
431
432    #[test]
433    fn test_extract_user_context_from_headers() {
434        let mut headers = HeaderMap::new();
435        headers.insert("x-user-id", HeaderValue::from_static("user123"));
436        headers.insert("x-username", HeaderValue::from_static("testuser"));
437        headers.insert("x-user-role", HeaderValue::from_static("admin"));
438        headers.insert("x-user-email", HeaderValue::from_static("test@example.com"));
439
440        let context = extract_user_context(&headers).unwrap();
441        assert_eq!(context.user_id, "user123");
442        assert_eq!(context.username, "testuser");
443        assert_eq!(context.role, UserRole::Admin);
444        assert_eq!(context.email, Some("test@example.com".to_string()));
445    }
446
447    #[test]
448    fn test_extract_user_context_missing_headers() {
449        let headers = HeaderMap::new();
450        let context = extract_user_context(&headers);
451        assert!(context.is_none());
452    }
453
454    #[test]
455    fn test_extract_user_context_partial_headers() {
456        let mut headers = HeaderMap::new();
457        headers.insert("x-user-id", HeaderValue::from_static("user123"));
458        // Missing username and role
459
460        let context = extract_user_context(&headers);
461        assert!(context.is_none());
462    }
463
464    #[test]
465    fn test_extract_user_context_without_email() {
466        let mut headers = HeaderMap::new();
467        headers.insert("x-user-id", HeaderValue::from_static("user123"));
468        headers.insert("x-username", HeaderValue::from_static("testuser"));
469        headers.insert("x-user-role", HeaderValue::from_static("viewer"));
470
471        let context = extract_user_context(&headers).unwrap();
472        assert_eq!(context.user_id, "user123");
473        assert_eq!(context.username, "testuser");
474        assert_eq!(context.role, UserRole::Viewer);
475        assert_eq!(context.email, None);
476    }
477
478    #[test]
479    fn test_parse_jwt_payload() {
480        let payload_json = r#"{
481            "sub": "user456",
482            "username": "jwtuser",
483            "role": "editor",
484            "email": "jwt@example.com"
485        }"#;
486
487        let context = parse_jwt_payload(payload_json).unwrap();
488        assert_eq!(context.user_id, "user456");
489        assert_eq!(context.username, "jwtuser");
490        assert_eq!(context.role, UserRole::Editor);
491        assert_eq!(context.email, Some("jwt@example.com".to_string()));
492    }
493
494    #[test]
495    fn test_parse_jwt_payload_without_email() {
496        let payload_json = r#"{
497            "sub": "user456",
498            "username": "jwtuser",
499            "role": "viewer"
500        }"#;
501
502        let context = parse_jwt_payload(payload_json).unwrap();
503        assert_eq!(context.email, None);
504    }
505
506    #[test]
507    fn test_parse_jwt_payload_invalid_json() {
508        let payload_json = "invalid json";
509        let context = parse_jwt_payload(payload_json);
510        assert!(context.is_none());
511    }
512
513    #[test]
514    fn test_parse_jwt_payload_missing_fields() {
515        let payload_json = r#"{"sub": "user456"}"#;
516        let context = parse_jwt_payload(payload_json);
517        assert!(context.is_none());
518    }
519
520    #[test]
521    fn test_parse_jwt_payload_invalid_role() {
522        let payload_json = r#"{
523            "sub": "user456",
524            "username": "jwtuser",
525            "role": "invalid_role"
526        }"#;
527
528        let context = parse_jwt_payload(payload_json);
529        assert!(context.is_none());
530    }
531
532    #[test]
533    fn test_admin_action_permissions_config_changes() {
534        let perms = AdminActionPermissions::get_required_permissions("update_latency");
535        assert_eq!(perms, vec![Permission::ManageSettings]);
536
537        let perms = AdminActionPermissions::get_required_permissions("update_faults");
538        assert_eq!(perms, vec![Permission::ManageSettings]);
539
540        let perms = AdminActionPermissions::get_required_permissions("update_proxy");
541        assert_eq!(perms, vec![Permission::ManageSettings]);
542    }
543
544    #[test]
545    fn test_admin_action_permissions_fixture_management() {
546        let perms = AdminActionPermissions::get_required_permissions("create_fixture");
547        assert_eq!(perms, vec![Permission::MockCreate]);
548
549        let perms = AdminActionPermissions::get_required_permissions("update_fixture");
550        assert_eq!(perms, vec![Permission::MockUpdate]);
551
552        let perms = AdminActionPermissions::get_required_permissions("delete_fixture");
553        assert_eq!(perms, vec![Permission::MockDelete]);
554    }
555
556    #[test]
557    fn test_admin_action_permissions_user_management() {
558        let perms = AdminActionPermissions::get_required_permissions("create_user");
559        assert_eq!(perms, vec![Permission::ChangeRoles]);
560
561        let perms = AdminActionPermissions::get_required_permissions("change_role");
562        assert_eq!(perms, vec![Permission::ChangeRoles]);
563    }
564
565    #[test]
566    fn test_admin_action_permissions_read_operations() {
567        let perms = AdminActionPermissions::get_required_permissions("get_dashboard");
568        assert_eq!(perms, vec![Permission::WorkspaceRead, Permission::MockRead]);
569
570        let perms = AdminActionPermissions::get_required_permissions("get_logs");
571        assert_eq!(perms, vec![Permission::WorkspaceRead, Permission::MockRead]);
572    }
573
574    #[test]
575    fn test_admin_action_permissions_scenario_operations() {
576        let perms = AdminActionPermissions::get_required_permissions("modify_scenario_chaos_rules");
577        assert_eq!(perms, vec![Permission::ScenarioModifyChaosRules]);
578
579        let perms =
580            AdminActionPermissions::get_required_permissions("modify_scenario_reality_defaults");
581        assert_eq!(perms, vec![Permission::ScenarioModifyRealityDefaults]);
582
583        let perms = AdminActionPermissions::get_required_permissions("promote_scenario");
584        assert_eq!(perms, vec![Permission::ScenarioPromote]);
585
586        let perms = AdminActionPermissions::get_required_permissions("approve_scenario_promotion");
587        assert_eq!(perms, vec![Permission::ScenarioApprove]);
588    }
589
590    #[test]
591    fn test_admin_action_permissions_unknown_action() {
592        let perms = AdminActionPermissions::get_required_permissions("unknown_action");
593        assert_eq!(perms, vec![Permission::ManageSettings]);
594    }
595
596    #[test]
597    fn test_get_default_user_context_without_env_var() {
598        std::env::remove_var("MOCKFORGE_ALLOW_UNAUTHENTICATED");
599        let context = get_default_user_context();
600        assert!(context.is_none());
601    }
602
603    #[test]
604    fn test_get_default_user_context_with_env_var() {
605        std::env::set_var("MOCKFORGE_ALLOW_UNAUTHENTICATED", "1");
606        let context = get_default_user_context();
607        assert!(context.is_some());
608
609        let context = context.unwrap();
610        assert_eq!(context.user_id, "system");
611        assert_eq!(context.username, "system");
612        assert_eq!(context.role, UserRole::Admin);
613
614        std::env::remove_var("MOCKFORGE_ALLOW_UNAUTHENTICATED");
615    }
616
617    #[test]
618    fn test_all_permission_actions_covered() {
619        // Test that all defined actions map to valid permissions
620        let actions = vec![
621            "update_latency",
622            "update_faults",
623            "restart_servers",
624            "create_fixture",
625            "update_fixture",
626            "delete_fixture",
627            "enable_route",
628            "create_user",
629            "grant_permission",
630            "create_api_key",
631            "get_dashboard",
632            "get_audit_logs",
633            "modify_scenario_chaos_rules",
634            "promote_scenario",
635            "approve_scenario_promotion",
636        ];
637
638        for action in actions {
639            let perms = AdminActionPermissions::get_required_permissions(action);
640            assert!(!perms.is_empty(), "Action {} should have permissions", action);
641        }
642    }
643
644    #[test]
645    fn test_role_permissions_admin_has_all() {
646        // Admin should have all permissions
647        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ManageSettings));
648        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockCreate));
649        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockUpdate));
650        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::MockDelete));
651        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::WorkspaceRead));
652        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ChangeRoles));
653    }
654
655    #[test]
656    fn test_role_permissions_editor_limited() {
657        // Editor should have some permissions but not all
658        assert!(!RolePermissions::has_permission(UserRole::Editor, Permission::ManageSettings));
659        assert!(RolePermissions::has_permission(UserRole::Editor, Permission::MockUpdate));
660        assert!(!RolePermissions::has_permission(UserRole::Editor, Permission::ChangeRoles));
661    }
662
663    #[test]
664    fn test_role_permissions_viewer_readonly() {
665        // Viewer should only have read permissions
666        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::ManageSettings));
667        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockCreate));
668        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockUpdate));
669        assert!(!RolePermissions::has_permission(UserRole::Viewer, Permission::MockDelete));
670        assert!(RolePermissions::has_permission(UserRole::Viewer, Permission::WorkspaceRead));
671        assert!(RolePermissions::has_permission(UserRole::Viewer, Permission::MockRead));
672    }
673
674    #[test]
675    fn test_scenario_permissions() {
676        // Test scenario-specific permissions
677        assert!(RolePermissions::has_permission(
678            UserRole::Admin,
679            Permission::ScenarioModifyChaosRules
680        ));
681        assert!(RolePermissions::has_permission(
682            UserRole::Admin,
683            Permission::ScenarioModifyRealityDefaults
684        ));
685        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ScenarioPromote));
686        assert!(RolePermissions::has_permission(UserRole::Admin, Permission::ScenarioApprove));
687    }
688
689    #[tokio::test]
690    async fn test_rbac_middleware_public_routes() {
691        use axum::routing::get;
692        use axum::{body::Body, middleware::from_fn, Router};
693        use tower::ServiceExt;
694
695        async fn handler() -> &'static str {
696            "OK"
697        }
698
699        let app = Router::new().route("/", get(handler)).layer(from_fn(rbac_middleware));
700
701        let request = axum::http::Request::builder()
702            .uri("/")
703            .method(Method::GET)
704            .body(Body::empty())
705            .unwrap();
706
707        let response = app.oneshot(request).await.unwrap();
708        assert_eq!(response.status(), StatusCode::OK);
709    }
710
711    #[tokio::test]
712    async fn test_rbac_middleware_health_route() {
713        use axum::routing::get;
714        use axum::{body::Body, middleware::from_fn, Router};
715        use tower::ServiceExt;
716
717        async fn handler() -> &'static str {
718            "OK"
719        }
720
721        let app = Router::new()
722            .route("/__mockforge/health", get(handler))
723            .layer(from_fn(rbac_middleware));
724
725        let request = axum::http::Request::builder()
726            .uri("/__mockforge/health")
727            .method(Method::GET)
728            .body(Body::empty())
729            .unwrap();
730
731        let response = app.oneshot(request).await.unwrap();
732        assert_eq!(response.status(), StatusCode::OK);
733    }
734
735    #[tokio::test]
736    async fn test_rbac_middleware_assets_route() {
737        use axum::routing::get;
738        use axum::{body::Body, middleware::from_fn, Router};
739        use tower::ServiceExt;
740
741        async fn handler() -> &'static str {
742            "OK"
743        }
744
745        let app = Router::new()
746            .route("/assets/style.css", get(handler))
747            .layer(from_fn(rbac_middleware));
748
749        let request = axum::http::Request::builder()
750            .uri("/assets/style.css")
751            .method(Method::GET)
752            .body(Body::empty())
753            .unwrap();
754
755        let response = app.oneshot(request).await.unwrap();
756        assert_eq!(response.status(), StatusCode::OK);
757    }
758
759    #[tokio::test]
760    async fn test_rbac_middleware_allows_get_mockforge_api() {
761        use axum::routing::get;
762        use axum::{body::Body, middleware::from_fn, Router};
763        use tower::ServiceExt;
764
765        async fn handler() -> &'static str {
766            "OK"
767        }
768
769        let app = Router::new()
770            .route("/__mockforge/dashboard", get(handler))
771            .layer(from_fn(rbac_middleware));
772
773        // GET to /__mockforge/* should be allowed without auth
774        let request = axum::http::Request::builder()
775            .uri("/__mockforge/dashboard")
776            .method(Method::GET)
777            .body(Body::empty())
778            .unwrap();
779
780        let response = app.oneshot(request).await.unwrap();
781        assert_eq!(response.status(), StatusCode::OK);
782    }
783
784    #[tokio::test]
785    async fn test_rbac_middleware_with_valid_headers() {
786        use axum::routing::get;
787        use axum::{body::Body, middleware::from_fn, Router};
788        use tower::ServiceExt;
789
790        async fn handler() -> &'static str {
791            "OK"
792        }
793
794        let app = Router::new().route("/api/test", get(handler)).layer(from_fn(rbac_middleware));
795
796        let request = axum::http::Request::builder()
797            .uri("/api/test")
798            .method(Method::GET)
799            .header("x-user-id", "user123")
800            .header("x-username", "testuser")
801            .header("x-user-role", "admin")
802            .body(Body::empty())
803            .unwrap();
804
805        let response = app.oneshot(request).await.unwrap();
806        assert_eq!(response.status(), StatusCode::OK);
807    }
808
809    #[test]
810    fn test_action_name_mapping() {
811        // Test route to action mapping logic
812        let test_cases = vec![
813            ("/config/latency", "update_latency"),
814            ("/config/faults", "update_faults"),
815            ("/config/proxy", "update_proxy"),
816            ("/logs", "clear_logs"),              // DELETE method
817            ("/fixtures/test", "delete_fixture"), // DELETE method
818            ("/audit/logs", "get_audit_logs"),    // GET method
819        ];
820
821        // These would be tested in the actual middleware
822        // Here we verify the logic exists
823        for (_path, expected_action) in test_cases {
824            assert!(!expected_action.is_empty());
825        }
826    }
827
828    #[test]
829    fn test_user_context_clone() {
830        let context = UserContext {
831            user_id: "user123".to_string(),
832            username: "testuser".to_string(),
833            role: UserRole::Editor,
834            email: Some("test@example.com".to_string()),
835        };
836
837        let cloned = context.clone();
838        assert_eq!(cloned.user_id, context.user_id);
839        assert_eq!(cloned.username, context.username);
840        assert_eq!(cloned.role, context.role);
841        assert_eq!(cloned.email, context.email);
842    }
843
844    #[test]
845    fn test_user_context_debug() {
846        let context = UserContext {
847            user_id: "user123".to_string(),
848            username: "testuser".to_string(),
849            role: UserRole::Viewer,
850            email: None,
851        };
852
853        let debug_str = format!("{:?}", context);
854        assert!(debug_str.contains("user123"));
855        assert!(debug_str.contains("testuser"));
856    }
857}