mdb-engine 0.1.6__py3-none-any.whl → 0.4.12__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.
Files changed (92) hide show
  1. mdb_engine/__init__.py +116 -11
  2. mdb_engine/auth/ARCHITECTURE.md +112 -0
  3. mdb_engine/auth/README.md +654 -11
  4. mdb_engine/auth/__init__.py +136 -29
  5. mdb_engine/auth/audit.py +592 -0
  6. mdb_engine/auth/base.py +252 -0
  7. mdb_engine/auth/casbin_factory.py +265 -70
  8. mdb_engine/auth/config_defaults.py +5 -5
  9. mdb_engine/auth/config_helpers.py +19 -18
  10. mdb_engine/auth/cookie_utils.py +12 -16
  11. mdb_engine/auth/csrf.py +483 -0
  12. mdb_engine/auth/decorators.py +10 -16
  13. mdb_engine/auth/dependencies.py +69 -71
  14. mdb_engine/auth/helpers.py +3 -3
  15. mdb_engine/auth/integration.py +61 -88
  16. mdb_engine/auth/jwt.py +11 -15
  17. mdb_engine/auth/middleware.py +79 -35
  18. mdb_engine/auth/oso_factory.py +21 -41
  19. mdb_engine/auth/provider.py +270 -171
  20. mdb_engine/auth/rate_limiter.py +505 -0
  21. mdb_engine/auth/restrictions.py +21 -36
  22. mdb_engine/auth/session_manager.py +24 -41
  23. mdb_engine/auth/shared_middleware.py +977 -0
  24. mdb_engine/auth/shared_users.py +775 -0
  25. mdb_engine/auth/token_lifecycle.py +10 -12
  26. mdb_engine/auth/token_store.py +17 -32
  27. mdb_engine/auth/users.py +99 -159
  28. mdb_engine/auth/utils.py +236 -42
  29. mdb_engine/cli/commands/generate.py +546 -10
  30. mdb_engine/cli/commands/validate.py +3 -7
  31. mdb_engine/cli/utils.py +7 -7
  32. mdb_engine/config.py +13 -28
  33. mdb_engine/constants.py +65 -0
  34. mdb_engine/core/README.md +117 -6
  35. mdb_engine/core/__init__.py +39 -7
  36. mdb_engine/core/app_registration.py +31 -50
  37. mdb_engine/core/app_secrets.py +289 -0
  38. mdb_engine/core/connection.py +20 -12
  39. mdb_engine/core/encryption.py +222 -0
  40. mdb_engine/core/engine.py +2862 -115
  41. mdb_engine/core/index_management.py +12 -16
  42. mdb_engine/core/manifest.py +628 -204
  43. mdb_engine/core/ray_integration.py +436 -0
  44. mdb_engine/core/seeding.py +13 -21
  45. mdb_engine/core/service_initialization.py +20 -30
  46. mdb_engine/core/types.py +40 -43
  47. mdb_engine/database/README.md +140 -17
  48. mdb_engine/database/__init__.py +17 -6
  49. mdb_engine/database/abstraction.py +37 -50
  50. mdb_engine/database/connection.py +51 -30
  51. mdb_engine/database/query_validator.py +367 -0
  52. mdb_engine/database/resource_limiter.py +204 -0
  53. mdb_engine/database/scoped_wrapper.py +747 -237
  54. mdb_engine/dependencies.py +427 -0
  55. mdb_engine/di/__init__.py +34 -0
  56. mdb_engine/di/container.py +247 -0
  57. mdb_engine/di/providers.py +206 -0
  58. mdb_engine/di/scopes.py +139 -0
  59. mdb_engine/embeddings/README.md +54 -24
  60. mdb_engine/embeddings/__init__.py +31 -24
  61. mdb_engine/embeddings/dependencies.py +38 -155
  62. mdb_engine/embeddings/service.py +78 -75
  63. mdb_engine/exceptions.py +104 -12
  64. mdb_engine/indexes/README.md +30 -13
  65. mdb_engine/indexes/__init__.py +1 -0
  66. mdb_engine/indexes/helpers.py +11 -11
  67. mdb_engine/indexes/manager.py +59 -123
  68. mdb_engine/memory/README.md +95 -4
  69. mdb_engine/memory/__init__.py +1 -2
  70. mdb_engine/memory/service.py +363 -1168
  71. mdb_engine/observability/README.md +4 -2
  72. mdb_engine/observability/__init__.py +26 -9
  73. mdb_engine/observability/health.py +17 -17
  74. mdb_engine/observability/logging.py +10 -10
  75. mdb_engine/observability/metrics.py +40 -19
  76. mdb_engine/repositories/__init__.py +34 -0
  77. mdb_engine/repositories/base.py +325 -0
  78. mdb_engine/repositories/mongo.py +233 -0
  79. mdb_engine/repositories/unit_of_work.py +166 -0
  80. mdb_engine/routing/README.md +1 -1
  81. mdb_engine/routing/__init__.py +1 -3
  82. mdb_engine/routing/websockets.py +41 -75
  83. mdb_engine/utils/__init__.py +3 -1
  84. mdb_engine/utils/mongo.py +117 -0
  85. mdb_engine-0.4.12.dist-info/METADATA +492 -0
  86. mdb_engine-0.4.12.dist-info/RECORD +97 -0
  87. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/WHEEL +1 -1
  88. mdb_engine-0.1.6.dist-info/METADATA +0 -213
  89. mdb_engine-0.1.6.dist-info/RECORD +0 -75
  90. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/entry_points.txt +0 -0
  91. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/licenses/LICENSE +0 -0
  92. {mdb_engine-0.1.6.dist-info → mdb_engine-0.4.12.dist-info}/top_level.txt +0 -0
