mdb-engine 0.4.8__tar.gz → 0.4.10__tar.gz

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.
Files changed (104) hide show
  1. {mdb_engine-0.4.8/mdb_engine.egg-info → mdb_engine-0.4.10}/PKG-INFO +1 -1
  2. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/__init__.py +5 -2
  3. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/csrf.py +44 -3
  4. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/engine.py +248 -74
  5. {mdb_engine-0.4.8 → mdb_engine-0.4.10/mdb_engine.egg-info}/PKG-INFO +1 -1
  6. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/pyproject.toml +1 -1
  7. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/LICENSE +0 -0
  8. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/MANIFEST.in +0 -0
  9. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/README.md +0 -0
  10. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/README.md +0 -0
  11. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/ARCHITECTURE.md +0 -0
  12. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/README.md +0 -0
  13. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/__init__.py +0 -0
  14. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/audit.py +0 -0
  15. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/base.py +0 -0
  16. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/casbin_factory.py +0 -0
  17. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/casbin_models.py +0 -0
  18. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/config_defaults.py +0 -0
  19. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/config_helpers.py +0 -0
  20. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/cookie_utils.py +0 -0
  21. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/decorators.py +0 -0
  22. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/dependencies.py +0 -0
  23. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/helpers.py +0 -0
  24. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/integration.py +0 -0
  25. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/jwt.py +0 -0
  26. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/middleware.py +0 -0
  27. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/oso_factory.py +0 -0
  28. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/provider.py +0 -0
  29. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/rate_limiter.py +0 -0
  30. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/restrictions.py +0 -0
  31. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/session_manager.py +0 -0
  32. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/shared_middleware.py +0 -0
  33. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/shared_users.py +0 -0
  34. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/token_lifecycle.py +0 -0
  35. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/token_store.py +0 -0
  36. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/users.py +0 -0
  37. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/auth/utils.py +0 -0
  38. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/__init__.py +0 -0
  39. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/commands/__init__.py +0 -0
  40. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/commands/generate.py +0 -0
  41. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/commands/migrate.py +0 -0
  42. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/commands/show.py +0 -0
  43. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/commands/validate.py +0 -0
  44. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/main.py +0 -0
  45. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/cli/utils.py +0 -0
  46. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/config.py +0 -0
  47. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/constants.py +0 -0
  48. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/README.md +0 -0
  49. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/__init__.py +0 -0
  50. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/app_registration.py +0 -0
  51. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/app_secrets.py +0 -0
  52. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/connection.py +0 -0
  53. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/encryption.py +0 -0
  54. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/index_management.py +0 -0
  55. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/manifest.py +0 -0
  56. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/ray_integration.py +0 -0
  57. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/seeding.py +0 -0
  58. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/service_initialization.py +0 -0
  59. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/core/types.py +0 -0
  60. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/README.md +0 -0
  61. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/__init__.py +0 -0
  62. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/abstraction.py +0 -0
  63. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/connection.py +0 -0
  64. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/query_validator.py +0 -0
  65. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/resource_limiter.py +0 -0
  66. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/database/scoped_wrapper.py +0 -0
  67. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/dependencies.py +0 -0
  68. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/di/__init__.py +0 -0
  69. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/di/container.py +0 -0
  70. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/di/providers.py +0 -0
  71. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/di/scopes.py +0 -0
  72. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/embeddings/README.md +0 -0
  73. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/embeddings/__init__.py +0 -0
  74. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/embeddings/dependencies.py +0 -0
  75. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/embeddings/service.py +0 -0
  76. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/exceptions.py +0 -0
  77. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/indexes/README.md +0 -0
  78. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/indexes/__init__.py +0 -0
  79. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/indexes/helpers.py +0 -0
  80. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/indexes/manager.py +0 -0
  81. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/memory/README.md +0 -0
  82. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/memory/__init__.py +0 -0
  83. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/memory/service.py +0 -0
  84. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/observability/README.md +0 -0
  85. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/observability/__init__.py +0 -0
  86. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/observability/health.py +0 -0
  87. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/observability/logging.py +0 -0
  88. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/observability/metrics.py +0 -0
  89. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/repositories/__init__.py +0 -0
  90. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/repositories/base.py +0 -0
  91. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/repositories/mongo.py +0 -0
  92. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/repositories/unit_of_work.py +0 -0
  93. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/routing/README.md +0 -0
  94. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/routing/__init__.py +0 -0
  95. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/routing/websockets.py +0 -0
  96. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/utils/__init__.py +0 -0
  97. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine/utils/mongo.py +0 -0
  98. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine.egg-info/SOURCES.txt +0 -0
  99. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine.egg-info/dependency_links.txt +0 -0
  100. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine.egg-info/entry_points.txt +0 -0
  101. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine.egg-info/requires.txt +0 -0
  102. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/mdb_engine.egg-info/top_level.txt +0 -0
  103. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/setup.cfg +0 -0
  104. {mdb_engine-0.4.8 → mdb_engine-0.4.10}/setup.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -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.8" # Fix: WebSocket routes now work correctly with mounted apps
