mockforge_core/multi_tenant/
middleware.rs

1//! Multi-tenant workspace routing middleware
2//!
3//! This module provides middleware for routing requests to the appropriate workspace
4//! based on path-based or port-based routing strategies.
5
6use super::{MultiTenantWorkspaceRegistry, TenantWorkspace};
7use crate::{Error, Result};
8use std::sync::Arc;
9
10/// Workspace routing context extracted from request
11#[derive(Debug, Clone)]
12pub struct WorkspaceContext {
13    /// Workspace ID
14    pub workspace_id: String,
15    /// Original request path
16    pub original_path: String,
17    /// Path with workspace prefix stripped
18    pub stripped_path: String,
19    /// Tenant workspace
20    pub workspace: TenantWorkspace,
21}
22
23/// Workspace router for handling multi-tenant routing
24#[derive(Debug, Clone)]
25pub struct WorkspaceRouter {
26    /// Multi-tenant registry
27    registry: Arc<MultiTenantWorkspaceRegistry>,
28}
29
30impl WorkspaceRouter {
31    /// Create a new workspace router
32    pub fn new(registry: Arc<MultiTenantWorkspaceRegistry>) -> Self {
33        Self { registry }
34    }
35
36    /// Extract workspace context from request path
37    pub fn extract_workspace_context(&self, path: &str) -> Result<WorkspaceContext> {
38        let config = self.registry.config();
39
40        // If multi-tenant is disabled, use default workspace
41        if !config.enabled {
42            let workspace = self.registry.get_default_workspace()?;
43            return Ok(WorkspaceContext {
44                workspace_id: config.default_workspace.clone(),
45                original_path: path.to_string(),
46                stripped_path: path.to_string(),
47                workspace,
48            });
49        }
50
51        // Try to extract workspace ID from path
52        if let Some(workspace_id) = self.registry.extract_workspace_id_from_path(path) {
53            // Verify workspace exists and is enabled
54            let workspace = self.registry.get_workspace(&workspace_id)?;
55
56            if !workspace.enabled {
57                return Err(Error::generic(format!("Workspace '{}' is disabled", workspace_id)));
58            }
59
60            let stripped_path = self.registry.strip_workspace_prefix(path, &workspace_id);
61
62            Ok(WorkspaceContext {
63                workspace_id: workspace_id.clone(),
64                original_path: path.to_string(),
65                stripped_path,
66                workspace,
67            })
68        } else {
69            // No workspace ID in path, use default workspace
70            let workspace = self.registry.get_default_workspace()?;
71
72            Ok(WorkspaceContext {
73                workspace_id: config.default_workspace.clone(),
74                original_path: path.to_string(),
75                stripped_path: path.to_string(),
76                workspace,
77            })
78        }
79    }
80
81    /// Get the multi-tenant registry
82    pub fn registry(&self) -> &Arc<MultiTenantWorkspaceRegistry> {
83        &self.registry
84    }
85
86    /// Get workspace by ID
87    pub fn get_workspace(&self, workspace_id: &str) -> Result<TenantWorkspace> {
88        self.registry.get_workspace(workspace_id)
89    }
90
91    /// Check if multi-tenant mode is enabled
92    pub fn is_multi_tenant_enabled(&self) -> bool {
93        self.registry.config().enabled
94    }
95
96    /// Get workspace prefix
97    pub fn workspace_prefix(&self) -> &str {
98        &self.registry.config().workspace_prefix
99    }
100}
101
102/// Workspace middleware layer for Axum
103pub mod axum_middleware {
104    use super::*;
105    use ::axum::http::StatusCode;
106    use ::axum::{
107        extract::Request,
108        middleware::Next,
109        response::{IntoResponse, Response},
110    };
111
112    /// Axum middleware for workspace routing
113    pub async fn workspace_middleware(
114        router: Arc<WorkspaceRouter>,
115        mut request: Request,
116        next: Next,
117    ) -> Response {
118        let path = request.uri().path();
119
120        // Extract workspace context
121        let context = match router.extract_workspace_context(path) {
122            Ok(ctx) => ctx,
123            Err(e) => {
124                return (StatusCode::NOT_FOUND, format!("Workspace error: {}", e)).into_response();
125            }
126        };
127
128        // Store workspace context in request extensions
129        request.extensions_mut().insert(context.clone());
130
131        // Update request URI with stripped path
132        if context.original_path != context.stripped_path {
133            let mut parts = request.uri().clone().into_parts();
134            parts.path_and_query = context.stripped_path.parse().ok().or(parts.path_and_query);
135
136            if let Ok(uri) = ::axum::http::Uri::from_parts(parts) {
137                *request.uri_mut() = uri;
138            }
139        }
140
141        // Continue with the request
142        next.run(request).await
143    }
144
145    /// Extension trait for extracting workspace context from Axum requests
146    pub trait WorkspaceContextExt {
147        /// Get the workspace context from the request
148        fn workspace_context(&self) -> Option<&WorkspaceContext>;
149
150        /// Get the workspace ID
151        fn workspace_id(&self) -> Option<&str>;
152
153        /// Get the stripped path
154        fn stripped_path(&self) -> Option<&str>;
155    }
156
157    impl WorkspaceContextExt for Request {
158        fn workspace_context(&self) -> Option<&WorkspaceContext> {
159            self.extensions().get::<WorkspaceContext>()
160        }
161
162        fn workspace_id(&self) -> Option<&str> {
163            self.workspace_context().map(|ctx| ctx.workspace_id.as_str())
164        }
165
166        fn stripped_path(&self) -> Option<&str> {
167            self.workspace_context().map(|ctx| ctx.stripped_path.as_str())
168        }
169    }
170}
171
172#[cfg(test)]
173mod tests {
174    use super::*;
175    use crate::multi_tenant::{MultiTenantConfig, MultiTenantWorkspaceRegistry};
176    use crate::workspace::Workspace;
177
178    fn create_test_router() -> WorkspaceRouter {
179        let config = MultiTenantConfig {
180            enabled: true,
181            ..Default::default()
182        };
183
184        let mut registry = MultiTenantWorkspaceRegistry::new(config);
185
186        // Create default workspace
187        let default_ws = Workspace::new("Default".to_string());
188        registry.register_workspace("default".to_string(), default_ws).unwrap();
189
190        // Create test workspace
191        let test_ws = Workspace::new("Test Workspace".to_string());
192        registry.register_workspace("test".to_string(), test_ws).unwrap();
193
194        WorkspaceRouter::new(Arc::new(registry))
195    }
196
197    #[test]
198    fn test_extract_workspace_context_with_prefix() {
199        let router = create_test_router();
200
201        let context = router.extract_workspace_context("/workspace/test/api/users").unwrap();
202
203        assert_eq!(context.workspace_id, "test");
204        assert_eq!(context.original_path, "/workspace/test/api/users");
205        assert_eq!(context.stripped_path, "/api/users");
206        assert_eq!(context.workspace.name(), "Test Workspace");
207    }
208
209    #[test]
210    fn test_extract_workspace_context_default() {
211        let router = create_test_router();
212
213        let context = router.extract_workspace_context("/api/users").unwrap();
214
215        assert_eq!(context.workspace_id, "default");
216        assert_eq!(context.original_path, "/api/users");
217        assert_eq!(context.stripped_path, "/api/users");
218        assert_eq!(context.workspace.name(), "Default");
219    }
220
221    #[test]
222    fn test_extract_workspace_context_nonexistent() {
223        let router = create_test_router();
224
225        let result = router.extract_workspace_context("/workspace/nonexistent/api/users");
226
227        assert!(result.is_err());
228    }
229
230    #[test]
231    fn test_multi_tenant_disabled() {
232        let config = MultiTenantConfig {
233            enabled: false,
234            ..Default::default()
235        };
236
237        let mut registry = MultiTenantWorkspaceRegistry::new(config);
238
239        let default_ws = Workspace::new("Default".to_string());
240        registry.register_workspace("default".to_string(), default_ws).unwrap();
241
242        let router = WorkspaceRouter::new(Arc::new(registry));
243
244        let context = router.extract_workspace_context("/workspace/test/api/users").unwrap();
245
246        // Should use default workspace when multi-tenant is disabled
247        assert_eq!(context.workspace_id, "default");
248        assert_eq!(context.stripped_path, "/workspace/test/api/users");
249    }
250}