@@ -9,14 +9,17 @@ This module is part of MDB_ENGINE - MongoDB Engine.
9
9
  import logging
10
10
  import os
11
11
  import uuid
12
+ from collections.abc import Mapping
12
13
  from datetime import datetime, timedelta
13
- from typing import Any, Dict, Mapping, Optional, Tuple
14
+ from typing import Any
14
15
 
15
16
  import jwt
16
17
  from fastapi import Cookie, Depends, HTTPException, Request, status
18
+ from pymongo.errors import PyMongoError
17
19
 
18
20
  from ..exceptions import ConfigurationError
19
21
  from .jwt import decode_jwt_token, extract_token_metadata
22
+
20
23
  # Import from local modules
21
24
  from .provider import AuthorizationProvider
22
25
  from .session_manager import SessionManager
@@ -24,7 +27,7 @@ from .token_store import TokenBlacklist
24
27
 
25
28
  logger = logging.getLogger(__name__)
26
29
 
27
- _SECRET_KEY_CACHE: Optional[str] = None
30
+ _SECRET_KEY_CACHE: str | None = None
28
31
 
29
32
 
30
33
  def _get_secret_key() -> str:
@@ -39,15 +42,20 @@ def _get_secret_key() -> str:
39
42
  if _SECRET_KEY_CACHE is not None:
40
43
  return _SECRET_KEY_CACHE
41
44
 
42
- secret_key = os.environ.get("FLASK_SECRET_KEY") or os.environ.get("SECRET_KEY")
45
+ secret_key = (
46
+ os.environ.get("FLASK_SECRET_KEY")
47
+ or os.environ.get("SECRET_KEY")
48
+ or os.environ.get("APP_SECRET_KEY")
49
+ )
43
50
 
44
51
  if not secret_key:
45
52
  raise ConfigurationError(
46
- "FLASK_SECRET_KEY environment variable is required for JWT token security. "
47
- "Set a strong secret key (minimum 32 characters, cryptographically random). "
48
- "Example: export FLASK_SECRET_KEY=$(python -c "
53
+ "SECRET_KEY environment variable is required for JWT token security. "
54
+ "Set FLASK_SECRET_KEY, SECRET_KEY, or APP_SECRET_KEY with a strong secret key "
55
+ "(minimum 32 characters, cryptographically random). "
56
+ "Example: export SECRET_KEY=$(python -c "
49
57
  "'import secrets; print(secrets.token_urlsafe(32))')",
50
- config_key="FLASK_SECRET_KEY",
58
+ config_key="SECRET_KEY",
51
59
  )
52
60
 
53
61
  if len(secret_key) < 32:
@@ -87,7 +95,7 @@ def _get_secret_key_value() -> str:
87
95
  SECRET_KEY = _SecretKey()
88
96
 
89
97
 
90
- def _validate_next_url(next_url: Optional[str]) -> str:
98
+ def _validate_next_url(next_url: str | None) -> str:
91
99
  """
92
100
  Sanitizes a 'next' URL parameter to prevent Open Redirect vulnerabilities.
93
101
  """
@@ -120,7 +128,7 @@ async def get_authz_provider(request: Request) -> AuthorizationProvider:
120
128
  return provider
121
129
 
122
130
 
123
- async def get_token_blacklist(request: Request) -> Optional[TokenBlacklist]:
131
+ async def get_token_blacklist(request: Request) -> TokenBlacklist | None:
124
132
  """
125
133
  FastAPI Dependency: Retrieves token blacklist from app.state.
126
134
 
@@ -130,7 +138,7 @@ async def get_token_blacklist(request: Request) -> Optional[TokenBlacklist]:
130
138
  return blacklist
131
139
 
132
140
 
133
- async def get_session_manager(request: Request) -> Optional[SessionManager]:
141
+ async def get_session_manager(request: Request) -> SessionManager | None:
134
142
  """