86
- # WebSocket routes are registered on parent app with full mount path prefix
85
+ "0.4.10" # Fix: allow_credentials preserved in CORS config merge
86
+ # - Fixed bug where allow_credentials was set to False even when child apps require True
87
+ # - Dynamic CORS middleware reads from app.state.cors_config at request time
88
+ # - Merge logic now ensures if ANY child app has allow_credentials: True, parent gets True
89
+ # - Essential for SSO cookie-based authentication in WebSocket connections
87
90
  )
88
91
 
89
92
  __all__ = [
@@ -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):
@@ -2179,6 +2179,130 @@ 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
+
2217
+ # CRITICAL: If ANY child app requires credentials, parent must allow them
2218
+ # This is essential for SSO cookie-based authentication
2219
+ child_requires_credentials = child_cors.get("allow_credentials", False)
2220
+ parent_allows_credentials = parent_cors.get("allow_credentials", False)
2221
+ merged_allow_credentials = (
2222
+ child_requires_credentials or parent_allows_credentials
2223
+ )
2224
+
2225
+ parent_app.state.cors_config = {
2226
+ **parent_cors,
2227
+ **child_cors,
2228
+ "allow_origins": merged_origins if merged_origins else ["*"],
2229
+ # If ANY child requires credentials, parent gets True (for SSO)
2230
+ "allow_credentials": merged_allow_credentials,
2231
+ }
2232
+ else:
2233
+ # Parent has no CORS config, use child's
2234
+ parent_app.state.cors_config = child_cors
2235
+ logger.debug(
2236
+ f"✅ Merged CORS config from child app '{slug}' to parent app "
2237
+ f"(allow_credentials={parent_app.state.cors_config.get('allow_credentials')})"
2238
+ )
2239
+
2240
+ async def _register_websocket_routes(
2241
+ parent_app: "FastAPI",
2242
+ child_manifest: dict[str, Any],
2243
+ slug: str,
2244
+ path_prefix: str,
2245
+ ) -> None:
2246
+ """Register WebSocket routes on parent app for a child app."""
2247
+ websockets_config = child_manifest.get("websockets")
2248
+ if not websockets_config:
2249
+ return
2250
+
2251
+ try:
2252
+ from fastapi import APIRouter
2253
+
2254
+ from ..routing.websockets import create_websocket_endpoint
2255
+
2256
+ for endpoint_name, endpoint_config in websockets_config.items():
2257
+ ws_path = endpoint_config.get("path", f"/{endpoint_name}")
2258
+ # Combine mount prefix with WebSocket path
2259
+ full_ws_path = f"{path_prefix.rstrip('/')}{ws_path}"
2260
+
2261
+ # Handle auth configuration
2262
+ auth_config = endpoint_config.get("auth", {})
2263
+ if isinstance(auth_config, dict) and "required" in auth_config:
2264
+ require_auth = auth_config.get("required", True)
2265
+ elif "require_auth" in endpoint_config:
2266
+ require_auth = endpoint_config.get("require_auth", True)
2267
+ else:
2268
+ # Use app's auth_policy if available
2269
+ if "auth_policy" in child_manifest:
2270
+ require_auth = child_manifest["auth_policy"].get("required", True)
2271
+ else:
2272
+ require_auth = True
2273
+
2274
+ ping_interval = endpoint_config.get("ping_interval", 30)
2275
+
2276
+ # Create WebSocket handler
2277
+ handler = create_websocket_endpoint(
2278
+ app_slug=slug,
2279
+ path=ws_path,
2280
+ endpoint_name=endpoint_name,
2281
+ handler=None,
2282
+ require_auth=require_auth,
2283
+ ping_interval=ping_interval,
2284
+ )
2285
+
2286
+ # Register on parent app with full path
2287
+ ws_router = APIRouter()
2288
+ ws_router.websocket(full_ws_path)(handler)
2289
+ parent_app.include_router(ws_router)
2290
+
2291
+ logger.info(
2292
+ f"✅ Registered WebSocket route '{full_ws_path}' "
2293
+ f"for mounted app '{slug}' (mounted at '{path_prefix}')"
2294
+ )
2295
+ except ImportError:
2296
+ logger.warning(
2297
+ f"WebSocket support not available - skipping WebSocket routes "
2298
+ f"for mounted app '{slug}'"
2299
+ )
2300
+ except (ValueError, TypeError, AttributeError, RuntimeError) as e:
2301
+ logger.error(
2302
+ f"Failed to register WebSocket routes for mounted app '{slug}': {e}",
2303
+ exc_info=True,
2304
+ )
2305
+
2182
2306
  @asynccontextmanager
