mdb-engine 0.4.6__py3-none-any.whl → 0.4.9__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
mdb_engine/__init__.py CHANGED
@@ -82,8 +82,11 @@ from .repositories import Entity, MongoRepository, Repository, UnitOfWork
82
82
  from .utils import clean_mongo_doc, clean_mongo_docs
83
83
 
84
84
  __version__ = (
85
- "0.4.3" # Feature: Automatic route import for multi-app deployments
86
- # Routes from web.py/routes.py are now automatically imported when using create_multi_app()
85
+ "0.4.9" # Fix: WebSocket + CSRF + Multi-App architecture
86
+ # - CSRF middleware now added to parent app when child apps use shared auth
87
+ # - CORS config properly merged from child apps to parent app
88
+ # - WebSocket origin validation uses parent app's CORS config
89
+ # - Comprehensive integration tests added
87
90
  )
88
91
 
89
92
  __all__ = [
mdb_engine/auth/csrf.py CHANGED
@@ -190,6 +190,8 @@ class CSRFMiddleware(BaseHTTPMiddleware):
190
190
 
191
191
  def _is_exempt(self, path: str) -> bool:
192
192
  """Check if a path is exempt from CSRF validation."""
193
+ # WebSocket upgrade requests are handled separately in dispatch()
194
+ # Don't exempt them here - they need origin validation
193
195
  for pattern in self.exempt_routes:
194
196
  if fnmatch.fnmatch(path, pattern):
195
197
  return True
@@ -201,14 +203,42 @@ class CSRFMiddleware(BaseHTTPMiddleware):
201
203
  return upgrade_header == "websocket"
202
204
 
203
205
  def _get_allowed_origins(self, request: Request) -> list[str]:
204
- """Get allowed origins from app state (CORS config) or use request host as fallback."""
206
+ """
207
+ Get allowed origins from app state (CORS config) or use request host as fallback.
208
+
209
+ For multi-app setups, checks parent app's CORS config first (since WebSocket routes
210
+ are registered on parent app), then falls back to request host.
211
+ """
205
212
  try:
213
+ # Check current app's CORS config (parent app for WebSocket routes in multi-app)
206
214
  cors_config = getattr(request.app.state, "cors_config", None)
207
215
  if cors_config and cors_config.get("allow_origins"):
208
- return cors_config["allow_origins"]
216
+ origins = cors_config["allow_origins"]
217
+ if origins:
218
+ return origins if isinstance(origins, list) else [origins]
209
219
  except (AttributeError, TypeError, KeyError):
210
220
  pass
211
221
 
222
+ # Fallback: Check if this is a multi-app setup and try to find mounted app's CORS config
223
+ try:
224
+ if hasattr(request.app.state, "mounted_apps"):
225
+ # This is a parent app in multi-app setup
226
+ # Try to find which mounted app this request is for
227
+ path = request.url.path
228
+ mounted_apps = request.app.state.mounted_apps
229
+
230
+ # Find matching mounted app by path prefix
231
+ for app_info in mounted_apps:
232
+ path_prefix = app_info.get("path_prefix", "")
233
+ if path_prefix and path.startswith(path_prefix):
234
+ # Try to get child app's CORS config if available
235
+ # Note: Child app might not be directly accessible, so we rely on
236
+ # parent app's merged CORS config (set during mounting)
237
+ break
238
+ except (AttributeError, TypeError, KeyError):
239
+ pass
240
+
241
+ # Final fallback: Use request host
212
242
  try:
213
243
  host = request.url.hostname
214
244
  scheme = request.url.scheme
@@ -247,7 +277,8 @@ class CSRFMiddleware(BaseHTTPMiddleware):
247
277
 
248
278
  logger.warning(
249
279
  f"WebSocket upgrade rejected - invalid Origin: {origin} "
250
- f"(allowed: {allowed_origins})"
280
+ f"(allowed: {allowed_origins}, app: {getattr(request.app, 'title', 'unknown')}, "
281
+ f"has_cors_config: {hasattr(request.app.state, 'cors_config')})"
251
282
  )
252
283
  return False
253
284
 
@@ -262,12 +293,22 @@ class CSRFMiddleware(BaseHTTPMiddleware):
262
293
  path = request.url.path
263
294
  method = request.method
264
295
 
296
+ # CRITICAL: Handle WebSocket upgrade requests BEFORE other CSRF checks
297
+ # WebSocket upgrades don't use CSRF tokens, but need origin validation
265
298
  if self._is_websocket_upgrade(request):
299
+ # Validate origin for WebSocket connections (CSWSH protection)
266
300
  if not self._validate_websocket_origin(request):
301
+ logger.warning(
302
+ f"WebSocket origin validation failed for {path}: "
303
+ f"origin={request.headers.get('origin')}, "
304
+ f"allowed={self._get_allowed_origins(request)}"
305
+ )
267
306
  return JSONResponse(
268
307
  status_code=status.HTTP_403_FORBIDDEN,
269
308
  content={"detail": "Invalid origin for WebSocket connection"},
270
309
  )
310
+ # Origin validated - allow WebSocket upgrade to proceed
311
+ # No CSRF token check needed for WebSocket upgrades
271
312
  return await call_next(request)
272
313
 
273
314
  if self._is_exempt(path):
mdb_engine/core/engine.py CHANGED
@@ -2179,6 +2179,116 @@ class MongoDBEngine:
2179
2179
  return entry
2180
2180
  return None
2181
2181
 
2182
+ async def _merge_cors_config_to_parent(
2183
+ parent_app: "FastAPI",
2184
+ child_app: "FastAPI",
2185
+ child_manifest: dict[str, Any],
2186
+ slug: str,
2187
+ ) -> None:
2188
+ """Merge CORS config from child app to parent app."""
2189
+ child_cors = None
2190
+ if hasattr(child_app.state, "cors_config"):
2191
+ child_cors = child_app.state.cors_config
2192
+ else:
2193
+ # CORS config might not be set yet (lifespan runs asynchronously)
2194
+ # Get it from manifest directly
2195
+ cors_config_from_manifest = child_manifest.get("cors", {})
2196
+ if cors_config_from_manifest:
2197
+ from ..auth.config_helpers import (
2198
+ CORS_DEFAULTS,
2199
+ merge_config_with_defaults,
2200
+ )
2201
+
2202
+ child_cors = merge_config_with_defaults(
2203
+ cors_config_from_manifest, CORS_DEFAULTS
2204
+ )
2205
+ # Also set it on child app state for future reference
2206
+ child_app.state.cors_config = child_cors
2207
+
2208
+ if child_cors:
2209
+ if hasattr(parent_app.state, "cors_config"):
2210
+ # Merge child CORS into parent (child takes precedence for its routes)
2211
+ parent_cors = parent_app.state.cors_config
2212
+ # Merge allow_origins lists
2213
+ child_origins = child_cors.get("allow_origins", [])
2214
+ parent_origins = parent_cors.get("allow_origins", [])
2215
+ merged_origins = list(set(parent_origins + child_origins))
2216
+ parent_app.state.cors_config = {
2217
+ **parent_cors,
2218
+ **child_cors,
2219
+ "allow_origins": merged_origins if merged_origins else ["*"],
2220
+ }
2221
+ else:
2222
+ # Parent has no CORS config, use child's
2223
+ parent_app.state.cors_config = child_cors
2224
+ logger.debug(f"✅ Merged CORS config from child app '{slug}' to parent app")
2225
+
2226
+ async def _register_websocket_routes(
2227
+ parent_app: "FastAPI",
2228
+ child_manifest: dict[str, Any],
2229
+ slug: str,
2230
+ path_prefix: str,
2231
+ ) -> None:
2232
+ """Register WebSocket routes on parent app for a child app."""
2233
+ websockets_config = child_manifest.get("websockets")
2234
+ if not websockets_config:
2235
+ return
2236
+
2237
+ try:
2238
+ from fastapi import APIRouter
2239
+
2240
+ from ..routing.websockets import create_websocket_endpoint
2241
+
2242
+ for endpoint_name, endpoint_config in websockets_config.items():
2243
+ ws_path = endpoint_config.get("path", f"/{endpoint_name}")
2244
+ # Combine mount prefix with WebSocket path
2245
+ full_ws_path = f"{path_prefix.rstrip('/')}{ws_path}"
2246
+
2247
+ # Handle auth configuration
2248
+ auth_config = endpoint_config.get("auth", {})
2249
+ if isinstance(auth_config, dict) and "required" in auth_config:
2250
+ require_auth = auth_config.get("required", True)
2251
+ elif "require_auth" in endpoint_config:
2252
+ require_auth = endpoint_config.get("require_auth", True)
2253
+ else:
2254
+ # Use app's auth_policy if available
2255
+ if "auth_policy" in child_manifest:
2256
+ require_auth = child_manifest["auth_policy"].get("required", True)
2257
+ else:
2258
+ require_auth = True
2259
+
2260
+ ping_interval = endpoint_config.get("ping_interval", 30)
2261
+
2262
+ # Create WebSocket handler
2263
+ handler = create_websocket_endpoint(
2264
+ app_slug=slug,
2265
+ path=ws_path,
2266
+ endpoint_name=endpoint_name,
2267
+ handler=None,
2268
+ require_auth=require_auth,
2269
+ ping_interval=ping_interval,
2270
+ )
2271
+
2272
+ # Register on parent app with full path
2273
+ ws_router = APIRouter()
2274
+ ws_router.websocket(full_ws_path)(handler)
2275
+ parent_app.include_router(ws_router)
2276
+
2277
+ logger.info(
2278
+ f"✅ Registered WebSocket route '{full_ws_path}' "
2279
+ f"for mounted app '{slug}' (mounted at '{path_prefix}')"
2280
+ )
2281
+ except ImportError:
2282
+ logger.warning(
2283
+ f"WebSocket support not available - skipping WebSocket routes "
2284
+ f"for mounted app '{slug}'"
2285
+ )
2286
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
2287
+ logger.error(
2288
+ f"Failed to register WebSocket routes for mounted app '{slug}': {e}",
2289
+ exc_info=True,
2290
+ )
2291
+
2182
2292
  @asynccontextmanager
2183
2293
  async def lifespan(app: FastAPI):
2184
2294
  """Lifespan context manager for parent app."""
@@ -2375,6 +2485,13 @@ class MongoDBEngine:
2375
2485
 
2376
2486
  # Mount child app at path prefix
2377
2487
  app.mount(path_prefix, child_app)
2488
+
2489
+ # CRITICAL FIX: Merge CORS config from child app to parent app
2490
+ await _merge_cors_config_to_parent(app, child_app, app_manifest_data, slug)
2491
+
2492
+ # CRITICAL FIX: Register WebSocket routes on parent app with full path
2493
+ await _register_websocket_routes(app, app_manifest_data, slug, path_prefix)
2494
+
2378
2495
  # Update existing entry instead of appending
2379
2496
  entry = _find_mounted_app_entry(slug)
2380
2497
  if entry:
@@ -2550,6 +2667,16 @@ class MongoDBEngine:
2550
2667
  parent_app.state.is_multi_app = True
2551
2668
  parent_app.state.engine = engine
2552
2669
 
2670
+ # Set default CORS config on parent app for WebSocket origin validation
2671
+ # This ensures CSRF middleware can validate WebSocket origins even if child apps
2672
+ # don't configure CORS
2673
+ from ..auth.config_helpers import CORS_DEFAULTS
2674
+
2675
+ parent_app.state.cors_config = CORS_DEFAULTS.copy()
2676
+ parent_app.state.cors_config["enabled"] = True
2677
+ parent_app.state.cors_config["allow_origins"] = ["*"] # Default to allow all for WebSocket
2678
+ logger.debug("Set default CORS config on parent app for WebSocket origin validation")
2679
+
2553
2680
  # Store app reference in engine for get_mounted_apps()
2554
2681
  engine._multi_app_instance = parent_app
2555
2682
 
@@ -2572,15 +2699,35 @@ class MongoDBEngine:
2572
2699
  parent_app.add_middleware(RequestScopeMiddleware)
2573
2700
  logger.debug("RequestScopeMiddleware added for parent app")
2574
2701
 
2702
+ # CRITICAL: Add CSRF middleware to parent app if any child app uses shared auth
2703
+ # WebSocket routes are registered on parent app, so parent app middleware runs first
2704
+ # CSRF middleware on parent app validates WebSocket origin using parent app's CORS config
2705
+ if has_shared_auth:
2706
+ from ..auth.csrf import create_csrf_middleware
2707
+
2708
+ # Create CSRF middleware with default config (will use parent app's CORS config)
2709
+ # Exempt routes that don't need CSRF (health checks, etc.)
2710
+ parent_csrf_config = {
2711
+ "csrf_protection": True,
2712
+ "public_routes": ["/health", "/docs", "/openapi.json", "/_mdb/routes"],
2713
+ }
2714
+ csrf_middleware = create_csrf_middleware(parent_csrf_config)
2715
+ parent_app.add_middleware(csrf_middleware)
2716
+ logger.info("CSRFMiddleware added to parent app for WebSocket origin validation")
2717
+
2575
2718
  # Add shared CORS middleware if configured
2576
2719
  # (Individual apps can add their own CORS, but parent-level is useful)
2577
2720
  try:
2578
2721
  from fastapi.middleware.cors import CORSMiddleware
2579
2722
 
2723
+ # Use CORS config from parent app state (set above)
2724
+ cors_origins = parent_app.state.cors_config.get("allow_origins", ["*"])
2725
+ cors_credentials = parent_app.state.cors_config.get("allow_credentials", True)
2726
+
2580
2727
  parent_app.add_middleware(
2581
2728
  CORSMiddleware,
2582
- allow_origins=["*"], # Can be configured via manifest later
2583
- allow_credentials=True,
2729
+ allow_origins=cors_origins,
2730
+ allow_credentials=cors_credentials,
2584
2731
  allow_methods=["*"],
2585
2732
  allow_headers=["*"],
2586
2733
  )
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.6
3
+ Version: 0.4.9
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -1,5 +1,5 @@
1
1
  mdb_engine/README.md,sha256=T3EFGcPopY9LslYW3lxgG3hohWkAOmBNbYG0FDMUJiY,3502
2
- mdb_engine/__init__.py,sha256=Hm7dL74-37z0tXYLcgCV5f6CZ_ZDOaTACYw4N863OCA,3262
2
+ mdb_engine/__init__.py,sha256=AFQ20lpN4UqRakhVC_JPQzqJkvLAbwgYbgCnFBaYet4,3413
3
3
  mdb_engine/config.py,sha256=DTAyxfKB8ogyI0v5QR9Y-SJOgXQr_eDBCKxNBSqEyLc,7269
4
4
  mdb_engine/constants.py,sha256=eaotvW57TVOg7rRbLziGrVNoP7adgw_G9iVByHezc_A,7837
5
5
  mdb_engine/dependencies.py,sha256=MJuYQhZ9ZGzXlip1ha5zba9Rvn04HDPWahJFJH81Q2s,14107
@@ -14,7 +14,7 @@ mdb_engine/auth/casbin_models.py,sha256=7XtFmRBhhjw1nKprnluvjyJoTj5fzdPeQwVvo6fI
14
14
  mdb_engine/auth/config_defaults.py,sha256=1YI_hIHuTiEXpkEYMcufNHdLr1oxPiJylg3CKrJCSGY,2012
15
15
  mdb_engine/auth/config_helpers.py,sha256=Qharb2YagLOKDGtE7XhYRDbBoQ_KGykrcIKrsOwWIJ4,6303
16
16
  mdb_engine/auth/cookie_utils.py,sha256=j04qXq5GiJrnnJUAP5Z_N1CAFbx9CZiyF5u9xIiQ3vo,4876
17
- mdb_engine/auth/csrf.py,sha256=Xt_u4V8QXxsCS6KPh5Q54pN5BTbvWMBdJW-CFmBQtEY,14916
17
+ mdb_engine/auth/csrf.py,sha256=__vbWkqt6LcEoib53q14Or7hGkG7yKC2UZdbf8Si3h4,17211
18
18
  mdb_engine/auth/decorators.py,sha256=LkVVEuRrT0Iz8EwctN14BEi3fSV-xtN6DaGXgtbiYYo,12287
19
19
  mdb_engine/auth/dependencies.py,sha256=JB1iYvZJgTR6gcaiGe_GJFCS6NdUKMxWBZRv6vVxnzw,27112
20
20
  mdb_engine/auth/helpers.py,sha256=BCrid985cYh-3h5ZMUV9TES0q40uJXio4oYKQZta7KA,1970
@@ -46,7 +46,7 @@ mdb_engine/core/app_registration.py,sha256=7szt2a7aBkpSppjmhdkkPPYMKGKo0MkLKZeEe
46
46
  mdb_engine/core/app_secrets.py,sha256=bo-syg9UUATibNyXEZs-0TTYWG-JaY-2S0yNSGA12n0,10524
47
47
  mdb_engine/core/connection.py,sha256=XnwuPG34pJ7kJGJ84T0mhj1UZ6_CLz_9qZf6NRYGIS8,8346
48
48
  mdb_engine/core/encryption.py,sha256=RZ5LPF5g28E3ZBn6v1IMw_oas7u9YGFtBcEj8lTi9LM,7515
49
- mdb_engine/core/engine.py,sha256=uknOG_98LIkmgwTUAnm_8ue-S8pWs3XjMZKa9oDAIxM,133799
49
+ mdb_engine/core/engine.py,sha256=1coGBNfC1f-tFV5mlRMN9WBkk3IkV3atH36dSUZyETQ,140964
50
50
  mdb_engine/core/index_management.py,sha256=9-r7MIy3JnjQ35sGqsbj8K_I07vAUWtAVgSWC99lJcE,5555
51
51
  mdb_engine/core/manifest.py,sha256=jguhjVPAHMZGxOJcdSGouv9_XiKmxUjDmyjn2yXHCj4,139205
52
52
  mdb_engine/core/ray_integration.py,sha256=vexYOzztscvRYje1xTNmXJbi99oJxCaVJAwKfTNTF_E,13610
@@ -89,9 +89,9 @@ mdb_engine/routing/__init__.py,sha256=reupjHi_RTc2ZBA4AH5XzobAmqy4EQIsfSUcTkFknU
89
89
  mdb_engine/routing/websockets.py,sha256=3X4OjQv_Nln4UmeifJky0gFhMG8A6alR77I8g1iIOLY,29311
90
90
  mdb_engine/utils/__init__.py,sha256=lDxQSGqkV4fVw5TWIk6FA6_eey_ZnEtMY0fir3cpAe8,236
91
91
  mdb_engine/utils/mongo.py,sha256=Oqtv4tQdpiiZzrilGLEYQPo8Vmh8WsTQypxQs8Of53s,3369
92
- mdb_engine-0.4.6.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
93
- mdb_engine-0.4.6.dist-info/METADATA,sha256=LJYFe_FP0gBUVt2uDSXtvS15Vp7qNQxe4WVrX8zsthY,15810
94
- mdb_engine-0.4.6.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
95
- mdb_engine-0.4.6.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
96
- mdb_engine-0.4.6.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
97
- mdb_engine-0.4.6.dist-info/RECORD,,
92
+ mdb_engine-0.4.9.dist-info/licenses/LICENSE,sha256=hIahDEOTzuHCU5J2nd07LWwkLW7Hko4UFO__ffsvB-8,34523
93
+ mdb_engine-0.4.9.dist-info/METADATA,sha256=ncHUSb3JIKOLV1bUrn1BZWoMR-qUg2QEvmWQ0dzFlnM,15810
94
+ mdb_engine-0.4.9.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
95
+ mdb_engine-0.4.9.dist-info/entry_points.txt,sha256=INCbYdFbBzJalwPwxliEzLmPfR57IvQ7RAXG_pn8cL8,48
96
+ mdb_engine-0.4.9.dist-info/top_level.txt,sha256=PH0UEBwTtgkm2vWvC9He_EOMn7hVn_Wg_Jyc0SmeO8k,11
97
+ mdb_engine-0.4.9.dist-info/RECORD,,