135
143
  FastAPI Dependency: Retrieves session manager from app.state.
136
144
 
@@ -142,8 +150,8 @@ async def get_session_manager(request: Request) -> Optional[SessionManager]:
142
150
 
143
151
  async def get_current_user(
144
152
  request: Request,
145
- token: Optional[str] = Cookie(default=None),
146
- ) -> Optional[Dict[str, Any]]:
153
+ token: str | None = Cookie(default=None),
154
+ ) -> dict[str, Any] | None:
147
155
  """
148
156
  FastAPI Dependency: Decodes and validates the JWT stored in the 'token' cookie.
149
157
 
@@ -164,9 +172,7 @@ async def get_current_user(
164
172
  if blacklist:
165
173
  is_revoked = await blacklist.is_revoked(jti)
166
174
  if is_revoked:
167
- logger.info(
168
- f"get_current_user: Token {jti} is blacklisted (revoked)"
169
- )
175
+ logger.info(f"get_current_user: Token {jti} is blacklisted (revoked)")
170
176
  return None
171
177
 
172
178
  # Also check user-level revocation
@@ -174,9 +180,7 @@ async def get_current_user(
174
180
  if user_id:
175
181
  user_revoked = await blacklist.is_user_revoked(user_id)
176
182
  if user_revoked:
177
- logger.info(
178
- f"get_current_user: All tokens for user {user_id} are revoked"
179
- )
183
+ logger.info(f"get_current_user: All tokens for user {user_id} are revoked")
180
184
  return None
181
185
 
182
186
  payload = decode_jwt_token(token, str(SECRET_KEY))
@@ -184,9 +188,7 @@ async def get_current_user(
184
188
  # Verify token type (should be access token for backward compatibility, or no type)
185
189
  token_type = payload.get("type")
186
190
  if token_type and token_type not in ("access", None):
187
- logger.warning(
188
- f"get_current_user: Invalid token type '{token_type}' for access token"
189
- )
191
+ logger.warning(f"get_current_user: Invalid token type '{token_type}' for access token")
190
192
  return None
191
193
 
192
194
  logger.debug(
@@ -203,13 +205,15 @@ async def get_current_user(
203
205
  except (ValueError, TypeError):
204
206
  logger.exception("Validation error decoding JWT token")
205
207
  return None
206
- except Exception:
207
- logger.exception("Unexpected error decoding JWT token")
208
- # Re-raise unexpected errors for debugging
209
- raise
208
+ except PyMongoError:
209
+ logger.exception("Database error checking token blacklist")
210
+ return None
211
+ except (AttributeError, KeyError):
212
+ logger.exception("State access error in get_current_user")
213
+ return None
210
214
 
211
215
 
212
- async def get_current_user_from_request(request: Request) -> Optional[Dict[str, Any]]:
216
+ async def get_current_user_from_request(request: Request) -> dict[str, Any] | None:
213
217
  """
214
218
  Helper function to get current user from a Request object.
215
219
  This is useful when you need to call get_current_user outside of FastAPI dependency injection.