2183
2307
  async def lifespan(app: FastAPI):
2184
2308
  """Lifespan context manager for parent app."""
@@ -2376,71 +2500,11 @@ class MongoDBEngine:
2376
2500
  # Mount child app at path prefix
2377
2501
  app.mount(path_prefix, child_app)
2378
2502
 
2379
- # CRITICAL FIX: Register WebSocket routes on parent app with full path
2380
- # FastAPI's app.mount() doesn't handle WebSocket routes correctly,
2381
- # so we need to register them on the parent app with the mount prefix
2382
- # Get WebSocket config from manifest directly (app registration happens
2383
- # asynchronously in lifespan, so config may not be available yet)
2384
- websockets_config = app_manifest_data.get("websockets")
2385
- if websockets_config:
2386
- try:
2387
- from fastapi import APIRouter
2388
-
2389
- from ..routing.websockets import create_websocket_endpoint
2390
-
2391
- for endpoint_name, endpoint_config in websockets_config.items():
2392
- ws_path = endpoint_config.get("path", f"/{endpoint_name}")
2393
- # Combine mount prefix with WebSocket path
2394
- full_ws_path = f"{path_prefix.rstrip('/')}{ws_path}"
2395
-
2396
- # Handle auth configuration
2397
- auth_config = endpoint_config.get("auth", {})
2398
- if isinstance(auth_config, dict) and "required" in auth_config:
2399
- require_auth = auth_config.get("required", True)
2400
- elif "require_auth" in endpoint_config:
2401
- require_auth = endpoint_config.get("require_auth", True)
2402
- else:
2403
- # Use app's auth_policy if available
2404
- if "auth_policy" in app_manifest_data:
2405
- require_auth = app_manifest_data["auth_policy"].get(
2406
- "required", True
2407
- )
2408
- else:
2409
- require_auth = True
2410
-
2411
- ping_interval = endpoint_config.get("ping_interval", 30)
2412
-
2413
- # Create WebSocket handler
2414
- # Use original path for handler (mount handled internally)
2415
- handler = create_websocket_endpoint(
2416
- app_slug=slug,
2417
- path=ws_path,
2418
- endpoint_name=endpoint_name,
2419
- handler=None,
2420
- require_auth=require_auth,
2421
- ping_interval=ping_interval,
2422
- )
2503
+ # CRITICAL FIX: Merge CORS config from child app to parent app
2504
+ await _merge_cors_config_to_parent(app, child_app, app_manifest_data, slug)
2423
2505
 