@@ -276,16 +280,18 @@ async def get_current_user_from_request(request: Request) -> Optional[Dict[str,
276
280
  except (ValueError, TypeError):
277
281
  logger.exception("Validation error decoding JWT token from request")
278
282
  return None
279
- except Exception:
280
- logger.exception("Unexpected error decoding JWT token from request")
281
- # Re-raise unexpected errors for debugging
282
- raise
283
+ except PyMongoError:
284
+ logger.exception("Database error checking token blacklist from request")
285
+ return None
286
+ except (AttributeError, KeyError):
287
+ logger.exception("State access error in get_current_user_from_request")
288
+ return None
283
289
 
284
290
 
285
291
  async def get_refresh_token(
286
292
  request: Request,
287
- refresh_token: Optional[str] = Cookie(default=None),
288
- ) -> Optional[Dict[str, Any]]:
293
+ refresh_token: str | None = Cookie(default=None),
294
+ ) -> dict[str, Any] | None:
289
295
  """
290
296
  FastAPI Dependency: Validates refresh token from cookie.
291
297
 
@@ -314,9 +320,7 @@ async def get_refresh_token(
314
320
  if blacklist:
315
321
  is_revoked = await blacklist.is_revoked(jti)
316
322
  if is_revoked:
317
- logger.info(
318
- f"get_refresh_token: Refresh token {jti} is blacklisted"
319
- )
323
+ logger.info(f"get_refresh_token: Refresh token {jti} is blacklisted")
320
324
  return None
321
325
 
322
326
  payload = decode_jwt_token(refresh_token, str(SECRET_KEY))
@@ -350,13 +354,9 @@ async def get_refresh_token(
350
354
  if stored_fingerprint:
351
355
  from .utils import generate_session_fingerprint
352
356
 
353
- device_id = request.cookies.get("device_id") or payload.get(
354
- "device_id"
355
- )
357
+ device_id = request.cookies.get("device_id") or payload.get("device_id")
356
358
  if device_id:
357
- current_fingerprint = generate_session_fingerprint(
358
- request, device_id
359
- )
359
+ current_fingerprint = generate_session_fingerprint(request, device_id)
360
360
  if current_fingerprint != stored_fingerprint:
361
361
  logger.warning(
362
362
  f"get_refresh_token: Session fingerprint mismatch "
@@ -377,16 +377,18 @@ async def get_refresh_token(
377
377
  except (ValueError, TypeError):
378
378
  logger.exception("Validation error decoding refresh token")
379
379
  return None
380
- except Exception:
381
- logger.exception("Unexpected error decoding refresh token")
382
- # Re-raise unexpected errors for debugging
383
- raise
380
+ except PyMongoError:
381
+ logger.exception("Database error checking refresh token")
382
+ return None
383
+ except (AttributeError, KeyError):
384
+ logger.exception("State access error in get_refresh_token")
385
+ return None
384
386
 
385
387
 
386
388
  async def require_admin(
387
- user: Optional[Mapping[str, Any]] = Depends(get_current_user),
389
+ user: Mapping[str, Any] | None = Depends(get_current_user),
388
390
  authz: AuthorizationProvider = Depends(get_authz_provider),
389
- ) -> Dict[str, Any]:
391
+ ) -> dict[str, Any]:
390
392
  """
391
393
  FastAPI Dependency: Enforces admin privileges via the pluggable AuthZ provider.
392
394
  """
@@ -420,9 +422,9 @@ async def require_admin(
420
422
 
421
423
 
422
424
  async def require_admin_or_developer(
423
- user: Optional[Mapping[str, Any]] = Depends(get_current_user),
425
+ user: Mapping[str, Any] | None = Depends(get_current_user),
424
426
  authz: AuthorizationProvider = Depends(get_authz_provider),
425
- ) -> Dict[str, Any]:
427
+ ) -> dict[str, Any]:
426
428
  """
427
429
  FastAPI Dependency: Enforces admin OR developer privileges.
428
430
  Developers can upload apps, admins can upload any app.
@@ -481,8 +483,8 @@ async def require_admin_or_developer(
481
483
 
482
484
 
483
485
  async def get_current_user_or_redirect(
484
- request: Request, user: Optional[Mapping[str, Any]] = Depends(get_current_user)
485
- ) -> Dict[str, Any]:
486
+ request: Request, user: Mapping[str, Any] | None = Depends(get_current_user)
487
+ ) -> dict[str, Any]:
486
488
  """
487
489
  FastAPI Dependency: Enforces user authentication. Redirects to login if not authenticated.
488
490
  """
@@ -504,14 +506,14 @@ async def get_current_user_or_redirect(
504
506
  headers={"Location": redirect_url},
505
507
  detail="Not authenticated. Redirecting to login.",
506
508
  )
507
- except (ValueError, KeyError, AttributeError):
509
+ except (ValueError, KeyError, AttributeError) as e:
508
510
  logger.exception(
509
511
  f"Failed to generate login redirect URL for route '{login_route_name}'"
510
512
  )
511
513
  raise HTTPException(
512
514
  status_code=status.HTTP_401_UNAUTHORIZED,
513
515
  detail="Authentication required, but redirect failed.",
514
- )
516
+ ) from e
515
517
  return dict(user)
516
518
 
517
519
 
@@ -533,10 +535,10 @@ def require_permission(obj: str, act: str, force_login: bool = True):
533
535
 
534
536
  async def _check_permission(
535
537
  # 2. The type hint MUST be Optional now
536
- user: Optional[Dict[str, Any]] = Depends(user_dependency),
538
+ user: dict[str, Any] | None = Depends(user_dependency),
537
539
  # 3. Ask for the generic INTERFACE
538
540
  authz: AuthorizationProvider = Depends(get_authz_provider),
539
- ) -> Optional[Dict[str, Any]]: # 4. Return type is also Optional
541
+ ) -> dict[str, Any] | None: # 4. Return type is also Optional
540
542
  """Internal dependency function performing the AuthZ check."""
541
543
 
542
544
  # 5. Check for 'anonymous' if user is None
@@ -594,9 +596,9 @@ def require_permission(obj: str, act: str, force_login: bool = True):
594
596
 
595
597
  async def refresh_access_token(
596
598
  request: Request,
597
- refresh_token_payload: Dict[str, Any],
598
- device_info: Optional[Dict[str, Any]] = None,
599
- ) -> Optional[Tuple[str, str, Dict[str, Any]]]:
599
+ refresh_token_payload: dict[str, Any],
600
+ device_info: dict[str, Any] | None = None,
601
+ ) -> tuple[str, str, dict[str, Any]] | None:
600
602
  """
601
603
  Refresh an access token using a valid refresh token.
602
604
 
@@ -619,9 +621,7 @@ async def refresh_access_token(
619
621
  from ..config import TOKEN_ROTATION_ENABLED
620
622
  from .jwt import generate_token_pair
621
623
 
622
- user_id = refresh_token_payload.get("user_id") or refresh_token_payload.get(
623
- "email"
624
- )
624
+ user_id = refresh_token_payload.get("user_id") or refresh_token_payload.get("email")
625
625
  old_refresh_jti = refresh_token_payload.get("jti")
626
626
  device_id = refresh_token_payload.get("device_id")
627
627
 
@@ -653,9 +653,7 @@ async def refresh_access_token(
653
653
 
654
654
  device_id = device_id or request.cookies.get("device_id")
655
655
  if device_id:
656
- current_fingerprint = generate_session_fingerprint(
657
- request, device_id
658
- )
656
+ current_fingerprint = generate_session_fingerprint(request, device_id)
659
657
  if current_fingerprint != stored_fingerprint:
660
658
  logger.warning(
661
659
  f"refresh_access_token: Session fingerprint mismatch "
@@ -671,9 +669,7 @@ async def refresh_access_token(
671
669
 
672
670
  # Use existing device_id or generate new one
673
671
  if not device_id:
674
- device_id = (
675
- str(uuid.uuid4()) if not device_info else device_info.get("device_id")
676
- )
672
+ device_id = str(uuid.uuid4()) if not device_info else device_info.get("device_id")
677
673
 
678
674
  if device_info:
679
675
  device_info["device_id"] = device_id
@@ -741,7 +737,9 @@ async def refresh_access_token(
741
737
  except (ValueError, TypeError, jwt.InvalidTokenError):
742
738
  logger.exception("Validation error refreshing token")
743
739
  return None
744
- except Exception:
745
- logger.exception("Unexpected error refreshing token")
746
- # Re-raise unexpected errors for debugging
747
- raise
740
+ except PyMongoError:
741
+ logger.exception("Database error refreshing token")
742
+ return None
743
+ except (AttributeError, KeyError):
744
+ logger.exception("State access error refreshing token")
745
+ return None
@@ -19,7 +19,7 @@ async def initialize_token_management(app, db):
19
19
 
20
20
  Args:
21
21
  app: FastAPI application instance
22
- db: MongoDB database instance (Motor AsyncIOMotorDatabase)
22
+ db: Scoped MongoDB database instance (ScopedMongoWrapper)
23
23
 
24
24
  Example:
25
25
  from mdb_engine.auth.helpers import initialize_token_management
@@ -27,8 +27,8 @@ async def initialize_token_management(app, db):
27
27
 
28
28
  @app.on_event("startup")
29
29
  async def startup():
30
- # Get database from engine
31
- db = engine.get_database()
30
+ # Get scoped database from engine
31
+ db = engine.get_scoped_db("my_app")
32
32
 
33
33
  # Initialize token management
34
34
  await initialize_token_management(app, db)
@@ -8,20 +8,23 @@ This module is part of MDB_ENGINE - MongoDB Engine.
8
8
 
9
9
  import logging
10
10
  import os
11
- from typing import Any, Dict, Optional
11
+ from typing import Any
12
12
 
13
13
  from fastapi import FastAPI
14
14
 
15
- from .config_defaults import (CORS_DEFAULTS, OBSERVABILITY_DEFAULTS,
16
- SECURITY_CONFIG_DEFAULTS,
17
- TOKEN_MANAGEMENT_DEFAULTS)
15
+ from .config_defaults import (
16
+ CORS_DEFAULTS,
17
+ OBSERVABILITY_DEFAULTS,
18
+ SECURITY_CONFIG_DEFAULTS,
19
+ TOKEN_MANAGEMENT_DEFAULTS,
20
+ )
18
21
  from .config_helpers import merge_config_with_defaults
19
22
  from .helpers import initialize_token_management
20
23
 
21
24
  logger = logging.getLogger(__name__)
22
25
 
23
26
  # Cache for auth configs
24
- _auth_config_cache: Dict[str, Dict[str, Any]] = {}
27
+ _auth_config_cache: dict[str, dict[str, Any]] = {}
25
28
 
26
29
 
27
30
  def _has_cors_middleware(app: FastAPI) -> bool:
@@ -45,8 +48,7 @@ def _has_cors_middleware(app: FastAPI) -> bool:
45
48
  middleware_cls = middleware[0]
46
49
  # Check if it's CORSMiddleware or a subclass
47
50
  if middleware_cls == CORSMiddleware or (
48
- hasattr(middleware_cls, "__name__")
49
- and "CORS" in middleware_cls.__name__
51
+ hasattr(middleware_cls, "__name__") and "CORS" in middleware_cls.__name__
50
52
  ):
51
53
  return True
52
54
  return False
@@ -55,7 +57,7 @@ def _has_cors_middleware(app: FastAPI) -> bool:
55
57
  return False
56
58
 
57
59
 
58
- def invalidate_auth_config_cache(slug_id: Optional[str] = None) -> None:
60
+ def invalidate_auth_config_cache(slug_id: str | None = None) -> None:
59
61
  """