2424
- # Register on parent app with full path
2425
- ws_router = APIRouter()
2426
- ws_router.websocket(full_ws_path)(handler)
2427
- app.include_router(ws_router)
2428
-
2429
- logger.info(
2430
- f"✅ Registered WebSocket route '{full_ws_path}' "
2431
- f"for mounted app '{slug}' (mounted at '{path_prefix}')"
2432
- )
2433
- except ImportError:
2434
- logger.warning(
2435
- f"WebSocket support not available - skipping WebSocket routes "
2436
- f"for mounted app '{slug}'"
2437
- )
2438
- except (ValueError, TypeError, AttributeError, RuntimeError) as e:
2439
- logger.error(
2440
- f"Failed to register WebSocket routes for mounted app "
2441
- f"'{slug}': {e}",
2442
- exc_info=True,
2443
- )
2506
+ # CRITICAL FIX: Register WebSocket routes on parent app with full path
2507
+ await _register_websocket_routes(app, app_manifest_data, slug, path_prefix)
2444
2508
 
2445
2509
  # Update existing entry instead of appending
2446
2510
  entry = _find_mounted_app_entry(slug)
@@ -2617,6 +2681,19 @@ class MongoDBEngine:
2617
2681
  parent_app.state.is_multi_app = True
2618
2682
  parent_app.state.engine = engine
2619
2683
 
2684
+ # Set default CORS config on parent app for WebSocket origin validation
2685
+ # This ensures CSRF middleware can validate WebSocket origins even if child apps
2686
+ # don't configure CORS
2687
+ # NOTE: allow_credentials defaults to False here, but will be set to True
2688
+ # during merge if any child app requires it (essential for SSO cookie-based auth)
2689
+ from ..auth.config_helpers import CORS_DEFAULTS
2690
+
2691
+ parent_app.state.cors_config = CORS_DEFAULTS.copy()
2692
+ parent_app.state.cors_config["enabled"] = True
2693
+ parent_app.state.cors_config["allow_origins"] = ["*"] # Default to allow all for WebSocket
2694
+ # Keep allow_credentials as False initially - will be merged from child apps
2695
+ logger.debug("Set default CORS config on parent app for WebSocket origin validation")
2696
+
2620
2697
  # Store app reference in engine for get_mounted_apps()
2621
2698
  engine._multi_app_instance = parent_app
2622
2699
 
@@ -2639,19 +2716,116 @@ class MongoDBEngine:
2639
2716
  parent_app.add_middleware(RequestScopeMiddleware)
2640
2717
  logger.debug("RequestScopeMiddleware added for parent app")
2641
2718
 
2719
+ # CRITICAL: Add CSRF middleware to parent app if any child app uses shared auth
2720
+ # WebSocket routes are registered on parent app, so parent app middleware runs first
2721
+ # CSRF middleware on parent app validates WebSocket origin using parent app's CORS config
2722
+ if has_shared_auth:
2723
+ from ..auth.csrf import create_csrf_middleware
2724
+
2725
+ # Create CSRF middleware with default config (will use parent app's CORS config)
2726
+ # Exempt routes that don't need CSRF (health checks, etc.)
2727
+ parent_csrf_config = {
2728
+ "csrf_protection": True,
2729
+ "public_routes": ["/health", "/docs", "/openapi.json", "/_mdb/routes"],
2730
+ }
2731
+ csrf_middleware = create_csrf_middleware(parent_csrf_config)
2732
+ parent_app.add_middleware(csrf_middleware)
2733
+ logger.info("CSRFMiddleware added to parent app for WebSocket origin validation")
2734
+
2642
2735
  # Add shared CORS middleware if configured
2643
- # (Individual apps can add their own CORS, but parent-level is useful)
2736
+ # NOTE: We create a dynamic CORS middleware that reads from app.state.cors_config
2737
+ # This allows the config to be updated after child apps are mounted and merged
2644
2738
  try:
2645
- from fastapi.middleware.cors import CORSMiddleware
2646
-
2647
- parent_app.add_middleware(
2648
- CORSMiddleware,
2649
- allow_origins=["*"], # Can be configured via manifest later
2650
- allow_credentials=True,
2651
- allow_methods=["*"],
2652
- allow_headers=["*"],
2739
+ from starlette.middleware.base import BaseHTTPMiddleware
2740
+ from starlette.requests import Request
2741
+ from starlette.responses import Response
2742
+
2743
+ class DynamicCORSMiddleware(BaseHTTPMiddleware):
2744
+ """
2745
+ Dynamic CORS middleware that reads config from app.state.cors_config.
2746
+
2747
+ This allows CORS config to be updated after child apps are mounted
2748
+ and their configs are merged, which is essential for SSO multi-app
2749
+ setups where allow_credentials must be True for cookie-based auth.
2750
+ """
2751
+
2752
+ async def dispatch(self, request: Request, call_next):
2753
+ # Read CORS config from app.state (may have been merged from child apps)
2754
+ cors_config = getattr(request.app.state, "cors_config", {})
2755
+
2756
+ if not cors_config.get("enabled", False):
2757
+ # CORS not enabled, pass through
2758
+ return await call_next(request)
2759
+
2760
+ # Handle preflight OPTIONS request
2761
+ if request.method == "OPTIONS":
2762
+ origin = request.headers.get("origin")
2763
+ allowed_origins = cors_config.get("allow_origins", ["*"])
2764
+ allow_credentials = cors_config.get("allow_credentials", False)
2765
+
2766
+ # Check if origin is allowed
2767
+ origin_allowed = False
2768
+ if "*" in allowed_origins:
2769
+ origin_allowed = True
2770
+ elif origin in allowed_origins:
2771
+ origin_allowed = True
2772
+
2773
+ if origin_allowed:
2774
+ headers = {
2775
+ "Access-Control-Allow-Methods": ", ".join(
2776
+ cors_config.get("allow_methods", ["*"])
2777
+ ),
2778
+ "Access-Control-Allow-Headers": ", ".join(
2779
+ cors_config.get("allow_headers", ["*"])
2780
+ ),
2781
+ "Access-Control-Max-Age": str(cors_config.get("max_age", 3600)),
2782
+ }
2783
+ if allow_credentials:
2784
+ headers["Access-Control-Allow-Credentials"] = "true"
2785
+ if origin:
2786
+ headers["Access-Control-Allow-Origin"] = origin
2787
+
2788
+ expose_headers = cors_config.get("expose_headers", [])
2789
+ if expose_headers:
2790
+ headers["Access-Control-Expose-Headers"] = ", ".join(expose_headers)
2791
+
2792
+ return Response(status_code=200, headers=headers)
2793
+ else:
2794
+ return Response(status_code=403)
2795
+
2796
+ # Handle actual request
2797
+ response = await call_next(request)
2798
+
2799
+ # Add CORS headers to response
2800
+ origin = request.headers.get("origin")
2801
+ allowed_origins = cors_config.get("allow_origins", ["*"])
2802
+ allow_credentials = cors_config.get("allow_credentials", False)
2803
+
2804
+ # Check if origin is allowed
2805
+ origin_allowed = False
2806
+ if "*" in allowed_origins:
2807
+ origin_allowed = True
2808
+ elif origin and origin in allowed_origins:
2809
+ origin_allowed = True
2810
+
2811
+ if origin_allowed:
2812
+ if origin:
2813
+ response.headers["Access-Control-Allow-Origin"] = origin
2814
+ if allow_credentials:
2815
+ response.headers["Access-Control-Allow-Credentials"] = "true"
2816
+
2817
+ expose_headers = cors_config.get("expose_headers", [])
2818
+ if expose_headers:
2819
+ response.headers["Access-Control-Expose-Headers"] = ", ".join(
2820
+ expose_headers
2821
+ )
2822
+
2823
+ return response
2824
+
2825
+ parent_app.add_middleware(DynamicCORSMiddleware)
2826
+ logger.debug(
2827
+ "Dynamic CORS middleware added for parent app (reads from app.state.cors_config)"
2653
2828
  )
2654
- logger.debug("CORS middleware added for parent app")
2655
2829
  except ImportError:
2656
2830
  logger.warning("CORS middleware not available")
2657
2831
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: mdb-engine
3
- Version: 0.4.8
3
+ Version: 0.4.10
4
4
  Summary: MongoDB Engine
5
5
  Home-page: https://github.com/ranfysvalle02/mdb-engine
6
6
  Author: Fabian Valle
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "mdb-engine"
7
- version = "0.4.8"
7
+ version = "0.4.10"
8
8
  description = "MongoDB Engine"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
File without changes
File without changes
File without changes
File without changes
File without changes