60
62
  Invalidate auth config cache for a specific app or all apps.
61
63
 
@@ -70,7 +72,7 @@ def invalidate_auth_config_cache(slug_id: Optional[str] = None) -> None:
70
72
  logger.debug("Invalidated entire auth config cache")
71
73
 
72
74
 
73
- async def get_auth_config(slug_id: str, engine) -> Dict[str, Any]:
75
+ async def get_auth_config(slug_id: str, engine) -> dict[str, Any]:
74
76
  """
75
77
  Retrieve authentication configuration from manifest.
76
78
 
@@ -119,7 +121,7 @@ async def get_auth_config(slug_id: str, engine) -> Dict[str, Any]:
119
121
 
120
122
 
121
123
  async def _setup_authorization_provider(
122
- app: FastAPI, engine, slug_id: str, config: Dict[str, Any]
124
+ app: FastAPI, engine, slug_id: str, config: dict[str, Any]
123
125
  ) -> None:
124
126
  """Set up authorization provider (Casbin/OSO/custom) from manifest."""
125
127
  auth = config.get("auth", {})
@@ -131,18 +133,12 @@ async def _setup_authorization_provider(
131
133
  try:
132
134
  from .casbin_factory import initialize_casbin_from_manifest
133
135
 
134
- authz_provider = await initialize_casbin_from_manifest(
135
- engine, slug_id, config
136
- )
136
+ authz_provider = await initialize_casbin_from_manifest(engine, slug_id, config)
137
137
  if authz_provider:
138
138
  app.state.authz_provider = authz_provider
139
- logger.info(
140
- f"Authorization provider (Casbin) auto-created for {slug_id}"
141
- )
139
+ logger.info(f"Authorization provider (Casbin) auto-created for {slug_id}")
142
140
  else:
143
- logger.debug(
144
- f"Casbin provider not created for {slug_id} (may not be installed)"
145
- )
141
+ logger.debug(f"Casbin provider not created for {slug_id} (may not be installed)")
146
142
  except (
147
143
  ImportError,
148
144
  AttributeError,
@@ -160,9 +156,7 @@ async def _setup_authorization_provider(
160
156
  authz_provider = await initialize_oso_from_manifest(engine, slug_id, config)
161
157
  if authz_provider:
162
158
  app.state.authz_provider = authz_provider
163
- logger.info(
164
- f"✅ Authorization provider (OSO Cloud) auto-created for {slug_id}"
165
- )
159
+ logger.info(f"✅ Authorization provider (OSO Cloud) auto-created for {slug_id}")
166
160
  else:
167
161
  logger.error(
168
162
  f"❌ OSO Cloud provider not created for {slug_id}. "
@@ -187,9 +181,7 @@ async def _setup_authorization_provider(
187
181
  logger.info(f"Custom provider specified for {slug_id} - manual setup required")
188
182
 
189
183
 
190
- async def _setup_demo_users(
191
- app: FastAPI, engine, slug_id: str, config: Dict[str, Any]
192
- ) -> list:
184
+ async def _setup_demo_users(app: FastAPI, engine, slug_id: str, config: dict[str, Any]) -> list:
193
185
  """Set up demo users and link with OSO roles if applicable."""
194
186
  auth = config.get("auth", {})
195
187
  users_config = auth.get("users", {})
@@ -260,9 +252,7 @@ async def _setup_demo_users(
260
252
  f"Demo user creation disabled for {slug_id} (demo_user_seed_strategy: disabled)"
261
253
  )
262
254
  elif seed_strategy == "manual":
263
- logger.debug(
264
- f"Demo user creation set to manual for {slug_id} - skipping auto-creation"
265
- )
255
+ logger.debug(f"Demo user creation set to manual for {slug_id} - skipping auto-creation")
266
256
 
267
257
  # Link demo users with OSO initial_roles if auth provider is OSO
268
258
  if hasattr(app.state, "authz_provider") and demo_users:
@@ -283,14 +273,30 @@ async def _setup_demo_users(
283
273
  if role_assignment.get("user") == user_email:
284
274
  role = role_assignment.get("role")
285
275
  resource = role_assignment.get("resource", "documents")
276
+
277
+ # Check if provider is Casbin (uses email as subject for initial_roles)
278
+ is_casbin = hasattr(app.state.authz_provider, "_enforcer")
279
+
286
280
  try:
287
- await app.state.authz_provider.add_role_for_user(
288
- user_email, role, resource
289
- )
290
- logger.info(
291
- f"✅ Assigned role '{role}' on resource '{resource}' "
292
- f"to demo user '{user_email}' for {slug_id}"
293
- )
281
+ if is_casbin:
282
+ # For Casbin, use email as subject to match initial_roles format
283
+ # This ensures consistency with how initial_roles are set up
284
+ await app.state.authz_provider.add_role_for_user(
285
+ user_email, role
286
+ )
287
+ logger.info(
288
+ f"✅ Assigned Casbin role '{role}' "
289
+ f"to demo user '{user_email}' for {slug_id}"
290
+ )
291
+ else:
292
+ # For OSO, use email, role, resource
293
+ await app.state.authz_provider.add_role_for_user(
294
+ user_email, role, resource
295
+ )
296
+ logger.info(
297
+ f"✅ Assigned role '{role}' on resource '{resource}' "
298
+ f"to demo user '{user_email}' for {slug_id}"
299
+ )
294
300
  except (
295
301
  ValueError,
296
302
  TypeError,
@@ -319,20 +325,18 @@ async def _setup_demo_users(
319
325
 
320
326
 
321
327
  async def _setup_token_management(
322
- app: FastAPI, engine, slug_id: str, token_management: Dict[str, Any]
328
+ app: FastAPI, engine, slug_id: str, token_management: dict[str, Any]
323
329
  ) -> None:
324
330
  """Initialize token management (blacklist and session manager)."""
325
331
  if token_management.get("auto_setup", True):
326
332
  try:
327
- db = engine.get_database()
333
+ db = engine.get_scoped_db(slug_id)
328
334
  await initialize_token_management(app, db)
329
335
 
330
336
  # Configure session fingerprinting if session manager exists
331
337
  session_mgr = getattr(app.state, "session_manager", None)
332
338
  if session_mgr:
333
- fingerprinting_config = app.state.security_config.get(
334
- "session_fingerprinting", {}
335
- )
339
+ fingerprinting_config = app.state.security_config.get("session_fingerprinting", {})
336
340
  session_mgr.configure_fingerprinting(
337
341
  enabled=fingerprinting_config.get("enabled", True),
338
342
  strict=fingerprinting_config.get("strict_mode", False),
@@ -352,12 +356,10 @@ async def _setup_token_management(
352
356
 
353
357
 
354
358
  async def _setup_security_middleware(
355
- app: FastAPI, slug_id: str, security_config: Dict[str, Any]
359
+ app: FastAPI, slug_id: str, security_config: dict[str, Any]
356
360
  ) -> None:
357
361
  """Set up security middleware (if not already added)."""
358
- if security_config.get("csrf_protection", True) or security_config.get(
359
- "require_https", False
360
- ):
362
+ if security_config.get("csrf_protection", True) or security_config.get("require_https", False):
361
363
  try:
362
364
  from .middleware import SecurityMiddleware
363
365
 
@@ -384,15 +386,13 @@ async def _setup_security_middleware(
384
386
  f"This is normal when using lifespan context managers."
385
387
  )
386
388
  else:
387
- logger.warning(
388
- f"Could not set up security middleware for {slug_id}: {e}"
389
- )
389
+ logger.warning(f"Could not set up security middleware for {slug_id}: {e}")
390
390
  except (AttributeError, TypeError, ValueError, RuntimeError, ImportError) as e:
391
391
  logger.warning(f"Could not set up security middleware for {slug_id}: {e}")
392
392
 
393
393
 
394
394
  async def _setup_cors_and_observability(
395
- app: FastAPI, engine, slug_id: str, config: Dict[str, Any]
395
+ app: FastAPI, engine, slug_id: str, config: dict[str, Any]
396
396
  ) -> None:
397
397
  """Set up CORS and observability configs and middleware."""
398
398
  # Get manifest data first if available
@@ -409,9 +409,7 @@ async def _setup_cors_and_observability(
409
409
  app.state.cors_config = merge_config_with_defaults(cors_config, CORS_DEFAULTS)
410
410
 
411
411
  # Extract and store observability config
412
- observability_config = (
413
- manifest_data.get("observability", {}) if manifest_data else {}
414
- )
412
+ observability_config = manifest_data.get("observability", {}) if manifest_data else {}
415
413
  app.state.observability_config = merge_config_with_defaults(
416
414
  observability_config, OBSERVABILITY_DEFAULTS
417
415
  )
@@ -421,9 +419,7 @@ async def _setup_cors_and_observability(
421
419
  try:
422
420
  # Check if CORS middleware already exists to avoid duplication
423
421
  if _has_cors_middleware(app):
424
- logger.debug(
425
- f"CORS middleware already exists for {slug_id}, skipping addition"
426
- )
422
+ logger.debug(f"CORS middleware already exists for {slug_id}, skipping addition")
427
423
  else:
428
424
  from fastapi.middleware.cors import CORSMiddleware
429
425
 
@@ -431,44 +427,29 @@ async def _setup_cors_and_observability(
431
427
  try:
432
428
  app.add_middleware(
433
429
  CORSMiddleware,
434
- allow_origins=app.state.cors_config.get(
435
- "allow_origins", ["*"]
436
- ),
437
- allow_credentials=app.state.cors_config.get(
438
- "allow_credentials", False
439
- ),
430
+ allow_origins=app.state.cors_config.get("allow_origins", ["*"]),
431
+ allow_credentials=app.state.cors_config.get("allow_credentials", False),
440
432
  allow_methods=app.state.cors_config.get(
441
433
  "allow_methods",
442
434
  ["GET", "POST", "PUT", "DELETE", "PATCH"],
443
435
  ),
444
- allow_headers=app.state.cors_config.get(
445
- "allow_headers", ["*"]
446
- ),
447
- expose_headers=app.state.cors_config.get(
448
- "expose_headers", []
449
- ),
436
+ allow_headers=app.state.cors_config.get("allow_headers", ["*"]),
437
+ expose_headers=app.state.cors_config.get("expose_headers", []),
450
438
  max_age=app.state.cors_config.get("max_age", 3600),
451
439
  )
452
440
  logger.info(f"CORS middleware added for {slug_id}")
453
441
  except (RuntimeError, ValueError) as e:
454
442
  error_msg = str(e).lower()
455
- if (
456
- "cannot add middleware" in error_msg
457
- or "middleware" in error_msg
458
- ):
443
+ if "cannot add middleware" in error_msg or "middleware" in error_msg:
459
444
  logger.debug(
460
445
  f"CORS middleware not added for {slug_id} - "
461
446
  f"app middleware stack already initialized. "
462
447
  f"This is normal when using lifespan context managers."
463
448
  )
464
449
  else:
465
- logger.warning(
466
- f"Could not set up CORS middleware for {slug_id}: {e}"
467
- )
450
+ logger.warning(f"Could not set up CORS middleware for {slug_id}: {e}")
468
451
  else:
469
- logger.warning(
470
- f"CORS middleware not added for {slug_id} - app already started"
471
- )
452
+ logger.warning(f"CORS middleware not added for {slug_id} - app already started")
472
453
  except (AttributeError, TypeError, ValueError, RuntimeError, ImportError) as e:
473
454
  logger.warning(f"Could not set up CORS middleware for {slug_id}: {e}")
474
455
 
@@ -480,9 +461,7 @@ async def _setup_cors_and_observability(
480
461
  from .middleware import StaleSessionMiddleware
481
462
 
482
463
  try:
483
- app.add_middleware(
484
- StaleSessionMiddleware, slug_id=slug_id, engine=engine
485
- )
464
+ app.add_middleware(StaleSessionMiddleware, slug_id=slug_id, engine=engine)
486
465
  logger.info(f"Stale session cleanup middleware added for {slug_id}")
487
466
  except (RuntimeError, ValueError) as e:
488
467
  error_msg = str(e).lower()
@@ -493,13 +472,9 @@ async def _setup_cors_and_observability(
493
472
  f"This is normal when using lifespan context managers."
494
473
  )
495
474
  else:
496
- logger.warning(
497
- f"Could not set up stale session middleware for {slug_id}: {e}"
498
- )
475
+ logger.warning(f"Could not set up stale session middleware for {slug_id}: {e}")
499
476
  except (AttributeError, TypeError, ValueError, RuntimeError, ImportError) as e:
500
- logger.warning(
501
- f"Could not set up stale session middleware for {slug_id}: {e}"
502
- )
477
+ logger.warning(f"Could not set up stale session middleware for {slug_id}: {e}")
503
478
 
504
479
 
505
480
  async def setup_auth_from_manifest(app: FastAPI, engine, slug_id: str) -> bool:
@@ -572,7 +547,5 @@ async def setup_auth_from_manifest(app: FastAPI, engine, slug_id: str) -> bool:
572
547
  KeyError,
573
548
  ConnectionError,
574
549
  ) as e:
575
- logger.error(
576
- f"Error setting up auth from manifest for {slug_id}: {e}", exc_info=True
577
- )
550
+ logger.error(f"Error setting up auth from manifest for {slug_id}: {e}", exc_info=True)
578
551
  return False